From 1b8d3c8e269da9ec99e78d8a7850d682ab97bab5 Mon Sep 17 00:00:00 2001 From: Simon Dudley Date: Fri, 6 Mar 2026 16:18:43 +1000 Subject: [PATCH 01/77] Add -Pcases to jmh (#9982) e.g. ./gradlew --no-daemon :ethereum:core:jmh -Pincludes=Mod -Pexcludes=Mul,Add,SMod -Pcases=MOD_256_128,MOD_256_192 Signed-off-by: Simon Dudley --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 8a3b25e862e..fc9b42d371d 100644 --- a/build.gradle +++ b/build.gradle @@ -640,6 +640,7 @@ subprojects { "\nRun JMH benchmarks in each of the projects. Allows for controlling JMH execution directly from the command line.\n" + "\t-Pincludes=\tInclude pattern (regular expression) for benchmarks to be executed. Defaults to including all benchmarks.\n" + "\t-Pexcludes=\tComma-separated exclude pattern (regular expression) for benchmarks to not be executed. Defaults to including all benchmarks.\n" + + "\t-Pcases=\t\tComma-separated list of @Param caseName values to run (e.g. MOD_32_32,MOD_256_256). Defaults to running all cases.\n" + "\t-PasyncProfiler=\tLibrary path to fetch the Async profiler from. Default is to disable profiling.\n" + "\t-PasyncProfilerOptions=\tOptions to pass on to the Async profiler separated by ';'. Default is to produce a flamegraph with all other default profiler options.\n" } @@ -654,6 +655,12 @@ subprojects { var inc = _strListCmdArg('includes', ['']) includes = inc.size() > 1 ? [".*(${inc.join('|')}).*"] : inc excludes = _strListCmdArg('excludes', []) + var cases = _strListCmdArg('cases', []) + if (!cases.isEmpty()) { + def casesListProp = project.objects.listProperty(String) + casesListProp.addAll(cases) + benchmarkParameters = ['caseName': casesListProp] + } var asyncProfiler = _strCmdArg('asyncProfiler') var asyncProfilerOptions = _strCmdArg('asyncProfilerOptions', 'output=flamegraph') zip64.set(true) From 8ede37b9b3758f66b7afbf517ebdb8bd92bb7bd1 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Fri, 6 Mar 2026 17:11:12 +1000 Subject: [PATCH 02/77] Preserve caller-provided gas pricing in eth_simulateV1 results (#9972) * preserve caller-provided gas prices in TransactionSimulator When isAllowExceedingBalance is true but the caller explicitly provided non-zero gas pricing parameters, preserve them and the block header's baseFee so effective gas price is computed correctly during execution. This ensures gas fees are actually charged so that stateRoot and block hash are correct in eth_simulateV1 results. When gas params are absent or zero (typical eth_call, or explicitly zero maxFeePerGas), behavior is unchanged - all fields stay zero and baseFee is zeroed to avoid validation failures. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .../ethereum/transaction/BlockSimulator.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java index f0a88336d6c..585c3fff670 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java @@ -423,6 +423,15 @@ protected BlockStateCallSimulationResult processTransactions( transactionUpdater.commit(); blockUpdater.commit(); + // Restore the original gas pricing from CallParameter into the Transaction. + // TransactionSimulator zeroes gas prices when isAllowExceedingBalance is true to avoid + // upfront cost failures, but for eth_simulateV1 results we need the caller-provided values + // so that gasPrice, maxFeePerGas, maxPriorityFeePerGas, transactionHash, and + // transactionsRoot are correct in the response. + if (transactionValidationParams.isAllowExceedingBalance()) { + transactionSimulationResult = restoreGasPricing(transactionSimulationResult, callParameter); + } + blockStateCallSimulationResult.add(transactionSimulationResult, ws, finalOperationTracer); } @@ -430,6 +439,37 @@ protected BlockStateCallSimulationResult processTransactions( return blockStateCallSimulationResult; } + /** + * Restores the original gas pricing from the CallParameter into the processed Transaction. During + * simulation, TransactionSimulator zeroes gas prices when isAllowExceedingBalance is true to + * avoid upfront cost failures. This method rebuilds the Transaction with the caller's original + * gas pricing so that the result fields (gasPrice, maxFeePerGas, etc.) and derived values + * (transactionHash, transactionsRoot) are correct. + */ + private TransactionSimulatorResult restoreGasPricing( + final TransactionSimulatorResult result, final CallParameter callParameter) { + final Transaction original = result.transaction(); + final boolean hasGasPrice = callParameter.getGasPrice().isPresent(); + final boolean hasMaxFeePerGas = callParameter.getMaxFeePerGas().isPresent(); + final boolean hasMaxPriorityFeePerGas = callParameter.getMaxPriorityFeePerGas().isPresent(); + final boolean hasMaxFeePerBlobGas = + original.getType().supportsBlob() && callParameter.getMaxFeePerBlobGas().isPresent(); + + if (!hasGasPrice && !hasMaxFeePerGas && !hasMaxPriorityFeePerGas && !hasMaxFeePerBlobGas) { + return result; + } + + final Transaction.Builder builder = Transaction.builder().copiedFrom(original); + callParameter.getGasPrice().ifPresent(builder::gasPrice); + callParameter.getMaxFeePerGas().ifPresent(builder::maxFeePerGas); + callParameter.getMaxPriorityFeePerGas().ifPresent(builder::maxPriorityFeePerGas); + if (original.getType().supportsBlob()) { + callParameter.getMaxFeePerBlobGas().ifPresent(builder::maxFeePerBlobGas); + } + + return new TransactionSimulatorResult(builder.build(), result.result()); + } + private Optional createTransactionAccessLocationTracker( final Optional blockAccessListBuilder, final int transactionLocation) { From 102461a3cf92296b52d8d93e766fb3535e5b36d2 Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Fri, 6 Mar 2026 15:28:26 +0100 Subject: [PATCH 03/77] Fix edge cases for MOD variants (#9934) This commit fixes 2 issues: - in mulSubOverflow there was no addBack in case of limb overflow; - reductions were too few in the case that the higher limb exceeds modulus. Co-authored-by: Luis Pinto Signed-off-by: daniellehrner --- .../org/hyperledger/besu/evm/UInt256.java | 59 +++++++++++++---- .../org/hyperledger/besu/evm/UInt256Test.java | 63 +++++++++++++++++++ 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java index b93388b7a3b..83df96c3af1 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java @@ -1226,6 +1226,10 @@ private UInt128 mulSubOverflow(final long v1, final long v0) { long carry = u0 - 1 + ((Long.compareUnsigned(v0, z0) <= 0) ? 1 : 0); long z1 = v1 + u1 - carry; + // q = MAX may still be 1 too high; check if result >= modulus (i.e. negative wrapped) + if (Long.compareUnsigned(z1, u1) > 0 || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)) { + return addBack(z1, z0); + } return new UInt128(z1, z0); } @@ -1427,6 +1431,13 @@ private UInt192 mulSubOverflow(final long v2, final long v1, final long v0) { carry = u1 - 1 + ((Long.compareUnsigned(v1, res) < 0) ? 1 : 0); long z2 = v2 - carry + u2 - borrow; + // q = MAX may still be 1 too high; check if result >= modulus (i.e. negative wrapped) + if (Long.compareUnsigned(z2, u2) > 0 + || (z2 == u2 + && (Long.compareUnsigned(z1, u1) > 0 + || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)))) { + return addBack(z2, z1, z0); + } return new UInt192(z2, z1, z0); } @@ -1439,26 +1450,39 @@ private UInt192 reduceStep( } private UInt256 reduceNormalised(final UInt256 that, final int shift, final long inv) { - UInt192 r; UInt320 v = that.shiftLeftWide(shift); - if (v.u4 != 0 || Long.compareUnsigned(v.u3, u2) >= 0) { - r = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); - } else { - r = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); + if (Long.compareUnsigned(v.u4, u2) < 0) { + UInt192 r; + if (v.u4 != 0 || Long.compareUnsigned(v.u3, u2) >= 0) { + r = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); + r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); + } else { + r = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); + } + return new UInt256(0, r.u2, r.u1, r.u0).shiftRight(shift); } - return new UInt256(0, r.u2, r.u1, r.u0).shiftRight(shift); + return reduceNormalisedSlowPath(v, shift, inv); } private UInt256 reduceNormalised(final UInt257 that, final int shift, final long inv) { - UInt192 r; UInt320 v = that.shiftLeftWide(shift); - if (v.u4 != 0 || Long.compareUnsigned(v.u3, u2) >= 0) { - r = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); - } else { - r = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); + if (Long.compareUnsigned(v.u4, u2) < 0) { + UInt192 r; + if (v.u4 != 0 || Long.compareUnsigned(v.u3, u2) >= 0) { + r = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); + r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); + } else { + r = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); + } + return new UInt256(0, r.u2, r.u1, r.u0).shiftRight(shift); } + return reduceNormalisedSlowPath(v, shift, inv); + } + + private UInt256 reduceNormalisedSlowPath(final UInt320 v, final int shift, final long inv) { + UInt192 r = reduceStep(0, v.u4, v.u3, v.u2, inv); + r = reduceStep(r.u2, r.u1, r.u0, v.u1, inv); + r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); return new UInt256(0, r.u2, r.u1, r.u0).shiftRight(shift); } @@ -1660,6 +1684,15 @@ private UInt256 mulSubOverflow(final long v3, final long v2, final long v1, fina carry = u2 - 1 + ((Long.compareUnsigned(v2, res) < 0) ? 1 : 0); long z3 = v3 + u3 - carry - borrow; + // q = MAX may still be 1 too high; check if result >= modulus (i.e. negative wrapped) + if (Long.compareUnsigned(z3, u3) > 0 + || (z3 == u3 + && (Long.compareUnsigned(z2, u2) > 0 + || (z2 == u2 + && (Long.compareUnsigned(z1, u1) > 0 + || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)))))) { + return addBack(z3, z2, z1, z0); + } return new UInt256(z3, z2, z1, z0); } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java b/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java index fa9db2b6b23..e296cf22085 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java @@ -544,6 +544,69 @@ public void mulMod() { } } + @Test + public void addModTestReduceNormalisedTopLimb() { + UInt256 a = + UInt256.fromBytesBE( + new BigInteger("62d900c9700000000000000000023f00bc1814ff00000000000000ca22300806", 16) + .toByteArray()); + UInt256 b = + UInt256.fromBytesBE( + new BigInteger("ffffffffffffffffb4fffff4befff4f4f4d4f4f504f4f4bef5f5100b0bf4f5f6", 16) + .toByteArray()); + UInt256 m = + UInt256.fromBytesBE(new BigInteger("13464637e8bdc0e53b895d7b79348a784", 16).toByteArray()); + BigInteger A = new BigInteger(1, a.toBytesBE()); + BigInteger B = new BigInteger(1, b.toBytesBE()); + BigInteger M = new BigInteger(1, m.toBytesBE()); + BigInteger expected = A.add(B).mod(M); + assertThat(new BigInteger(1, a.addMod(b, m).toBytesBE())).isEqualTo(expected); + } + + @Test + public void mulSubOverflowWithAddBackBug() { + // When the dividend's leading limb equals the modulus's leading limb, the trial quotient + // overflows and is clamped to 2^64-1. Verify correctness for each Modulus size. + + // Modulus192 path (b.u3==0, b.u2!=0) + UInt256 a1 = + UInt256.fromBytesBE( + new BigInteger("7effffff8000000000000000000000000000000000000000d900000000000001", 16) + .toByteArray()); + UInt256 b1 = + UInt256.fromBytesBE( + new BigInteger("7effffff800000007effffff800000008000ff0000010000", 16).toByteArray()); + BigInteger expected1 = new BigInteger("7effffff800000007dff00feffff0001d901fe0000020001", 16); + assertThat(new BigInteger(1, a1.mod(b1).toBytesBE())).isEqualTo(expected1); + + // Modulus128 path (b.u3==0, b.u2==0, b.u1!=0) + UInt256 a2 = + UInt256.fromBytesBE( + new BigInteger("7effffff800000000000000000000000d900000000000001", 16).toByteArray()); + UInt256 b2 = + UInt256.fromBytesBE(new BigInteger("7effffff800000007fffffffffffffff", 16).toByteArray()); + BigInteger aBI2 = new BigInteger(1, a2.toBytesBE()); + BigInteger bBI2 = new BigInteger(1, b2.toBytesBE()); + BigInteger expected2 = aBI2.mod(bBI2); + assertThat(new BigInteger(1, a2.mod(b2).toBytesBE())).isEqualTo(expected2); + + // Modulus256 path (b.u3!=0) via mulMod + UInt256 a3 = + UInt256.fromBytesBE( + new BigInteger("7effffff8000000000000000000000000000000000000000d900000000000001", 16) + .toByteArray()); + UInt256 x3 = UInt256.fromBytesBE(new BigInteger("10000000000000000", 16).toByteArray()); // 2^64 + UInt256 m3 = + UInt256.fromBytesBE( + new BigInteger("7effffff800000007effffff800000008000ff00000100007effffff80000000", 16) + .toByteArray()); + BigInteger aBI3 = new BigInteger(1, a3.toBytesBE()); + BigInteger xBI3 = new BigInteger(1, x3.toBytesBE()); + BigInteger mBI3 = new BigInteger(1, m3.toBytesBE()); + BigInteger expected3 = aBI3.multiply(xBI3).mod(mBI3); + assertThat(new BigInteger(1, a3.mulMod(x3, m3).toBytesBE())).isEqualTo(expected3); + } + @Test public void signedMod() { final Random random = new Random(432); From cb92692f14a61624951cf27ec014bfaf4105df95 Mon Sep 17 00:00:00 2001 From: mamoralesiob Date: Mon, 9 Mar 2026 00:54:09 +0100 Subject: [PATCH 04/77] Removing memoize from the signature algorithm (#8619) (#9777) Removing memoize from the signature algorithm (#8619) Signed-off-by: mamoralesiob Signed-off-by: Simon Dudley --- ...AbstractTestTransactionSelectorPlugin.java | 16 ++----- .../acceptance/plugins/TestBundlePlugin.java | 16 ++----- .../tests/acceptance/dsl/account/Account.java | 14 ++---- .../chainimport/internal/TransactionData.java | 10 ++-- .../operator/GenerateBlockchainConfig.java | 14 ++---- .../services/BlockSimulatorServiceImpl.java | 17 +++---- .../besu/services/BesuEventsImplTest.java | 8 ++-- .../consensus/clique/CliqueExtraDataTest.java | 10 ++-- .../CliqueMiningCoordinatorTest.java | 10 ++-- .../ibft/IbftExtraDataRLPEncoderTest.java | 30 ++++++------ .../ibft/payload/PreparedCertificateTest.java | 10 ++-- .../ibft/payload/RoundChangePayloadTest.java | 10 ++-- .../ibftlegacy/IbftExtraDataCodec.java | 10 ++-- .../blockcreation/MergeCoordinatorTest.java | 15 +++--- .../core/statemachine/RoundStateTest.java | 26 +++------- .../qbft/QbftExtraDataCodecTest.java | 30 ++++++------ .../hyperledger/besu/crypto/KeyPairUtil.java | 19 ++++---- .../engine/AbstractEngineGetPayloadTest.java | 8 ++-- .../methods/engine/EngineGetBlobsV1Test.java | 16 +++---- .../engine/EngineNewPayloadV3Test.java | 8 ++-- .../AbstractBlockCreatorTest.java | 23 ++++----- .../TestingBuildBlockIntegrationTest.java | 25 +++------- .../BlobSizeTransactionSelectorTest.java | 7 ++- .../BlockRlpSizeTransactionSelectorTest.java | 7 ++- .../BlockSizeTransactionSelectorTest.java | 7 ++- .../besu/ethereum/core/CodeDelegation.java | 23 ++++----- .../AccessListTransactionDecoder.java | 16 +++---- .../core/encoding/BlobTransactionDecoder.java | 17 +++---- .../CodeDelegationTransactionDecoder.java | 10 ++-- .../encoding/EIP1559TransactionDecoder.java | 16 +++---- .../encoding/FrontierTransactionDecoder.java | 9 ++-- .../mainnet/MainnetProtocolSpecs.java | 10 ++-- .../transaction/TransactionSimulator.java | 15 +++--- .../ethereum/core/TransactionTestFixture.java | 10 ++-- .../ethereum/core/TransactionBuilderTest.java | 8 ++-- .../core/TransactionIntegrationTest.java | 8 ++-- .../core/TransactionSizeAndHashTest.java | 16 +++---- .../CodeDelegationTransactionEncoderTest.java | 48 ++++++++----------- .../MainnetTransactionValidatorTest.java | 10 ++-- .../PermissionTransactionValidatorTest.java | 8 ++-- .../transaction/TransactionSimulatorTest.java | 10 ++-- ...stractPrioritizedTransactionsTestBase.java | 2 +- .../BaseFeePrioritizedTransactionsTest.java | 8 ++-- .../layered/BaseTransactionPoolTest.java | 10 ++-- .../GasPricePrioritizedTransactionsTest.java | 2 +- .../LayeredPendingTransactionsTest.java | 7 ++- .../eth/transactions/layered/LayersTest.java | 2 +- .../AbstractPendingTransactionsTestBase.java | 17 +++---- .../p2p/discovery/NodeRecordManager.java | 11 ++--- .../handshake/ecies/EncryptedMessage.java | 12 ++--- .../ecies/InitiatorHandshakeMessageV1.java | 22 +++------ .../ecies/InitiatorHandshakeMessageV4.java | 13 ++--- .../discv4/PeerDiscoveryAgentV4Test.java | 42 ++++++---------- .../discv4/internal/PeerTableTest.java | 15 ++---- .../handshake/ecies/ECIESHandshakeTest.java | 46 ++++++------------ 55 files changed, 303 insertions(+), 506 deletions(-) diff --git a/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/AbstractTestTransactionSelectorPlugin.java b/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/AbstractTestTransactionSelectorPlugin.java index 4ca2d3f24bb..ea451b858a6 100644 --- a/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/AbstractTestTransactionSelectorPlugin.java +++ b/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/AbstractTestTransactionSelectorPlugin.java @@ -45,10 +45,8 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Supplier; import java.util.stream.Collectors; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes32; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,17 +54,13 @@ public abstract class AbstractTestTransactionSelectorPlugin implements BesuPlugin { private static final Logger LOG = LoggerFactory.getLogger(AbstractTestTransactionSelectorPlugin.class); - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private static final AtomicLong NONCE = new AtomicLong(0); private static final KeyPair SENDER_KEYS = - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - Bytes32.fromHexString(Accounts.GENESIS_ACCOUNT_THREE_PRIVATE_KEY))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey( + Bytes32.fromHexString(Accounts.GENESIS_ACCOUNT_THREE_PRIVATE_KEY))); private ServiceManager serviceManager; private File callbackDir; diff --git a/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestBundlePlugin.java b/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestBundlePlugin.java index c0a031d8fca..4bf96175248 100644 --- a/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestBundlePlugin.java +++ b/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestBundlePlugin.java @@ -44,10 +44,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.function.Supplier; import com.google.auto.service.AutoService; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes32; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,16 +55,12 @@ public class TestBundlePlugin implements BesuPlugin { private static final Logger LOG = LoggerFactory.getLogger(TestBundlePlugin.class); private final List events = new ArrayList<>(); - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private static final KeyPair SENDER_KEYS = - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - Bytes32.fromHexString(Accounts.GENESIS_ACCOUNT_THREE_PRIVATE_KEY))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey( + Bytes32.fromHexString(Accounts.GENESIS_ACCOUNT_THREE_PRIVATE_KEY))); private ServiceManager serviceManager; private File callbackDir; diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/account/Account.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/account/Account.java index b4fcf307701..d92ea590aae 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/account/Account.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/account/Account.java @@ -32,8 +32,6 @@ import java.math.BigInteger; import java.util.Optional; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes32; import org.web3j.crypto.Credentials; import org.web3j.utils.Convert.Unit; @@ -47,8 +45,8 @@ public class Account { private final Address address; private long nonce = 0; - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private Account( final EthTransactions eth, @@ -76,7 +74,7 @@ public static Account create(final EthTransactions eth, final Address address) { } public static Account create(final EthTransactions eth, final String name) { - return new Account(eth, name, SIGNATURE_ALGORITHM.get().generateKeyPair()); + return new Account(eth, name, SIGNATURE_ALGORITHM.generateKeyPair()); } public static Account fromPrivateKey( @@ -84,10 +82,8 @@ public static Account fromPrivateKey( return new Account( eth, name, - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM.get().createPrivateKey(Bytes32.fromHexString(privateKey)))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey(Bytes32.fromHexString(privateKey)))); } public Optional web3jCredentials() { diff --git a/app/src/main/java/org/hyperledger/besu/chainimport/internal/TransactionData.java b/app/src/main/java/org/hyperledger/besu/chainimport/internal/TransactionData.java index 6358021af17..a22948de071 100644 --- a/app/src/main/java/org/hyperledger/besu/chainimport/internal/TransactionData.java +++ b/app/src/main/java/org/hyperledger/besu/chainimport/internal/TransactionData.java @@ -27,8 +27,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; @@ -44,8 +42,8 @@ public class TransactionData { private final Optional
to; private final SECPPrivateKey privateKey; - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); /** * Instantiates a new Transaction data. @@ -70,7 +68,7 @@ public TransactionData( this.data = data.map(Bytes::fromHexString).orElse(Bytes.EMPTY); this.value = value.map(Wei::fromHexString).orElse(Wei.ZERO); this.to = to.map(Address::fromHexString); - this.privateKey = SIGNATURE_ALGORITHM.get().createPrivateKey(Bytes32.fromHexString(secretKey)); + this.privateKey = SIGNATURE_ALGORITHM.createPrivateKey(Bytes32.fromHexString(secretKey)); } /** @@ -80,7 +78,7 @@ public TransactionData( * @return the signed transaction */ public Transaction getSignedTransaction(final NonceProvider nonceProvider) { - final KeyPair keyPair = SIGNATURE_ALGORITHM.get().createKeyPair(privateKey); + final KeyPair keyPair = SIGNATURE_ALGORITHM.createKeyPair(privateKey); final Address fromAddress = Address.extract(keyPair.getPublicKey()); final long nonce = nonceProvider.get(fromAddress); diff --git a/app/src/main/java/org/hyperledger/besu/cli/subcommands/operator/GenerateBlockchainConfig.java b/app/src/main/java/org/hyperledger/besu/cli/subcommands/operator/GenerateBlockchainConfig.java index 368f6da3800..70ff0c07f36 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/subcommands/operator/GenerateBlockchainConfig.java +++ b/app/src/main/java/org/hyperledger/besu/cli/subcommands/operator/GenerateBlockchainConfig.java @@ -49,8 +49,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import com.google.common.io.Resources; import jakarta.validation.constraints.NotBlank; import org.apache.tuweni.bytes.Bytes; @@ -68,9 +66,6 @@ class GenerateBlockchainConfig implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(GenerateBlockchainConfig.class); - private final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - @NotBlank @Option( required = true, @@ -176,14 +171,15 @@ private void importPublicKey(final JsonNode publicKeyJson) { final String publicKeyText = publicKeyJson.asText(); try { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); final SECPPublicKey publicKey = - SIGNATURE_ALGORITHM.get().createPublicKey(Bytes.fromHexString(publicKeyText)); + signatureAlgorithm.createPublicKey(Bytes.fromHexString(publicKeyText)); - if (!SIGNATURE_ALGORITHM.get().isValidPublicKey(publicKey)) { + if (!signatureAlgorithm.isValidPublicKey(publicKey)) { throw new IllegalArgumentException( publicKeyText + " is not a valid public key for elliptic curve " - + SIGNATURE_ALGORITHM.get().getCurveName()); + + signatureAlgorithm.getCurveName()); } writeKeypair(publicKey, null); @@ -208,7 +204,7 @@ private void generateNodesKeys() { private void generateNodeKeypair(final int node) { try { LOG.info("Generating keypair for node {}.", node); - final KeyPair keyPair = SIGNATURE_ALGORITHM.get().generateKeyPair(); + final KeyPair keyPair = SignatureAlgorithmFactory.getInstance().generateKeyPair(); writeKeypair(keyPair.getPublicKey(), keyPair.getPrivateKey()); } catch (final IOException e) { diff --git a/app/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java b/app/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java index 904ce4e8f59..1f2639ef1b9 100644 --- a/app/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java +++ b/app/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java @@ -40,9 +40,6 @@ import org.hyperledger.besu.plugin.services.BlockSimulationService; import java.util.List; -import java.util.function.Supplier; - -import com.google.common.base.Suppliers; /** This class is a service that simulates the processing of a block */ public class BlockSimulatorServiceImpl implements BlockSimulationService { @@ -50,16 +47,14 @@ public class BlockSimulatorServiceImpl implements BlockSimulationService { private final WorldStateArchive worldStateArchive; private final Blockchain blockchain; - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); // Dummy signature for transactions to not fail being processed. private static final SECPSignature FAKE_SIGNATURE = - SIGNATURE_ALGORITHM - .get() - .createSignature( - SIGNATURE_ALGORITHM.get().getHalfCurveOrder(), - SIGNATURE_ALGORITHM.get().getHalfCurveOrder(), - (byte) 0); + SIGNATURE_ALGORITHM.createSignature( + SIGNATURE_ALGORITHM.getHalfCurveOrder(), + SIGNATURE_ALGORITHM.getHalfCurveOrder(), + (byte) 0); /** * This constructor creates a BlockSimulatorServiceImpl object diff --git a/app/src/test/java/org/hyperledger/besu/services/BesuEventsImplTest.java b/app/src/test/java/org/hyperledger/besu/services/BesuEventsImplTest.java index 5a1b70704a6..04857d9fc9d 100644 --- a/app/src/test/java/org/hyperledger/besu/services/BesuEventsImplTest.java +++ b/app/src/test/java/org/hyperledger/besu/services/BesuEventsImplTest.java @@ -80,8 +80,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -93,9 +91,9 @@ @ExtendWith(MockitoExtension.class) public class BesuEventsImplTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private static final KeyPair KEY_PAIR1 = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + private static final KeyPair KEY_PAIR1 = SIGNATURE_ALGORITHM.generateKeyPair(); private static final org.hyperledger.besu.ethereum.core.Transaction TX1 = createTransaction(0); private static final org.hyperledger.besu.ethereum.core.Transaction TX2 = createTransaction(1); diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/CliqueExtraDataTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/CliqueExtraDataTest.java index a03fe07a4a5..9889d572cac 100644 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/CliqueExtraDataTest.java +++ b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/CliqueExtraDataTest.java @@ -32,21 +32,19 @@ import java.util.List; import java.util.stream.Collectors; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import com.google.common.collect.Lists; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Test; public class CliqueExtraDataTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); @Test public void encodeAndDecodingDoNotAlterData() { final SECPSignature proposerSeal = - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.ONE, (byte) 0); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.ONE, (byte) 0); final List
validators = Arrays.asList( AddressHelpers.ofValue(1), AddressHelpers.ofValue(2), AddressHelpers.ofValue(3)); @@ -97,7 +95,7 @@ public void sufficientlyLargeButIllegallySizedInputThrowsException() { public void addressToExtraDataString() { final List nodeKeys = Lists.newArrayList(); for (int i = 0; i < 4; i++) { - nodeKeys.add(SIGNATURE_ALGORITHM.get().generateKeyPair()); + nodeKeys.add(SIGNATURE_ALGORITHM.generateKeyPair()); } final List
addresses = diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java index fd39a46cfcf..dcd869d588c 100644 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java +++ b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java @@ -47,8 +47,6 @@ import java.util.List; import java.util.Optional; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.assertj.core.util.Lists; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -61,11 +59,11 @@ @SuppressWarnings("DirectInvocationOnMock") public class CliqueMiningCoordinatorTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); - private final KeyPair proposerKeys = SIGNATURE_ALGORITHM.get().generateKeyPair(); - private final KeyPair validatorKeys = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private final KeyPair proposerKeys = SIGNATURE_ALGORITHM.generateKeyPair(); + private final KeyPair validatorKeys = SIGNATURE_ALGORITHM.generateKeyPair(); private final Address proposerAddress = Util.publicKeyToAddress(proposerKeys.getPublicKey()); private final Address validatorAddress = Util.publicKeyToAddress(validatorKeys.getPublicKey()); diff --git a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/IbftExtraDataRLPEncoderTest.java b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/IbftExtraDataRLPEncoderTest.java index f595519264e..781204daf4c 100644 --- a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/IbftExtraDataRLPEncoderTest.java +++ b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/IbftExtraDataRLPEncoderTest.java @@ -34,17 +34,15 @@ import java.util.List; import java.util.Optional; import java.util.Random; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import com.google.common.collect.Lists; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Test; public class IbftExtraDataRLPEncoderTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private final String RAW_HEX_ENCODING_STRING = "f8f1a00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea9400000000000000000000000000000000000" @@ -64,8 +62,8 @@ private static BftExtraData getDecodedExtraDataForRawHexEncodingString() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -265,8 +263,8 @@ public void fullyPopulatedDataProducesCorrectlyFormedExtraDataObject() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create randomised vanity data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -308,8 +306,8 @@ public void fullyPopulatedDataIsEncodedAndDecodedCorrectly() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -351,8 +349,8 @@ public void extraDataCanBeEncodedWithoutCommitSeals() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -389,8 +387,8 @@ public void extraDataCanBeEncodedwithoutCommitSealsOrRoundNumber() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -459,8 +457,8 @@ public void incorrectVoteTypeThrowsException() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = new byte[32]; diff --git a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/payload/PreparedCertificateTest.java b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/payload/PreparedCertificateTest.java index 9204b679795..72ccaaa1d1f 100644 --- a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/payload/PreparedCertificateTest.java +++ b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/payload/PreparedCertificateTest.java @@ -34,8 +34,6 @@ import java.util.Collections; import java.util.List; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; @@ -44,8 +42,8 @@ public class PreparedCertificateTest { private static final ConsensusRoundIdentifier ROUND_IDENTIFIER = new ConsensusRoundIdentifier(0x1234567890ABCDEFL, 0xFEDCBA98); - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); @Test public void roundTripRlpWithNoPreparePayloads() { @@ -71,7 +69,7 @@ public void roundTripRlpWithPreparePayload() { final PreparePayload preparePayload = new PreparePayload(ROUND_IDENTIFIER, Hash.fromHexStringLenient("0x8523ba6e7c5f59ae87")); final SECPSignature signature = - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0); final SignedData signedPrepare = PayloadDeserializers.from(preparePayload, signature); @@ -94,7 +92,7 @@ private SignedData signedProposal() { singletonList(AddressHelpers.ofValue(1)), ROUND_IDENTIFIER); final ProposalPayload proposalPayload = new ProposalPayload(ROUND_IDENTIFIER, block.getHash()); final SECPSignature signature = - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0); return PayloadDeserializers.from(proposalPayload, signature); } } diff --git a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/payload/RoundChangePayloadTest.java b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/payload/RoundChangePayloadTest.java index 39e070cf456..e5bbc8775fd 100644 --- a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/payload/RoundChangePayloadTest.java +++ b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/payload/RoundChangePayloadTest.java @@ -36,8 +36,6 @@ import java.util.Collections; import java.util.Optional; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; @@ -45,10 +43,10 @@ public class RoundChangePayloadTest { private static final ConsensusRoundIdentifier ROUND_IDENTIFIER = new ConsensusRoundIdentifier(0x1234567890ABCDEFL, 0xFEDCBA98); - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private static final SECPSignature SIGNATURE = - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0); @Test public void roundTripRlpWithNoPreparedCertificate() { @@ -113,7 +111,7 @@ private SignedData signedProposal() { singletonList(AddressHelpers.ofValue(1)), ROUND_IDENTIFIER); final ProposalPayload proposalPayload = new ProposalPayload(ROUND_IDENTIFIER, block.getHash()); final SECPSignature signature = - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0); return PayloadDeserializers.from(proposalPayload, signature); } } diff --git a/consensus/ibftlegacy/src/main/java/org/hyperledger/besu/consensus/ibftlegacy/IbftExtraDataCodec.java b/consensus/ibftlegacy/src/main/java/org/hyperledger/besu/consensus/ibftlegacy/IbftExtraDataCodec.java index 06a0be202e1..7eabcc02a8c 100644 --- a/consensus/ibftlegacy/src/main/java/org/hyperledger/besu/consensus/ibftlegacy/IbftExtraDataCodec.java +++ b/consensus/ibftlegacy/src/main/java/org/hyperledger/besu/consensus/ibftlegacy/IbftExtraDataCodec.java @@ -28,8 +28,6 @@ import java.util.Collection; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,8 +41,8 @@ public class IbftExtraDataCodec extends BftExtraDataCodec { /** The constant EXTRA_VANITY_LENGTH. */ public static final int EXTRA_VANITY_LENGTH = 32; - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private static final Logger LOG = LoggerFactory.getLogger(IbftExtraDataCodec.class); @@ -90,7 +88,7 @@ public IbftLegacyExtraData decodeRaw(final Bytes input) { final Collection
validators = rlpInput.readList(Address::readFrom); final SECPSignature proposerSeal = parseProposerSeal(rlpInput); final Collection seals = - rlpInput.readList(rlp -> SIGNATURE_ALGORITHM.get().decodeSignature(rlp.readBytes())); + rlpInput.readList(rlp -> SIGNATURE_ALGORITHM.decodeSignature(rlp.readBytes())); rlpInput.leaveList(); return new IbftLegacyExtraData(vanityData, seals, proposerSeal, validators); @@ -98,7 +96,7 @@ public IbftLegacyExtraData decodeRaw(final Bytes input) { private static SECPSignature parseProposerSeal(final RLPInput rlpInput) { final Bytes data = rlpInput.readBytes(); - return data.isZero() ? null : SIGNATURE_ALGORITHM.get().decodeSignature(data); + return data.isZero() ? null : SIGNATURE_ALGORITHM.decodeSignature(data); } /** diff --git a/consensus/merge/src/test/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinatorTest.java b/consensus/merge/src/test/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinatorTest.java index cf52add9750..f3de4dd38ba 100644 --- a/consensus/merge/src/test/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinatorTest.java +++ b/consensus/merge/src/test/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinatorTest.java @@ -97,7 +97,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.BeforeEach; @@ -120,18 +119,16 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class MergeCoordinatorTest implements MergeGenesisConfigHelper { - private static final com.google.common.base.Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private static final Logger LOG = LoggerFactory.getLogger(MergeCoordinatorTest.class); private static final SECPPrivateKey PRIVATE_KEY1 = - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - Bytes32.fromHexString( - "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f")); + SIGNATURE_ALGORITHM.createPrivateKey( + Bytes32.fromHexString( + "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f")); private static final KeyPair KEYS1 = - new KeyPair(PRIVATE_KEY1, SIGNATURE_ALGORITHM.get().createPublicKey(PRIVATE_KEY1)); + new KeyPair(PRIVATE_KEY1, SIGNATURE_ALGORITHM.createPublicKey(PRIVATE_KEY1)); private static final long REPETITION_MIN_DURATION = 100; diff --git a/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundStateTest.java b/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundStateTest.java index b1046a2a1ce..6562d32ddf5 100644 --- a/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundStateTest.java +++ b/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundStateTest.java @@ -44,8 +44,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import com.google.common.collect.Lists; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -56,8 +54,8 @@ @ExtendWith(MockitoExtension.class) public class RoundStateTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private final List validatorMessageFactories = Lists.newArrayList(); private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 1); @@ -124,9 +122,7 @@ public void singleValidatorRequiresCommitMessageToBeCommitted() { .createCommit( roundIdentifier, blockHash, - SIGNATURE_ALGORITHM - .get() - .createSignature(BigInteger.ONE, BigInteger.ONE, (byte) 1)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.ONE, (byte) 1)); roundState.addCommitMessage(commit); assertThat(roundState.isPrepared()).isFalse(); @@ -229,9 +225,7 @@ public void invalidPriorCommitMessagesAreDiscardedUponSubsequentProposal() { .createCommit( roundIdentifier, blockHash, - SIGNATURE_ALGORITHM - .get() - .createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 1)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 1)); final Commit invalidCommit = validatorMessageFactories @@ -239,9 +233,7 @@ public void invalidPriorCommitMessagesAreDiscardedUponSubsequentProposal() { .createCommit( roundIdentifier, blockHash, - SIGNATURE_ALGORITHM - .get() - .createSignature(BigInteger.TEN, BigInteger.TEN, (byte) 1)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.TEN, (byte) 1)); // RoundState has a quorum size of 3, meaning 1 proposal and 2 commits are required to be // 'committed'. @@ -317,9 +309,7 @@ public void commitSealsAreExtractedFromReceivedMessages() { .createCommit( roundIdentifier, blockHash, - SIGNATURE_ALGORITHM - .get() - .createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 1)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 1)); final Commit secondCommit = validatorMessageFactories @@ -327,9 +317,7 @@ public void commitSealsAreExtractedFromReceivedMessages() { .createCommit( roundIdentifier, blockHash, - SIGNATURE_ALGORITHM - .get() - .createSignature(BigInteger.TEN, BigInteger.TEN, (byte) 1)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.TEN, (byte) 1)); final Proposal proposal = validatorMessageFactories diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodecTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodecTest.java index c584e3c105f..2d7a816064b 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodecTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodecTest.java @@ -35,17 +35,15 @@ import java.util.List; import java.util.Optional; import java.util.Random; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import com.google.common.collect.Lists; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Test; public class QbftExtraDataCodecTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private final String RAW_HEX_ENCODING_STRING = "0xf8f0a00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea940000000000000000" @@ -66,8 +64,8 @@ private static BftExtraData getDecodedExtraDataForRawHexEncodingString() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -265,8 +263,8 @@ public void fullyPopulatedDataProducesCorrectlyFormedExtraDataObject() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create randomised vanity data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -308,8 +306,8 @@ public void fullyPopulatedDataIsEncodedAndDecodedCorrectly() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -349,8 +347,8 @@ public void extraDataCanBeEncodedWithoutCommitSeals() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -390,8 +388,8 @@ public void extraDataCanBeEncodedWithoutCommitSealsOrRoundNumber() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = createNonEmptyVanityData(); @@ -463,8 +461,8 @@ public void incorrectVoteTypeThrowsException() { final int round = 0x00FEDCBA; final List committerSeals = Arrays.asList( - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), - SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + SIGNATURE_ALGORITHM.createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); // Create a byte buffer with no data. final byte[] vanity_bytes = new byte[32]; diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/KeyPairUtil.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/KeyPairUtil.java index ac1c9a7bf2e..9c98c9da29d 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/KeyPairUtil.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/KeyPairUtil.java @@ -25,8 +25,6 @@ import java.nio.file.Path; import java.util.List; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import com.google.common.io.Resources; import org.apache.tuweni.bytes.Bytes32; import org.slf4j.Logger; @@ -35,8 +33,8 @@ /** The Key pair util. */ public class KeyPairUtil { private static final Logger LOG = LoggerFactory.getLogger(KeyPairUtil.class); - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); /** Default constructor */ private KeyPairUtil() {} @@ -69,8 +67,8 @@ public static KeyPair loadKeyPairFromResource(final String resourcePath) { throw new IllegalArgumentException("Unable to load resource: " + resourcePath); } SECPPrivateKey privateKey = - SIGNATURE_ALGORITHM.get().createPrivateKey(Bytes32.fromHexString((keyData))); - keyPair = SIGNATURE_ALGORITHM.get().createKeyPair(privateKey); + SIGNATURE_ALGORITHM.createPrivateKey(Bytes32.fromHexString((keyData))); + keyPair = SIGNATURE_ALGORITHM.createKeyPair(privateKey); LOG.info("Loaded keyPair {} from {}", keyPair.getPublicKey().toString(), resourcePath); return keyPair; @@ -92,13 +90,12 @@ public static KeyPair loadKeyPair(final File keyFile) { LOG.info( "Loaded public key {} from {}", key.getPublicKey().toString(), keyFile.getAbsolutePath()); } else { - final SignatureAlgorithm signatureAlgorithm = SIGNATURE_ALGORITHM.get(); - key = signatureAlgorithm.generateKeyPair(); + key = SIGNATURE_ALGORITHM.generateKeyPair(); storeKeyFile(key, keyFile.getParentFile().toPath()); LOG.info( "Generated new {} public key {} and stored it to {}", - signatureAlgorithm.getCurveName(), + SIGNATURE_ALGORITHM.getCurveName(), key.getPublicKey().toString(), keyFile.getAbsolutePath()); } @@ -146,7 +143,7 @@ public static File getDefaultKeyFile(final Path directory) { * @return the key pair */ public static KeyPair load(final File file) { - return SIGNATURE_ALGORITHM.get().createKeyPair(loadPrivateKey(file)); + return SIGNATURE_ALGORITHM.createKeyPair(loadPrivateKey(file)); } /** @@ -161,7 +158,7 @@ static SECPPrivateKey loadPrivateKey(final File file) { if (info.size() != 1) { throw new IllegalArgumentException("Supplied file does not contain valid keyPair pair."); } - return SIGNATURE_ALGORITHM.get().createPrivateKey(Bytes32.fromHexString((info.get(0)))); + return SIGNATURE_ALGORITHM.createPrivateKey(Bytes32.fromHexString((info.get(0)))); } catch (IOException ex) { throw new IllegalArgumentException("Supplied file does not contain valid keyPair pair."); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/AbstractEngineGetPayloadTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/AbstractEngineGetPayloadTest.java index 63d0ab4afe8..2aea0c55c29 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/AbstractEngineGetPayloadTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/AbstractEngineGetPayloadTest.java @@ -44,9 +44,7 @@ import java.util.Collections; import java.util.Optional; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import io.vertx.core.Vertx; import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.BeforeEach; @@ -60,9 +58,9 @@ @ExtendWith(MockitoExtension.class) public abstract class AbstractEngineGetPayloadTest extends AbstractScheduledApiTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - protected static final KeyPair senderKeys = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + protected static final KeyPair senderKeys = SIGNATURE_ALGORITHM.generateKeyPair(); @FunctionalInterface interface MethodFactory { diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV1Test.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV1Test.java index 5e9f3f7dfd9..65b7bd3f066 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV1Test.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV1Test.java @@ -52,9 +52,7 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import io.vertx.core.Vertx; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -70,16 +68,14 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class EngineGetBlobsV1Test extends AbstractScheduledApiTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private static final SECPPrivateKey PRIVATE_KEY1 = - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - Bytes32.fromHexString( - "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63")); + SIGNATURE_ALGORITHM.createPrivateKey( + Bytes32.fromHexString( + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63")); private static final KeyPair KEYS1 = - new KeyPair(PRIVATE_KEY1, SIGNATURE_ALGORITHM.get().createPublicKey(PRIVATE_KEY1)); + new KeyPair(PRIVATE_KEY1, SIGNATURE_ALGORITHM.createPublicKey(PRIVATE_KEY1)); public static final VersionedHash VERSIONED_HASH_ZERO = new VersionedHash((byte) 1, Hash.ZERO); @Mock private ProtocolContext protocolContext; diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineNewPayloadV3Test.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineNewPayloadV3Test.java index 877a3963c70..1a61cf8e777 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineNewPayloadV3Test.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineNewPayloadV3Test.java @@ -64,9 +64,7 @@ import java.util.Optional; import java.util.Random; import java.util.Set; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.BeforeEach; @@ -76,9 +74,9 @@ @ExtendWith({MockitoExtension.class}) public class EngineNewPayloadV3Test extends EngineNewPayloadV2Test { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - protected static final KeyPair senderKeys = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + protected static final KeyPair senderKeys = SIGNATURE_ALGORITHM.generateKeyPair(); public EngineNewPayloadV3Test() {} diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreatorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreatorTest.java index 2bb39de434c..7d466958b86 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreatorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreatorTest.java @@ -97,7 +97,6 @@ import java.util.List; import java.util.Optional; -import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -109,16 +108,14 @@ @ExtendWith({MockitoExtension.class}) class AbstractBlockCreatorTest extends TrustedSetupClassLoaderExtension { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private static final SECPPrivateKey PRIVATE_KEY1 = - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - Bytes32.fromHexString( - "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63")); + SIGNATURE_ALGORITHM.createPrivateKey( + Bytes32.fromHexString( + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63")); private static final KeyPair KEYS1 = - new KeyPair(PRIVATE_KEY1, SIGNATURE_ALGORITHM.get().createPublicKey(PRIVATE_KEY1)); + new KeyPair(PRIVATE_KEY1, SIGNATURE_ALGORITHM.createPublicKey(PRIVATE_KEY1)); @Mock private WithdrawalsProcessor withdrawalsProcessor; protected EthScheduler ethScheduler = new DeterministicEthScheduler(); @@ -331,9 +328,7 @@ public void blockAccessListIncludesAccountChanges() { final GenesisAccount recipient = accounts.get(2); final Address coinbase = Address.fromHexString(genesisConfig.getCoinbase().get()); final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + SIGNATURE_ALGORITHM.createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); final BigInteger delta = Wei.fromEth(1).toBigInteger(); final Transaction txn = new TransactionTestFixture() @@ -378,9 +373,7 @@ public void blockAccessListShouldNotIncludesAccountWithoutBalSupport() { final GenesisAccount sender = accounts.get(1); final GenesisAccount recipient = accounts.get(2); final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + SIGNATURE_ALGORITHM.createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); final BigInteger delta = Wei.fromEth(1).toBigInteger(); final Transaction txn = new TransactionTestFixture() diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java index de598bac6f0..01c0b7c46ba 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java @@ -75,7 +75,6 @@ import java.util.List; import java.util.Optional; -import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Test; @@ -92,8 +91,8 @@ @MockitoSettings(strictness = Strictness.LENIENT) class TestingBuildBlockIntegrationTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); protected final GenesisConfig genesisConfig = GenesisConfig.fromResource("/block-creation-genesis.json"); @@ -109,9 +108,7 @@ void shouldCreateBlockWithBAL() { final GenesisAccount sender = accounts.get(1); final GenesisAccount recipient = accounts.get(2); final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + SIGNATURE_ALGORITHM.createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); final Transaction txn = new TransactionTestFixture() @@ -165,9 +162,7 @@ void shouldCreateBlockWithoutBAL() { final GenesisAccount sender = accounts.get(1); final GenesisAccount recipient = accounts.get(2); final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + SIGNATURE_ALGORITHM.createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); final Transaction txn = new TransactionTestFixture() @@ -237,9 +232,7 @@ void shouldHaveValidBALStructure() { final GenesisAccount sender = accounts.get(1); final GenesisAccount recipient = accounts.get(2); final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + SIGNATURE_ALGORITHM.createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); final Transaction txn = new TransactionTestFixture() @@ -281,9 +274,7 @@ void shouldIncludeMultipleTransactionsInBAL() { final GenesisAccount recipient1 = accounts.get(2); final GenesisAccount recipient2 = accounts.get(0); final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + SIGNATURE_ALGORITHM.createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); final Transaction txn1 = new TransactionTestFixture() @@ -327,9 +318,7 @@ void shouldTrackNonceChangesInBAL() { final GenesisAccount sender = accounts.get(1); final GenesisAccount recipient = accounts.get(2); final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + SIGNATURE_ALGORITHM.createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); final Transaction txn = new TransactionTestFixture() diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java index 6b888541bd2..0afb4566b04 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java @@ -51,7 +51,6 @@ import java.util.stream.IntStream; import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes48; import org.junit.jupiter.api.BeforeEach; @@ -62,9 +61,9 @@ @ExtendWith(MockitoExtension.class) class BlobSizeTransactionSelectorTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private static final KeyPair KEYS = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + private static final KeyPair KEYS = SIGNATURE_ALGORITHM.generateKeyPair(); @SuppressWarnings("UnnecessaryLambda") private static final Supplier NEVER_CANCELLED = () -> false; diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockRlpSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockRlpSizeTransactionSelectorTest.java index 66bbc67be36..8cbd07e4e22 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockRlpSizeTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockRlpSizeTransactionSelectorTest.java @@ -38,7 +38,6 @@ import java.util.Optional; import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -48,9 +47,9 @@ @ExtendWith(MockitoExtension.class) class BlockRlpSizeTransactionSelectorTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private static final KeyPair KEYS = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + private static final KeyPair KEYS = SIGNATURE_ALGORITHM.generateKeyPair(); @SuppressWarnings("UnnecessaryLambda") private static final Supplier NEVER_CANCELLED = () -> false; diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java index 3eccb894cb8..ed4e2bc3a95 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java @@ -45,7 +45,6 @@ import java.util.stream.IntStream; import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -54,9 +53,9 @@ @ExtendWith(MockitoExtension.class) class BlockSizeTransactionSelectorTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private static final KeyPair KEYS = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + private static final KeyPair KEYS = SIGNATURE_ALGORITHM.generateKeyPair(); private static final long TRANSFER_GAS_LIMIT = 21_000L; private static final long BLOCK_GAS_LIMIT = 1_000_000L; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java index 3b0ef053753..d3c49ebb895 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -39,8 +39,8 @@ // ignore `signer` field used in execution-spec-tests @JsonIgnoreProperties(ignoreUnknown = true) public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); public static final Bytes MAGIC = Bytes.fromHexString("05"); @@ -94,12 +94,10 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation chainId, address, Bytes.fromHexStringLenient(nonce).toLong(), - SIGNATURE_ALGORITHM - .get() - .createCodeDelegationSignature( - Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(v).get(0))); + SIGNATURE_ALGORITHM.createCodeDelegationSignature( + Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(v).get(0))); } @JsonProperty("chainId") @@ -164,7 +162,6 @@ private Optional
computeAuthority() { try { authorityAddress = SIGNATURE_ALGORITHM - .get() .recoverPublicKeyFromSignature(Bytes32.wrap(hash.getBytes()), signature) .map(Address::extract); } catch (final IllegalArgumentException e) { @@ -252,11 +249,9 @@ public org.hyperledger.besu.datatypes.CodeDelegation signAndBuild(final KeyPair output.endList(); signature( - SIGNATURE_ALGORITHM - .get() - .sign( - Bytes32.wrap(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())).getBytes()), - keyPair)); + SIGNATURE_ALGORITHM.sign( + Bytes32.wrap(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())).getBytes()), + keyPair)); return build(); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AccessListTransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AccessListTransactionDecoder.java index 72e09aaa088..c7df51d7054 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AccessListTransactionDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AccessListTransactionDecoder.java @@ -26,14 +26,12 @@ import org.hyperledger.besu.ethereum.rlp.RLPInput; import java.math.BigInteger; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; class AccessListTransactionDecoder { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private AccessListTransactionDecoder() { // private constructor @@ -73,12 +71,10 @@ public static Transaction decode(final Bytes input) { final Transaction transaction = preSignatureTransactionBuilder .signature( - SIGNATURE_ALGORITHM - .get() - .createSignature( - txRlp.readUInt256Scalar().toUnsignedBigInteger(), - txRlp.readUInt256Scalar().toUnsignedBigInteger(), - recId)) + SIGNATURE_ALGORITHM.createSignature( + txRlp.readUInt256Scalar().toUnsignedBigInteger(), + txRlp.readUInt256Scalar().toUnsignedBigInteger(), + recId)) .build(); txRlp.leaveList(); return transaction; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/BlobTransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/BlobTransactionDecoder.java index 3c6f4c1f15d..af9be437b35 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/BlobTransactionDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/BlobTransactionDecoder.java @@ -26,14 +26,11 @@ import org.hyperledger.besu.ethereum.rlp.RLP; import org.hyperledger.besu.ethereum.rlp.RLPInput; -import java.util.function.Supplier; - -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; public class BlobTransactionDecoder { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private BlobTransactionDecoder() { // private constructor @@ -98,12 +95,10 @@ static void readTransactionPayloadInner(final Transaction.Builder builder, final final byte recId = (byte) input.readUnsignedByteScalar(); builder.signature( - SIGNATURE_ALGORITHM - .get() - .createSignature( - input.readUInt256Scalar().toUnsignedBigInteger(), - input.readUInt256Scalar().toUnsignedBigInteger(), - recId)); + SIGNATURE_ALGORITHM.createSignature( + input.readUInt256Scalar().toUnsignedBigInteger(), + input.readUInt256Scalar().toUnsignedBigInteger(), + recId)); input.leaveList(); } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java index d8f035af99e..403dcb1bd41 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java @@ -28,14 +28,12 @@ import org.hyperledger.besu.ethereum.rlp.RLPInput; import java.math.BigInteger; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; public class CodeDelegationTransactionDecoder { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private CodeDelegationTransactionDecoder() { // private constructor @@ -79,7 +77,7 @@ public static Transaction decode(final Bytes input) { txRlp.leaveList(); - return builder.signature(SIGNATURE_ALGORITHM.get().createSignature(r, s, recId)).build(); + return builder.signature(SIGNATURE_ALGORITHM.createSignature(r, s, recId)).build(); } public static CodeDelegation decodeInnerPayload(final RLPInput input) { @@ -96,7 +94,7 @@ public static CodeDelegation decodeInnerPayload(final RLPInput input) { input.leaveList(); final SECPSignature signature = - SIGNATURE_ALGORITHM.get().createCodeDelegationSignature(r, s, yParity); + SIGNATURE_ALGORITHM.createCodeDelegationSignature(r, s, yParity); return new org.hyperledger.besu.ethereum.core.CodeDelegation( chainId, address, nonce, signature); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/EIP1559TransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/EIP1559TransactionDecoder.java index 4b5aef98f43..8eefda02c2c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/EIP1559TransactionDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/EIP1559TransactionDecoder.java @@ -26,14 +26,12 @@ import org.hyperledger.besu.ethereum.rlp.RLPInput; import java.math.BigInteger; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; public class EIP1559TransactionDecoder { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private EIP1559TransactionDecoder() { // private constructor @@ -73,12 +71,10 @@ public static Transaction decode(final Bytes input) { final Transaction transaction = builder .signature( - SIGNATURE_ALGORITHM - .get() - .createSignature( - txRlp.readUInt256Scalar().toUnsignedBigInteger(), - txRlp.readUInt256Scalar().toUnsignedBigInteger(), - recId)) + SIGNATURE_ALGORITHM.createSignature( + txRlp.readUInt256Scalar().toUnsignedBigInteger(), + txRlp.readUInt256Scalar().toUnsignedBigInteger(), + recId)) .build(); txRlp.leaveList(); return transaction; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/FrontierTransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/FrontierTransactionDecoder.java index b7dad725819..46e5933fcaa 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/FrontierTransactionDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/FrontierTransactionDecoder.java @@ -33,15 +33,12 @@ import java.math.BigInteger; import java.util.Optional; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; public class FrontierTransactionDecoder { - // Supplier for the signature algorithm - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); public static Transaction decode(final Bytes input) { return decode(RLP.input(input)); @@ -78,7 +75,7 @@ public static Transaction decode(final RLPInput transactionRlp) { } final BigInteger r = transactionRlp.readUInt256Scalar().toUnsignedBigInteger(); final BigInteger s = transactionRlp.readUInt256Scalar().toUnsignedBigInteger(); - final SECPSignature signature = SIGNATURE_ALGORITHM.get().createSignature(r, s, recId); + final SECPSignature signature = SIGNATURE_ALGORITHM.createSignature(r, s, recId); transactionRlp.leaveList(); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java index 0a77673b94f..4ed6275e245 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java @@ -122,8 +122,6 @@ import java.util.function.Function; import java.util.stream.IntStream; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import com.google.common.io.Resources; import io.vertx.core.json.JsonArray; import org.slf4j.Logger; @@ -135,8 +133,8 @@ public abstract class MainnetProtocolSpecs { private static final Address RIPEMD160_PRECOMPILE = Address.fromHexString("0x0000000000000000000000000000000000000003"); - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); // A consensus bug at Ethereum mainnet transaction 0xcf416c53 // deleted an empty account even when the message execution scope @@ -956,7 +954,7 @@ static ProtocolSpecBuilder pragueDefinition( .codeDelegationProcessor( new CodeDelegationProcessor( chainId, - SIGNATURE_ALGORITHM.get().getHalfCurveOrder(), + SIGNATURE_ALGORITHM.getHalfCurveOrder(), new CodeDelegationService())) .build()) // EIP-2935 Blockhash processor @@ -1219,7 +1217,7 @@ static ProtocolSpecBuilder amsterdamDefinition( .codeDelegationProcessor( new CodeDelegationProcessor( chainId, - SIGNATURE_ALGORITHM.get().getHalfCurveOrder(), + SIGNATURE_ALGORITHM.getHalfCurveOrder(), new CodeDelegationService())) .transferLogEmitter(EIP7708TransferLogEmitter.INSTANCE) .build()) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java index b9d8193d04a..8e0572bb6df 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java @@ -59,7 +59,6 @@ import java.util.function.Supplier; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Suppliers; import jakarta.validation.constraints.NotNull; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; @@ -74,17 +73,15 @@ */ public class TransactionSimulator { private static final Logger LOG = LoggerFactory.getLogger(TransactionSimulator.class); - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); // Dummy signature for transactions to not fail being processed. private static final SECPSignature FAKE_SIGNATURE = - SIGNATURE_ALGORITHM - .get() - .createSignature( - SIGNATURE_ALGORITHM.get().getHalfCurveOrder(), - SIGNATURE_ALGORITHM.get().getHalfCurveOrder(), - (byte) 0); + SIGNATURE_ALGORITHM.createSignature( + SIGNATURE_ALGORITHM.getHalfCurveOrder(), + SIGNATURE_ALGORITHM.getHalfCurveOrder(), + (byte) 0); // TODO: Identify a better default from account to use, such as the registered // coinbase or an account currently unlocked by the client. diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java index 95b2958f16a..2b649f856fa 100644 --- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java +++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java @@ -32,15 +32,13 @@ import java.util.List; import java.util.Optional; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; public class TransactionTestFixture { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private static final KeyPair KEY_PAIR = SIGNATURE_ALGORITHM.get().generateKeyPair(); + 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); @@ -223,7 +221,7 @@ public static CodeDelegation createSignedCodeDelegation( Bytes.concatenate( org.hyperledger.besu.ethereum.core.CodeDelegation.MAGIC, rlpOutput.encoded())); - final var signature = SIGNATURE_ALGORITHM.get().sign(Bytes32.wrap(hash.getBytes()), keys); + final var signature = SIGNATURE_ALGORITHM.sign(Bytes32.wrap(hash.getBytes()), keys); return new org.hyperledger.besu.ethereum.core.CodeDelegation( chainId, diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionBuilderTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionBuilderTest.java index 2fcdecdb124..625190b530d 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionBuilderTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionBuilderTest.java @@ -32,16 +32,14 @@ import java.math.BigInteger; import java.util.List; import java.util.Optional; -import java.util.function.Supplier; import java.util.stream.Stream; -import com.google.common.base.Suppliers; import org.junit.jupiter.api.Test; class TransactionBuilderTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private static final KeyPair senderKeys = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + private static final KeyPair senderKeys = SIGNATURE_ALGORITHM.generateKeyPair(); @Test void guessTypeCanGuessAllTypes() { diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionIntegrationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionIntegrationTest.java index 5520c86d43a..f8ff3a5d1ec 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionIntegrationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionIntegrationTest.java @@ -28,16 +28,14 @@ import java.math.BigInteger; import java.util.Optional; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Test; public class TransactionIntegrationTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private static final KeyPair senderKeys = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + private static final KeyPair senderKeys = SIGNATURE_ALGORITHM.generateKeyPair(); @Test public void diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionSizeAndHashTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionSizeAndHashTest.java index 0ec6fda9fec..7bd2271dace 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionSizeAndHashTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/TransactionSizeAndHashTest.java @@ -36,9 +36,7 @@ import java.math.BigInteger; import java.util.List; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.bytes.Bytes48; @@ -46,17 +44,15 @@ public class TransactionSizeAndHashTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); // Fake signature for transactions to not fail being processed. private static final SECPSignature FAKE_SIGNATURE = - SIGNATURE_ALGORITHM - .get() - .createSignature( - SIGNATURE_ALGORITHM.get().getHalfCurveOrder(), - SIGNATURE_ALGORITHM.get().getHalfCurveOrder(), - (byte) 0); + SIGNATURE_ALGORITHM.createSignature( + SIGNATURE_ALGORITHM.getHalfCurveOrder(), + SIGNATURE_ALGORITHM.getHalfCurveOrder(), + (byte) 0); @Test public void returnsRightSizeForFrontierTx() { diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionEncoderTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionEncoderTest.java index b09fde48dd1..8c0fb0cefec 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionEncoderTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionEncoderTest.java @@ -23,16 +23,14 @@ import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import java.math.BigInteger; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class CodeDelegationTransactionEncoderTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); BytesValueRLPOutput output; @@ -50,14 +48,12 @@ void shouldEncodeSingleCodeDelegationWithNonceAndChainId() { BigInteger.ONE, Address.fromHexString("0x633688abc3cCf8B0C03088D2d1C6ae4958c2fA56"), 42, - SIGNATURE_ALGORITHM - .get() - .createSignature( - new BigInteger( - "840798fa67118e034c1eb7e42fe89e28d7cd5006dc813d5729e5f75b0d1a7ec5", 16), - new BigInteger( - "3b1dbace38ceb862a65bf2eac0637693b5c3493bcb2a022dd614c0a74cce0b99", 16), - (byte) 0)); + SIGNATURE_ALGORITHM.createSignature( + new BigInteger( + "840798fa67118e034c1eb7e42fe89e28d7cd5006dc813d5729e5f75b0d1a7ec5", 16), + new BigInteger( + "3b1dbace38ceb862a65bf2eac0637693b5c3493bcb2a022dd614c0a74cce0b99", 16), + (byte) 0)); CodeDelegationTransactionEncoder.encodeSingleCodeDelegation(authorization, output); @@ -76,14 +72,12 @@ void shouldEncodeSingleCodeDelegationWithNonceZero() { BigInteger.ONE, Address.fromHexString("0x633688abc3cCf8B0C03088D2d1C6ae4958c2fA56"), 0, - SIGNATURE_ALGORITHM - .get() - .createSignature( - new BigInteger( - "dd6b24048be1b7d7fe5bbbb73ffc37eb2ce1997ecb4ae5b6096532ef19363148", 16), - new BigInteger( - "25b58a1ff8ad00bddbbfa1d5c2411961cbb6d08dcdc8ae88303db3c6cf983031", 16), - (byte) 1)); + SIGNATURE_ALGORITHM.createSignature( + new BigInteger( + "dd6b24048be1b7d7fe5bbbb73ffc37eb2ce1997ecb4ae5b6096532ef19363148", 16), + new BigInteger( + "25b58a1ff8ad00bddbbfa1d5c2411961cbb6d08dcdc8ae88303db3c6cf983031", 16), + (byte) 1)); CodeDelegationTransactionEncoder.encodeSingleCodeDelegation(authorization, output); @@ -102,14 +96,12 @@ void shouldEncodeSingleCodeDelegationWithChainIdZero() { BigInteger.ZERO, Address.fromHexString("0x633688abc3cCf8B0C03088D2d1C6ae4958c2fA56"), 5, - SIGNATURE_ALGORITHM - .get() - .createSignature( - new BigInteger( - "25c1240d7ffec0daeedb752d3357aff2e3cd58468f0c2d43ee0ee999e02ace2", 16), - new BigInteger( - "3c8a25b2becd6e666f69803d1ae3322f2e137b7745c2c7f19da80f993ffde4df", 16), - (byte) 1)); + SIGNATURE_ALGORITHM.createSignature( + new BigInteger( + "25c1240d7ffec0daeedb752d3357aff2e3cd58468f0c2d43ee0ee999e02ace2", 16), + new BigInteger( + "3c8a25b2becd6e666f69803d1ae3322f2e137b7745c2c7f19da80f993ffde4df", 16), + (byte) 1)); CodeDelegationTransactionEncoder.encodeSingleCodeDelegation(authorization, output); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java index 71cbb033c45..c22acadae83 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java @@ -58,10 +58,8 @@ import java.util.Optional; import java.util.OptionalInt; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Stream; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes48; import org.junit.jupiter.api.Test; @@ -79,9 +77,9 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class MainnetTransactionValidatorTest extends TrustedSetupClassLoaderExtension { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - protected static final KeyPair senderKeys = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + protected static final KeyPair senderKeys = SIGNATURE_ALGORITHM.generateKeyPair(); private static final TransactionValidationParams transactionProcessingParams = processingBlockParams; @@ -300,7 +298,7 @@ public void transactionWithNullSenderCanBeValidIfGasPriceAndValueIsZero() { gasCalculator, GasLimitCalculator.constant(), false, Optional.of(BigInteger.ONE)); final TransactionTestFixture builder = new TransactionTestFixture(); - final KeyPair senderKeyPair = SIGNATURE_ALGORITHM.get().generateKeyPair(); + final KeyPair senderKeyPair = SIGNATURE_ALGORITHM.generateKeyPair(); final Address arbitrarySender = Address.fromHexString("1"); builder.gasPrice(Wei.ZERO).nonce(0).sender(arbitrarySender).value(Wei.ZERO); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PermissionTransactionValidatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PermissionTransactionValidatorTest.java index 3dfe5996118..38917062fe0 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PermissionTransactionValidatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PermissionTransactionValidatorTest.java @@ -35,9 +35,7 @@ import java.math.BigInteger; import java.util.Optional; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -45,9 +43,9 @@ @ExtendWith(MockitoExtension.class) public class PermissionTransactionValidatorTest extends MainnetTransactionValidatorTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private static final KeyPair senderKeys = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + private static final KeyPair senderKeys = SIGNATURE_ALGORITHM.generateKeyPair(); private final Transaction basicTransaction = new TransactionTestFixture() diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorTest.java index 54f6e1f4db1..56117e3e549 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorTest.java @@ -70,8 +70,6 @@ import java.util.OptionalLong; import java.util.stream.Stream; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; import org.junit.jupiter.api.BeforeEach; @@ -90,11 +88,11 @@ @SuppressWarnings({"rawtypes", "unchecked"}) public class TransactionSimulatorTest extends TrustedSetupClassLoaderExtension { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private static final BigInteger HALF_CURVE_ORDER = SIGNATURE_ALGORITHM.get().getHalfCurveOrder(); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); + private static final BigInteger HALF_CURVE_ORDER = SIGNATURE_ALGORITHM.getHalfCurveOrder(); private static final SECPSignature FAKE_SIGNATURE = - SIGNATURE_ALGORITHM.get().createSignature(HALF_CURVE_ORDER, HALF_CURVE_ORDER, (byte) 0); + SIGNATURE_ALGORITHM.createSignature(HALF_CURVE_ORDER, HALF_CURVE_ORDER, (byte) 0); private static final Address DEFAULT_FROM = Address.fromHexString("0x0000000000000000000000000000000000000000"); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactionsTestBase.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactionsTestBase.java index c0389a69ddd..607a9c5d4a1 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactionsTestBase.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactionsTestBase.java @@ -105,7 +105,7 @@ public void prioritizeLocalTransactionThenValue() { createTransaction( 0, Wei.of(DEFAULT_MIN_GAS_PRICE.multiply(2).toBigInteger().pow(i + 1)), - SIGNATURE_ALGORITHM.get().generateKeyPair())); + SIGNATURE_ALGORITHM.generateKeyPair())); remoteTxs.add(highValueRemoteTx); prioritizeResult = prioritizeTransaction(highValueRemoteTx); assertThat(prioritizeResult).isEqualTo(ADDED); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactionsTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactionsTest.java index 915a766694d..8b779440bbb 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactionsTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactionsTest.java @@ -143,7 +143,7 @@ public void shouldPrioritizeEffectivePriorityFeeThenTimeAddedToPoolOnMixedTypes( createTransaction( 0, DEFAULT_MIN_GAS_PRICE.add(1), - SIGNATURE_ALGORITHM.get().generateKeyPair()))) + SIGNATURE_ALGORITHM.generateKeyPair()))) .collect(Collectors.toUnmodifiableList()); final var lowestPriorityFee = @@ -227,7 +227,7 @@ public void shouldPrioritizePriorityFeeThenTimeAddedToPoolSameTypeTxs( DEFAULT_MIN_GAS_PRICE.add(1).multiply(20), 0, null, - SIGNATURE_ALGORITHM.get().generateKeyPair()))) + SIGNATURE_ALGORITHM.generateKeyPair()))) .collect(Collectors.toUnmodifiableList()); shouldPrioritizeValueThenTimeAddedToPool( @@ -250,7 +250,7 @@ public void maxNumberOfTxsForTypeIsEnforced() { 1, BlobType.KZG_PROOF, null, - SIGNATURE_ALGORITHM.get().generateKeyPair()); + SIGNATURE_ALGORITHM.generateKeyPair()); addedTxs.add(tx); assertThat(prioritizeTransaction(tx)).isEqualTo(ADDED); } @@ -265,7 +265,7 @@ public void maxNumberOfTxsForTypeIsEnforced() { 1, BlobType.KZG_PROOF, null, - SIGNATURE_ALGORITHM.get().generateKeyPair()); + SIGNATURE_ALGORITHM.generateKeyPair()); assertThat(prioritizeTransaction(overflowTx)).isEqualTo(DROPPED); addedTxs.forEach(this::assertTransactionPrioritized); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java index 4c71f36f0db..86592933a2a 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java @@ -50,17 +50,15 @@ import java.util.Random; import java.util.stream.IntStream; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.BeforeEach; public class BaseTransactionPoolTest extends TrustedSetupClassLoaderExtension { - protected static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - protected static final KeyPair KEYS1 = SIGNATURE_ALGORITHM.get().generateKeyPair(); - protected static final KeyPair KEYS2 = SIGNATURE_ALGORITHM.get().generateKeyPair(); + 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 = diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/GasPricePrioritizedTransactionsTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/GasPricePrioritizedTransactionsTest.java index 15923875e7e..aa8ba77e587 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/GasPricePrioritizedTransactionsTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/GasPricePrioritizedTransactionsTest.java @@ -91,7 +91,7 @@ public void shouldPrioritizeGasPriceThenTimeAddedToPool() { createTransaction( 0, DEFAULT_MIN_GAS_PRICE.add(1), - SIGNATURE_ALGORITHM.get().generateKeyPair()))) + SIGNATURE_ALGORITHM.generateKeyPair()))) .toList(); final PendingTransaction highGasPriceTransaction = diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java index ab1cc33a235..96f1508d1f8 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java @@ -246,7 +246,7 @@ public void evictTransactionsWhenSizeLimitExceeded() { i, DEFAULT_BASE_FEE.add(i), (int) smallPoolConfig.getPendingTransactionsLayerMaxCapacityBytes() + 1, - SIGNATURE_ALGORITHM.get().generateKeyPair()); + SIGNATURE_ALGORITHM.generateKeyPair()); smallPendingTransactions.addTransaction( createRemotePendingTransaction(tx), Optional.of(sender)); firstTxs.add(tx); @@ -260,7 +260,7 @@ public void evictTransactionsWhenSizeLimitExceeded() { 0, DEFAULT_MIN_GAS_PRICE.multiply(1000), (int) smallPoolConfig.getPendingTransactionsLayerMaxCapacityBytes(), - SIGNATURE_ALGORITHM.get().generateKeyPair()); + SIGNATURE_ALGORITHM.generateKeyPair()); final Account lastSender = mock(Account.class); when(lastSender.getNonce()).thenReturn(0L); smallPendingTransactions.addTransaction( @@ -294,8 +294,7 @@ public void txsMovingToNextLayerWhenFirstIsFull() { final Account sender = mock(Account.class); when(sender.getNonce()).thenReturn((long) i); final var tx = - createTransaction( - i, DEFAULT_BASE_FEE.add(i), SIGNATURE_ALGORITHM.get().generateKeyPair()); + createTransaction(i, DEFAULT_BASE_FEE.add(i), SIGNATURE_ALGORITHM.generateKeyPair()); pendingTransactions.addTransaction(createRemotePendingTransaction(tx), Optional.of(sender)); txs.add(tx); assertTransactionPending(pendingTransactions, tx); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java index a0572ae8239..2a4dc67d3f6 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java @@ -2097,7 +2097,7 @@ enum Sender { final boolean hasPriority; Sender(final boolean hasPriority, final int gasFeeMultiplier) { - this.key = SIGNATURE_ALGORITHM.get().generateKeyPair(); + this.key = SIGNATURE_ALGORITHM.generateKeyPair(); this.address = Util.publicKeyToAddress(key.getPublicKey()); this.gasFeeMultiplier = gasFeeMultiplier; this.hasPriority = hasPriority; diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java index 03e89956e45..80046f69074 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java @@ -65,8 +65,6 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.junit.jupiter.api.Test; public abstract class AbstractPendingTransactionsTestBase { @@ -74,10 +72,10 @@ public abstract class AbstractPendingTransactionsTestBase { protected static final int MAX_TRANSACTIONS = 5; protected static final int MAX_TRANSACTIONS_LARGE_POOL = 15; private static final float LIMITED_TRANSACTIONS_BY_SENDER_PERCENTAGE = 0.8f; - protected static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - protected static final KeyPair KEYS1 = SIGNATURE_ALGORITHM.get().generateKeyPair(); - protected static final KeyPair KEYS2 = SIGNATURE_ALGORITHM.get().generateKeyPair(); + 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 String ADDED_COUNTER = "transactions_added_total"; protected static final String REMOVED_COUNTER = "transactions_removed_total"; protected static final String REMOTE = "remote"; @@ -172,7 +170,7 @@ public void shouldGetTransactionByHash() { @Test public void shouldDropOldestTransactionWhenLimitExceeded() { final Transaction oldestTransaction = - transactionWithNonceSenderAndGasPrice(0, SIGNATURE_ALGORITHM.get().generateKeyPair(), 10L); + transactionWithNonceSenderAndGasPrice(0, SIGNATURE_ALGORITHM.generateKeyPair(), 10L); final Account oldestSender = mock(Account.class); when(oldestSender.getNonce()).thenReturn(0L); senderLimitedTransactions.addTransaction( @@ -182,8 +180,7 @@ public void shouldDropOldestTransactionWhenLimitExceeded() { when(sender.getNonce()).thenReturn((long) i); senderLimitedTransactions.addTransaction( createRemotePendingTransaction( - transactionWithNonceSenderAndGasPrice( - i, SIGNATURE_ALGORITHM.get().generateKeyPair(), 10L)), + transactionWithNonceSenderAndGasPrice(i, SIGNATURE_ALGORITHM.generateKeyPair(), 10L)), Optional.of(sender)); } assertThat(senderLimitedTransactions.size()).isEqualTo(MAX_TRANSACTIONS); @@ -887,7 +884,7 @@ public void shouldPrioritizeGasPriceThenTimeAddedToPool() { final Account randomSender = mock(Account.class); final Transaction lowPriceTx = transactionWithNonceSenderAndGasPrice( - 0, SIGNATURE_ALGORITHM.get().generateKeyPair(), 10); + 0, SIGNATURE_ALGORITHM.generateKeyPair(), 10); transactions.addTransaction( createRemotePendingTransaction(lowPriceTx), Optional.of(randomSender)); return lowPriceTx; diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/NodeRecordManager.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/NodeRecordManager.java index 92fa6db64e0..73f4d38f96c 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/NodeRecordManager.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/NodeRecordManager.java @@ -33,7 +33,6 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; -import com.google.common.base.Suppliers; import com.google.common.net.InetAddresses; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt64; @@ -60,8 +59,8 @@ */ public class NodeRecordManager { private static final Logger LOG = LoggerFactory.getLogger(NodeRecordManager.class); - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private static final String FORK_ID_ENR_FIELD = "eth"; @@ -345,14 +344,12 @@ private NodeRecord createAndPersistNodeRecord( final UInt64 sequence = existingRecord.map(NodeRecord::getSeq).orElse(UInt64.ZERO).add(1); - final SignatureAlgorithm signatureAlgorithm = SIGNATURE_ALGORITHM.get(); - final List fields = new ArrayList<>(); fields.add(new EnrField(EnrField.ID, IdentitySchema.V4)); fields.add( new EnrField( - signatureAlgorithm.getCurveName(), - signatureAlgorithm.compressPublicKey(signatureAlgorithm.createPublicKey(nodeId)))); + SIGNATURE_ALGORITHM.getCurveName(), + SIGNATURE_ALGORITHM.compressPublicKey(SIGNATURE_ALGORITHM.createPublicKey(nodeId)))); fields.add(new EnrField(FORK_ID_ENR_FIELD, Collections.singletonList(forkId))); if (primaryEndpoint.isIpv4()) { diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java index eec3dcca8d2..c3e84173646 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java @@ -23,8 +23,6 @@ import java.nio.ByteBuffer; import java.security.SecureRandom; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.MutableBytes; import org.bouncycastle.crypto.InvalidCipherTextException; @@ -34,8 +32,8 @@ final class EncryptedMessage { private static final int IV_SIZE = 16; private static final SecureRandom RANDOM = SecureRandomProvider.createSecureRandom(); - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private EncryptedMessage() { // Utility Class @@ -54,8 +52,7 @@ public static Bytes decryptMsg(final Bytes msgBytes, final NodeKey nodeKey) // Extract the ephemeral public key, stripping off the first byte (0x04), which designates it's // an uncompressed key. - final SECPPublicKey ephPubKey = - SIGNATURE_ALGORITHM.get().createPublicKey(msgBytes.slice(1, 64)); + final SECPPublicKey ephPubKey = SIGNATURE_ALGORITHM.createPublicKey(msgBytes.slice(1, 64)); // Strip off the IV to use. final Bytes iv = msgBytes.slice(65, IV_SIZE); @@ -79,8 +76,7 @@ public static Bytes decryptMsg(final Bytes msgBytes, final NodeKey nodeKey) */ public static Bytes decryptMsgEIP8(final Bytes msgBytes, final NodeKey nodeKey) throws InvalidCipherTextException { - final SECPPublicKey ephPubKey = - SIGNATURE_ALGORITHM.get().createPublicKey(msgBytes.slice(3, 64)); + final SECPPublicKey ephPubKey = SIGNATURE_ALGORITHM.createPublicKey(msgBytes.slice(3, 64)); // Strip off the IV to use. final Bytes iv = msgBytes.slice(3 + 64, IV_SIZE); diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java index eedf394e290..45c9c6d1453 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java @@ -24,8 +24,6 @@ import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; import org.hyperledger.besu.cryptoservices.NodeKey; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.bytes.MutableBytes; @@ -68,8 +66,8 @@ public final class InitiatorHandshakeMessageV1 implements InitiatorHandshakeMess private final Bytes32 nonce; private final boolean token; - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); InitiatorHandshakeMessageV1( final SECPPublicKey pubKey, @@ -96,7 +94,7 @@ public static InitiatorHandshakeMessageV1 create( // XOR of the static shared secret and the generated nonce. final SECPSignature signature = - SIGNATURE_ALGORITHM.get().sign(staticSharedSecret.xor(nonce), ephKeyPair); + SIGNATURE_ALGORITHM.sign(staticSharedSecret.xor(nonce), ephKeyPair); return new InitiatorHandshakeMessageV1( ourPubKey, signature, ephKeyPair.getPublicKey(), ephPubKeyHash, nonce, token); } @@ -113,21 +111,16 @@ public static InitiatorHandshakeMessageV1 decode(final Bytes bytes, final NodeKe int offset = 0; final SECPSignature signature = - SIGNATURE_ALGORITHM - .get() - .decodeSignature(bytes.slice(offset, ECIESHandshaker.SIGNATURE_LENGTH)); + SIGNATURE_ALGORITHM.decodeSignature(bytes.slice(offset, ECIESHandshaker.SIGNATURE_LENGTH)); final Bytes32 ephPubKeyHash = Bytes32.wrap( bytes.slice( offset += ECIESHandshaker.SIGNATURE_LENGTH, ECIESHandshaker.HASH_EPH_PUBKEY_LENGTH), 0); final SECPPublicKey pubKey = - SIGNATURE_ALGORITHM - .get() - .createPublicKey( - bytes.slice( - offset += ECIESHandshaker.HASH_EPH_PUBKEY_LENGTH, - ECIESHandshaker.PUBKEY_LENGTH)); + SIGNATURE_ALGORITHM.createPublicKey( + bytes.slice( + offset += ECIESHandshaker.HASH_EPH_PUBKEY_LENGTH, ECIESHandshaker.PUBKEY_LENGTH)); final Bytes32 nonce = Bytes32.wrap( bytes.slice(offset += ECIESHandshaker.PUBKEY_LENGTH, ECIESHandshaker.NONCE_LENGTH), 0); @@ -136,7 +129,6 @@ public static InitiatorHandshakeMessageV1 decode(final Bytes bytes, final NodeKe final Bytes32 staticSharedSecret = nodeKey.calculateECDHKeyAgreement(pubKey); final SECPPublicKey ephPubKey = SIGNATURE_ALGORITHM - .get() .recoverPublicKeyFromSignature(staticSharedSecret.xor(nonce), signature) .orElseThrow(() -> new RuntimeException("Could not recover public key from signature")); diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4.java index 88791244ef3..3b9863e005a 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4.java @@ -25,16 +25,14 @@ import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.rlp.RLPInput; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; public final class InitiatorHandshakeMessageV4 implements InitiatorHandshakeMessage { public static final int VERSION = 4; - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private final SECPPublicKey pubKey; private final SECPSignature signature; @@ -49,7 +47,7 @@ public static InitiatorHandshakeMessageV4 create( final Bytes32 nonce) { return new InitiatorHandshakeMessageV4( ourPubKey, - SIGNATURE_ALGORITHM.get().sign(staticSharedSecret.xor(nonce), ephKeyPair), + SIGNATURE_ALGORITHM.sign(staticSharedSecret.xor(nonce), ephKeyPair), ephKeyPair.getPublicKey(), nonce); } @@ -64,13 +62,12 @@ public static InitiatorHandshakeMessageV4 create( public static InitiatorHandshakeMessageV4 decode(final Bytes bytes, final NodeKey nodeKey) { final RLPInput input = new BytesValueRLPInput(bytes, true); input.enterList(); - final SECPSignature signature = SIGNATURE_ALGORITHM.get().decodeSignature(input.readBytes()); - final SECPPublicKey pubKey = SIGNATURE_ALGORITHM.get().createPublicKey(input.readBytes()); + final SECPSignature signature = SIGNATURE_ALGORITHM.decodeSignature(input.readBytes()); + final SECPPublicKey pubKey = SIGNATURE_ALGORITHM.createPublicKey(input.readBytes()); final Bytes32 nonce = input.readBytes32(); final Bytes32 staticSharedSecret = nodeKey.calculateECDHKeyAgreement(pubKey); final SECPPublicKey ephPubKey = SIGNATURE_ALGORITHM - .get() .recoverPublicKeyFromSignature(staticSharedSecret.xor(nonce), signature) .orElseThrow(() -> new RuntimeException("Could not recover public key from signature")); diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/PeerDiscoveryAgentV4Test.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/PeerDiscoveryAgentV4Test.java index 7276f917318..c5d71583f4e 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/PeerDiscoveryAgentV4Test.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/PeerDiscoveryAgentV4Test.java @@ -55,8 +55,6 @@ import java.util.Optional; import java.util.stream.Collectors; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt64; @@ -67,8 +65,8 @@ public class PeerDiscoveryAgentV4Test { private static final int BROADCAST_TCP_PORT = 30303; - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private PeerDiscoveryTestHelper helper; private PacketPackage packetPackage; @@ -98,14 +96,10 @@ public void createAgentWithInvalidBootnodes() { @Test public void testNodeRecordCreated() { final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - Bytes32.fromHexString( - "0xb71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey( + Bytes32.fromHexString( + "0xb71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"))); final MockPeerDiscoveryAgent agent = helper.startDiscoveryAgent( helper @@ -132,14 +126,10 @@ public void testNodeRecordCreated() { @Test public void testNodeRecordCreatedUpdatesDiscoveryPeer() { final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - Bytes32.fromHexString( - "0xb71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey( + Bytes32.fromHexString( + "0xb71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"))); final MockPeerDiscoveryAgent agent = helper.startDiscoveryAgent( helper @@ -157,14 +147,10 @@ public void testNodeRecordCreatedUpdatesDiscoveryPeer() { @Test public void testNodeRecordNotUpdatedIfNoPeerDiscovery() { final KeyPair keyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - Bytes32.fromHexString( - "0xb71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey( + Bytes32.fromHexString( + "0xb71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"))); final MockPeerDiscoveryAgent agent = helper.startDiscoveryAgent( helper diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/internal/PeerTableTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/internal/PeerTableTest.java index aee877cb3cb..f0ececcd8ab 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/internal/PeerTableTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/internal/PeerTableTest.java @@ -29,15 +29,13 @@ import java.util.List; import java.util.Optional; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Test; public class PeerTableTest { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); private final PeerDiscoveryTestHelper helper = new PeerDiscoveryTestHelper(); @Test @@ -87,8 +85,7 @@ public void peerExists() { @Test public void peerExists_withDifferentIp() { final PeerTable table = new PeerTable(Peer.randomId()); - final Bytes peerId = - SIGNATURE_ALGORITHM.get().generateKeyPair().getPublicKey().getEncodedBytes(); + final Bytes peerId = SIGNATURE_ALGORITHM.generateKeyPair().getPublicKey().getEncodedBytes(); final DiscoveryPeerV4 peer = DiscoveryPeerV4.fromIdAndEndpoint(peerId, new Endpoint("1.1.1.1", 30303, Optional.empty())); @@ -107,8 +104,7 @@ public void peerExists_withDifferentIp() { @Test public void peerExists_withDifferentUdpPort() { final PeerTable table = new PeerTable(Peer.randomId()); - final Bytes peerId = - SIGNATURE_ALGORITHM.get().generateKeyPair().getPublicKey().getEncodedBytes(); + final Bytes peerId = SIGNATURE_ALGORITHM.generateKeyPair().getPublicKey().getEncodedBytes(); final DiscoveryPeerV4 peer = DiscoveryPeerV4.fromIdAndEndpoint(peerId, new Endpoint("1.1.1.1", 30303, Optional.empty())); @@ -127,8 +123,7 @@ public void peerExists_withDifferentUdpPort() { @Test public void peerExists_withDifferentIdAndUdpPort() { final PeerTable table = new PeerTable(Peer.randomId()); - final Bytes peerId = - SIGNATURE_ALGORITHM.get().generateKeyPair().getPublicKey().getEncodedBytes(); + final Bytes peerId = SIGNATURE_ALGORITHM.generateKeyPair().getPublicKey().getEncodedBytes(); final DiscoveryPeerV4 peer = DiscoveryPeerV4.fromIdAndEndpoint(peerId, new Endpoint("1.1.1.1", 30303, Optional.empty())); diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java index 25c10954bef..dec93e1ccca 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java @@ -24,8 +24,6 @@ import java.util.Optional; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import org.apache.tuweni.bytes.Bytes; @@ -38,42 +36,26 @@ public class ECIESHandshakeTest { // Input data. private static class Input { - static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = + SignatureAlgorithmFactory.getInstance(); // Keys. private static final KeyPair initiatorKeyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - h32("0x5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey( + h32("0x5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"))); private static final KeyPair initiatorEphKeyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - h32("0x19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey( + h32("0x19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"))); private static final KeyPair responderKeyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - h32("0xc45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey( + h32("0xc45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"))); private static final KeyPair responderEphKeyPair = - SIGNATURE_ALGORITHM - .get() - .createKeyPair( - SIGNATURE_ALGORITHM - .get() - .createPrivateKey( - h32("0xd25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"))); + SIGNATURE_ALGORITHM.createKeyPair( + SIGNATURE_ALGORITHM.createPrivateKey( + h32("0xd25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"))); // Nonces. private static final Bytes32 initiatorNonce = From ca2d95321232f81cdf99e7221e692588473df7b3 Mon Sep 17 00:00:00 2001 From: Simon Dudley Date: Mon, 9 Mar 2026 12:11:40 +1000 Subject: [PATCH 05/77] Benchmarking docs (#9998) * Update benchmarking docs with -Pcases Signed-off-by: Simon Dudley * CHANGELOG for https://github.com/hyperledger/besu/pull/9982 Signed-off-by: Simon Dudley --------- Signed-off-by: Simon Dudley --- BENCHMARKING.md | 9 +++++---- CHANGELOG.md | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/BENCHMARKING.md b/BENCHMARKING.md index a666cf2094e..d23fa75a13c 100644 --- a/BENCHMARKING.md +++ b/BENCHMARKING.md @@ -32,12 +32,12 @@ Gradle won't rerun a task by default with no changes, so to run subsequent times --- -## 🎯 Filter Benchmarks by Name +## 🎯 Filter Benchmarks by Name and Case -To run a specific benchmark class, use the `-Pincludes` and/or `-Pexcludes` project properties: +To run a specific benchmark class, use the `-Pincludes` and/or `-Pexcludes` project properties. To filter by case name, use `-Pcases`: ```bash -./gradlew :ethereum:core:jmh -Pincludes=SomeBenchmark -Pexcludes=TransientStorage,BlockHash +./gradlew :ethereum:core:jmh -Pincludes=Mod -Pexcludes=Mul,Add,SMod -Pcases=MOD_256_128,MOD_256_192 --rerun-tasks ``` This uses a regex pattern so other kinds of regexes can be used. @@ -62,7 +62,8 @@ To profile benchmarks with [Async Profiler](https://github.com/jvm-profiling-too ./gradlew :ethereum:core:jmh \ -Pincludes=SomeBenchmark \ -PasyncProfiler=/path/to/libasyncProfiler.so \ - -PasyncProfilerOptions="output=flamegraph" + -PasyncProfilerOptions="output=flamegraph" \ + --rerun-tasks ``` This will generate two html profiling files flame-cpu-forward.html and flame-cpu-reverse.html after the benchmark run. diff --git a/CHANGELOG.md b/CHANGELOG.md index fe30521b73d..a9ae0d767c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Add `--max-blobs-per-transaction` CLI option to configure the maximum number of blobs per transaction [#9912](https://github.com/hyperledger/besu/pull/9912) - Add blockTimestamp to transaction RPC results [#9887](https://github.com/hyperledger/besu/pull/9887) - Plugin API: Allow the registration of multiple PluginTransactionPoolValidatorFactory [#9964](https://github.com/hyperledger/besu/pull/9964) +- Add `-Pcases` case name filtering to JMH benchmark suite [#9982](https://github.com/hyperledger/besu/pull/9982) ## 26.2.0 From ef201377ec3c4b8214de7cd65c6c1469343d66c8 Mon Sep 17 00:00:00 2001 From: Stefan Pingel <16143240+pinges@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:49:01 +1000 Subject: [PATCH 06/77] Disconnect reason (#9901) * return the correct disconnect reason Signed-off-by: stefan.pingel@consensys.net Signed-off-by: Stefan Pingel <16143240+pinges@users.noreply.github.com> --- .../org/hyperledger/besu/RunnerBuilder.java | 4 +- .../besu/ethereum/eth/manager/EthPeers.java | 13 +++-- .../ethereum/eth/transactions/TestNode.java | 7 +-- .../ethereum/p2p/testing/MockNetwork.java | 10 ++-- .../p2p/network/DefaultP2PNetwork.java | 10 +--- .../ethereum/p2p/network/NetworkRunner.java | 24 ++++----- .../ethereum/p2p/network/NoopP2PNetwork.java | 10 ++-- .../besu/ethereum/p2p/network/P2PNetwork.java | 7 +-- .../besu/ethereum/p2p/rlpx/RlpxAgent.java | 25 ++++++---- .../rlpx/wire/PeerConnectionGatekeeper.java | 50 +++++++++++++++++++ .../p2p/rlpx/wire/ShouldConnectCallback.java | 23 --------- .../p2p/network/NetworkRunnerTest.java | 8 ++- .../ethereum/p2p/network/P2PNetworkTest.java | 41 +++++++++++---- .../p2p/network/P2PPlainNetworkTest.java | 44 ++++++++++------ .../besu/ethereum/p2p/rlpx/RlpxAgentTest.java | 2 +- 15 files changed, 171 insertions(+), 107 deletions(-) create mode 100644 ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/PeerConnectionGatekeeper.java delete mode 100644 ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/ShouldConnectCallback.java diff --git a/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java b/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java index f0d5e35c8be..d0beb616401 100644 --- a/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java +++ b/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java @@ -816,10 +816,10 @@ public Runner build() { .subProtocols(subProtocols) .network(p2pEnabled ? activeNetwork : inactiveNetwork) .metricsSystem(metricsSystem) - .ethPeersShouldConnect(ethPeers::shouldTryToConnect) + .peerConnectionGatekeeper(ethPeers::gatePeerConnection) .build(); - ethPeers.setRlpxAgent(networkRunner.getRlpxAgent()); + networkRunner.getRlpxAgent().ifPresent(ethPeers::setRlpxAgent); final P2PNetwork network = networkRunner.getNetwork(); // ForkId in Ethereum Node Record needs updating when we transition to a new diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeers.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeers.java index 00ad5609b37..518e3099bb3 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeers.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeers.java @@ -34,6 +34,7 @@ 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; @@ -423,7 +424,7 @@ public Stream streamAllConnections() { .filter(c -> !c.isDisconnected()); } - public boolean shouldTryToConnect(final Peer peer, final boolean inbound) { + public Optional gatePeerConnection(final Peer peer, final boolean inbound) { if (peer.getForkId().isPresent()) { final ForkId forkId = peer.getForkId().get(); @@ -433,7 +434,7 @@ public boolean shouldTryToConnect(final Peer peer, final boolean inbound) { .addArgument(peer::getId) .log(); - return false; + return Optional.of(DisconnectReason.USELESS_PEER_BY_CHAIN_COMPARATOR); } } @@ -443,10 +444,14 @@ public boolean shouldTryToConnect(final Peer peer, final boolean inbound) { .setMessage("not connecting to peer {} - already connected") .addArgument(peer.getLoggableId()) .log(); - return false; + return Optional.of(DisconnectReason.ALREADY_CONNECTED); } - return peerCount() < getMaxPeers() || needMoreSnapServers() || canExceedPeerLimits(id); + 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) { diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TestNode.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TestNode.java index ebbe00492db..b59b75811ba 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TestNode.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TestNode.java @@ -69,7 +69,6 @@ import org.hyperledger.besu.ethereum.p2p.peers.DefaultPeer; import org.hyperledger.besu.ethereum.p2p.peers.Peer; import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions; -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.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; @@ -221,7 +220,7 @@ public TestNode( NetworkRunner.builder() .subProtocols(EthProtocol.get()) .protocolManagers(singletonList(ethProtocolManager)) - .ethPeersShouldConnect((p, d) -> true) + .peerConnectionGatekeeper((p, d) -> Optional.empty()) .network( capabilities -> createP2PNetwork( @@ -234,9 +233,7 @@ public TestNode( .metricsSystem(new NoOpMetricsSystem()) .build(); network = networkRunner.getNetwork(); - final RlpxAgent rlpxAgent = network.getRlpxAgent(); - rlpxAgent.subscribeConnectRequest((p, d) -> true); - ethPeers.setRlpxAgent(rlpxAgent); + network.getRlpxAgent().ifPresent(ethPeers::setRlpxAgent); network.subscribeDisconnect( (connection, reason, initiatedByPeer) -> disconnections.put(connection, reason)); diff --git a/ethereum/mock-p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/testing/MockNetwork.java b/ethereum/mock-p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/testing/MockNetwork.java index 50d7791155a..50646d254d2 100644 --- a/ethereum/mock-p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/testing/MockNetwork.java +++ b/ethereum/mock-p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/testing/MockNetwork.java @@ -21,13 +21,13 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.ConnectCallback; import org.hyperledger.besu.ethereum.p2p.rlpx.DisconnectCallback; import org.hyperledger.besu.ethereum.p2p.rlpx.MessageCallback; +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.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.DefaultMessage; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Message; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.PeerInfo; -import org.hyperledger.besu.ethereum.p2p.rlpx.wire.ShouldConnectCallback; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import org.hyperledger.besu.util.Subscribers; @@ -174,9 +174,6 @@ public void subscribeConnect(final ConnectCallback callback) { connectCallbacks.subscribe(callback); } - @Override - public void subscribeConnectRequest(final ShouldConnectCallback callback) {} - @Override public void subscribeDisconnect(final DisconnectCallback callback) { disconnectCallbacks.subscribe(callback); @@ -236,6 +233,11 @@ public Optional getLocalEnode() { @Override public void updateNodeRecord() {} + + @Override + public Optional getRlpxAgent() { + return Optional.empty(); + } } /** diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java index 8eaefa1364d..06778d3cf4d 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java @@ -43,7 +43,6 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerLookup; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; -import org.hyperledger.besu.ethereum.p2p.rlpx.wire.ShouldConnectCallback; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import org.hyperledger.besu.nat.NatMethod; import org.hyperledger.besu.nat.NatService; @@ -337,8 +336,8 @@ public void awaitStop() { } @Override - public RlpxAgent getRlpxAgent() { - return rlpxAgent; + public Optional getRlpxAgent() { + return Optional.of(rlpxAgent); } @Override @@ -440,11 +439,6 @@ public void subscribeConnect(final ConnectCallback callback) { rlpxAgent.subscribeConnect(callback); } - @Override - public void subscribeConnectRequest(final ShouldConnectCallback callback) { - rlpxAgent.subscribeConnectRequest(callback); - } - @Override public void subscribeDisconnect(final DisconnectCallback callback) { rlpxAgent.subscribeDisconnect(callback); diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/NetworkRunner.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/NetworkRunner.java index e221f426d94..e8cd0e81e68 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/NetworkRunner.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/NetworkRunner.java @@ -30,6 +30,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; @@ -50,18 +51,15 @@ public class NetworkRunner implements AutoCloseable { private final List protocolManagers; private final LabelledMetric inboundMessageCounter; private final LabelledMetric inboundBytesCounter; - private final BiFunction ethPeersShouldConnect; private NetworkRunner( final P2PNetwork network, final Map subProtocols, final List protocolManagers, - final MetricsSystem metricsSystem, - final BiFunction ethPeersShouldConnect) { + final MetricsSystem metricsSystem) { this.network = network; this.protocolManagers = protocolManagers; this.subProtocols = subProtocols; - this.ethPeersShouldConnect = ethPeersShouldConnect; this.inboundMessageCounter = metricsSystem.createLabelledCounter( BesuMetricCategory.NETWORK, @@ -178,8 +176,6 @@ private void setupHandlers() { protocolManager.handleNewConnection(connection); }); - network.subscribeConnectRequest(ethPeersShouldConnect::apply); - network.subscribeDisconnect( (connection, disconnectReason, initiatedByPeer) -> { if (Collections.disjoint( @@ -196,7 +192,7 @@ public void close() { stop(); } - public RlpxAgent getRlpxAgent() { + public Optional getRlpxAgent() { return network.getRlpxAgent(); } @@ -205,7 +201,8 @@ public static class Builder { List protocolManagers = new ArrayList<>(); List subProtocols = new ArrayList<>(); MetricsSystem metricsSystem; - private BiFunction ethPeersShouldConnect; + private BiFunction> peerConnectionGatekeeper = + (peer, incoming) -> Optional.empty(); public NetworkRunner build() { final Map subProtocolMap = new HashMap<>(); @@ -223,8 +220,10 @@ public NetworkRunner build() { } } final P2PNetwork network = networkProvider.build(caps); - return new NetworkRunner( - network, subProtocolMap, protocolManagers, metricsSystem, ethPeersShouldConnect); + network + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper(peerConnectionGatekeeper::apply)); + return new NetworkRunner(network, subProtocolMap, protocolManagers, metricsSystem); } public Builder protocolManagers(final List protocolManagers) { @@ -252,8 +251,9 @@ public Builder metricsSystem(final MetricsSystem metricsSystem) { return this; } - public Builder ethPeersShouldConnect(final BiFunction shouldConnect) { - this.ethPeersShouldConnect = shouldConnect; + public Builder peerConnectionGatekeeper( + final BiFunction> gatekeeper) { + this.peerConnectionGatekeeper = gatekeeper; return this; } } diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/NoopP2PNetwork.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/NoopP2PNetwork.java index 8941fb346ea..4ceb5ef1749 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/NoopP2PNetwork.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/NoopP2PNetwork.java @@ -21,9 +21,9 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.ConnectCallback; import org.hyperledger.besu.ethereum.p2p.rlpx.DisconnectCallback; import org.hyperledger.besu.ethereum.p2p.rlpx.MessageCallback; +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.Capability; -import org.hyperledger.besu.ethereum.p2p.rlpx.wire.ShouldConnectCallback; import java.io.IOException; import java.util.Collection; @@ -55,9 +55,6 @@ public void subscribe(final Capability capability, final MessageCallback callbac @Override public void subscribeConnect(final ConnectCallback callback) {} - @Override - public void subscribeConnectRequest(final ShouldConnectCallback callback) {} - @Override public void subscribeDisconnect(final DisconnectCallback callback) {} @@ -115,4 +112,9 @@ public void close() throws IOException {} @Override public void start() {} + + @Override + public Optional getRlpxAgent() { + return Optional.empty(); + } } diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/P2PNetwork.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/P2PNetwork.java index c64c7f754a2..5ffaa6eaeda 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/P2PNetwork.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/P2PNetwork.java @@ -24,7 +24,6 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Message; -import org.hyperledger.besu.ethereum.p2p.rlpx.wire.ShouldConnectCallback; import java.io.Closeable; import java.util.Collection; @@ -87,8 +86,6 @@ default int getPeerCount() { */ void subscribeConnect(final ConnectCallback callback); - void subscribeConnectRequest(final ShouldConnectCallback callback); - /** * Subscribe a {@link Consumer} to all incoming new Peer disconnect events. * @@ -173,7 +170,5 @@ default Optional getLocalEnr() { return Optional.empty(); } - default RlpxAgent getRlpxAgent() { - return null; - } + Optional getRlpxAgent(); } diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/RlpxAgent.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/RlpxAgent.java index 76a966f5c0e..b2a99128952 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/RlpxAgent.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/RlpxAgent.java @@ -31,7 +31,7 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerRlpxPermissions; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.netty.NettyConnectionInitializer; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; -import org.hyperledger.besu.ethereum.p2p.rlpx.wire.ShouldConnectCallback; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.PeerConnectionGatekeeper; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import org.hyperledger.besu.plugin.data.EnodeURL; import org.hyperledger.besu.plugin.services.MetricsSystem; @@ -39,7 +39,6 @@ import java.net.InetSocketAddress; import java.time.Duration; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -65,7 +64,7 @@ public class RlpxAgent { private final PeerConnectionEvents connectionEvents; private final ConnectionInitializer connectionInitializer; private final Subscribers connectSubscribers = Subscribers.create(); - private final List connectRequestSubscribers = new ArrayList<>(); + private volatile PeerConnectionGatekeeper peerConnectionGatekeeper; private final PeerRlpxPermissions peerPermissions; private final PeerPrivileges peerPrivileges; private final AtomicBoolean started = new AtomicBoolean(false); @@ -233,7 +232,9 @@ public CompletableFuture connect(final Peer peer) { } final CompletableFuture peerConnectionCompletableFuture; - if (checkWhetherToConnect(peer, false)) { + final Optional maybeDisconnectReason = gatePeerConnection(peer, false); + + if (maybeDisconnectReason.isEmpty()) { try { synchronized (this) { peerConnectionCompletableFuture = @@ -266,9 +267,10 @@ private CompletableFuture createPeerConnectionCompletableFuture( return peerConnectionCompletableFuture; } - private boolean checkWhetherToConnect(final Peer peer, final boolean incoming) { - return connectRequestSubscribers.stream() - .anyMatch(callback -> callback.shouldConnect(peer, incoming)); + private Optional gatePeerConnection(final Peer peer, final boolean incoming) { + return peerConnectionGatekeeper != null + ? peerConnectionGatekeeper.checkPeerConnection(peer, incoming) + : Optional.empty(); } private void setupListeners() { @@ -344,10 +346,11 @@ private void handleIncomingConnection(final PeerConnection peerConnection) { return; } - if (checkWhetherToConnect(peer, true)) { + final Optional maybeDisconnectReason = gatePeerConnection(peer, true); + if (maybeDisconnectReason.isEmpty()) { dispatchConnect(peerConnection); } else { - peerConnection.disconnect(DisconnectReason.UNKNOWN); + peerConnection.disconnect(maybeDisconnectReason.get()); } } @@ -359,8 +362,8 @@ public void subscribeConnect(final ConnectCallback callback) { connectSubscribers.subscribe(callback); } - public void subscribeConnectRequest(final ShouldConnectCallback callback) { - connectRequestSubscribers.add(callback); + public void setPeerConnectionGatekeeper(final PeerConnectionGatekeeper gatekeeper) { + this.peerConnectionGatekeeper = gatekeeper; } public void subscribeDisconnect(final DisconnectCallback callback) { diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/PeerConnectionGatekeeper.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/PeerConnectionGatekeeper.java new file mode 100644 index 00000000000..ae864436c2f --- /dev/null +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/PeerConnectionGatekeeper.java @@ -0,0 +1,50 @@ +/* + * 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.p2p.rlpx.wire; + +import org.hyperledger.besu.ethereum.p2p.peers.Peer; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage; + +import java.util.Optional; + +/** + * Decides whether a peer connection attempt should be allowed or rejected. + * + *

The return value uses the following convention: + * + *

    + *
  • {@code Optional.empty()} — allow the connection to proceed. + *
  • {@code Optional.of(reason)} — reject the connection. For inbound connection attempts + * ({@code incoming == true}), a disconnect message will be sent to the remote peer with the + * provided {@link DisconnectMessage.DisconnectReason} before the session is closed. For + * outbound connection attempts ({@code incoming == false}), the connection will not be + * established and no disconnect message is sent because no RLPx session exists yet. + *
+ */ +@FunctionalInterface +public interface PeerConnectionGatekeeper { + + /** + * Check whether the connection with the given peer should be allowed. + * + * @param peer the remote peer for which a connection is being considered + * @param incoming {@code true} if this is an inbound connection attempt initiated by the remote + * peer, {@code false} if this node is attempting to establish an outbound connection + * @return {@code Optional.empty()} to allow the connection, or an {@code Optional} containing a + * {@link DisconnectMessage.DisconnectReason} to reject it + */ + Optional checkPeerConnection( + final Peer peer, final boolean incoming); +} diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/ShouldConnectCallback.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/ShouldConnectCallback.java deleted file mode 100644 index f6473528e6a..00000000000 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/ShouldConnectCallback.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.p2p.rlpx.wire; - -import org.hyperledger.besu.ethereum.p2p.peers.Peer; - -@FunctionalInterface -public interface ShouldConnectCallback { - - boolean shouldConnect(final Peer peer, final boolean incoming); -} diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/NetworkRunnerTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/NetworkRunnerTest.java index 27237e6390e..3277ed9224c 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/NetworkRunnerTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/NetworkRunnerTest.java @@ -32,11 +32,13 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Message; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage; import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.metrics.Counter; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import java.util.List; +import java.util.Optional; import java.util.function.BiFunction; import org.junit.jupiter.api.BeforeEach; @@ -84,8 +86,10 @@ public void setUp() { // Setup network mocks to allow start() to complete when(network.isListening()).thenReturn(true); + when(network.getRlpxAgent()).thenReturn(Optional.empty()); - BiFunction ethPeersShouldConnect = (peer, incoming) -> true; + BiFunction> + peerConnectionGatekeeper = (peer, incoming) -> Optional.empty(); NetworkRunner.NetworkBuilder networkBuilder = caps -> network; @@ -95,7 +99,7 @@ public void setUp() { .subProtocols(subProtocol) .network(networkBuilder) .metricsSystem(metricsSystem) - .ethPeersShouldConnect(ethPeersShouldConnect) + .peerConnectionGatekeeper(peerConnectionGatekeeper) .build(); } diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/P2PNetworkTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/P2PNetworkTest.java index dd44ac3f9af..9c6de517830 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/P2PNetworkTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/P2PNetworkTest.java @@ -43,6 +43,7 @@ import java.net.InetAddress; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -76,8 +77,12 @@ public void handshaking() throws Exception { final NodeKey nodeKey = NodeKeyUtils.generate(); try (final P2PNetwork listener = createP2PNetwork(nodeKey); final P2PNetwork connector = createP2PNetwork(NodeKeyUtils.generate())) { - listener.getRlpxAgent().subscribeConnectRequest((p, d) -> true); - connector.getRlpxAgent().subscribeConnectRequest((p, d) -> true); + listener + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); + connector + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); listener.start(); connector.start(); @@ -100,8 +105,12 @@ public void preventMultipleConnections() throws Exception { final NodeKey listenNodeKey = NodeKeyUtils.generate(); try (final P2PNetwork listener = createP2PNetwork(listenNodeKey); final P2PNetwork connector = createP2PNetwork(NodeKeyUtils.generate())) { - listener.getRlpxAgent().subscribeConnectRequest((p, d) -> true); - connector.getRlpxAgent().subscribeConnectRequest((p, d) -> true); + listener + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); + connector + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); listener.start(); connector.start(); @@ -137,8 +146,12 @@ public void rejectPeerWithNoSharedCaps() throws Exception { Capability.create(subprotocol2.getName(), EthProtocolHelper.LATEST.getVersion()); try (final P2PNetwork listener = createP2PNetwork(listenerNodeKey, List.of(cap1)); final P2PNetwork connector = createP2PNetwork(connectorNodeKey, List.of(cap2))) { - listener.getRlpxAgent().subscribeConnectRequest((p, d) -> true); - connector.getRlpxAgent().subscribeConnectRequest((p, d) -> true); + listener + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); + connector + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); listener.start(); connector.start(); @@ -158,8 +171,12 @@ public void rejectIncomingConnectionFromDenylistedPeer() throws Exception { try (final P2PNetwork localNetwork = createP2PNetwork(localDenylist); final P2PNetwork remoteNetwork = createP2PNetwork()) { - localNetwork.getRlpxAgent().subscribeConnectRequest((p, d) -> true); - remoteNetwork.getRlpxAgent().subscribeConnectRequest((p, d) -> true); + localNetwork + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); + remoteNetwork + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); localNetwork.start(); remoteNetwork.start(); @@ -207,8 +224,12 @@ public void rejectIncomingConnectionFromDisallowedPeer() throws Exception { try (final P2PNetwork localNetwork = createP2PNetwork(peerPermissions); final P2PNetwork remoteNetwork = createP2PNetwork()) { - localNetwork.getRlpxAgent().subscribeConnectRequest((p, d) -> true); - remoteNetwork.getRlpxAgent().subscribeConnectRequest((p, d) -> true); + localNetwork + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); + remoteNetwork + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper((p, d) -> Optional.empty())); localNetwork.start(); remoteNetwork.start(); diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/P2PPlainNetworkTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/P2PPlainNetworkTest.java index 00a28823476..e1837ad22fa 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/P2PPlainNetworkTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/P2PPlainNetworkTest.java @@ -37,13 +37,14 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MockSubProtocol; -import org.hyperledger.besu.ethereum.p2p.rlpx.wire.ShouldConnectCallback; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.PeerConnectionGatekeeper; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import org.hyperledger.besu.plugin.data.EnodeURL; import java.net.InetAddress; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -78,8 +79,8 @@ public void handshaking() throws Exception { final NodeKey nodeKey = NodeKeyUtils.generate(); try (final P2PNetwork listener = createP2PNetwork(nodeKey); final P2PNetwork connector = createP2PNetwork()) { - listener.getRlpxAgent().subscribeConnectRequest(testCallback); - connector.getRlpxAgent().subscribeConnectRequest(testCallback); + listener.getRlpxAgent().ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); + connector.getRlpxAgent().ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); listener.start(); connector.start(); @@ -102,8 +103,8 @@ public void preventMultipleConnections() throws Exception { final NodeKey listenNodeKey = NodeKeyUtils.generate(); try (final P2PNetwork listener = createP2PNetwork(listenNodeKey); final P2PNetwork connector = createP2PNetwork()) { - listener.getRlpxAgent().subscribeConnectRequest(testCallback); - connector.getRlpxAgent().subscribeConnectRequest(testCallback); + listener.getRlpxAgent().ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); + connector.getRlpxAgent().ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); listener.start(); connector.start(); @@ -146,9 +147,14 @@ public void limitMaxPeers() throws Exception { try (final P2PNetwork listener = createP2PNetwork(listenerConfig, nodeKey); final P2PNetwork connector1 = createP2PNetwork(); final P2PNetwork connector2 = createP2PNetwork()) { - listener.getRlpxAgent().subscribeConnectRequest(testCallback); - connector1.getRlpxAgent().subscribeConnectRequest(testCallback); - connector2.getRlpxAgent().subscribeConnectRequest((p, d) -> false); + listener.getRlpxAgent().ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); + connector1.getRlpxAgent().ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); + connector2 + .getRlpxAgent() + .ifPresent( + agent -> + agent.setPeerConnectionGatekeeper( + (p, d) -> Optional.of(DisconnectReason.TOO_MANY_PEERS))); // Setup listener and first connection listener.start(); @@ -190,8 +196,8 @@ public void rejectPeerWithNoSharedCaps() throws Exception { final Capability cap2 = Capability.create(subprotocol2.getName(), 68); try (final P2PNetwork listener = createP2PNetwork(listenerNodeKey, List.of(cap1)); final P2PNetwork connector = createP2PNetwork(connectorNodeKey, List.of(cap2))) { - listener.getRlpxAgent().subscribeConnectRequest(testCallback); - connector.getRlpxAgent().subscribeConnectRequest(testCallback); + listener.getRlpxAgent().ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); + connector.getRlpxAgent().ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); listener.start(); connector.start(); @@ -211,8 +217,12 @@ public void rejectIncomingConnectionFromDenylistedPeer() throws Exception { try (final P2PNetwork localNetwork = createP2PNetwork(localDenylist); final P2PNetwork remoteNetwork = createP2PNetwork()) { - localNetwork.getRlpxAgent().subscribeConnectRequest(testCallback); - remoteNetwork.getRlpxAgent().subscribeConnectRequest(testCallback); + localNetwork + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); + remoteNetwork + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); localNetwork.start(); remoteNetwork.start(); @@ -260,8 +270,12 @@ public void rejectIncomingConnectionFromDisallowedPeer() throws Exception { try (final P2PNetwork localNetwork = createP2PNetwork(peerPermissions); final P2PNetwork remoteNetwork = createP2PNetwork()) { - localNetwork.getRlpxAgent().subscribeConnectRequest(testCallback); - remoteNetwork.getRlpxAgent().subscribeConnectRequest(testCallback); + localNetwork + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); + remoteNetwork + .getRlpxAgent() + .ifPresent(agent -> agent.setPeerConnectionGatekeeper(testCallback)); localNetwork.start(); remoteNetwork.start(); @@ -300,7 +314,7 @@ public void rejectIncomingConnectionFromDisallowedPeer() throws Exception { } } - private final ShouldConnectCallback testCallback = (p, d) -> true; + private final PeerConnectionGatekeeper testCallback = (p, d) -> Optional.empty(); private Peer createPeer(final Bytes nodeId, final int listenPort) { return DefaultPeer.fromEnodeURL( diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/RlpxAgentTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/RlpxAgentTest.java index 4bd6a1b78a7..1d097b3cedf 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/RlpxAgentTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/RlpxAgentTest.java @@ -87,7 +87,7 @@ public class RlpxAgentTest { public void setup() { // Set basic defaults when(peerPrivileges.canExceedConnectionLimits(any())).thenReturn(false); - agent.subscribeConnectRequest((a, b) -> true); + agent.setPeerConnectionGatekeeper((a, b) -> Optional.empty()); } @AfterEach From 77028f1b0da608a76ed6605b9dab58a8ae04b671 Mon Sep 17 00:00:00 2001 From: Mykim Date: Mon, 9 Mar 2026 16:12:03 +0900 Subject: [PATCH 07/77] Migrate JSR305 nullness annotations to JSpecify (#9995) - Replace javax.annotation.Nullable and javax.annotation.CheckForNull with org.jspecify.annotations.Nullable across 18 Java files - Update platform constraint to org.jspecify:jspecify:1.0.0 - Replace compileOnly com.google.code.findbugs:jsr305 with compileOnly org.jspecify:jspecify in affected modules Signed-off-by: Mykim <38449976+Apisapple@users.noreply.github.com> Co-authored-by: Simon Dudley Signed-off-by: Mykim --- consensus/common/build.gradle | 2 +- .../consensus/common/bft/BftEventQueue.java | 2 +- ethereum/api/build.gradle | 2 +- .../pojoadapter/TransactionAdapter.java | 2 +- .../api/jsonrpc/EngineJsonRpcService.java | 2 +- .../api/jsonrpc/JsonRpcHttpService.java | 2 +- .../DefaultAuthenticationService.java | 2 +- .../methods/engine/EngineGetBlobsV1.java | 2 +- .../methods/engine/EngineGetBlobsV3.java | 2 +- .../parameters/TransactionTraceParams.java | 17 ++++++----------- .../jsonrpc/internal/results/FeeHistory.java | 2 +- ethereum/core/build.gradle | 2 +- .../receipt/TransactionReceiptDecoder.java | 2 +- ethereum/eth/build.gradle | 2 +- .../messages/snap/GetStorageRangeMessage.java | 5 ++--- ethereum/ethstats/build.gradle | 2 +- .../ethstats/util/EthStatsConnectOptions.java | 8 +++----- ethereum/referencetests/build.gradle | 2 +- .../GeneralStateTestCaseSpec.java | 2 +- .../StateTestVersionedTransaction.java | 2 +- evm/build.gradle | 2 +- .../besu/collections/undo/UndoTable.java | 8 ++++---- .../besu/evm/worldstate/JournaledAccount.java | 2 +- .../evm/worldstate/UpdateTrackingAccount.java | 2 +- platform/build.gradle | 2 +- testutil/build.gradle | 2 +- .../besu/testutil/JsonTestParameters.java | 2 +- 27 files changed, 38 insertions(+), 46 deletions(-) diff --git a/consensus/common/build.gradle b/consensus/common/build.gradle index ad996797ac8..499c974f110 100644 --- a/consensus/common/build.gradle +++ b/consensus/common/build.gradle @@ -43,7 +43,7 @@ dependencies { implementation project(':evm') implementation project(':util') - compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'org.jspecify:jspecify' implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.google.guava:guava' diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftEventQueue.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftEventQueue.java index 8f6190c4cfc..d6138b646d4 100644 --- a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftEventQueue.java +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftEventQueue.java @@ -21,8 +21,8 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/ethereum/api/build.gradle b/ethereum/api/build.gradle index 61020c32aa3..c013f6147ee 100644 --- a/ethereum/api/build.gradle +++ b/ethereum/api/build.gradle @@ -34,7 +34,7 @@ dependencies { api 'org.slf4j:slf4j-api' api 'org.apache.logging.log4j:log4j-api' - compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'org.jspecify:jspecify' implementation project(':config') implementation project(':consensus:merge') diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java index 02946365cbf..d1a4ef866b6 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java @@ -36,11 +36,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import javax.annotation.Nullable; import graphql.schema.DataFetchingEnvironment; import jakarta.validation.constraints.NotNull; import org.apache.tuweni.bytes.Bytes; +import org.jspecify.annotations.Nullable; /** * The TransactionAdapter class provides methods to retrieve the transaction details. diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/EngineJsonRpcService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/EngineJsonRpcService.java index 5550f57ddcf..34595c94982 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/EngineJsonRpcService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/EngineJsonRpcService.java @@ -59,7 +59,6 @@ import java.util.StringJoiner; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import io.opentelemetry.api.OpenTelemetry; @@ -96,6 +95,7 @@ import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.CorsHandler; import jakarta.validation.constraints.NotNull; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java index 76f3f9e8b47..666de8bd24b 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java @@ -51,7 +51,6 @@ import java.util.StringJoiner; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import io.opentelemetry.api.OpenTelemetry; @@ -84,6 +83,7 @@ import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.CorsHandler; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/DefaultAuthenticationService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/DefaultAuthenticationService.java index 9182cc895a0..18318330e04 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/DefaultAuthenticationService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/DefaultAuthenticationService.java @@ -21,7 +21,6 @@ import java.io.File; import java.util.Collection; import java.util.Optional; -import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import io.netty.handler.codec.http.HttpResponseStatus; @@ -39,6 +38,7 @@ import io.vertx.ext.auth.jwt.JWTAuth; import io.vertx.ext.auth.jwt.JWTAuthOptions; import io.vertx.ext.web.RoutingContext; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV1.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV1.java index 1d30101a3a3..410228ab1b8 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV1.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV1.java @@ -39,10 +39,10 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import javax.annotation.Nullable; import io.vertx.core.Vertx; import jakarta.validation.constraints.NotNull; +import org.jspecify.annotations.Nullable; /** * #### Specification diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV3.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV3.java index dd0cdc82a44..0da8e2335d7 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV3.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV3.java @@ -41,10 +41,10 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; -import javax.annotation.Nullable; import io.vertx.core.Vertx; import jakarta.validation.constraints.NotNull; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParams.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParams.java index e0c0cb89893..509d6af4c04 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParams.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParams.java @@ -23,7 +23,6 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Set; -import javax.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -31,6 +30,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.immutables.value.Value; +import org.jspecify.annotations.Nullable; @Value.Immutable @JsonSerialize(as = ImmutableTransactionTraceParams.class) @@ -39,28 +39,24 @@ public interface TransactionTraceParams { @JsonProperty("txHash") - @Nullable - String getTransactionHash(); + @Nullable String getTransactionHash(); @JsonProperty(value = "disableStorage") - @Nullable - Boolean disableStorageNullable(); + @Nullable Boolean disableStorageNullable(); default boolean disableStorage() { return Boolean.TRUE.equals(disableStorageNullable()); } @JsonProperty(value = "disableMemory") - @Nullable - Boolean disableMemoryNullable(); + @Nullable Boolean disableMemoryNullable(); default boolean disableMemory() { return Boolean.TRUE.equals(disableMemoryNullable()); } @JsonProperty(value = "disableStack") - @Nullable - Boolean disableStackNullable(); + @Nullable Boolean disableStackNullable(); default boolean disableStack() { return Boolean.TRUE.equals(disableStackNullable()); @@ -68,8 +64,7 @@ default boolean disableStack() { @JsonProperty("tracer") @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable - String tracer(); + @Nullable String tracer(); @JsonProperty("tracerConfig") @Nullable diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/FeeHistory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/FeeHistory.java index 0985479ea5f..6c95da0a209 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/FeeHistory.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/FeeHistory.java @@ -20,11 +20,11 @@ import java.util.List; import java.util.Optional; -import javax.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import org.immutables.value.Value; +import org.jspecify.annotations.Nullable; @Value.Immutable public interface FeeHistory { diff --git a/ethereum/core/build.gradle b/ethereum/core/build.gradle index 052e8589cb2..d812b34249c 100644 --- a/ethereum/core/build.gradle +++ b/ethereum/core/build.gradle @@ -32,7 +32,7 @@ dependencies { api 'org.slf4j:slf4j-api' api 'org.web3j:core' - compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'org.jspecify:jspecify' implementation project(path: ':util') diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/TransactionReceiptDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/TransactionReceiptDecoder.java index 56b1fa5239f..3efddec3126 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/TransactionReceiptDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/TransactionReceiptDecoder.java @@ -24,9 +24,9 @@ import java.util.List; import java.util.Optional; -import javax.annotation.Nullable; import org.apache.tuweni.bytes.Bytes; +import org.jspecify.annotations.Nullable; /** * Decodes transaction receipts from RLP. diff --git a/ethereum/eth/build.gradle b/ethereum/eth/build.gradle index 74de9d84814..0f28e763b62 100644 --- a/ethereum/eth/build.gradle +++ b/ethereum/eth/build.gradle @@ -38,7 +38,7 @@ dependencies { api 'org.slf4j:slf4j-api' api project(':util') - compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'org.jspecify:jspecify' implementation project(':config') implementation project(':datatypes') diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/GetStorageRangeMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/GetStorageRangeMessage.java index facea47028b..639a95f3cbe 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/GetStorageRangeMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/GetStorageRangeMessage.java @@ -24,12 +24,12 @@ import java.math.BigInteger; import java.util.List; import java.util.Optional; -import javax.annotation.Nullable; import kotlin.collections.ArrayDeque; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.immutables.value.Value; +import org.jspecify.annotations.Nullable; public final class GetStorageRangeMessage extends AbstractSnapMessageData { @@ -134,8 +134,7 @@ public interface StorageRange { Hash startKeyHash(); - @Nullable - Hash endKeyHash(); + @Nullable Hash endKeyHash(); BigInteger responseBytes(); } diff --git a/ethereum/ethstats/build.gradle b/ethereum/ethstats/build.gradle index d81c8a4b215..ac03bb2a7c7 100644 --- a/ethereum/ethstats/build.gradle +++ b/ethereum/ethstats/build.gradle @@ -31,7 +31,7 @@ jar { dependencies { api 'org.slf4j:slf4j-api' - compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'org.jspecify:jspecify' implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' diff --git a/ethereum/ethstats/src/main/java/org/hyperledger/besu/ethstats/util/EthStatsConnectOptions.java b/ethereum/ethstats/src/main/java/org/hyperledger/besu/ethstats/util/EthStatsConnectOptions.java index 530a2e2d924..1837e8aeb0c 100644 --- a/ethereum/ethstats/src/main/java/org/hyperledger/besu/ethstats/util/EthStatsConnectOptions.java +++ b/ethereum/ethstats/src/main/java/org/hyperledger/besu/ethstats/util/EthStatsConnectOptions.java @@ -18,9 +18,9 @@ import java.net.URI; import java.nio.file.Path; -import javax.annotation.Nullable; import org.immutables.value.Value; +import org.jspecify.annotations.Nullable; import org.slf4j.LoggerFactory; /** @@ -34,8 +34,7 @@ public interface EthStatsConnectOptions { * * @return the scheme of the connection. */ - @Nullable - String getScheme(); + @Nullable String getScheme(); /** * Gets the node name of the connection. @@ -77,8 +76,7 @@ public interface EthStatsConnectOptions { * * @return the CA certificate of the connection. */ - @Nullable - Path getCaCert(); + @Nullable Path getCaCert(); /** * Gets the ethstats report interval. diff --git a/ethereum/referencetests/build.gradle b/ethereum/referencetests/build.gradle index 496eabee7e0..e1ff91883eb 100644 --- a/ethereum/referencetests/build.gradle +++ b/ethereum/referencetests/build.gradle @@ -254,7 +254,7 @@ configurations { } dependencies { - compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'org.jspecify:jspecify' implementation project(':config') implementation project(':crypto:algorithms') diff --git a/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/GeneralStateTestCaseSpec.java b/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/GeneralStateTestCaseSpec.java index 394cb9a2695..31a865694b2 100644 --- a/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/GeneralStateTestCaseSpec.java +++ b/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/GeneralStateTestCaseSpec.java @@ -28,11 +28,11 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; -import javax.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import org.jspecify.annotations.Nullable; /** A Transaction test case specification. */ @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/StateTestVersionedTransaction.java b/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/StateTestVersionedTransaction.java index 5d1a6078715..4f612d39a8b 100644 --- a/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/StateTestVersionedTransaction.java +++ b/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/StateTestVersionedTransaction.java @@ -28,7 +28,6 @@ import java.util.List; import java.util.Optional; import java.util.function.Function; -import javax.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -37,6 +36,7 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; +import org.jspecify.annotations.Nullable; /** * Represents the "transaction" part of the JSON of a general state tests. diff --git a/evm/build.gradle b/evm/build.gradle index 99418878c91..b416c99ac89 100644 --- a/evm/build.gradle +++ b/evm/build.gradle @@ -54,7 +54,7 @@ dependencies { implementation 'io.consensys.protocols:jc-kzg-4844' compileOnly 'com.fasterxml.jackson.core:jackson-databind' - compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'org.jspecify:jspecify' compileOnly 'io.vertx:vertx-core' testImplementation 'com.fasterxml.jackson.core:jackson-databind' diff --git a/evm/src/main/java/org/hyperledger/besu/collections/undo/UndoTable.java b/evm/src/main/java/org/hyperledger/besu/collections/undo/UndoTable.java index 967cd14f7a3..004aa00a54a 100644 --- a/evm/src/main/java/org/hyperledger/besu/collections/undo/UndoTable.java +++ b/evm/src/main/java/org/hyperledger/besu/collections/undo/UndoTable.java @@ -21,10 +21,10 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import javax.annotation.CheckForNull; import com.google.common.collect.Table; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import org.jspecify.annotations.Nullable; /** * A Table that supports rolling back the Table to a prior state. @@ -123,7 +123,7 @@ public boolean containsValue(final Object value) { } @Override - @CheckForNull + @Nullable public V get(final Object rowKey, final Object columnKey) { return delegate.get(rowKey, columnKey); } @@ -135,7 +135,7 @@ public void clear() { @Override @CanIgnoreReturnValue - @CheckForNull + @Nullable public V put(final R rowKey, final C columnKey, final V value) { V oldV = delegate.put(rowKey, columnKey, value); undoLog.add(new UndoEntry<>(rowKey, columnKey, oldV)); @@ -156,7 +156,7 @@ public void putAll(final Table table) { @SuppressWarnings("unchecked") @Override @CanIgnoreReturnValue - @CheckForNull + @Nullable public V remove(final Object rowKey, final Object columnKey) { V oldV = delegate.remove(rowKey, columnKey); undoLog.add(new UndoEntry<>((R) rowKey, (C) columnKey, oldV)); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/JournaledAccount.java b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/JournaledAccount.java index d2fc60c7f18..9e22c348ef0 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/JournaledAccount.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/JournaledAccount.java @@ -29,11 +29,11 @@ import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; -import javax.annotation.Nullable; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; +import org.jspecify.annotations.Nullable; /** * An implementation of {@link MutableAccount} that tracks updates made to the account since the diff --git a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/UpdateTrackingAccount.java b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/UpdateTrackingAccount.java index ba9a6989559..22b97c1a2c8 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/UpdateTrackingAccount.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/UpdateTrackingAccount.java @@ -29,11 +29,11 @@ import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; -import javax.annotation.Nullable; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; +import org.jspecify.annotations.Nullable; /** * An implementation of {@link MutableAccount} that tracks updates made to the account since the diff --git a/platform/build.gradle b/platform/build.gradle index 9c94c91c302..384da7562db 100644 --- a/platform/build.gradle +++ b/platform/build.gradle @@ -84,7 +84,7 @@ dependencies { api 'org.checkerframework:checker-qual:3.43.0' - api 'com.google.code.findbugs:jsr305:3.0.2' + api 'org.jspecify:jspecify:1.0.0' api 'com.google.protobuf:protobuf-java:3.25.5' diff --git a/testutil/build.gradle b/testutil/build.gradle index f435d14be35..fd62ab319a1 100644 --- a/testutil/build.gradle +++ b/testutil/build.gradle @@ -33,7 +33,7 @@ sonarqube { } dependencies { - compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'org.jspecify:jspecify' implementation project(':ethereum:eth') implementation project(':plugin-api') diff --git a/testutil/src/main/java/org/hyperledger/besu/testutil/JsonTestParameters.java b/testutil/src/main/java/org/hyperledger/besu/testutil/JsonTestParameters.java index 60d47d42138..b458263757a 100644 --- a/testutil/src/main/java/org/hyperledger/besu/testutil/JsonTestParameters.java +++ b/testutil/src/main/java/org/hyperledger/besu/testutil/JsonTestParameters.java @@ -33,7 +33,6 @@ import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Stream; -import javax.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -42,6 +41,7 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import org.jspecify.annotations.Nullable; /** * Utility class for generating JUnit test parameters from json files. Each set of test parameters From df8849a42c85bbbae6d28816508b09b9048181cb Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 10 Mar 2026 01:56:03 +1000 Subject: [PATCH 08/77] Fix flaky BundleSelectorPluginTest on slow CI runners (#9999) Increase poa-block-txs-selection-max-time to 95% for the bundle failure tests so the plugin has ~712ms instead of ~562ms to process transactions. The failure tests are not testing timeout behaviour, so the tighter budget was only causing intermittent PLUGIN_SELECTION_TIMEOUT events on loaded CI machines before the invalid transaction could be reached. Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .../tests/acceptance/plugins/BundleSelectorPluginTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/plugins/BundleSelectorPluginTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/plugins/BundleSelectorPluginTest.java index 300d3adbbcf..503aad107a5 100644 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/plugins/BundleSelectorPluginTest.java +++ b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/plugins/BundleSelectorPluginTest.java @@ -76,7 +76,8 @@ public void bundleIsNotMinedLastTxFails() throws IOException { "--plugin-bundle-test-enabled=true", "--plugin-bundle-size=2", "--plugin-bundle-failing-nonce=1", - "--plugin-block-txs-selection-max-time=75")); + "--plugin-block-txs-selection-max-time=75", + "--poa-block-txs-selection-max-time=95")); cluster.start(node); // since the last tx of the bundle fails, the first was initially selected, but eventually not @@ -95,7 +96,8 @@ public void bundleIsNotMinedMiddleTxFails() throws IOException { "--plugin-bundle-test-enabled=true", "--plugin-bundle-size=3", "--plugin-bundle-failing-nonce=1", - "--plugin-block-txs-selection-max-time=75")); + "--plugin-block-txs-selection-max-time=75", + "--poa-block-txs-selection-max-time=95")); cluster.start(node); // since the last tx of the bundle fails, the first was initially selected, but eventually not From 9d73b53fcf2b60b5c87deb15f8bc1a20e661f040 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 10 Mar 2026 02:36:59 +1000 Subject: [PATCH 09/77] Fix flaky BackwardSyncContextTest on slow CI runners (#10000) shouldSyncUntilHash and shouldSyncUntilRemoteBranch used isCompleted() inside untilAsserted, which only passes for normal completion. On slow CI machines the backward sync occasionally completes the future exceptionally, causing isCompleted() to fail on every retry until the 30s Awaitility timeout, which then rethrows the AssertionError. Change to isDone() inside the Awaitility block (matching the fix applied to shouldAddExpectedBlock in #9856) so Awaitility exits as soon as the future reaches any terminal state. The subsequent future.get() will then surface the real exception if the sync failed. Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .../eth/sync/backwardsync/BackwardSyncContextTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/backwardsync/BackwardSyncContextTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/backwardsync/BackwardSyncContextTest.java index 7b144d9d1e7..041140c479b 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/backwardsync/BackwardSyncContextTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/backwardsync/BackwardSyncContextTest.java @@ -303,7 +303,7 @@ public void shouldSyncUntilHash() throws Exception { .untilAsserted( () -> { respondUntilFutureIsDone(future); - assertThat(future).isCompleted(); + assertThat(future).isDone(); }); future.get(); @@ -337,10 +337,10 @@ public void shouldSyncUntilRemoteBranch() throws Exception { .untilAsserted( () -> { respondUntilFutureIsDone(future); - assertThat(future).isCompleted(); + assertThat(future).isDone(); }); - future.get(); // Should succeed since we waited for completion + future.get(); assertThat(localBlockchain.getChainHeadBlock()).isEqualTo(remoteBlockchain.getChainHeadBlock()); } From 304d3c817f09ab749912fd780f41e4bdac9305f4 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 10 Mar 2026 03:05:10 +1000 Subject: [PATCH 10/77] potential fix for race condidion affecting ATs (#9929) Signed-off-by: Sally MacFarlane --- .../besu/tests/acceptance/dsl/node/BesuNode.java | 10 ++++++++-- .../acceptance/dsl/node/ProcessBesuNodeRunner.java | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java index 3421072ff09..97f739ce872 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java @@ -365,8 +365,14 @@ private String getDiscoveryPort() { public Optional jsonRpcBaseUrl() { if (isJsonRpcEnabled()) { - return Optional.of( - HTTP + jsonRpcConfiguration.getHost() + ":" + portsProperties.getProperty(JSON_RPC)); + final String port = portsProperties.getProperty(JSON_RPC); + if (port == null) { + throw new IllegalStateException( + "JSON-RPC port not available for node " + + name + + ". Node may have failed to start or ports file was not written."); + } + return Optional.of(HTTP + jsonRpcConfiguration.getHost() + ":" + port); } else { return Optional.empty(); } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java index 1cd5b4f8c48..729b68001ba 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java @@ -461,7 +461,13 @@ private void waitForFileOrExit(final BesuNode node, final String fileName) { Awaitility.waitAtMost(60, TimeUnit.SECONDS) .until( () -> { - if (!besuProcesses.get(node.getName()).isAlive()) { + final Process process = besuProcesses.get(node.getName()); + if (!process.isAlive()) { + LOG.warn( + "Besu process for node {} exited with code {} before writing {}", + node.getName(), + process.exitValue(), + fileName); return true; } From 1f8da2517823f39c026ed4c342a3c8ee6cb1af99 Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Mon, 9 Mar 2026 19:19:20 +0100 Subject: [PATCH 11/77] Use sha256 hardware implementation (#9924) * use sha256 hardware implementation if available, if not fall back to sun.security.provider.SHA2$SHA256 Signed-off-by: daniellehrner --------- Signed-off-by: daniellehrner --- CHANGELOG.md | 1 + .../org/hyperledger/besu/crypto/Hash.java | 8 ++- .../besu/crypto/MessageDigestFactory.java | 4 +- .../vm/operations/SHA256Benchmark.java | 59 +++++++++++++++++++ 4 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SHA256Benchmark.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ae0d767c8..bafc0458a57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Add blockTimestamp to transaction RPC results [#9887](https://github.com/hyperledger/besu/pull/9887) - Plugin API: Allow the registration of multiple PluginTransactionPoolValidatorFactory [#9964](https://github.com/hyperledger/besu/pull/9964) - Add `-Pcases` case name filtering to JMH benchmark suite [#9982](https://github.com/hyperledger/besu/pull/9982) +- Use JDK SHA-256 provider to leverage hardware SHA-NI instructions instead of BouncyCastle [#9924](https://github.com/hyperledger/besu/pull/9924) ## 26.2.0 diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/Hash.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/Hash.java index b1b66f0f961..9fba0fda4f7 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/Hash.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/Hash.java @@ -33,8 +33,8 @@ private Hash() {} private static final Supplier KECCAK256_SUPPLIER = Suppliers.memoize(() -> messageDigest(KECCAK256_ALG)); - private static final Supplier SHA256_SUPPLIER = - Suppliers.memoize(() -> messageDigest(SHA256_ALG)); + private static final ThreadLocal SHA256_DIGEST = + ThreadLocal.withInitial(() -> messageDigest(SHA256_ALG)); private static final Supplier RIPEMD160_SUPPLIER = Suppliers.memoize(() -> messageDigest(RIPEMD160_ALG)); private static final Supplier BLAKE2BF_SUPPLIER = @@ -73,7 +73,9 @@ private static byte[] digestUsingAlgorithm( * @return A digest. */ public static Bytes32 sha256(final Bytes input) { - return Bytes32.wrap(digestUsingAlgorithm(input, SHA256_SUPPLIER)); + final MessageDigest digest = SHA256_DIGEST.get(); + input.update(digest); + return Bytes32.wrap(digest.digest()); } /** diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/MessageDigestFactory.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/MessageDigestFactory.java index 24c13ba377c..efa3249a46c 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/MessageDigestFactory.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/MessageDigestFactory.java @@ -20,7 +20,6 @@ import org.bouncycastle.jcajce.provider.digest.Keccak; import org.bouncycastle.jcajce.provider.digest.RIPEMD160; -import org.bouncycastle.jcajce.provider.digest.SHA256; import org.bouncycastle.jce.provider.BouncyCastleProvider; /** The Message digest factory. */ @@ -56,9 +55,10 @@ private MessageDigestFactory() {} public static MessageDigest create(final String algorithm) throws NoSuchAlgorithmException { return switch (algorithm) { case KECCAK256_ALG -> new Keccak.Digest256(); - case SHA256_ALG -> new SHA256.Digest(); case RIPEMD160_ALG -> new RIPEMD160.Digest(); case BLAKE2BF_ALG -> new Blake2bfMessageDigest(); + // SHA-256 (and other standard algorithms) resolved via JCA provider priority order + // enabling SHA-NI hardware acceleration if available default -> MessageDigest.getInstance(algorithm); }; } diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SHA256Benchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SHA256Benchmark.java new file mode 100644 index 00000000000..0a6aee2b81e --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SHA256Benchmark.java @@ -0,0 +1,59 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.vm.operations; + +import org.hyperledger.besu.crypto.Hash; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Thread) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(value = TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class SHA256Benchmark { + + @Param({"32", "64", "128", "256", "512", "1024", "2048", "4096"}) + private String inputSize; + + public Bytes bytes; + + @Setup + public void setUp() { + final Random random = new Random(42); + final byte[] byteArray = new byte[Integer.parseInt(inputSize)]; + random.nextBytes(byteArray); + bytes = Bytes.wrap(byteArray); + } + + @Benchmark + public Bytes32 sha256() { + return Hash.sha256(bytes); + } +} From c47cde29706b2c6e611ec4742b7e08f927a4ed23 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 10 Mar 2026 09:49:04 +1000 Subject: [PATCH 12/77] Log ENR URL at startup alongside enode URL (#10003) When discovery produces a local ENR, log it at INFO level right after the enode URL so operators can easily find both identifiers. Closes #9967 Signed-off-by: Usman Saleem --- .../hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java | 1 + 1 file changed, 1 insertion(+) diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java index 06778d3cf4d..f1cc27bc4db 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java @@ -505,6 +505,7 @@ private void setLocalNode( .build(); LOG.info("Enode URL {}", localEnode.toString()); + getLocalEnr().ifPresent(enr -> LOG.info("ENR URL {}", enr)); LOG.info("Node address {}", Util.publicKeyToAddress(localEnode.getNodeId())); localNode.setEnode(localEnode); } From f95939f3dab4baa06a6cd87c93fe7d4822a82ece Mon Sep 17 00:00:00 2001 From: Kanchan Kaur <87459628+kkaur01@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:39:50 +1000 Subject: [PATCH 13/77] fix(metrics): prevent duplicate OTel callback registration (#9653) (#9957) Signed-off-by: kkaur01 Co-authored-by: Sally MacFarlane Signed-off-by: Kanchan Kaur <87459628+kkaur01@users.noreply.github.com> --- .../opentelemetry/OpenTelemetrySystem.java | 21 ++++-- .../opentelemetry/OpenTelemetryTimer.java | 30 +++++---- .../OpenTelemetryMetricsSystemTest.java | 64 +++++++++++++++++++ 3 files changed, 97 insertions(+), 18 deletions(-) diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java index 8dd8d2e1f8f..8ad0fed5a25 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java @@ -90,6 +90,11 @@ public class OpenTelemetrySystem implements ObservableMetricsSystem { private final Map> cachedCounters = new ConcurrentHashMap<>(); private final Map> cachedTimers = new ConcurrentHashMap<>(); + private final Map cachedSuppliedGauges = + new ConcurrentHashMap<>(); + private final Map cachedSuppliedCounters = + new ConcurrentHashMap<>(); + private final Set registeredGaugeNames = ConcurrentHashMap.newKeySet(); private final SdkMeterProvider sdkMeterProvider; private final DebugMetricReader debugMetricReader; private final SdkTracerProvider sdkTracerProvider; @@ -283,7 +288,7 @@ public void createGauge( final String help, final DoubleSupplier valueSupplier) { LOG.trace("Creating a gauge {}", name); - if (isCategoryEnabled(category)) { + if (isCategoryEnabled(category) && registeredGaugeNames.add(name)) { final Meter meter = sdkMeterProvider.get(category.getName()); meter .gaugeBuilder(name) @@ -315,8 +320,11 @@ public LabelledSuppliedMetric createLabelledSuppliedCounter( final String... labelNames) { LOG.trace("Creating a labelled supplied counter {}", name); if (isCategoryEnabled(category)) { - return new OpenTelemetrySuppliedCounter( - name, help, sdkMeterProvider.get(category.getName()), List.of(labelNames)); + return cachedSuppliedCounters.computeIfAbsent( + name, + k -> + new OpenTelemetrySuppliedCounter( + name, help, sdkMeterProvider.get(category.getName()), List.of(labelNames))); } return NoOpMetricsSystem.getLabelledSuppliedMetric(labelNames.length); } @@ -329,8 +337,11 @@ public LabelledSuppliedMetric createLabelledSuppliedGauge( final String... labelNames) { LOG.trace("Creating a labelled gauge {}", name); if (isCategoryEnabled(category)) { - return new OpenTelemetryGauge( - name, help, sdkMeterProvider.get(category.getName()), List.of(labelNames)); + return cachedSuppliedGauges.computeIfAbsent( + name, + k -> + new OpenTelemetryGauge( + name, help, sdkMeterProvider.get(category.getName()), List.of(labelNames))); } return NoOpMetricsSystem.getLabelledSuppliedMetric(labelNames.length); } diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryTimer.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryTimer.java index 05090f5e808..3b4da8c0c68 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryTimer.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryTimer.java @@ -17,6 +17,9 @@ import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.metrics.Meter; @@ -24,10 +27,9 @@ /** The Open telemetry timer. */ public class OpenTelemetryTimer implements LabelledMetric { - private final String help; - private final Meter meter; - private final String metricName; private final String[] labelNames; + private final ConcurrentHashMap elapsedNanosMap = + new ConcurrentHashMap<>(); /** * Instantiates a new Open telemetry timer. @@ -39,28 +41,30 @@ public class OpenTelemetryTimer implements LabelledMetric { */ public OpenTelemetryTimer( final String metricName, final String help, final Meter meter, final String... labelNames) { - this.metricName = metricName; - this.help = help; - this.meter = meter; this.labelNames = labelNames; + // Register a single permanent callback; it reads the latest elapsed time for each label set. + meter + .gaugeBuilder(metricName) + .setDescription(help) + .buildWithCallback( + measurement -> + elapsedNanosMap.forEach( + (attrs, nanos) -> measurement.record(nanos.get() / 1e9, attrs))); } @Override public OperationTimer labels(final String... labelValues) { - AttributesBuilder builder = Attributes.builder(); + final AttributesBuilder builder = Attributes.builder(); for (int i = 0; i < labelNames.length; i++) { builder.put(labelNames[i], labelValues[i]); } final Attributes labels = builder.build(); - + // Lazily create the AtomicLong slot for this label set on first stop(). return () -> { final long startTime = System.nanoTime(); return () -> { - long elapsed = System.nanoTime() - startTime; - meter - .gaugeBuilder(metricName) - .setDescription(help) - .buildWithCallback((measurement) -> measurement.record((double) elapsed, labels)); + final long elapsed = System.nanoTime() - startTime; + elapsedNanosMap.computeIfAbsent(labels, k -> new AtomicLong()).set(elapsed); return elapsed / 1e9; }; }; diff --git a/metrics/core/src/test/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryMetricsSystemTest.java b/metrics/core/src/test/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryMetricsSystemTest.java index 5324d8cf3f9..dec42404707 100644 --- a/metrics/core/src/test/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryMetricsSystemTest.java +++ b/metrics/core/src/test/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryMetricsSystemTest.java @@ -163,6 +163,70 @@ public void shouldHandleDuplicateTimerCreation() { assertThat(timer1).isEqualTo(timer2); } + @Test + public void shouldNotAccumulateCallbacksForTimer() { + final LabelledMetric timer = + metricsSystem.createLabelledTimer(RPC, "request", "Some help", "method"); + + // Stop the timer many times — before the fix each stop() registered a new OTel callback. + for (int i = 0; i < 10; i++) { + try (final OperationTimer.TimingContext ignored = timer.labels("eth_getBlock").startTimer()) { + // intentionally empty + } + } + + List observations = + metricsSystem + .streamObservations() + .filter(o -> o.metricName().equals("request")) + .collect(Collectors.toList()); + + // Despite 10 stops there must be exactly one observation per label set, not 10. + assertThat(observations).hasSize(1); + assertThat(observations.get(0).labels()).containsExactly("eth_getBlock"); + assertThat((Double) observations.get(0).value()).isPositive(); + } + + @Test + public void shouldHandleDuplicateGaugeCreation() { + metricsSystem.createGauge(RPC, "myGauge", "help", () -> 42.0); + // Second call with the same name must not register a second OTel callback. + metricsSystem.createGauge(RPC, "myGauge", "help", () -> 42.0); + + List observations = + metricsSystem.streamObservations().collect(Collectors.toList()); + assertThat(observations).hasSize(1); + assertThat(observations).containsExactly(new Observation(RPC, "myGauge", 42.0, emptyList())); + } + + @Test + public void shouldHandleDuplicateSuppliedGaugeCreation() { + final LabelledSuppliedMetric gauge1 = + metricsSystem.createLabelledSuppliedGauge(RPC, "gaugeName", "help", "label"); + final LabelledSuppliedMetric gauge2 = + metricsSystem.createLabelledSuppliedGauge(RPC, "gaugeName", "help", "label"); + + // Both calls must return the exact same cached instance. + assertThat(gauge1).isSameAs(gauge2); + + gauge1.labels(() -> 7.0, "value1"); + + // Exactly one observation, not two. + assertThat(metricsSystem.streamObservations()) + .containsExactly(new Observation(RPC, "gaugeName", 7.0, singletonList("value1"))); + } + + @Test + public void shouldHandleDuplicateSuppliedCounterCreation() { + final LabelledSuppliedMetric counter1 = + metricsSystem.createLabelledSuppliedCounter(RPC, "myCounter", "help"); + final LabelledSuppliedMetric counter2 = + metricsSystem.createLabelledSuppliedCounter(RPC, "myCounter", "help"); + + // Both calls must return the exact same cached instance — only one OTel callback registered. + assertThat(counter1).isSameAs(counter2); + } + @Test public void shouldCreateObservationsFromTimerWithLabels() { final LabelledMetric timer = From b7090f0bf86face34a639eb40487d4c2d5ae4163 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 10 Mar 2026 13:58:58 +1000 Subject: [PATCH 14/77] disable forest TraceJsonRpcHttpBySpecTest (#10005) Signed-off-by: Sally MacFarlane --- .../ethereum/api/jsonrpc/forest/TraceJsonRpcHttpBySpecTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/TraceJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/TraceJsonRpcHttpBySpecTest.java index b33be910059..078d4fdbe1b 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/TraceJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/TraceJsonRpcHttpBySpecTest.java @@ -21,8 +21,10 @@ import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +@Disabled("Forest storage is deprecated - trace tests are covered by the Bonsai variant") public class TraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override From 9887a56a9be0c9ee290e92d3a3d7438c34deba9d Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 10 Mar 2026 16:04:21 +1000 Subject: [PATCH 15/77] Add config option for max blobs per block (#9983) * add max-blobs-per-block CLI option Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sally MacFarlane Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + .../org/hyperledger/besu/cli/BesuCommand.java | 5 + .../cli/ConfigurationOverviewBuilder.java | 16 ++++ .../besu/cli/options/MiningOptions.java | 29 +++++- .../besu/cli/options/MiningOptionsTest.java | 2 +- app/src/test/resources/everything_config.toml | 5 +- .../BlobSizeTransactionSelector.java | 2 +- .../BlobSizeTransactionSelectorTest.java | 3 +- .../besu/ethereum/GasLimitCalculator.java | 12 +++ .../ethereum/core/MiningConfiguration.java | 11 +++ .../mainnet/MainnetProtocolSpecs.java | 3 +- .../OsakaTargetingGasLimitCalculator.java | 18 +++- ...CancunTargetingGasLimitCalculatorTest.java | 1 + .../MainnetTransactionValidatorTest.java | 2 +- .../OsakaTargetingGasLimitCalculatorTest.java | 91 +++++++++++++++++-- 15 files changed, 185 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bafc0458a57..336b6001353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Add IPv6 dual-stack support for DiscV5 peer discovery (enabled via `--Xv5-discovery-enabled`): new `--p2p-host-ipv6`, `--p2p-interface-ipv6`, and `--p2p-port-ipv6` CLI options enable a second UDP discovery socket; `--p2p-ipv6-outbound-enabled` controls whether IPv6 is preferred for outbound connections when a peer advertises both address families [#9763](https://github.com/hyperledger/besu/pull/9763); RLPx now also binds a second TCP socket on the IPv6 interface so IPv6-only peers can establish connections [#9873](https://github.com/hyperledger/besu/pull/9873) - Stop EngineQosTimer as part of shutdown [#9903](https://github.com/hyperledger/besu/pull/9903) - Add `--max-blobs-per-transaction` CLI option to configure the maximum number of blobs per transaction [#9912](https://github.com/hyperledger/besu/pull/9912) +- Add `--max-blobs-per-block` CLI option to configure the maximum number of blobs per block when block building [#9983](https://github.com/hyperledger/besu/pull/9983) - Add blockTimestamp to transaction RPC results [#9887](https://github.com/hyperledger/besu/pull/9887) - Plugin API: Allow the registration of multiple PluginTransactionPoolValidatorFactory [#9964](https://github.com/hyperledger/besu/pull/9964) - Add `-Pcases` case name filtering to JMH benchmark suite [#9982](https://github.com/hyperledger/besu/pull/9982) diff --git a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 74079b0a59e..747c2e92cb0 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -2945,6 +2945,11 @@ private String generateConfigurationOverview() { .getMaxBlobsPerTransaction() .ifPresent(v -> builder.setMaxBlobsPerTransaction(v)); + miningParametersSupplier + .get() + .getMaxBlobsPerBlock() + .ifPresent(v -> builder.setMaxBlobsPerBlock(v)); + builder .setDiscoveryEnabled(p2PDiscoveryOptions.peerDiscoveryEnabled) .setSnapServerEnabled(this.unstableSynchronizerOptions.isSnapsyncServerEnabled()) diff --git a/app/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java b/app/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java index e0f21ec889e..9d6d7f1a4d3 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java +++ b/app/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java @@ -76,6 +76,7 @@ public class ConfigurationOverviewBuilder { private RocksDBCLIOptions.BlobDBSettings blobDBSettings; private Long targetGasLimit; private Integer maxBlobsPerTransaction; + private Integer maxBlobsPerBlock; /** * Create a new ConfigurationOverviewBuilder. @@ -411,6 +412,17 @@ public ConfigurationOverviewBuilder setMaxBlobsPerTransaction( return this; } + /** + * Sets the max blobs per block for block building. + * + * @param maxBlobsPerBlock the max blobs per block + * @return the builder + */ + public ConfigurationOverviewBuilder setMaxBlobsPerBlock(final Integer maxBlobsPerBlock) { + this.maxBlobsPerBlock = maxBlobsPerBlock; + return this; + } + /** * Sets the chain pruning configuration. * @@ -576,6 +588,10 @@ public String build() { lines.add("Max Blobs Per Transaction: " + maxBlobsPerTransaction); } + if (maxBlobsPerBlock != null) { + lines.add("Max Blobs Per Block (builder): " + maxBlobsPerBlock); + } + lines.add(""); lines.add("Host:"); diff --git a/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java b/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java index d0b440cfac0..cb4523da662 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java +++ b/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java @@ -39,6 +39,7 @@ import org.hyperledger.besu.util.number.PositiveNumber; import java.util.List; +import java.util.OptionalInt; import jakarta.validation.constraints.Positive; import org.apache.tuweni.bytes.Bytes; @@ -122,6 +123,14 @@ public class MiningOptions implements CLIOptions { arity = "1") private Integer maxBlobsPerTransaction = null; + @Option( + names = {"--max-blobs-per-block"}, + description = + "Maximum number of blobs allowed per block when building blocks. " + + "Only applies from Osaka hardfork onwards. Values above the protocol maximum are clamped. (default: protocol maximum)", + arity = "1") + private Integer maxBlobsPerBlock = null; + @CommandLine.ArgGroup(validate = false) private final Unstable unstableOptions = new Unstable(); @@ -232,7 +241,22 @@ public void validate( if (maxBlobsPerTransaction != null && maxBlobsPerTransaction < 0) { throw new ParameterException( - commandLine, "--max-blobs-per-transaction must be a positive value"); + commandLine, "--max-blobs-per-transaction must be a non-negative value"); + } + + if (maxBlobsPerBlock != null && maxBlobsPerBlock < 0) { + throw new ParameterException( + commandLine, "--max-blobs-per-block must be a non-negative value"); + } + + if (maxBlobsPerBlock != null + && maxBlobsPerTransaction != null + && maxBlobsPerBlock < maxBlobsPerTransaction) { + logger.warn( + "--max-blobs-per-block ({}) is less than --max-blobs-per-transaction ({}). " + + "The block limit will be the binding constraint during block building.", + maxBlobsPerBlock, + maxBlobsPerTransaction); } } @@ -253,6 +277,7 @@ static MiningOptions fromConfig(final MiningConfiguration miningConfiguration) { miningConfiguration .getMaxBlobsPerTransaction() .ifPresent(v -> miningOptions.maxBlobsPerTransaction = v); + miningConfiguration.getMaxBlobsPerBlock().ifPresent(v -> miningOptions.maxBlobsPerBlock = v); miningOptions.unstableOptions.posBlockCreationMaxTime = miningConfiguration.getUnstable().getPosBlockCreationMaxTime(); @@ -291,6 +316,8 @@ public MiningConfiguration toDomainObject() { return ImmutableMiningConfiguration.builder() .transactionSelectionService(transactionSelectionService) .mutableInitValues(updatableInitValuesBuilder.build()) + .maxBlobsPerBlock( + maxBlobsPerBlock != null ? OptionalInt.of(maxBlobsPerBlock) : OptionalInt.empty()) .nonPoaBlockTxsSelectionMaxTime(nonPoaBlockTxsSelectionMaxTime) .poaBlockTxsSelectionMaxTime(poaBlockTxsSelectionMaxTime) .pluginBlockTxsSelectionMaxTime(pluginBlockTxsSelectionMaxTime) diff --git a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java index a60273fe61f..fcfd103cb43 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java @@ -394,7 +394,7 @@ public void maxBlobsOptionWithZero() { @Test public void maxBlobsOptionWithNegativeValue() { internalTestFailure( - "--max-blobs-per-transaction must be a positive value", + "--max-blobs-per-transaction must be a non-negative value", "--max-blobs-per-transaction", "-9"); } diff --git a/app/src/test/resources/everything_config.toml b/app/src/test/resources/everything_config.toml index 8b7d4317011..0c1f19ad3cd 100644 --- a/app/src/test/resources/everything_config.toml +++ b/app/src/test/resources/everything_config.toml @@ -170,7 +170,8 @@ block-txs-selection-max-time=5000 poa-block-txs-selection-max-time=75 plugin-block-txs-selection-max-time=50 Xpos-block-creation-max-time=5 -max-blobs-per-transaction=0 +max-blobs-per-transaction=4 +max-blobs-per-block=8 # Permissioning permissions-nodes-config-file-enabled=false @@ -252,4 +253,4 @@ snapsync-synchronizer-transaction-indexing-enabled=true snapsync-synchronizer-pre-checkpoint-headers-only-enabled=true # history expiry -history-expiry-prune=false \ No newline at end of file +history-expiry-prune=false diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java index 9a8dc44bee1..6a5be7d52bb 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java @@ -56,7 +56,7 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( final var cumulativeBlobGasUsed = getWorkingState(); final var remainingBlobGas = - context.gasLimitCalculator().currentBlobGasLimit() - cumulativeBlobGasUsed; + context.gasLimitCalculator().blockBuilderBlobGasLimit() - cumulativeBlobGasUsed; if (remainingBlobGas == 0) { LOG.atTrace() diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java index 0afb4566b04..3d6ea9d578b 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java @@ -82,7 +82,8 @@ class BlobSizeTransactionSelectorTest { @BeforeEach void setup() { - when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS); + when(blockSelectionContext.gasLimitCalculator().blockBuilderBlobGasLimit()) + .thenReturn(MAX_BLOB_GAS); when(blockSelectionContext.gasCalculator().blobGasCost(anyLong())) .thenAnswer(iom -> BLOB_GAS_PER_BLOB * iom.getArgument(0, Long.class)); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/GasLimitCalculator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/GasLimitCalculator.java index d83442939c1..9047ff8cb15 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/GasLimitCalculator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/GasLimitCalculator.java @@ -97,4 +97,16 @@ default long transactionGasLimitCap() { default long transactionBlobGasLimitCap() { return BLOB_GAS_LIMIT; } + + /** + * Returns the blob gas limit to use when building blocks. This may be lower than {@link + * #currentBlobGasLimit()} when the user has configured {@code --max-blobs-per-block} to + * self-limit block building. Validation of incoming blocks from peers always uses {@link + * #currentBlobGasLimit()}. + * + * @return the blob gas limit for block building + */ + default long blockBuilderBlobGasLimit() { + return currentBlobGasLimit(); + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java index 921a4fd54ac..ab9b393612d 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java @@ -133,6 +133,17 @@ public OptionalInt getMaxBlobsPerTransaction() { return getMutableInitValues().getMaxBlobsPerTransaction(); } + /** + * Returns the maximum blobs per block for block building. Note: Only applies from Osaka hardfork + * onwards. Returns empty if not explicitly set by the user. + * + * @return the maximum blobs per block, or empty if not set + */ + @Value.Default + public OptionalInt getMaxBlobsPerBlock() { + return OptionalInt.empty(); + } + public Optional> getNonceGenerator() { return getMutableRuntimeValues().nonceGenerator; } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java index 4ed6275e245..fd2ef40a5c5 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java @@ -1019,7 +1019,8 @@ static ProtocolSpecBuilder osakaDefinition( gasCalculator, blobSchedule.getMax(), blobSchedule.getTarget(), - miningConfiguration.getMaxBlobsPerTransaction())) + miningConfiguration.getMaxBlobsPerTransaction(), + miningConfiguration.getMaxBlobsPerBlock())) .evmBuilder( (gasCalculator, __) -> MainnetEVMs.osaka(gasCalculator, chainId.orElse(BigInteger.ZERO), evmConfiguration)) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculator.java index 91c404d1b95..a80b44e1b0c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculator.java @@ -39,6 +39,7 @@ public class OsakaTargetingGasLimitCalculator extends CancunTargetingGasLimitCal private final long transactionGasLimitCap; private final long transactionBlobGasLimitCap; + private final long blockBuilderBlobGasLimit; public OsakaTargetingGasLimitCalculator( final long londonForkBlock, @@ -46,8 +47,10 @@ public OsakaTargetingGasLimitCalculator( final GasCalculator gasCalculator, final int maxBlobsPerBlock, final int targetBlobsPerBlock, - final OptionalInt maxBlobsPerTransaction) { + final OptionalInt maxBlobsPerTransaction, + final OptionalInt userMaxBlobsPerBlock) { super(londonForkBlock, feeMarket, gasCalculator, maxBlobsPerBlock, targetBlobsPerBlock); + final long blobGasPerBlob = gasCalculator.getBlobGasPerBlob(); int effectiveMaxBlobsPerTx = maxBlobsPerTransaction.orElse(DEFAULT_MAX_BLOBS_PER_TRANSACTION); if (effectiveMaxBlobsPerTx > maxBlobsPerBlock) { LOG.warn( @@ -58,7 +61,13 @@ public OsakaTargetingGasLimitCalculator( effectiveMaxBlobsPerTx = maxBlobsPerBlock; } this.transactionGasLimitCap = DEFAULT_TRANSACTION_GAS_LIMIT_CAP_OSAKA; - this.transactionBlobGasLimitCap = gasCalculator.getBlobGasPerBlob() * effectiveMaxBlobsPerTx; + this.transactionBlobGasLimitCap = blobGasPerBlob * effectiveMaxBlobsPerTx; + if (userMaxBlobsPerBlock.isPresent()) { + final int effectiveMax = Math.min(userMaxBlobsPerBlock.getAsInt(), maxBlobsPerBlock); + this.blockBuilderBlobGasLimit = blobGasPerBlob * effectiveMax; + } else { + this.blockBuilderBlobGasLimit = getMaxBlobGasPerBlock(); + } } @Override @@ -100,4 +109,9 @@ public long computeExcessBlobGas( public long transactionBlobGasLimitCap() { return transactionBlobGasLimitCap; } + + @Override + public long blockBuilderBlobGasLimit() { + return blockBuilderBlobGasLimit; + } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/CancunTargetingGasLimitCalculatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/CancunTargetingGasLimitCalculatorTest.java index f08f4084063..1e79e1cec69 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/CancunTargetingGasLimitCalculatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/CancunTargetingGasLimitCalculatorTest.java @@ -158,6 +158,7 @@ void shouldCalculateCorrectlyPragueBlobGasPerBlob() { osakaGasCalculator, BlobSchedule.PRAGUE_DEFAULT.getMax(), newTargetCount, + OptionalInt.empty(), OptionalInt.empty()); private static final long TARGET_BLOB_GAS_PER_BLOCK_OSAKA = 0x120000; diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java index c22acadae83..c1e65ca4b68 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java @@ -767,7 +767,7 @@ public void shouldSupportTransactionGasLimitCap_EIP_7825( createTransactionValidator( gasCalculator, new OsakaTargetingGasLimitCalculator( - 0L, feeMarket, gasCalculator, 6, 3, OptionalInt.of(6)), + 0L, feeMarket, gasCalculator, 6, 3, OptionalInt.of(6), OptionalInt.empty()), feeMarket, false, Optional.of(BigInteger.ONE), diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculatorTest.java index 175183de6bb..99c73d07310 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculatorTest.java @@ -50,7 +50,13 @@ public void shouldCalculateOsakaExcessBlobGasCorrectly( int targetBlobs = 9; final OsakaTargetingGasLimitCalculator osakaTargetingGasLimitCalculator = new OsakaTargetingGasLimitCalculator( - 0L, feeMarket, osakaGasCalculator, maxBlobs, targetBlobs, OptionalInt.empty()); + 0L, + feeMarket, + osakaGasCalculator, + maxBlobs, + targetBlobs, + OptionalInt.empty(), + OptionalInt.empty()); final long usedBlobGas = osakaGasCalculator.blobGasCost(used); assertThat( @@ -91,7 +97,13 @@ void shouldUseCorrectBlobGasPerBlob() { int targetBlobs = 9; var osakaTargetingGasLimitCalculator = new OsakaTargetingGasLimitCalculator( - 0L, feeMarket, osakaGasCalculator, maxBlobs, targetBlobs, OptionalInt.empty()); + 0L, + feeMarket, + osakaGasCalculator, + maxBlobs, + targetBlobs, + OptionalInt.empty(), + OptionalInt.empty()); // if maxBlobs = 10, then the gas limit would be 131072 * 10 = 1310720 assertThat(osakaTargetingGasLimitCalculator.currentBlobGasLimit()) @@ -108,7 +120,13 @@ void testComputeExcessBlobGasWithDifferentConditions() { int targetBlobs = 9; var calculator = new OsakaTargetingGasLimitCalculator( - 0L, feeMarket, osakaGasCalculator, maxBlobs, targetBlobs, OptionalInt.empty()); + 0L, + feeMarket, + osakaGasCalculator, + maxBlobs, + targetBlobs, + OptionalInt.empty(), + OptionalInt.empty()); assertThat(calculator.maxBlobsPerBlock).isEqualTo(maxBlobs); assertThat(calculator.targetBlobsPerBlock).isEqualTo(targetBlobs); @@ -136,7 +154,13 @@ void osakaBlobGasLimitPerTransaction() { int targetBlobs = 9; var calculator = new OsakaTargetingGasLimitCalculator( - 0L, feeMarket, osakaGasCalculator, maxBlobs, targetBlobs, OptionalInt.empty()); + 0L, + feeMarket, + osakaGasCalculator, + maxBlobs, + targetBlobs, + OptionalInt.empty(), + OptionalInt.empty()); assertThat(calculator.transactionBlobGasLimitCap()).isEqualTo(0xC0000); // 6 * 131072 } @@ -152,7 +176,8 @@ void osakaBlobGasLimitPerTransactionWithCustomValue() { osakaGasCalculator, maxBlobs, targetBlobs, - OptionalInt.of(customMaxBlobsPerTx)); + OptionalInt.of(customMaxBlobsPerTx), + OptionalInt.empty()); // 3 * 131072 = 393216 assertThat(calculator.transactionBlobGasLimitCap()) .isEqualTo(osakaGasCalculator.getBlobGasPerBlob() * customMaxBlobsPerTx); @@ -170,12 +195,66 @@ void maxBlobsPerTransactionClampedToMaxBlobsPerBlock() { osakaGasCalculator, maxBlobsPerBlock, targetBlobs, - OptionalInt.of(tooManyBlobsPerTx)); + OptionalInt.of(tooManyBlobsPerTx), + OptionalInt.empty()); // Should be clamped to maxBlobsPerBlock (10), not the user value (15) assertThat(calculator.transactionBlobGasLimitCap()) .isEqualTo(osakaGasCalculator.getBlobGasPerBlob() * maxBlobsPerBlock); } + @Test + void blockBuilderBlobGasLimitDefaultsToProtocolMax() { + int maxBlobs = 9; + int targetBlobs = 6; + var calculator = + new OsakaTargetingGasLimitCalculator( + 0L, + feeMarket, + osakaGasCalculator, + maxBlobs, + targetBlobs, + OptionalInt.empty(), + OptionalInt.empty()); + assertThat(calculator.blockBuilderBlobGasLimit()) + .isEqualTo(osakaGasCalculator.getBlobGasPerBlob() * maxBlobs); + } + + @Test + void blockBuilderBlobGasLimitRespectedWhenUserConfigured() { + int maxBlobs = 9; + int targetBlobs = 6; + int userMaxBlobs = 3; + var calculator = + new OsakaTargetingGasLimitCalculator( + 0L, + feeMarket, + osakaGasCalculator, + maxBlobs, + targetBlobs, + OptionalInt.empty(), + OptionalInt.of(userMaxBlobs)); + assertThat(calculator.blockBuilderBlobGasLimit()) + .isEqualTo(osakaGasCalculator.getBlobGasPerBlob() * userMaxBlobs); + } + + @Test + void blockBuilderBlobGasLimitClampedToProtocolMax() { + int maxBlobs = 9; + int targetBlobs = 6; + int userMaxBlobs = 20; // exceeds protocol max + var calculator = + new OsakaTargetingGasLimitCalculator( + 0L, + feeMarket, + osakaGasCalculator, + maxBlobs, + targetBlobs, + OptionalInt.empty(), + OptionalInt.of(userMaxBlobs)); + assertThat(calculator.blockBuilderBlobGasLimit()) + .isEqualTo(osakaGasCalculator.getBlobGasPerBlob() * maxBlobs); + } + @Test void dryRunDetector() { Assertions.assertThat(true) From 5218d76f4a120f5bbf6c678e1b23c33bc104fd77 Mon Sep 17 00:00:00 2001 From: Justin Florentine Date: Tue, 10 Mar 2026 13:57:25 -0400 Subject: [PATCH 16/77] Update GitHub repository references from hyperledger/besu to besu-eth/besu (#10015) Prepare for org migration by updating all GitHub repository references to the new besu-eth organization. External service URLs (Docker Hub, Artifactory, docs site, homebrew, wiki, email) are intentionally left unchanged for a separate rebranding phase. Closes #9911 (Phase 1 final step) Signed-off-by: jflo Co-authored-by: Claude Opus 4.6 (1M context) Signed-off-by: Justin Florentine --- .github/ISSUE_TEMPLATE/bug-report.md | 4 ++-- .github/ISSUE_TEMPLATE/feature-request.md | 4 ++-- .github/ISSUE_TEMPLATE/release-checklist.md | 8 ++++---- .github/issue_template.md | 4 ++-- .github/pull_request_template.md | 2 +- .github/workflows/bft-soak-test.yml | 2 +- .github/workflows/update-test-reports.yml | 4 ++-- CHANGELOG.md | 3 +++ CONTRIBUTING.md | 2 +- KNOWN_ISSUES.md | 2 +- MAINTAINERS.md | 2 +- README.md | 10 +++++----- SUPPORT.md | 2 +- build.gradle | 8 ++++---- docker/Dockerfile | 2 +- ethereum/evmtool/src/main/docker/Dockerfile | 2 +- platform/build.gradle | 8 ++++---- 17 files changed, 36 insertions(+), 33 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 7b88a843790..448a314334a 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -10,10 +10,10 @@ assignees: '' - + - + ### Steps to Reproduce 1. [Step 1] diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 73ebe5f060e..b79b93ad097 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -10,8 +10,8 @@ assignees: '' - - + + ### Description As an [Actor], I want [feature] so that [why]. diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md index 4a7a968c781..6e1696eab01 100644 --- a/.github/ISSUE_TEMPLATE/release-checklist.md +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -17,7 +17,7 @@ assignees: '' - [ ] `git tag 24.4.0-RC1` - [ ] `git push upstream 24.4.0-RC1` - [ ] Sign-off with team; announce the tag in #besu-release in Discord - - [ ] Targeting this tag for the burn-in: https://github.com/hyperledger/besu/releases/tag/24.4.0-RC1 + - [ ] Targeting this tag for the burn-in: https://github.com/besu-eth/besu/releases/tag/24.4.0-RC1 - [ ] Consensys staff start burn-in using this tag - [ ] start a new burn-in fleet with comparison nodes from previous release - [ ] update existing canaries and validators @@ -34,18 +34,18 @@ assignees: '' - [ ] `git checkout 24.4.0-RC1` - [ ] `git tag 24.4.0` - [ ] `git push upstream 24.4.0` -- [ ] Manually run https://github.com/hyperledger/besu/actions/workflows/draft-release.yml using `main` branch` and the FULL RELEASE tag name, i.e. `24.4.0`. Note, this workflow should always be run from `main` branch (hotfix tags will still be released even if they were created based on another branch) +- [ ] Manually run https://github.com/besu-eth/besu/actions/workflows/draft-release.yml using `main` branch` and the FULL RELEASE tag name, i.e. `24.4.0`. Note, this workflow should always be run from `main` branch (hotfix tags will still be released even if they were created based on another branch) - publishes artefacts and version-specific docker tags but does not fully publish the GitHub release so subscribers are not yet notified - [ ] Check all draft-release workflow jobs went green - [ ] Check binary SHAs are correct on the release page - [ ] Check artifacts exist in https://hyperledger.jfrog.io/ui/repos/tree/General/besu-maven - [ ] Update release notes in the GitHub draft release, save draft and sign-off with team -- [ ] IMPORTANT: confirm the tag name is the ONLY text in the "Release title", otherwise it will break the Docker Promote workflow https://github.com/hyperledger/besu/actions/workflows/docker-promote.yml +- [ ] IMPORTANT: confirm the tag name is the ONLY text in the "Release title", otherwise it will break the Docker Promote workflow https://github.com/besu-eth/besu/actions/workflows/docker-promote.yml - [ ] Publish draft release ensuring it is marked as latest release (if appropriate) - this is now public and notifies subscribed users - makes the release "latest" in github - publishes the docker `latest` tag variants -- [ ] Verify https://github.com/hyperledger/besu/actions/workflows/docker-promote.yml went green +- [ ] Verify https://github.com/besu-eth/besu/actions/workflows/docker-promote.yml went green - [ ] Create homebrew release PR using [update-version workflow](https://github.com/hyperledger/homebrew-besu/actions/workflows/update-version.yml) - If the PR has not been automatically created, create the PR manually using the created branch `update-` - [ ] Verify homebrew release once the PR has merged using `brew tap hyperledger/besu && brew install besu` on MacOSX to verify latest version has been installed diff --git a/.github/issue_template.md b/.github/issue_template.md index 7fe82b4f078..1cbd7e094c0 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -1,8 +1,8 @@ - - + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index df4f511263c..780f8105f6e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,7 +8,7 @@ ### Thanks for sending a pull request! Have you done the following? -- [ ] Checked out our [contribution guidelines](https://github.com/hyperledger/besu/blob/main/CONTRIBUTING.md)? +- [ ] Checked out our [contribution guidelines](https://github.com/besu-eth/besu/blob/main/CONTRIBUTING.md)? - [ ] Considered documentation and added the `doc-change-required` label to this PR [if updates are required](https://wiki.hyperledger.org/display/BESU/Documentation). - [ ] Considered the changelog and included an [update if required](https://wiki.hyperledger.org/display/BESU/Changelog). - [ ] For database changes (e.g. KeyValueSegmentIdentifier) considered compatibility and performed forwards and backwards compatibility tests diff --git a/.github/workflows/bft-soak-test.yml b/.github/workflows/bft-soak-test.yml index 0a851d42436..72b3d790e15 100644 --- a/.github/workflows/bft-soak-test.yml +++ b/.github/workflows/bft-soak-test.yml @@ -24,7 +24,7 @@ on: besu_repo: required: false type: string - description: 'Custom git repository to checkout branch from. Expected format: owner/repo (e.g., hyperledger/besu)' + description: 'Custom git repository to checkout branch from. Expected format: owner/repo (e.g., besu-eth/besu)' env: GRADLE_OPTS: "-Xmx6g -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true" diff --git a/.github/workflows/update-test-reports.yml b/.github/workflows/update-test-reports.yml index 585d7aae655..904721eeaca 100644 --- a/.github/workflows/update-test-reports.yml +++ b/.github/workflows/update-test-reports.yml @@ -7,12 +7,12 @@ on: jobs: syncTestReports: - if: github.repository == 'hyperledger/besu' + if: github.repository == 'besu-eth/besu' runs-on: ubuntu-latest steps: - name: Get latest merge PR number id: latest_merged_pr_number - run: echo "PULL_REQUEST_NUMBER=$(gh pr list --repo hyperledger/besu --base main --state merged --json "number,mergedAt" --search "sort:updated-desc" --jq 'max_by(.mergedAt)|.number')" >> "$GITHUB_OUTPUT" + run: echo "PULL_REQUEST_NUMBER=$(gh pr list --repo besu-eth/besu --base main --state merged --json "number,mergedAt" --search "sort:updated-desc" --jq 'max_by(.mergedAt)|.number')" >> "$GITHUB_OUTPUT" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Get unit test reports from latest merged PR diff --git a/CHANGELOG.md b/CHANGELOG.md index 336b6001353..7abb77ce95c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Repository Migration +- The Besu repository has moved from `hyperledger/besu` to `besu-eth/besu`. GitHub automatically redirects all existing links from the old location. + ### Breaking Changes - Clique consensus has been removed. Besu can no longer start or mine on pure Clique networks. Syncing networks that started as Clique and have since transitioned to PoS via `terminalTotalDifficulty` (e.g. Linea Mainnet) are still supported. [#9852](https://github.com/hyperledger/besu/pull/9852) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9dc350b04fd..34cc2b74784 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ Welcome to the Besu repository! The following links are a set of guidelines for Having the following accounts is necessary for contributing code/issues to Besu. * If you want to contribute code, you can make a [github account here](https://github.com). -* If you want to raise an issue, do so [in the issues tab](https://github.com/hyperledger/besu/issues). +* If you want to raise an issue, do so [in the issues tab](https://github.com/besu-eth/besu/issues). * To ask questions or chat with us, join our [Discord](https://discord.com/invite/hyperledger) * To edit pages in our wiki, you'll need a [Linux Foundation (LF) account]. diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 47c17ea149a..2ed18d76ab9 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -22,7 +22,7 @@ This behaviour has been seen on AWS and Digital Ocean. Workaround -> On AWS, a full restart of the AWS VM is required to restart the fast sync. Fast sync is not currently supported on Digital Ocean. We are investigating options to -[add support for fast sync on Digital Ocean](https://github.com/hyperledger/besu/issues/591). +[add support for fast sync on Digital Ocean](https://github.com/besu-eth/besu/issues/591). ## Privacy users with private transactions created using v1.3.4 or earlier diff --git a/MAINTAINERS.md b/MAINTAINERS.md index fb85b9f61dd..0ee6165c88a 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -148,7 +148,7 @@ I propose to add [maintainer github handle] as a Besu project maintainer. - [list significant achievements] -Here are [their past contributions on Besu project](https://github.com/hyperledger/besu/commits?author=[user github handle]). +Here are [their past contributions on Besu project](https://github.com/besu-eth/besu/commits?author=[user github handle]). Voting ends two weeks from today. diff --git a/README.md b/README.md index 6a254b901ca..a050f05d2d3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Besu Ethereum Client - [![CircleCI](https://circleci.com/gh/hyperledger/besu/tree/main.svg?style=svg)](https://circleci.com/gh/hyperledger/besu/tree/main) - [![CodeQL](https://github.com/hyperledger/besu/actions/workflows/codeql.yml/badge.svg)](https://github.com/hyperledger/besu/actions/workflows/codeql.yml) + [![CircleCI](https://circleci.com/gh/besu-eth/besu/tree/main.svg?style=svg)](https://circleci.com/gh/besu-eth/besu/tree/main) + [![CodeQL](https://github.com/besu-eth/besu/actions/workflows/codeql.yml/badge.svg)](https://github.com/besu-eth/besu/actions/workflows/codeql.yml) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3174/badge)](https://bestpractices.coreinfrastructure.org/projects/3174) - [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/hyperledger/besu/blob/main/LICENSE) + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/besu-eth/besu/blob/main/LICENSE) [![Discord](https://img.shields.io/discord/905194001349627914?logo=Hyperledger&style=plastic)](https://discord.com/invite/hyperledger) [![Twitter Follow](https://img.shields.io/twitter/follow/HyperledgerBesu)](https://twitter.com/HyperledgerBesu) -[Download](https://github.com/hyperledger/besu/releases) +[Download](https://github.com/besu-eth/besu/releases) Besu is an Apache 2.0 licensed, MainNet compatible, Ethereum client written in Java. @@ -66,7 +66,7 @@ Besu includes support for running Ethereum reference tests and generating detail To learn how to run the tests and enable opcode-level JSON tracing for debugging and correctness verification, see the [Reference Test Execution and Tracing Guide](REFERENCE_TESTS.md). -[Besu Issues]: https://github.com/hyperledger/besu/issues +[Besu Issues]: https://github.com/besu-eth/besu/issues [Besu User Documentation]: https://besu.hyperledger.org [Besu channel on Discord]: https://discord.com/invite/hyperledger [Contributing Guidelines]: CONTRIBUTING.md diff --git a/SUPPORT.md b/SUPPORT.md index 97715857cb4..34b9e8916ab 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -5,7 +5,7 @@ Welcome to the Besu repository! The following links are a set of guidelines for ### Github/Discord/LF Accounts Having Github, Discord, and Linux Foundation accounts is necessary for obtaining support for Besu through the community channels, wiki and issue management. -* If you want to raise an issue, you can do so [on the github issue tab](https://github.com/hyperledger/besu/issues). +* If you want to raise an issue, you can do so [on the github issue tab](https://github.com/besu-eth/besu/issues). * Discord requires a [Discord account]. * The Besu wiki also requires a [Linux Foundation (LF) account] in order to edit pages. diff --git a/build.gradle b/build.gradle index fc9b42d371d..224b67f8aa9 100644 --- a/build.gradle +++ b/build.gradle @@ -575,7 +575,7 @@ subprojects { } pom { name = "Besu - ${project.name}" - url = 'http://github.com/hyperledger/besu' + url = 'http://github.com/besu-eth/besu' licenses { license { name = 'The Apache License, Version 2.0' @@ -583,9 +583,9 @@ subprojects { } } scm { - connection = 'scm:git:git://github.com/hyperledger/besu.git' - developerConnection = 'scm:git:ssh://github.com/hyperledger/besu.git' - url = 'https://github.com/hyperledger/besu' + connection = 'scm:git:git://github.com/besu-eth/besu.git' + developerConnection = 'scm:git:ssh://github.com/besu-eth/besu.git' + url = 'https://github.com/besu-eth/besu' } } } diff --git a/docker/Dockerfile b/docker/Dockerfile index 1049583e268..736da30caaa 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -78,7 +78,7 @@ LABEL org.opencontainers.image.title="Besu" \ org.opencontainers.image.description="Enterprise Ethereum client" \ org.opencontainers.image.version=$VERSION \ org.opencontainers.image.url="https://besu.hyperledger.org/" \ - org.opencontainers.image.source="https://github.com/hyperledger/besu.git" \ + org.opencontainers.image.source="https://github.com/besu-eth/besu.git" \ org.opencontainers.image.revision=$VCS_REF \ org.opencontainers.image.vendor="Hyperledger" \ org.opencontainers.image.created=$BUILD_DATE \ diff --git a/ethereum/evmtool/src/main/docker/Dockerfile b/ethereum/evmtool/src/main/docker/Dockerfile index bade899d63e..9703130e45d 100644 --- a/ethereum/evmtool/src/main/docker/Dockerfile +++ b/ethereum/evmtool/src/main/docker/Dockerfile @@ -44,7 +44,7 @@ LABEL org.label-schema.build-date=$BUILD_DATE \ org.label-schema.description="EVM Execution Tool" \ org.label-schema.url="https://besu.hyperledger.org/" \ org.label-schema.vcs-ref=$VCS_REF \ - org.label-schema.vcs-url="https://github.com/hyperledger/besu.git" \ + org.label-schema.vcs-url="https://github.com/besu-eth/besu.git" \ org.label-schema.vendor="Hyperledger" \ org.label-schema.version=$VERSION \ org.label-schema.schema-version="1.0" \ No newline at end of file diff --git a/platform/build.gradle b/platform/build.gradle index 384da7562db..6070878141f 100644 --- a/platform/build.gradle +++ b/platform/build.gradle @@ -209,7 +209,7 @@ publishing { pom { name = "Besu BOM" - url = 'http://github.com/hyperledger/besu' + url = 'https://github.com/besu-eth/besu' licenses { license { name = 'The Apache License, Version 2.0' @@ -217,9 +217,9 @@ publishing { } } scm { - connection = 'scm:git:git://github.com/hyperledger/besu.git' - developerConnection = 'scm:git:ssh://github.com/hyperledger/besu.git' - url = 'https://github.com/hyperledger/besu' + connection = 'scm:git:git://github.com/besu-eth/besu.git' + developerConnection = 'scm:git:ssh://github.com/besu-eth/besu.git' + url = 'https://github.com/besu-eth/besu' } withXml { // Workaround since Gradle does not natively support adding classifiers in BOM From 6d94b5812e5277c0fd0099632c21221ed061a926 Mon Sep 17 00:00:00 2001 From: Justin Florentine Date: Tue, 10 Mar 2026 14:34:07 -0400 Subject: [PATCH 17/77] 26.3.0-rc0 prep (#10017) * Update GitHub repository references from hyperledger/besu to besu-eth/besu Prepare for org migration by updating all GitHub repository references to the new besu-eth organization. External service URLs (Docker Hub, Artifactory, docs site, homebrew, wiki, email) are intentionally left unchanged for a separate rebranding phase. Closes #9911 (Phase 1 final step) Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: jflo * Update platform/build.gradle Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Justin Florentine * updated changelog and version Signed-off-by: jflo --------- Signed-off-by: jflo Signed-off-by: Justin Florentine Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7abb77ce95c..0a68d0b12a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -## Unreleased +## 26.3.0 + +### Repository Migration +- The Besu repository has moved from `hyperledger/besu` to `besu-eth/besu`. GitHub automatically redirects all existing links from the old location. ### Repository Migration - The Besu repository has moved from `hyperledger/besu` to `besu-eth/besu`. GitHub automatically redirects all existing links from the old location. From 8ac78cf148f23afe0f9218ecc2f41dbec5356df2 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 11 Mar 2026 09:43:06 +1000 Subject: [PATCH 18/77] enable local gradle caching (#10008) * enable gradle caching and configureondemand Signed-off-by: Sally MacFarlane * comment Signed-off-by: Sally MacFarlane * add comment and remove configureondemand Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane --- gradle.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gradle.properties b/gradle.properties index e4d05fdb629..0943e3fb977 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,8 @@ org.gradle.welcome=never org.gradle.parallel=true +# caching is disabled in .github/workflow configs. enabling it here allows developers to utilize LOCAL caches +# use --no-build-cache or --rerun-tasks to bypass if needed. +org.gradle.caching=true # Optional - set custom build version # version=24.5.6-acme From f922aa2feafa12781dc3327f9bd5a862e4c3e592 Mon Sep 17 00:00:00 2001 From: Simon Dudley Date: Wed, 11 Mar 2026 11:49:49 +1000 Subject: [PATCH 19/77] Use existing RLP methods in trie log prune batch files (#10009) * feat: replace Java serialization with RLP in trie log prune batch files Replace saveTrieLogsInFile/readTrieLogsFromFile with the existing RLP-based saveTrieLogsAsRlpInFile/readTrieLogsAsRlpFromFile methods. Signed-off-by: Simon Dudley --- .../subcommands/storage/TrieLogHelper.java | 62 ++----------------- .../storage/TrieLogHelperTest.java | 41 +++++++++++- 2 files changed, 44 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelper.java b/app/src/main/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelper.java index 635c4c52de8..3067c6736eb 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelper.java +++ b/app/src/main/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelper.java @@ -33,11 +33,7 @@ import org.hyperledger.besu.ethereum.worldstate.PathBasedExtraStorageConfiguration; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; @@ -153,12 +149,7 @@ private void saveTrieLogBatches( final PathBasedWorldStateKeyValueStorage rootWorldStateStorage, final List trieLogKeys) { - try { - saveTrieLogsInFile(trieLogKeys, rootWorldStateStorage, batchFileName); - } catch (IOException e) { - LOG.error("Error saving trie logs to file: {}", e.getMessage()); - throw new RuntimeException(e); - } + saveTrieLogsAsRlpInFile(trieLogKeys, rootWorldStateStorage, batchFileName); } private void restoreTrieLogBatches( @@ -166,13 +157,8 @@ private void restoreTrieLogBatches( final long batchNumber, final String batchFileNameBase) { - try { - LOG.info("Restoring trie logs retained from batch {}...", batchNumber); - recreateTrieLogs(rootWorldStateStorage, batchNumber, batchFileNameBase); - } catch (IOException e) { - LOG.error("Error recreating trie logs from batch {}: {}", batchNumber, e.getMessage()); - throw new RuntimeException(e); - } + LOG.info("Restoring trie logs retained from batch {}...", batchNumber); + recreateTrieLogs(rootWorldStateStorage, batchNumber, batchFileNameBase); } private boolean deleteFiles(final String batchFileNameBase, final long numberOfBatches) { @@ -257,11 +243,10 @@ private boolean validatePruneRequirements( private void recreateTrieLogs( final PathBasedWorldStateKeyValueStorage rootWorldStateStorage, final long batchNumber, - final String batchFileNameBase) - throws IOException { + final String batchFileNameBase) { // process in chunk to avoid OOM final String batchFileName = batchFileNameBase + "-" + batchNumber; - IdentityHashMap trieLogsToRetain = readTrieLogsFromFile(batchFileName); + IdentityHashMap trieLogsToRetain = readTrieLogsAsRlpFromFile(batchFileName); final int chunkSize = ROCKSDB_MAX_INSERTS_PER_TRANSACTION; List keys = new ArrayList<>(trieLogsToRetain.keySet()); @@ -314,43 +299,6 @@ void validatePruneConfiguration(final DataStorageConfiguration config) { subStorageConfiguration.getMaxLayersToLoad())); } - private void saveTrieLogsInFile( - final List trieLogsKeys, - final PathBasedWorldStateKeyValueStorage rootWorldStateStorage, - final String batchFileName) - throws IOException { - - File file = new File(batchFileName); - if (file.exists()) { - LOG.warn("File already exists {}, skipping file creation", batchFileName); - return; - } - - try (FileOutputStream fos = new FileOutputStream(file)) { - ObjectOutputStream oos = new ObjectOutputStream(fos); - oos.writeObject(getTrieLogs(trieLogsKeys, rootWorldStateStorage)); - } catch (IOException e) { - LOG.error(e.getMessage()); - throw new RuntimeException(e); - } - } - - @SuppressWarnings("unchecked") - IdentityHashMap readTrieLogsFromFile(final String batchFileName) { - - IdentityHashMap trieLogs; - try (FileInputStream fis = new FileInputStream(batchFileName); - ObjectInputStream ois = new ObjectInputStream(fis)) { - - trieLogs = (IdentityHashMap) ois.readObject(); - } catch (IOException | ClassNotFoundException e) { - LOG.error(e.getMessage()); - throw new RuntimeException(e); - } - - return trieLogs; - } - private void saveTrieLogsAsRlpInFile( final List trieLogsKeys, final PathBasedWorldStateKeyValueStorage rootWorldStateStorage, diff --git a/app/src/test/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelperTest.java b/app/src/test/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelperTest.java index b1aa8b77050..726c23baf2d 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelperTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelperTest.java @@ -38,7 +38,6 @@ import org.hyperledger.besu.ethereum.worldstate.ImmutablePathBasedExtraStorageConfiguration; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; -import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -128,6 +127,44 @@ void mockBlockchainBase() { when(blockchain.getBlockHeader(any(Hash.class))).thenReturn(Optional.of(blockHeader3)); } + @Test + public void pruneFailsWhenBatchFileContainsJavaSerialization(final @TempDir Path dataDir) + throws IOException { + Files.createDirectories(dataDir.resolve("database")); + + DataStorageConfiguration dataStorageConfiguration = + ImmutableDataStorageConfiguration.builder() + .dataStorageFormat(BONSAI) + .pathBasedExtraStorageConfiguration( + ImmutablePathBasedExtraStorageConfiguration.builder() + .maxLayersToLoad(3L) + .limitTrieLogsEnabled(true) + .build()) + .build(); + + mockBlockchainBase(); + when(blockchain.getBlockHeader(5)).thenReturn(Optional.of(blockHeader5)); + when(blockchain.getBlockHeader(4)).thenReturn(Optional.of(blockHeader4)); + when(blockchain.getBlockHeader(3)).thenReturn(Optional.of(blockHeader3)); + + // Pre-place a Java-serialized file at the expected batch file path. + // saveTrieLogsAsRlpInFile's exists-check will skip overwriting it. + // readTrieLogsAsRlpFromFile will then fail to parse it as RLP during restore, + // confirming the code no longer silently deserializes Java-serialized objects. + Path batchFile = dataDir.resolve("database").resolve("trieLogsToRetain-1"); + try (var oos = + new java.io.ObjectOutputStream(new java.io.FileOutputStream(batchFile.toFile()))) { + oos.writeObject("notrlp"); + } + + assertThatThrownBy( + () -> + nonValidatingTrieLogHelper.prune( + dataStorageConfiguration, inMemoryWorldState, blockchain, dataDir)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("RLP"); + } + @Test public void prune(final @TempDir Path dataDir) throws IOException { Files.createDirectories(dataDir.resolve("database")); @@ -388,7 +425,7 @@ public void exceptionWhileSavingFileStopsPruneProcess(final @TempDir Path dataDi blockchain, dataDir.resolve("unknownPath"))) .isInstanceOf(RuntimeException.class) - .hasCauseExactlyInstanceOf(FileNotFoundException.class); + .hasCauseInstanceOf(java.io.IOException.class); // assert all trie logs are still in the DB assertThat(inMemoryWorldState.getTrieLog(blockHeader1.getHash()).get()) From de2a87de35679e013b3fac56cb90c0a1da1e8f8b Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Mar 2026 12:50:21 +1000 Subject: [PATCH 20/77] Add compressed ECDH key agreement to SecurityModule (#10007) Add `calculateECDHKeyAgreementCompressed` to the `SecurityModule` plugin API and its implementations, returning the full SEC1 compressed EC point (33 bytes) instead of just the x-coordinate. This is needed by the DiscV5 handshake which requires the compressed shared secret. Changes: - Add `calculateECDHKeyAgreementCompressed` default method to `SecurityModule` - Implement in `KeyPairSecurityModule` and expose via `NodeKey` - Extract shared `ecdhScalarMultiply` helper in `AbstractSECP256` to eliminate duplication between the x-only and compressed ECDH variants - Use `ECAlgorithms.cleanPoint` for EC point validation (curve membership check) - Add unit tests for ECDH key agreement across SECP256K1, SECP256R1, KeyPairSecurityModule, and NodeKey - Update plugin-api known checksum --------- Signed-off-by: Usman Saleem --- .../besu/crypto/AbstractSECP256.java | 35 ++++++++---- .../besu/crypto/SignatureAlgorithm.java | 15 ++++++ .../besu/crypto/SECP256K1Test.java | 53 +++++++++++++++++++ .../besu/crypto/SECP256R1Test.java | 52 ++++++++++++++++++ .../cryptoservices/KeyPairSecurityModule.java | 15 +++++- .../besu/cryptoservices/NodeKey.java | 20 ++++++- .../KeyPairSecurityModuleTest.java | 42 +++++++++++++++ .../besu/cryptoservices/NodeKeyTest.java | 38 +++++++++++-- plugin-api/build.gradle | 2 +- .../securitymodule/SecurityModule.java | 21 ++++++++ 10 files changed, 275 insertions(+), 18 deletions(-) diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java index 2098dfeadc1..5cc4c6e110f 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java @@ -31,7 +31,6 @@ import org.bouncycastle.asn1.sec.SECNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.asn1.x9.X9IntegerConverter; -import org.bouncycastle.crypto.agreement.ECDHBasicAgreement; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; @@ -142,18 +141,34 @@ public SECPSignature normaliseSignature( @Override public Bytes32 calculateECDHKeyAgreement( final SECPPrivateKey privKey, final SECPPublicKey theirPubKey) { - checkArgument(privKey != null, "missing private key"); - checkArgument(theirPubKey != null, "missing remote public key"); + final ECPoint point = ecdhScalarMultiply(privKey, theirPubKey); + return UInt256.valueOf(point.getAffineXCoord().toBigInteger()); + } - final ECPrivateKeyParameters privKeyP = new ECPrivateKeyParameters(privKey.getD(), curve); - final ECPublicKeyParameters pubKeyP = - new ECPublicKeyParameters(theirPubKey.asEcPoint(curve), curve); + @Override + public Bytes calculateECDHKeyAgreementCompressed( + final SECPPrivateKey privKey, final SECPPublicKey theirPubKey) { + final ECPoint point = ecdhScalarMultiply(privKey, theirPubKey); + return Bytes.wrap(point.getEncoded(true)); + } - final ECDHBasicAgreement agreement = new ECDHBasicAgreement(); - agreement.init(privKeyP); - final BigInteger agreed = agreement.calculateAgreement(pubKeyP); + /** + * Performs the ECDH scalar multiplication: {@code theirPubKey × privKey}. Both the existing + * x-coordinate-only method and the compressed-point method delegate here. + * + *

This implementation assumes an elliptic-curve domain with cofactor h = 1, so no + * explicit cofactor adjustment is required (matching the behaviour of {@code ECDHBasicAgreement} + * when h = 1). + */ + private ECPoint ecdhScalarMultiply( + final SECPPrivateKey privKey, final SECPPublicKey theirPubKey) { + checkArgument(privKey != null, "missing private key"); + checkArgument(theirPubKey != null, "missing remote public key"); - return UInt256.valueOf(agreed); + final ECPoint cleaned = ECAlgorithms.cleanPoint(curve.getCurve(), theirPubKey.asEcPoint(curve)); + final ECPoint point = cleaned.multiply(privKey.getD()).normalize(); + checkArgument(!point.isInfinity(), "ECDH key agreement point is at infinity"); + return point; } @Override diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java index 9e43ec40b8a..0e650e2e775 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java @@ -119,6 +119,21 @@ SECPSignature normaliseSignature( */ Bytes32 calculateECDHKeyAgreement(final SECPPrivateKey privKey, final SECPPublicKey theirPubKey); + /** + * Calculate ECDH key agreement returning the resulting EC point in compressed format. + * + *

Unlike {@link #calculateECDHKeyAgreement(SECPPrivateKey, SECPPublicKey)} which returns only + * the x-coordinate, this method returns the full compressed EC point (33 bytes: prefix byte + + * x-coordinate). This is required by protocols such as DiscV5 which use the compressed point as + * input to HKDF key derivation. + * + * @param privKey the private key + * @param theirPubKey the remote party's public key + * @return the compressed EC point (33 bytes) + */ + Bytes calculateECDHKeyAgreementCompressed( + final SECPPrivateKey privKey, final SECPPublicKey theirPubKey); + /** * Gets half curve order. * diff --git a/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/SECP256K1Test.java b/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/SECP256K1Test.java index 28fb0fb4f1c..33807801bee 100644 --- a/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/SECP256K1Test.java +++ b/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/SECP256K1Test.java @@ -116,6 +116,59 @@ public void signatureVerification() { assertThat(secp256K1.verify(data, signature, keyPair.getPublicKey(), Hash::keccak256)).isTrue(); } + @Test + public void ecdhKeyAgreementIsSymmetric() { + final KeyPair kp1 = secp256K1.generateKeyPair(); + final KeyPair kp2 = secp256K1.generateKeyPair(); + + final Bytes32 secret1 = + secp256K1.calculateECDHKeyAgreement(kp1.getPrivateKey(), kp2.getPublicKey()); + final Bytes32 secret2 = + secp256K1.calculateECDHKeyAgreement(kp2.getPrivateKey(), kp1.getPublicKey()); + + assertThat(secret1).isEqualTo(secret2); + } + + @Test + public void ecdhKeyAgreementCompressedIsSymmetric() { + final KeyPair kp1 = secp256K1.generateKeyPair(); + final KeyPair kp2 = secp256K1.generateKeyPair(); + + final Bytes compressed1 = + secp256K1.calculateECDHKeyAgreementCompressed(kp1.getPrivateKey(), kp2.getPublicKey()); + final Bytes compressed2 = + secp256K1.calculateECDHKeyAgreementCompressed(kp2.getPrivateKey(), kp1.getPublicKey()); + + assertThat(compressed1).isEqualTo(compressed2); + } + + @Test + public void ecdhCompressedReturns33BytesWithCorrectPrefix() { + final KeyPair kp1 = secp256K1.generateKeyPair(); + final KeyPair kp2 = secp256K1.generateKeyPair(); + + final Bytes compressed = + secp256K1.calculateECDHKeyAgreementCompressed(kp1.getPrivateKey(), kp2.getPublicKey()); + + assertThat(compressed.size()).isEqualTo(33); + final byte prefix = compressed.get(0); + assertThat(prefix == 0x02 || prefix == 0x03).isTrue(); + } + + @Test + public void ecdhCompressedXCoordinateMatchesUncompressed() { + final KeyPair kp1 = secp256K1.generateKeyPair(); + final KeyPair kp2 = secp256K1.generateKeyPair(); + + final Bytes32 xOnly = + secp256K1.calculateECDHKeyAgreement(kp1.getPrivateKey(), kp2.getPublicKey()); + final Bytes compressed = + secp256K1.calculateECDHKeyAgreementCompressed(kp1.getPrivateKey(), kp2.getPublicKey()); + + // The x-coordinate in the compressed point (bytes 1-32) should match the uncompressed result + assertThat(compressed.slice(1, 32)).isEqualTo(xOnly); + } + @Test public void invalidFileThrowsInvalidKeyPairException() throws Exception { final File tempFile = Files.createTempFile(suiteName(), ".keypair").toFile(); diff --git a/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/SECP256R1Test.java b/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/SECP256R1Test.java index 22bd5c76512..8d644ec515d 100644 --- a/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/SECP256R1Test.java +++ b/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/SECP256R1Test.java @@ -164,6 +164,58 @@ void signatureGenerationVerificationAndPubKeyRecovery() { }); } + @Test + void ecdhKeyAgreementIsSymmetric() { + final KeyPair kp1 = secp256R1.generateKeyPair(); + final KeyPair kp2 = secp256R1.generateKeyPair(); + + final Bytes32 secret1 = + secp256R1.calculateECDHKeyAgreement(kp1.getPrivateKey(), kp2.getPublicKey()); + final Bytes32 secret2 = + secp256R1.calculateECDHKeyAgreement(kp2.getPrivateKey(), kp1.getPublicKey()); + + assertThat(secret1).isEqualTo(secret2); + } + + @Test + void ecdhKeyAgreementCompressedIsSymmetric() { + final KeyPair kp1 = secp256R1.generateKeyPair(); + final KeyPair kp2 = secp256R1.generateKeyPair(); + + final Bytes compressed1 = + secp256R1.calculateECDHKeyAgreementCompressed(kp1.getPrivateKey(), kp2.getPublicKey()); + final Bytes compressed2 = + secp256R1.calculateECDHKeyAgreementCompressed(kp2.getPrivateKey(), kp1.getPublicKey()); + + assertThat(compressed1).isEqualTo(compressed2); + } + + @Test + void ecdhCompressedReturns33BytesWithCorrectPrefix() { + final KeyPair kp1 = secp256R1.generateKeyPair(); + final KeyPair kp2 = secp256R1.generateKeyPair(); + + final Bytes compressed = + secp256R1.calculateECDHKeyAgreementCompressed(kp1.getPrivateKey(), kp2.getPublicKey()); + + assertThat(compressed.size()).isEqualTo(33); + final byte prefix = compressed.get(0); + assertThat(prefix == 0x02 || prefix == 0x03).isTrue(); + } + + @Test + void ecdhCompressedXCoordinateMatchesUncompressed() { + final KeyPair kp1 = secp256R1.generateKeyPair(); + final KeyPair kp2 = secp256R1.generateKeyPair(); + + final Bytes32 xOnly = + secp256R1.calculateECDHKeyAgreement(kp1.getPrivateKey(), kp2.getPublicKey()); + final Bytes compressed = + secp256R1.calculateECDHKeyAgreementCompressed(kp1.getPrivateKey(), kp2.getPublicKey()); + + assertThat(compressed.slice(1, 32)).isEqualTo(xOnly); + } + @Test void invalidFileThrowsInvalidKeyPairException() throws Exception { final File tempFile = Files.createTempFile(suiteName(), ".keypair").toFile(); diff --git a/crypto/services/src/main/java/org/hyperledger/besu/cryptoservices/KeyPairSecurityModule.java b/crypto/services/src/main/java/org/hyperledger/besu/cryptoservices/KeyPairSecurityModule.java index 97759b54c60..1b73e398e36 100644 --- a/crypto/services/src/main/java/org/hyperledger/besu/cryptoservices/KeyPairSecurityModule.java +++ b/crypto/services/src/main/java/org/hyperledger/besu/cryptoservices/KeyPairSecurityModule.java @@ -83,9 +83,22 @@ public Bytes32 calculateECDHKeyAgreement(final PublicKey partyKey) final SECPPublicKey secp256KPartyKey = signatureAlgorithm.createPublicKey(encodedECPoint); return signatureAlgorithm.calculateECDHKeyAgreement( keyPair.getPrivateKey(), secp256KPartyKey); + } catch (final Exception e) { + throw new SecurityModuleException("Unexpected error while calculating ECDH key agreement", e); + } + } + + @Override + public Bytes calculateECDHKeyAgreementCompressed(final PublicKey partyKey) + throws SecurityModuleException { + try { + final Bytes encodedECPoint = ECPointUtil.getEncodedBytes(partyKey.getW()); + final SECPPublicKey secp256KPartyKey = signatureAlgorithm.createPublicKey(encodedECPoint); + return signatureAlgorithm.calculateECDHKeyAgreementCompressed( + keyPair.getPrivateKey(), secp256KPartyKey); } catch (final Exception e) { throw new SecurityModuleException( - "Unexpected error while calculating ECDH Key Agreement: " + e.getMessage(), e); + "Unexpected error while calculating compressed ECDH key agreement", e); } } diff --git a/crypto/services/src/main/java/org/hyperledger/besu/cryptoservices/NodeKey.java b/crypto/services/src/main/java/org/hyperledger/besu/cryptoservices/NodeKey.java index 698336d3fe6..02c7615f257 100644 --- a/crypto/services/src/main/java/org/hyperledger/besu/cryptoservices/NodeKey.java +++ b/crypto/services/src/main/java/org/hyperledger/besu/cryptoservices/NodeKey.java @@ -20,8 +20,10 @@ import org.hyperledger.besu.crypto.SignatureAlgorithm; import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; import org.hyperledger.besu.plugin.services.securitymodule.SecurityModule; +import org.hyperledger.besu.plugin.services.securitymodule.data.PublicKey; import org.hyperledger.besu.plugin.services.securitymodule.data.Signature; +import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; /** The Node key. */ @@ -69,7 +71,21 @@ public SECPPublicKey getPublicKey() { * @return the bytes32 */ public Bytes32 calculateECDHKeyAgreement(final SECPPublicKey partyKey) { - return securityModule.calculateECDHKeyAgreement( - () -> ECPointUtil.fromBouncyCastleECPoint(signatureAlgorithm.publicKeyAsEcPoint(partyKey))); + return securityModule.calculateECDHKeyAgreement(asPluginPublicKey(partyKey)); + } + + /** + * Calculate ECDH key agreement returning the compressed EC point. + * + * @param partyKey the party key + * @return the compressed EC point (33 bytes) + */ + public Bytes calculateECDHKeyAgreementCompressed(final SECPPublicKey partyKey) { + return securityModule.calculateECDHKeyAgreementCompressed(asPluginPublicKey(partyKey)); + } + + private PublicKey asPluginPublicKey(final SECPPublicKey partyKey) { + return () -> + ECPointUtil.fromBouncyCastleECPoint(signatureAlgorithm.publicKeyAsEcPoint(partyKey)); } } diff --git a/crypto/services/src/test/java/org/hyperledger/besu/cryptoservices/KeyPairSecurityModuleTest.java b/crypto/services/src/test/java/org/hyperledger/besu/cryptoservices/KeyPairSecurityModuleTest.java index fac403e041d..ff3a285c170 100644 --- a/crypto/services/src/test/java/org/hyperledger/besu/cryptoservices/KeyPairSecurityModuleTest.java +++ b/crypto/services/src/test/java/org/hyperledger/besu/cryptoservices/KeyPairSecurityModuleTest.java @@ -25,6 +25,7 @@ import java.security.spec.ECPoint; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -33,6 +34,47 @@ public class KeyPairSecurityModuleTest { @TempDir public Path keyFile; + @Test + public void ecdhKeyAgreementCompressedIsSymmetric() throws IOException { + final KeyPair kp1 = KeyPairUtil.loadKeyPair(keyFile.resolve("key1")); + final KeyPair kp2 = KeyPairUtil.loadKeyPair(keyFile.resolve("key2")); + + final KeyPairSecurityModule module1 = new KeyPairSecurityModule(kp1); + final KeyPairSecurityModule module2 = new KeyPairSecurityModule(kp2); + + final Bytes compressed1 = + module1.calculateECDHKeyAgreementCompressed( + () -> + ECPointUtil.fromBouncyCastleECPoint( + SignatureAlgorithmFactory.getInstance() + .publicKeyAsEcPoint(kp2.getPublicKey()))); + final Bytes compressed2 = + module2.calculateECDHKeyAgreementCompressed( + () -> + ECPointUtil.fromBouncyCastleECPoint( + SignatureAlgorithmFactory.getInstance() + .publicKeyAsEcPoint(kp1.getPublicKey()))); + + Assertions.assertThat(compressed1).isEqualTo(compressed2); + Assertions.assertThat(compressed1.size()).isEqualTo(33); + } + + @Test + public void ecdhKeyAgreementCompressedXCoordinateMatchesUncompressed() throws IOException { + final KeyPair kp1 = KeyPairUtil.loadKeyPair(keyFile.resolve("key1")); + final KeyPair kp2 = KeyPairUtil.loadKeyPair(keyFile.resolve("key2")); + + final KeyPairSecurityModule module1 = new KeyPairSecurityModule(kp1); + final var partyKey = + ECPointUtil.fromBouncyCastleECPoint( + SignatureAlgorithmFactory.getInstance().publicKeyAsEcPoint(kp2.getPublicKey())); + + final Bytes32 xOnly = module1.calculateECDHKeyAgreement(() -> partyKey); + final Bytes compressed = module1.calculateECDHKeyAgreementCompressed(() -> partyKey); + + Assertions.assertThat(compressed.slice(1, 32)).isEqualTo(xOnly); + } + @Test public void validatePublicKeyFromECPointCanBeConstructed() throws IOException { final KeyPair keyPair = KeyPairUtil.loadKeyPair(keyFile.resolve("key")); diff --git a/crypto/services/src/test/java/org/hyperledger/besu/cryptoservices/NodeKeyTest.java b/crypto/services/src/test/java/org/hyperledger/besu/cryptoservices/NodeKeyTest.java index 261271aee9b..d3adb87165f 100644 --- a/crypto/services/src/test/java/org/hyperledger/besu/cryptoservices/NodeKeyTest.java +++ b/crypto/services/src/test/java/org/hyperledger/besu/cryptoservices/NodeKeyTest.java @@ -14,13 +14,14 @@ */ package org.hyperledger.besu.cryptoservices; +import static org.assertj.core.api.Assertions.assertThat; + import org.hyperledger.besu.crypto.KeyPair; import org.hyperledger.besu.crypto.SignatureAlgorithm; import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,6 +32,36 @@ public void resetInstance() { SignatureAlgorithmFactory.resetInstance(); } + @Test + public void ecdhKeyAgreementCompressedIsSymmetric() { + final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); + final KeyPair kp1 = signatureAlgorithm.generateKeyPair(); + final KeyPair kp2 = signatureAlgorithm.generateKeyPair(); + + final NodeKey nodeKey1 = new NodeKey(new KeyPairSecurityModule(kp1)); + final NodeKey nodeKey2 = new NodeKey(new KeyPairSecurityModule(kp2)); + + final Bytes compressed1 = nodeKey1.calculateECDHKeyAgreementCompressed(kp2.getPublicKey()); + final Bytes compressed2 = nodeKey2.calculateECDHKeyAgreementCompressed(kp1.getPublicKey()); + + assertThat(compressed1).isEqualTo(compressed2); + assertThat(compressed1.size()).isEqualTo(33); + } + + @Test + public void ecdhCompressedXCoordinateMatchesUncompressed() { + final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); + final KeyPair kp1 = signatureAlgorithm.generateKeyPair(); + final KeyPair kp2 = signatureAlgorithm.generateKeyPair(); + + final NodeKey nodeKey1 = new NodeKey(new KeyPairSecurityModule(kp1)); + + final Bytes32 xOnly = nodeKey1.calculateECDHKeyAgreement(kp2.getPublicKey()); + final Bytes compressed = nodeKey1.calculateECDHKeyAgreementCompressed(kp2.getPublicKey()); + + assertThat(compressed.slice(1, 32)).isEqualTo(xOnly); + } + @Test public void publicKeyShouldMatch() { final Bytes keyPairPubKey = @@ -45,10 +76,9 @@ public void publicKeyShouldMatch() { final KeyPairSecurityModule keyPairSecurityModule = new KeyPairSecurityModule(keyPair); final NodeKey nodeKey = new NodeKey(keyPairSecurityModule); - Assertions.assertThat(nodeKey.getPublicKey().getEncodedBytes()) + assertThat(nodeKey.getPublicKey().getEncodedBytes()) .isEqualByComparingTo(keyPair.getPublicKey().getEncodedBytes()); - Assertions.assertThat(nodeKey.getPublicKey().getEncodedBytes()) - .isEqualByComparingTo(keyPairPubKey); + assertThat(nodeKey.getPublicKey().getEncodedBytes()).isEqualByComparingTo(keyPairPubKey); } } diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index 7ad3a7826bc..ce21a880080 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -71,7 +71,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'Hqbx2morgfuBxVBLgq4bgvp/zy67rj2hGoEanLGB3CI=' + knownHash = '0EGHYlomDjcBMwQmKy9qhqGaLhcIxkWUthAGdYSTxNc=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/securitymodule/SecurityModule.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/securitymodule/SecurityModule.java index 471be58f0d8..6e2a9c82ee2 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/securitymodule/SecurityModule.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/securitymodule/SecurityModule.java @@ -18,6 +18,7 @@ import org.hyperledger.besu.plugin.services.securitymodule.data.PublicKey; import org.hyperledger.besu.plugin.services.securitymodule.data.Signature; +import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; /** @@ -53,4 +54,24 @@ public interface SecurityModule { * @throws SecurityModuleException if calculateECDHKeyAgreement fails */ Bytes32 calculateECDHKeyAgreement(PublicKey partyKey) throws SecurityModuleException; + + /** + * Perform ECDH key agreement returning the compressed EC point. + * + *

Returns the full compressed EC point (SEC1 compressed format: prefix byte + x-coordinate) + * from the ECDH scalar multiplication. This is required by protocols such as DiscV5 which use the + * compressed point as input keying material for HKDF key derivation. + * + *

The default implementation throws {@link SecurityModuleException}. Implementations that need + * to support DiscV5 must override this method. + * + * @param partyKey the key with which an agreement is to be created. + * @return the compressed EC point in SEC1 format + * @throws SecurityModuleException if the operation is not supported or fails + */ + default Bytes calculateECDHKeyAgreementCompressed(final PublicKey partyKey) + throws SecurityModuleException { + throw new SecurityModuleException( + "Compressed ECDH key agreement is not supported by this security module"); + } } From 8a3f79bae9178a4f4692e7f08afd9b31791c7ef3 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 11 Mar 2026 15:29:06 +1000 Subject: [PATCH 21/77] Delay chain download if no peers, and handle cancellation ex (#9979) * delay snap chain download if zero peers * handle cancellation eg if no peers Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane --- CHANGELOG.md | 1 + .../common/DownloadBackwardHeadersStep.java | 60 ++++++++++++------- .../SnapSyncChainDownloadPipelineFactory.java | 5 +- .../snapsync/SnapSyncChainDownloader.java | 16 +++++ .../DownloadBackwardHeadersStepTest.java | 54 +++++++++++++++++ .../snapsync/SnapSyncChainDownloaderTest.java | 40 +++++++++++++ 6 files changed, 151 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a68d0b12a4..0eaf22bbb71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ ### Bug fixes - BFT forks that change block period on time-based forks don't take effect [9681](https://github.com/hyperledger/besu/issues/9681) - Fix QBFT `RLPException` when decoding proposals from pre-26.1.0 nodes that do not include the `blockAccessList` field [#9977](https://github.com/hyperledger/besu/pull/9977) +- Wait for peers before starting chain download. Prevents an OutOfMemory (OOM) error when the node has zero peers [#9979](https://github.com/hyperledger/besu/pull/9979) ### Additions and Improvements - Add IPv6 dual-stack support for DiscV5 peer discovery (enabled via `--Xv5-discovery-enabled`): new `--p2p-host-ipv6`, `--p2p-interface-ipv6`, and `--p2p-port-ipv6` CLI options enable a second UDP discovery socket; `--p2p-ipv6-outbound-enabled` controls whether IPv6 is preferred for outbound connections when a peer advertises both address families [#9763](https://github.com/hyperledger/besu/pull/9763); RLPx now also binds a second TCP socket on the IPv6 interface so IPv6-only peers can establish connections [#9873](https://github.com/hyperledger/besu/pull/9873) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadBackwardHeadersStep.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadBackwardHeadersStep.java index fbe95051a52..59107b30aff 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadBackwardHeadersStep.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadBackwardHeadersStep.java @@ -27,6 +27,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -135,29 +136,42 @@ public CompletableFuture> apply(final Long startBlockNumber) { final int currentTaskId = taskId.getAndIncrement(); final List downloadedHeaders = new ArrayList<>(headersToRequest); final AtomicBoolean cancelled = new AtomicBoolean(false); - return ethScheduler - .scheduleServiceTask( - () -> - downloadAllHeaders( - currentTaskId, - 0, - startBlockNumber, - headersToRequest, - downloadedHeaders, - cancelled)) - .orTimeout(timeoutDuration.toMillis(), TimeUnit.MILLISECONDS) - .whenComplete( - (unused, throwable) -> { - if (throwable instanceof TimeoutException) { - cancelled.set(true); - LOG.trace( - "[{}] Timed out after {} ms while downloading {} backward headers from block {}", - currentTaskId, - timeoutDuration.toMillis(), - headersToRequest, - startBlockNumber); - } - }); + final CompletableFuture> result = + ethScheduler + .scheduleServiceTask( + () -> + downloadAllHeaders( + currentTaskId, + 0, + startBlockNumber, + headersToRequest, + downloadedHeaders, + cancelled)) + .orTimeout(timeoutDuration.toMillis(), TimeUnit.MILLISECONDS) + .whenComplete( + (unused, throwable) -> { + if (throwable instanceof TimeoutException) { + cancelled.set(true); + LOG.trace( + "[{}] Timed out after {} ms while downloading {} backward headers from block {}", + currentTaskId, + timeoutDuration.toMillis(), + headersToRequest, + startBlockNumber); + } + }); + // Stop the retry chain if the result itself is cancelled externally, + // e.g. by AsyncOperationProcessor.abort() when the pipeline is aborted. + // The whenComplete above is attached to the inner P1 future; cancelling + // the returned result (P3) does not complete P1, so we need a separate + // callback on the returned future to catch that case. + result.whenComplete( + (unused, throwable) -> { + if (throwable instanceof CancellationException) { + cancelled.set(true); + } + }); + return result; } /** diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloadPipelineFactory.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloadPipelineFactory.java index dae38d8b062..8e96b4b4107 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloadPipelineFactory.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloadPipelineFactory.java @@ -89,11 +89,12 @@ public Pipeline createBackwardHeaderDownloadPipeline(final ChainSyncState final long pivotBlockNumber = chainState.pivotBlockHeader().getNumber(); LOG.info( - "Creating backward header download pipeline from pivot={} down to lowest block={}, parallelism={}, batchSize={}", + "Creating backward header download pipeline from pivot={} down to lowest block={}, parallelism={}, batchSize={}, peers={}", pivotBlockNumber, anchorForHeaderDownload.getNumber(), downloaderParallelism, - headerRequestSize); + headerRequestSize, + ethContext.getEthPeers().peerCount()); final BackwardBlockNumberSource headerSource = new BackwardBlockNumberSource( diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloader.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloader.java index be8aa8628b9..88fff662814 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloader.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloader.java @@ -66,6 +66,7 @@ public class SnapSyncChainDownloader implements ChainDownloader, PivotUpdateListener, WorldStateHealFinishedListener { private static final Logger LOG = LoggerFactory.getLogger(SnapSyncChainDownloader.class); public static final int SMALL_DELAY_MILLISECONDS = 100; + static final int NO_PEER_RETRY_DELAY_MILLISECONDS = 5_000; private final SnapSyncChainDownloadPipelineFactory pipelineFactory; private final ProtocolSchedule protocolSchedule; @@ -481,6 +482,21 @@ private void attemptDownload(final CompletableFuture overallResult) { return; } + // Guard against starting an expensive 160-concurrent-future pipeline with no peers. + // With no peers every future immediately hits NO_PEER_AVAILABLE, spins for 60 s, and + // the pipeline restarts every 60 s accumulating scheduler/thread overhead indefinitely. + if (ethContext.getEthPeers().peerCount() == 0) { + LOG.debug( + "No peers available, deferring chain sync pipeline start for {} ms", + NO_PEER_RETRY_DELAY_MILLISECONDS); + ethContext + .getScheduler() + .scheduleFutureTask( + () -> attemptDownload(overallResult), + Duration.ofMillis(NO_PEER_RETRY_DELAY_MILLISECONDS)); + return; + } + performSingleDownloadCycle() .whenComplete( (downloadResult, error) -> { diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadBackwardHeadersStepTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadBackwardHeadersStepTest.java index 383b8953403..3045492cd26 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadBackwardHeadersStepTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadBackwardHeadersStepTest.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.after; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -54,7 +55,9 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -608,6 +611,57 @@ public void shouldNotRetryAfterTimeout() throws Exception { } } + @Test + @SuppressWarnings("unchecked") + public void shouldNotRetryAfterCancellation() throws Exception { + // The first request fails transiently, scheduling a retry after RETRY_DELAY (1 s). + // The pipeline cancels the future (as AsyncOperationProcessor.abort() does) before + // the retry fires. The cancelled flag must be set so the retry exits immediately + // without making a second peer request. + final EthScheduler realScheduler = new EthScheduler(1, 1, 1, new NoOpMetricsSystem()); + final EthContext realEthContext = mock(EthContext.class); + when(realEthContext.getScheduler()).thenReturn(realScheduler); + when(realEthContext.getPeerTaskExecutor()).thenReturn(peerTaskExecutor); + + try { + final DownloadBackwardHeadersStep step = + new DownloadBackwardHeadersStep( + protocolSchedule, realEthContext, 10, 0, Duration.ofMinutes(1)); + + // Use a latch so we can wait until the first execute() has actually run before + // cancelling. Without this, cancel() can race with the scheduler starting the + // service task, causing the initial execute() call to never happen and the + // verify() below to report WantedButNotInvoked. + final CountDownLatch firstCallLatch = new CountDownLatch(1); + doAnswer( + invocation -> { + firstCallLatch.countDown(); + return new PeerTaskExecutorResult<>( + Optional.empty(), PeerTaskExecutorResponseCode.NO_PEER_AVAILABLE, emptyList()); + }) + .when(peerTaskExecutor) + .execute(any(GetHeadersFromPeerTask.class)); + + final CompletableFuture> result = step.apply(100L); + + // Wait until the first execute() has run before cancelling, so we are guaranteed + // to be racing only the retry (scheduled 1 s later) and not the initial attempt. + assertThat(firstCallLatch.await(5, TimeUnit.SECONDS)) + .as("peerTaskExecutor.execute should be called within 5 s") + .isTrue(); + + // Simulate pipeline abort cancelling the future before the 1-second retry fires. + result.cancel(true); + assertThat(result.isCancelled()).isTrue(); + + // Wait 2 s — longer than the 1 s RETRY_DELAY — and confirm no second call was made. + verify(peerTaskExecutor, after(2000).times(1)).execute(any(GetHeadersFromPeerTask.class)); + } finally { + realScheduler.stop(); + realScheduler.awaitStop(); + } + } + private List createMockHeaders(final int count, final long startBlock) { final BlockHeaderFunctions bhf = new LocalBlockHeaderFunctions(); final List headers = new ArrayList<>(); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloaderTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloaderTest.java index 7420bc5c316..131044f8134 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloaderTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloaderTest.java @@ -17,13 +17,19 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.chain.MutableBlockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; import org.hyperledger.besu.ethereum.eth.manager.EthContext; +import org.hyperledger.besu.ethereum.eth.manager.EthPeers; import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.sync.common.ChainSyncState; import org.hyperledger.besu.ethereum.eth.sync.common.ChainSyncStateStorage; @@ -34,7 +40,9 @@ import org.hyperledger.besu.metrics.SyncDurationMetrics; import java.nio.file.Path; +import java.time.Duration; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,6 +58,7 @@ public class SnapSyncChainDownloaderTest { @Mock private ProtocolSchedule protocolSchedule; @Mock private ProtocolContext protocolContext; @Mock private EthContext ethContext; + @Mock private EthPeers ethPeers; @Mock private SyncState syncState; @Mock private SyncDurationMetrics syncDurationMetrics; @Mock private MutableBlockchain blockchain; @@ -72,6 +81,7 @@ public void setUp() { lenient().when(blockchain.getChainHeadBlockNumber()).thenReturn(500L); lenient().when(blockchain.getChainHeadHeader()).thenReturn(checkpointBlockHeader); lenient().when(ethContext.getScheduler()).thenReturn(scheduler); + lenient().when(ethContext.getEthPeers()).thenReturn(ethPeers); lenient() .when(blockchain.getGenesisBlockHeader()) .thenReturn(new BlockHeaderTestFixture().number(0).buildHeader()); @@ -152,6 +162,36 @@ public void shouldReceiveWorldStateHealFinished() { assertThatCode(downloader::onWorldStateHealFinished).doesNotThrowAnyException(); } + @Test + public void shouldDeferPipelineStartWhenNoPeersAvailable() { + when(ethPeers.peerCount()).thenReturn(0); + when(scheduler.scheduleFutureTask(any(Runnable.class), any(Duration.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + SnapSyncChainDownloader downloader = + new SnapSyncChainDownloader( + pipelineFactory, + protocolSchedule, + protocolContext, + ethContext, + syncState, + syncDurationMetrics, + pivotBlockHeader, + chainSyncStateStorage, + headerDownloader); + + downloader.start(); + + // No pipeline should be created when there are no peers + verify(pipelineFactory, never()).createBackwardHeaderDownloadPipeline(any()); + + // A retry should be scheduled with the no-peer delay, not the fast retry delay + verify(scheduler) + .scheduleFutureTask( + any(Runnable.class), + eq(Duration.ofMillis(SnapSyncChainDownloader.NO_PEER_RETRY_DELAY_MILLISECONDS))); + } + @Test public void shouldHandleStateTransitionFromInitialToHeadersComplete() { // Create and store initial state From aeea9f65ce24a2dc0c265d7152944ab917170d99 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Mar 2026 15:58:18 +1000 Subject: [PATCH 22/77] Add ENR v5 bootnodes support for DiscV5 discovery (#9970) Add ENR (enr:) bootnode support for DiscV5 discovery by introducing a new v5Bootnodes section in genesis config alongside the existing bootnodes (enode) section. Genesis config changes: - Add v5Bootnodes to config.discovery for ENR-format bootnodes - Add getV5BootNodes() to DiscoveryOptions to parse the new section - Populate mainnet.json with 17 ENR bootnodes and hoodi.json with 9 Bootnode parsing simplification: - Use --Xv5-discovery-enabled flag to determine expected format (ENR vs enode) instead of prefix detection heuristic - EthNetworkConfig reads bootnodes and v5Bootnodes independently - CLI --bootnodes overrides the active protocol's list and clears the unused protocol's list; genesis defaults keep both intact - Replace heuristic fallback in getBootnodeIdentifiers() with explicit discoveryV5Enabled flag check DiscV5 peer connection pipeline fixes: - Handle compressed public keys (33-byte SEC1) in NodeKeySigner.deriveECDHKeyAgreement - Fix candidatePeers() to use isListening() instead of isReadyForConnections() which requires DiscV4 bonding status - Filter out the local node record from streamLiveNodes() - Explicitly use secp256k1 for ENR identity scheme v4 Cleanup: - Remove dead MAINNET fallback null-checks in RunnerBuilder --------- Signed-off-by: Usman Saleem --- .../org/hyperledger/besu/RunnerBuilder.java | 19 +-- .../org/hyperledger/besu/cli/BesuCommand.java | 95 ++++++++++----- .../besu/cli/config/EthNetworkConfig.java | 32 ++--- .../hyperledger/besu/cli/BesuCommandTest.java | 52 +++++++-- .../cli/CascadingDefaultProviderTest.java | 12 +- .../besu/cli/config/EthNetworkConfigTest.java | 2 + .../besu/config/DiscoveryOptions.java | 27 +++++ config/src/main/resources/hoodi.json | 11 ++ config/src/main/resources/mainnet.json | 19 +++ .../besu/config/DiscoveryOptionsTest.java | 109 ++++++++++++++++++ .../p2p/config/DiscoveryConfiguration.java | 2 +- .../discv5/PeerDiscoveryAgentFactoryV5.java | 17 ++- .../discv5/PeerDiscoveryAgentV5.java | 10 +- .../p2p/discovery/dns/EthereumNodeRecord.java | 11 +- 14 files changed, 332 insertions(+), 86 deletions(-) create mode 100644 config/src/test/java/org/hyperledger/besu/config/DiscoveryOptionsTest.java diff --git a/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java b/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java index d0beb616401..0901658cd24 100644 --- a/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java +++ b/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java @@ -22,7 +22,6 @@ import org.hyperledger.besu.cli.config.EthNetworkConfig; import org.hyperledger.besu.cli.options.EthstatsOptions; -import org.hyperledger.besu.config.NetworkDefinition; import org.hyperledger.besu.controller.BesuController; import org.hyperledger.besu.cryptoservices.NodeKey; import org.hyperledger.besu.ethereum.ProtocolContext; @@ -672,17 +671,8 @@ public Runner build() { }); discoveryConfiguration.setPreferIpv6Outbound(preferIpv6Outbound); if (discoveryEnabled) { - final List bootstrap; - if (ethNetworkConfig.enodeBootNodes() == null) { - bootstrap = EthNetworkConfig.getNetworkConfig(NetworkDefinition.MAINNET).enodeBootNodes(); - } else { - bootstrap = ethNetworkConfig.enodeBootNodes(); - } - discoveryConfiguration.setEnodeBootnodes(bootstrap); - discoveryConfiguration.setEnrBootnodes( - ethNetworkConfig.enrBootNodes() == null - ? EthNetworkConfig.getNetworkConfig(NetworkDefinition.MAINNET).enrBootNodes() - : ethNetworkConfig.enrBootNodes()); + discoveryConfiguration.setEnodeBootnodes(ethNetworkConfig.enodeBootNodes()); + discoveryConfiguration.setEnrBootnodes(ethNetworkConfig.enrBootNodes()); discoveryConfiguration.setIncludeBootnodesOnPeerRefresh( besuController.getGenesisConfigOptions().isPoa() && poaDiscoveryRetryBootnodes); @@ -690,7 +680,10 @@ public Runner build() { "Resolved {} bootnodes.", discoveryConfiguration.getEnodeBootnodes().size() + discoveryConfiguration.getEnrBootnodes().size()); - LOG.debug("Bootnodes = {}", bootstrap); + LOG.debug( + "Bootnodes enode={}, enr={}", + discoveryConfiguration.getEnodeBootnodes(), + discoveryConfiguration.getEnrBootnodes()); discoveryConfiguration.setDnsDiscoveryURL(ethNetworkConfig.dnsDiscoveryUrl()); discoveryConfiguration.setDiscoveryV5Enabled( networkingConfiguration.discoveryConfiguration().isDiscoveryV5Enabled()); diff --git a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 747c2e92cb0..354386f0491 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -88,6 +88,7 @@ import org.hyperledger.besu.cli.util.VersionProvider; import org.hyperledger.besu.components.BesuComponent; import org.hyperledger.besu.config.CheckpointConfigOptions; +import org.hyperledger.besu.config.DiscoveryOptions; import org.hyperledger.besu.config.GenesisConfig; import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.config.JsonUtil; @@ -2517,6 +2518,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkDefinition network) { if (p2PDiscoveryOptions.bootNodes == null) { builder.setEnodeBootNodes(new ArrayList<>()); + builder.setEnrBootNodes(new ArrayList<>()); } builder.setDnsDiscoveryUrl(null); } @@ -2539,46 +2541,75 @@ private EthNetworkConfig updateNetworkConfig(final NetworkDefinition network) { discoveryDnsUrlFromGenesis.ifPresent(builder::setDnsDiscoveryUrl); } - List listBootNodes = null; - if (p2PDiscoveryOptions.bootNodes != null) { + // Resolve bootnodes: CLI --bootnodes overrides genesis defaults. + // The discovery protocol version determines the expected format: + // V5 → ENR strings ("enr:..."), V4 → enode URLs ("enode://...") + final boolean isV5 = + unstableNetworkingOptions.toDomainObject().discoveryConfiguration().isDiscoveryV5Enabled(); + List rawBootnodes = null; + final boolean cliBootnodesProvided = p2PDiscoveryOptions.bootNodes != null; + if (cliBootnodesProvided) { try { - final List resolvedBootNodeArgs = - BootnodeResolver.resolve(p2PDiscoveryOptions.bootNodes); - if (!resolvedBootNodeArgs.isEmpty()) { - if (resolvedBootNodeArgs.getFirst().startsWith("enr:")) { - builder.setEnrBootNodes( - resolvedBootNodeArgs.stream().map(EthereumNodeRecord::fromEnr).toList()); - } else { - listBootNodes = buildEnodes(resolvedBootNodeArgs, getEnodeDnsConfiguration()); - } - } else { - listBootNodes = Collections.emptyList(); - } - - } catch (final BootnodeResolutionException e) { + rawBootnodes = BootnodeResolver.resolve(p2PDiscoveryOptions.bootNodes); + } catch (final BootnodeResolutionException | IllegalArgumentException e) { throw new ParameterException(commandLine, e.getMessage(), e); - - } catch (final IllegalArgumentException e) { - throw new ParameterException(commandLine, e.getMessage()); } } else { - final Optional> bootNodesFromGenesis = - genesisConfigOptionsSupplier.get().getDiscoveryOptions().getBootNodes(); - if (bootNodesFromGenesis.isPresent() && !bootNodesFromGenesis.get().isEmpty()) { - if (bootNodesFromGenesis.get().getFirst().startsWith("enr:")) { - builder.setEnrBootNodes( - bootNodesFromGenesis.get().stream().map(EthereumNodeRecord::fromEnr).toList()); - } else { - listBootNodes = buildEnodes(bootNodesFromGenesis.get(), getEnodeDnsConfiguration()); - } - } + final DiscoveryOptions discoveryOptions = + genesisConfigOptionsSupplier.get().getDiscoveryOptions(); + rawBootnodes = + isV5 + ? discoveryOptions.getV5BootNodes().orElse(null) + : discoveryOptions.getBootNodes().orElse(null); } - if (listBootNodes != null) { + + if (rawBootnodes != null && !rawBootnodes.isEmpty()) { if (!p2PDiscoveryOptions.peerDiscoveryEnabled) { logger.warn("Discovery disabled: bootnodes will be ignored."); } - DiscoveryConfiguration.assertValidBootnodes(listBootNodes); - builder.setEnodeBootNodes(listBootNodes); + try { + if (isV5) { + builder.setEnrBootNodes( + rawBootnodes.stream() + .map( + enr -> { + try { + return EthereumNodeRecord.fromEnr(enr); + } catch (final Exception e) { + throw new ParameterException( + commandLine, + "Invalid ENR bootnode: '" + + enr + + "'. ENR bootnodes must start with 'enr:'. Error: " + + e.getMessage(), + e); + } + }) + .toList()); + } else { + final List enodes = buildEnodes(rawBootnodes, getEnodeDnsConfiguration()); + DiscoveryConfiguration.assertValidBootnodes(enodes); + builder.setEnodeBootNodes(enodes); + } + // CLI --bootnodes is a full override: clear the unused protocol's list + if (cliBootnodesProvided) { + if (isV5) { + builder.setEnodeBootNodes(Collections.emptyList()); + } else { + builder.setEnrBootNodes(Collections.emptyList()); + } + } + } catch (final ParameterException e) { + throw e; // re-throw ParameterException from ENR parsing as-is + } catch (final IllegalArgumentException e) { + throw new ParameterException(commandLine, e.getMessage()); + } catch (final RuntimeException e) { + throw new ParameterException(commandLine, "Invalid bootnode format: " + e.getMessage(), e); + } + } else if (cliBootnodesProvided) { + // Explicitly empty --bootnodes clears all default bootnodes + builder.setEnodeBootNodes(Collections.emptyList()); + builder.setEnrBootNodes(Collections.emptyList()); } return builder.build(); } diff --git a/app/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java b/app/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java index 7a34f44fcb4..9845b727c37 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java +++ b/app/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java @@ -14,8 +14,8 @@ */ package org.hyperledger.besu.cli.config; +import org.hyperledger.besu.config.DiscoveryOptions; import org.hyperledger.besu.config.GenesisConfig; -import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.config.NetworkDefinition; import org.hyperledger.besu.ethereum.p2p.discovery.dns.EthereumNodeRecord; import org.hyperledger.besu.ethereum.p2p.peers.EnodeURLImpl; @@ -25,10 +25,8 @@ import java.math.BigInteger; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Optional; /** * The Eth network config. @@ -72,25 +70,27 @@ public record EthNetworkConfig( public static EthNetworkConfig getNetworkConfig(final NetworkDefinition networkDefinition) { final URL genesisSource = jsonConfigSource(networkDefinition.getGenesisFile()); final GenesisConfig genesisConfig = GenesisConfig.fromSource(genesisSource); - final GenesisConfigOptions genesisConfigOptions = genesisConfig.getConfigOptions(); - final Optional> rawBootNodes = - genesisConfigOptions.getDiscoveryOptions().getBootNodes(); - final List enodeBootNodes = new ArrayList<>(); - final List enrBootNodes = new ArrayList<>(); - if (rawBootNodes.isPresent() && !rawBootNodes.get().isEmpty()) { - if (rawBootNodes.get().getFirst().startsWith("enr:")) { - enrBootNodes.addAll(rawBootNodes.get().stream().map(EthereumNodeRecord::fromEnr).toList()); - } else { - enodeBootNodes.addAll(rawBootNodes.get().stream().map(EnodeURLImpl::fromString).toList()); - } - } + final DiscoveryOptions discoveryOptions = + genesisConfig.getConfigOptions().getDiscoveryOptions(); + + final List enodeBootNodes = + discoveryOptions + .getBootNodes() + .map(nodes -> nodes.stream().map(EnodeURLImpl::fromString).toList()) + .orElse(List.of()); + + final List enrBootNodes = + discoveryOptions + .getV5BootNodes() + .map(nodes -> nodes.stream().map(EthereumNodeRecord::fromEnr).toList()) + .orElse(List.of()); return new EthNetworkConfig( genesisConfig, networkDefinition.getNetworkId(), enodeBootNodes, enrBootNodes, - genesisConfigOptions.getDiscoveryOptions().getDiscoveryDnsUrl().orElse(null)); + discoveryOptions.getDiscoveryDnsUrl().orElse(null)); } private static URL jsonConfigSource(final String resourceName) { diff --git a/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index db3e7740abb..b4132438cfc 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -113,6 +113,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; @@ -306,14 +308,7 @@ public void callingBesuCommandWithoutOptionsMustSyncWithDefaultValues() { final ArgumentCaptor ethNetworkArg = ArgumentCaptor.forClass(EthNetworkConfig.class); verify(mockRunnerBuilder).discoveryEnabled(eq(true)); - verify(mockRunnerBuilder) - .ethNetworkConfig( - new EthNetworkConfig( - GenesisConfig.fromResource(MAINNET.getGenesisFile()), - MAINNET.getNetworkId(), - MAINNET_BOOTSTRAP_NODES, - Collections.emptyList(), - MAINNET_DISCOVERY_URL)); + verify(mockRunnerBuilder).ethNetworkConfig(EthNetworkConfig.getNetworkConfig(MAINNET)); verify(mockRunnerBuilder).p2pAdvertisedHost(eq("127.0.0.1")); verify(mockRunnerBuilder).p2pListenPort(eq(30303)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(DEFAULT_JSON_RPC_CONFIGURATION)); @@ -958,6 +953,7 @@ public void callingWithBootnodesOptionButNoValueMustPassEmptyBootnodeList() { verify(mockRunnerBuilder).build(); assertThat(ethNetworkConfigArgumentCaptor.getValue().enodeBootNodes()).isEmpty(); + assertThat(ethNetworkConfigArgumentCaptor.getValue().enrBootNodes()).isEmpty(); assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); @@ -1020,6 +1016,46 @@ public void callingWithInvalidBootnodeAndEqualSignMustDisplayError() { assertThat(commandErrorOutput.toString(UTF_8)).startsWith(expectedErrorOutputStart); } + private static final String VALID_ENR_1 = + "enr:-Iu4QLm7bZGdAt9NSeJG0cEnJohWcQTQaI9wFLu3Q7eHIDfrI4cwtzvEW3F3VbG9XdFXlrHyFGeXPn9snTCQJ9bnMRABgmlkgnY0gmlwhAOTJQCJc2VjcDI1NmsxoQIZdZD6tDYpkpEfVo5bgiU8MGRjhcOmHGD2nErK0UKRrIN0Y3CCIyiDdWRwgiMo"; + private static final String VALID_ENR_2 = + "enr:-Iu4QEDJ4Wa_UQNbK8Ay1hFEkXvd8psolVK6OhfTL9irqz3nbXxxWyKwEplPfkju4zduVQj6mMhUCm9R2Lc4YM5jPcIBgmlkgnY0gmlwhANrfESJc2VjcDI1NmsxoQJCYz2-nsqFpeEj6eov9HSi9QssIVIVNr0I89J1vXM9foN0Y3CCIyiDdWRwgiMo"; + + @Test + public void callingWithValidEnrBootnodeAndV5EnabledMustSucceed() { + parseCommand("--Xv5-discovery-enabled", "--bootnodes", VALID_ENR_1); + + verify(mockRunnerBuilder).ethNetworkConfig(ethNetworkConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(ethNetworkConfigArgumentCaptor.getValue().enrBootNodes()).hasSize(1); + assertThat(ethNetworkConfigArgumentCaptor.getValue().enodeBootNodes()).isEmpty(); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void callingWithMultipleValidEnrBootnodesAndV5EnabledMustSucceed() { + parseCommand("--Xv5-discovery-enabled", "--bootnodes", VALID_ENR_1 + "," + VALID_ENR_2); + + verify(mockRunnerBuilder).ethNetworkConfig(ethNetworkConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(ethNetworkConfigArgumentCaptor.getValue().enrBootNodes()).hasSize(2); + assertThat(ethNetworkConfigArgumentCaptor.getValue().enodeBootNodes()).isEmpty(); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"enr:-invalidenrdata", "enr:invalidvalue", "invalidvalue"}) + public void callingWithInvalidBootnodeAndV5EnabledMustDisplayError(final String bootnode) { + parseCommand("--Xv5-discovery-enabled", "--bootnodes", bootnode); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Invalid ENR bootnode: '" + bootnode + "'"); + } + @Test public void bootnodesOptionMustBeUsed() { parseCommand("--bootnodes", String.join(",", VALID_ENODE_STRINGS)); diff --git a/app/src/test/java/org/hyperledger/besu/cli/CascadingDefaultProviderTest.java b/app/src/test/java/org/hyperledger/besu/cli/CascadingDefaultProviderTest.java index 9fdeed43a37..28cc78a0907 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/CascadingDefaultProviderTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/CascadingDefaultProviderTest.java @@ -21,8 +21,6 @@ import static org.hyperledger.besu.config.NetworkDefinition.MAINNET; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ETH; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.WEB3; -import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.MAINNET_BOOTSTRAP_NODES; -import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.MAINNET_DISCOVERY_URL; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @@ -132,6 +130,7 @@ public void overrideDefaultValuesIfKeyIsPresentInConfigFile(final @TempDir File .setNetworkId(BigInteger.valueOf(42)) .setGenesisConfig(GenesisConfig.fromConfig(encodeJsonGenesis(GENESIS_VALID_JSON))) .setEnodeBootNodes(nodes) + .setEnrBootNodes(Collections.emptyList()) .setDnsDiscoveryUrl(null) .build(); verify(mockControllerBuilder).dataDirectory(eq(dataFolder.toPath())); @@ -165,14 +164,7 @@ public void noOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() { final MetricsConfiguration metricsConfiguration = MetricsConfiguration.builder().build(); verify(mockRunnerBuilder).discoveryEnabled(eq(true)); - verify(mockRunnerBuilder) - .ethNetworkConfig( - new EthNetworkConfig( - GenesisConfig.fromResource(MAINNET.getGenesisFile()), - MAINNET.getNetworkId(), - MAINNET_BOOTSTRAP_NODES, - Collections.emptyList(), - MAINNET_DISCOVERY_URL)); + verify(mockRunnerBuilder).ethNetworkConfig(EthNetworkConfig.getNetworkConfig(MAINNET)); verify(mockRunnerBuilder).p2pAdvertisedHost(eq("127.0.0.1")); verify(mockRunnerBuilder).p2pListenPort(eq(30303)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); diff --git a/app/src/test/java/org/hyperledger/besu/cli/config/EthNetworkConfigTest.java b/app/src/test/java/org/hyperledger/besu/cli/config/EthNetworkConfigTest.java index 79990d5df4d..0ecac5a6846 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/config/EthNetworkConfigTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/config/EthNetworkConfigTest.java @@ -40,6 +40,7 @@ public void testDefaultMainnetConfig() { EthNetworkConfig config = EthNetworkConfig.getNetworkConfig(NetworkDefinition.MAINNET); assertThat(config.dnsDiscoveryUrl()).isEqualTo(MAINNET_DISCOVERY_URL); assertThat(config.enodeBootNodes()).isEqualTo(MAINNET_BOOTSTRAP_NODES); + assertThat(config.enrBootNodes()).isNotEmpty(); assertThat(config.networkId()).isEqualTo(BigInteger.ONE); } @@ -56,6 +57,7 @@ public void testDefaultHoodiConfig() { EthNetworkConfig config = EthNetworkConfig.getNetworkConfig(NetworkDefinition.HOODI); assertThat(config.dnsDiscoveryUrl()).isEqualTo(HOODI_DISCOVERY_URL); assertThat(config.enodeBootNodes()).isEqualTo(HOODI_BOOTSTRAP_NODES); + assertThat(config.enrBootNodes()).isNotEmpty(); assertThat(config.networkId()).isEqualTo(BigInteger.valueOf(560048)); } diff --git a/config/src/main/java/org/hyperledger/besu/config/DiscoveryOptions.java b/config/src/main/java/org/hyperledger/besu/config/DiscoveryOptions.java index 7347615f3f3..c4aae63f7e6 100644 --- a/config/src/main/java/org/hyperledger/besu/config/DiscoveryOptions.java +++ b/config/src/main/java/org/hyperledger/besu/config/DiscoveryOptions.java @@ -28,6 +28,7 @@ public class DiscoveryOptions { new DiscoveryOptions(JsonUtil.createEmptyObjectNode()); private static final String ENODES_KEY = "bootnodes"; + private static final String V5_BOOTNODES_KEY = "v5bootnodes"; private static final String DNS_KEY = "dns"; private final ObjectNode discoveryConfigRoot; @@ -67,6 +68,32 @@ public Optional> getBootNodes() { return Optional.of(bootNodes); } + /** + * Gets V5 boot nodes (ENR format). + * + * @return optional list of ENR boot node strings + */ + public Optional> getV5BootNodes() { + final Optional bootNodesArray = + JsonUtil.getArrayNode(discoveryConfigRoot, V5_BOOTNODES_KEY); + if (bootNodesArray.isEmpty()) { + return Optional.empty(); + } + final List bootNodes = new ArrayList<>(); + bootNodesArray + .get() + .elements() + .forEachRemaining( + bootNodeElement -> { + if (!bootNodeElement.isTextual()) { + throw new IllegalArgumentException( + V5_BOOTNODES_KEY + " does not contain a string: " + bootNodeElement); + } + bootNodes.add(bootNodeElement.asText()); + }); + return Optional.of(bootNodes); + } + /** * Gets discovery dns url. * diff --git a/config/src/main/resources/hoodi.json b/config/src/main/resources/hoodi.json index b9e435bae6c..18932b25fed 100644 --- a/config/src/main/resources/hoodi.json +++ b/config/src/main/resources/hoodi.json @@ -52,6 +52,17 @@ "enode://2112dd3839dd752813d4df7f40936f06829fc54c0e051a93967c26e5f5d27d99d886b57b4ffcc3c475e930ec9e79c56ef1dbb7d86ca5ee83a9d2ccf36e5c240c@134.209.138.84:30303", "enode://60203fcb3524e07c5df60a14ae1c9c5b24023ea5d47463dfae051d2c9f3219f309657537576090ca0ae641f73d419f53d8e8000d7a464319d4784acd7d2abc41@209.38.124.160:30303", "enode://8ae4a48101b2299597341263da0deb47cc38aa4d3ef4b7430b897d49bfa10eb1ccfe1655679b1ed46928ef177fbf21b86837bd724400196c508427a6f41602cd@134.199.184.23:30303" + ], + "v5Bootnodes": [ + "enr:-Mq4QLkmuSwbGBUph1r7iHopzRpdqE-gcm5LNZfcE-6T37OCZbRHi22bXZkaqnZ6XdIyEDTelnkmMEQB8w6NbnJUt9GGAZWaowaYh2F0dG5ldHOIABgAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhNEmfKCEcXVpY4IyyIlzZWNwMjU2azGhA0hGa4jZJZYQAS-z6ZFK-m4GCFnWS8wfjO0bpSQn6hyEiHN5bmNuZXRzAIN0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QLVumWTwyOUVS4ajqq8ZuZz2ik6t3Gtq0Ozxqecj0qNZWpMnudcvTs-4jrlwYRQMQwBS8Pvtmu4ZPP2Lx3i2t7YBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhNEmfKCJc2VjcDI1NmsxoQLdRlI8aCa_ELwTJhVN8k7km7IDc3pYu-FMYBs5_FiigIN1ZHCCIyk", + "enr:-LK4QAYuLujoiaqCAs0-qNWj9oFws1B4iy-Hff1bRB7wpQCYSS-IIMxLWCn7sWloTJzC1SiH8Y7lMQ5I36ynGV1ASj4Eh2F0dG5ldHOIYAAAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQOmI5MlAu3f5WEThAYOqoygpS2wYn0XS5NV2aYq7T0a04N0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QIC89sMC0o-irosD4_23lJJ4qCGOvdUz7SmoShWx0k6AaxCFTKviEHa-sa7-EzsiXpDp0qP0xzX6nKdXJX3X-IQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQK_m0f1DzDc9Cjrspm36zuRa7072HSiMGYWLsKiVSbP34N1ZHCCIyk", + "enr:-Ku4QNkWjw5tNzo8DtWqKm7CnDdIq_y7xppD6c1EZSwjB8rMOkSFA1wJPLoKrq5UvA7wcxIotH6Usx3PAugEN2JMncIBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbHuBeJc2VjcDI1NmsxoQP3FwrhFYB60djwRjAoOjttq6du94DtkQuaN99wvgqaIYN1ZHCCIyk", + "enr:-OS4QMJGE13xEROqvKN1xnnt7U-noc51VXyM6wFMuL9LMhQDfo1p1dF_zFdS4OsnXz_vIYk-nQWnqJMWRDKvkSK6_CwDh2F0dG5ldHOIAAAAADAAAACGY2xpZW502IpMaWdodGhvdXNljDcuMC4wLWJldGEuM4RldGgykNLxmX9gAAkQAAgAAAAAAACCaWSCdjSCaXCEhse4F4RxdWljgiMqiXNlY3AyNTZrMaECef77P8k5l3PC_raLw42OAzdXfxeQ-58BJriNaqiRGJSIc3luY25ldHMAg3RjcIIjKIN1ZHCCIyg", + "enr:-LK4QDwhXMitMbC8xRiNL-XGMhRyMSOnxej-zGifjv9Nm5G8EF285phTU-CAsMHRRefZimNI7eNpAluijMQP7NDC8kEMh2F0dG5ldHOIAAAAAAAABgCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAOIT_SJc2VjcDI1NmsxoQMoHWNL4MAvh6YpQeM2SUjhUrLIPsAVPB8nyxbmckC6KIN0Y3CCIyiDdWRwgiMo", + "enr:-LK4QPYl2HnMPQ7b1es6Nf_tFYkyya5bj9IqAKOEj2cmoqVkN8ANbJJJK40MX4kciL7pZszPHw6vLNyeC-O3HUrLQv8Mh2F0dG5ldHOIAAAAAAAAAMCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAMYRG-Jc2VjcDI1NmsxoQPQ35tjr6q1qUqwAnegQmYQyfqxC_6437CObkZneI9n34N0Y3CCIyiDdWRwgiMo", + "enr:-KG4QKRSUi4IOAIK_xt5ERrwW_J47wmNCLWFh7Jo0hFE69drZsiZ5Pb5CEcM_njFTTLlIR6SCf67HTcSV1g6hCXdhWkCgmlkgnY0gmlwhLkvrBODaXA2kCoGxcAWAAAYAAAAAAAAABCJc2VjcDI1NmsxoQPU7g2jQGTz8BYbB2vLTb39S_PrcZAehwMM0b3bWsM5rIN1ZHCCIyiEdWRwNoIjKA" ] } }, diff --git a/config/src/main/resources/mainnet.json b/config/src/main/resources/mainnet.json index 3ba94ccf767..46695f6fe10 100644 --- a/config/src/main/resources/mainnet.json +++ b/config/src/main/resources/mainnet.json @@ -54,6 +54,25 @@ "enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303", "enode://2b252ab6a1d0f971d9722cb839a42cb81db019ba44c08754628ab4a823487071b5695317c8ccd085219c3a03af063495b2f1da8d18218da2d6a82981b45e6ffc@65.108.70.101:30303", "enode://4aeb4ab6c14b23e2c4cfdce879c04b0748a20d8e9b59e25ded2a08143e265c6c25936e74cbc8e641e3312ca288673d91f2f93f8e277de3cfa444ecdaaf982052@157.90.35.166:30303" + ], + "v5Bootnodes": [ + "enr:-Iu4QLm7bZGdAt9NSeJG0cEnJohWcQTQaI9wFLu3Q7eHIDfrI4cwtzvEW3F3VbG9XdFXlrHyFGeXPn9snTCQJ9bnMRABgmlkgnY0gmlwhAOTJQCJc2VjcDI1NmsxoQIZdZD6tDYpkpEfVo5bgiU8MGRjhcOmHGD2nErK0UKRrIN0Y3CCIyiDdWRwgiMo", + "enr:-Iu4QEDJ4Wa_UQNbK8Ay1hFEkXvd8psolVK6OhfTL9irqz3nbXxxWyKwEplPfkju4zduVQj6mMhUCm9R2Lc4YM5jPcIBgmlkgnY0gmlwhANrfESJc2VjcDI1NmsxoQJCYz2-nsqFpeEj6eov9HSi9QssIVIVNr0I89J1vXM9foN0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg", + "enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA", + "enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg", + "enr:-Le4QPUXJS2BTORXxyx2Ia-9ae4YqA_JWX3ssj4E_J-3z1A-HmFGrU8BpvpqhNabayXeOZ2Nq_sbeDgtzMJpLLnXFgAChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISsaa0Zg2lwNpAkAIkHAAAAAPA8kv_-awoTiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMohHVkcDaCI4I", + "enr:-Le4QLHZDSvkLfqgEo8IWGG96h6mxwe_PsggC20CL3neLBjfXLGAQFOPSltZ7oP6ol54OvaNqO02Rnvb8YmDR274uq8ChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLosQxg2lwNpAqAX4AAAAAAPA8kv_-ax65iXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMohHVkcDaCI4I", + "enr:-Le4QH6LQrusDbAHPjU_HcKOuMeXfdEB5NJyXgHWFadfHgiySqeDyusQMvfphdYWOzuSZO9Uq2AMRJR5O4ip7OvVma8BhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY9ncg2lwNpAkAh8AgQIBAAAAAAAAAAmXiXNlY3AyNTZrMaECDYCZTZEksF-kmgPholqgVt8IXr-8L7Nu7YrZ7HUpgxmDdWRwgiMohHVkcDaCI4I", + "enr:-Le4QIqLuWybHNONr933Lk0dcMmAB5WgvGKRyDihy1wHDIVlNuuztX62W51voT4I8qD34GcTEOTmag1bcdZ_8aaT4NUBhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY04ng2lwNpAkAh8AgAIBAAAAAAAAAA-fiXNlY3AyNTZrMaEDscnRV6n1m-D9ID5UsURk0jsoKNXt1TIrj8uKOGW6iluDdWRwgiMohHVkcDaCI4I", + "enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg", + "enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg", + "enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg", + "enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg", + "enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM", + "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM", + "enr:-IS4QPi-onjNsT5xAIAenhCGTDl4z-4UOR25Uq-3TmG4V3kwB9ljLTb_Kp1wdjHNj-H8VVLRBSSWVZo3GUe3z6k0E-IBgmlkgnY0gmlwhKB3_qGJc2VjcDI1NmsxoQMvAfgB4cJXvvXeM6WbCG86CstbSxbQBSGx31FAwVtOTYN1ZHCCIyg", + "enr:-KG4QPUf8-g_jU-KrwzG42AGt0wWM1BTnQxgZXlvCEIfTQ5hSmptkmgmMbRkpOqv6kzb33SlhPHJp7x4rLWWiVq5lSECgmlkgnY0gmlwhFPlR9KDaXA2kCoGxcAJAAAVAAAAAAAAABCJc2VjcDI1NmsxoQLdUv9Eo9sxCt0tc_CheLOWnX59yHJtkBSOL7kpxdJ6GYN1ZHCCIyiEdWRwNoIjKA" ] }, "checkpoint": { diff --git a/config/src/test/java/org/hyperledger/besu/config/DiscoveryOptionsTest.java b/config/src/test/java/org/hyperledger/besu/config/DiscoveryOptionsTest.java new file mode 100644 index 00000000000..05ef67f4696 --- /dev/null +++ b/config/src/test/java/org/hyperledger/besu/config/DiscoveryOptionsTest.java @@ -0,0 +1,109 @@ +/* + * 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 + */ +package org.hyperledger.besu.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.Test; + +class DiscoveryOptionsTest { + + @Test + void defaultHasNoBootNodes() { + assertThat(DiscoveryOptions.DEFAULT.getBootNodes()).isEmpty(); + } + + @Test + void defaultHasNoV5BootNodes() { + assertThat(DiscoveryOptions.DEFAULT.getV5BootNodes()).isEmpty(); + } + + @Test + void defaultHasNoDnsUrl() { + assertThat(DiscoveryOptions.DEFAULT.getDiscoveryDnsUrl()).isEmpty(); + } + + @Test + void parsesBootNodes() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + final ArrayNode bootnodes = root.putArray("bootnodes"); + bootnodes.add("enode://abc@127.0.0.1:30303"); + bootnodes.add("enode://def@127.0.0.1:30304"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getBootNodes()) + .isPresent() + .hasValue(List.of("enode://abc@127.0.0.1:30303", "enode://def@127.0.0.1:30304")); + } + + @Test + void parsesV5BootNodes() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + final ArrayNode v5Bootnodes = root.putArray("v5bootnodes"); + v5Bootnodes.add("enr:-abc123"); + v5Bootnodes.add("enr:-def456"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getV5BootNodes()) + .isPresent() + .hasValue(List.of("enr:-abc123", "enr:-def456")); + } + + @Test + void v5BootNodesEmptyWhenKeyAbsent() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + root.putArray("bootnodes").add("enode://abc@127.0.0.1:30303"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getV5BootNodes()).isEmpty(); + assertThat(options.getBootNodes()).isPresent(); + } + + @Test + void bootNodesAndV5BootNodesCanCoexist() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + root.putArray("bootnodes").add("enode://abc@127.0.0.1:30303"); + root.putArray("v5bootnodes").add("enr:-abc123"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getBootNodes()).isPresent().hasValue(List.of("enode://abc@127.0.0.1:30303")); + assertThat(options.getV5BootNodes()).isPresent().hasValue(List.of("enr:-abc123")); + } + + @Test + void v5BootNodesThrowsOnNonStringElement() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + root.putArray("v5bootnodes").add(42); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThatThrownBy(options::getV5BootNodes) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("v5bootnodes does not contain a string"); + } + + @Test + void parsesDnsUrl() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + root.put("dns", "enrtree://test@domain"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getDiscoveryDnsUrl()).isPresent().hasValue("enrtree://test@domain"); + } +} diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/config/DiscoveryConfiguration.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/config/DiscoveryConfiguration.java index dd0f18d4f62..ce941313ba7 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/config/DiscoveryConfiguration.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/config/DiscoveryConfiguration.java @@ -110,7 +110,7 @@ public DiscoveryConfiguration setEnrBootnodes(final List enr } public List getBootnodeIdentifiers() { - return enodeBootnodes.isEmpty() ? enrBootnodes : enodeBootnodes; + return discoveryV5Enabled ? enrBootnodes : enodeBootnodes; } public boolean getIncludeBootnodesOnPeerRefresh() { diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentFactoryV5.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentFactoryV5.java index ee02baa272a..68529c7963a 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentFactoryV5.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentFactoryV5.java @@ -169,8 +169,21 @@ public NodeKeySigner(final NodeKey nodeKey) { */ @Override public Bytes deriveECDHKeyAgreement(final Bytes remotePubKey) { - SECPPublicKey publicKey = signatureAlgorithm.createPublicKey(remotePubKey); - return nodeKey.calculateECDHKeyAgreement(publicKey); + final Bytes uncompressedKey; + if (remotePubKey.size() == 33) { + // Compressed key (0x02/0x03 prefix) — decompress to the 64-byte format without prefix + final byte[] encoded = + signatureAlgorithm + .getCurve() + .getCurve() + .decodePoint(remotePubKey.toArrayUnsafe()) + .getEncoded(false); + uncompressedKey = Bytes.wrap(encoded, 1, 64); + } else { + uncompressedKey = remotePubKey; + } + final SECPPublicKey publicKey = signatureAlgorithm.createPublicKey(uncompressedKey); + return nodeKey.calculateECDHKeyAgreementCompressed(publicKey); } /** diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentV5.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentV5.java index c4dbe2eb509..659909e7da3 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentV5.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentV5.java @@ -40,6 +40,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes; import org.ethereum.beacon.discovery.MutableDiscoverySystem; import org.ethereum.beacon.discovery.schema.NodeRecord; import org.ethereum.beacon.discovery.storage.NodeRecordListener; @@ -445,13 +446,20 @@ private Stream candidatePeers(final Collection newPee return Stream.empty(); } + final Bytes localNodeId = getLocalNodeRecord().map(NodeRecord::getNodeId).orElse(Bytes.EMPTY); + // Combine newly discovered peers with known peers and filter for suitability final Stream knownPeers = system.streamLiveNodes(); final List candidates = Stream.concat(newPeers.stream(), knownPeers) .distinct() + // Exclude the local node record that streamLiveNodes may include + .filter(nr -> !nr.getNodeId().equals(localNodeId)) .map(nr -> DiscoveryPeerFactory.fromNodeRecord(nr, preferIpv6Outbound)) - .filter(DiscoveryPeer::isReadyForConnections) + // Use isListening() instead of isReadyForConnections() because + // DiscoveryPeerV4.isReadyForConnections() requires DiscV4 bonding status, + // which is never set for DiscV5-discovered peers. + .filter(DiscoveryPeer::isListening) .filter(peer -> peer.getForkId().map(forkIdManager::peerCheck).orElse(true)) .toList(); if (LOG.isTraceEnabled() && !candidates.isEmpty()) { diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecord.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecord.java index 49518751267..9c03a7890df 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecord.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecord.java @@ -16,7 +16,8 @@ // Adapted from https://github.com/tmio/tuweni and licensed under Apache 2.0 package org.hyperledger.besu.ethereum.p2p.discovery.dns; -import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmType; import org.hyperledger.besu.ethereum.p2p.discovery.NodeIdentifier; import java.net.InetAddress; @@ -46,6 +47,10 @@ public record EthereumNodeRecord( NodeRecord nodeRecord) implements NodeIdentifier { + // ENR identity scheme v4 always uses secp256k1, independent of the node's signature algorithm + private static final SignatureAlgorithm SECP256K1 = + SignatureAlgorithmType.create("secp256k1").getInstance(); + @SuppressWarnings( "MethodInputParametersMustBeFinal") // needed since record constructors are not yet supported public EthereumNodeRecord { @@ -97,8 +102,8 @@ static Bytes initPublicKeyBytes(final Map fields) { throw new IllegalArgumentException("Missing secp256k1 entry in ENR"); } // convert 33 bytes compressed public key to uncompressed using Bouncy Castle - var curve = SignatureAlgorithmFactory.getInstance().getCurve(); - var ecPoint = curve.getCurve().decodePoint(keyBytes.toArrayUnsafe()); + // ENR identity scheme v4 always uses secp256k1 for the public key + var ecPoint = SECP256K1.getCurve().getCurve().decodePoint(keyBytes.toArrayUnsafe()); // uncompressed public key is 65 bytes, first byte is 0x04. var encodedPubKey = ecPoint.getEncoded(false); return Bytes.of(Arrays.copyOfRange(encodedPubKey, 1, encodedPubKey.length)); From 76516a9ec827464d10a8645c03b947715c0f3ef3 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 11 Mar 2026 11:46:37 +0100 Subject: [PATCH 23/77] Remove Clique RPC APIs and remaining DSL test infrastructure (Phase 2) (#9992) Delete the Clique JSON-RPC method implementations (clique_getSigners, clique_propose, clique_discard, clique_proposals, clique_getSignerMetrics, clique_getSignersAtHash) and all supporting classes: - consensus/clique/.../jsonrpc/ (production + test, 12 files) - CliqueQueryPluginServiceFactory (replaced with NoopPluginServiceFactory) - Acceptance-test DSL: condition/clique/ and transaction/clique/ packages - NodeRequests: drop CliqueRequestFactory field and clique() accessor - AcceptanceTestBase: drop clique/cliqueTransactions fields - BesuNode: remove CliqueRequestFactory from NodeRequests construction - NodeConfigurationFactory: remove createJsonRpcWithCliqueEnabledConfig() - BftConditions / AwaitValidatorSetChange: inline "latest" constant Remove RpcMethod entries CLIQUE_* and RpcApis.CLIQUE enum value. CliqueBesuControllerBuilder no longer overrides createAdditionalJsonRpcMethodFactory. Signed-off-by: Fabio Di Fabio Co-authored-by: Claude Sonnet 4.6 --- .../acceptance/dsl/AcceptanceTestBase.java | 6 - .../bft/AwaitValidatorSetChange.java | 4 +- .../dsl/condition/bft/BftConditions.java | 4 +- .../clique/AwaitSignerSetChange.java | 45 --- .../condition/clique/CliqueConditions.java | 132 --------- .../dsl/condition/clique/ExpectNonceVote.java | 46 --- .../dsl/condition/clique/ExpectProposals.java | 41 --- .../condition/clique/ExpectValidators.java | 41 --- .../clique/ExpectValidatorsAtBlock.java | 44 --- .../clique/ExpectValidatorsAtBlockHash.java | 45 --- .../clique/ExpectedBlockHasProposer.java | 52 ---- .../tests/acceptance/dsl/node/BesuNode.java | 2 - .../NodeConfigurationFactory.java | 7 - .../dsl/transaction/NodeRequests.java | 8 - .../dsl/transaction/clique/CliqueDiscard.java | 44 --- .../transaction/clique/CliqueGetSigners.java | 45 --- .../clique/CliqueGetSignersAtHash.java | 46 --- .../transaction/clique/CliqueProposals.java | 39 --- .../dsl/transaction/clique/CliquePropose.java | 45 --- .../clique/CliqueRequestFactory.java | 76 ----- .../clique/CliqueTransactions.java | 50 ---- .../CliqueBesuControllerBuilder.java | 12 +- .../CliqueQueryPluginServiceFactory.java | 49 ---- .../clique/jsonrpc/CliqueJsonRpcMethods.java | 98 ------- .../methods/CliqueGetSignerMetrics.java | 46 --- .../jsonrpc/methods/CliqueGetSigners.java | 83 ------ .../methods/CliqueGetSignersAtHash.java | 81 ------ .../jsonrpc/methods/CliqueProposals.java | 38 --- .../clique/jsonrpc/methods/Discard.java | 62 ----- .../clique/jsonrpc/methods/Propose.java | 81 ------ .../methods/CliqueGetSignerMetricsTest.java | 262 ------------------ .../methods/CliqueGetSignersAtHashTest.java | 135 --------- .../jsonrpc/methods/CliqueGetSignersTest.java | 122 -------- .../jsonrpc/methods/CliqueProposalsTest.java | 48 ---- .../clique/jsonrpc/methods/DiscardTest.java | 131 --------- .../clique/jsonrpc/methods/ProposeTest.java | 170 ------------ .../besu/ethereum/api/jsonrpc/RpcApis.java | 1 - .../besu/ethereum/api/jsonrpc/RpcMethod.java | 6 - 38 files changed, 4 insertions(+), 2243 deletions(-) delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/AwaitSignerSetChange.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/CliqueConditions.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectNonceVote.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectProposals.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidators.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidatorsAtBlock.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidatorsAtBlockHash.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectedBlockHasProposer.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueDiscard.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueGetSigners.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueGetSignersAtHash.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueProposals.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliquePropose.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueRequestFactory.java delete mode 100644 acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueTransactions.java delete mode 100644 app/src/main/java/org/hyperledger/besu/controller/CliqueQueryPluginServiceFactory.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/CliqueJsonRpcMethods.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignerMetrics.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSigners.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHash.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueProposals.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/Discard.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/Propose.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignerMetricsTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHashTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueProposalsTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/DiscardTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/ProposeTest.java diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java index 8146db2d089..201333f5988 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java @@ -21,7 +21,6 @@ import org.hyperledger.besu.tests.acceptance.dsl.blockchain.Blockchain; import org.hyperledger.besu.tests.acceptance.dsl.condition.admin.AdminConditions; import org.hyperledger.besu.tests.acceptance.dsl.condition.bft.BftConditions; -import org.hyperledger.besu.tests.acceptance.dsl.condition.clique.CliqueConditions; import org.hyperledger.besu.tests.acceptance.dsl.condition.eth.EthConditions; import org.hyperledger.besu.tests.acceptance.dsl.condition.login.LoginConditions; import org.hyperledger.besu.tests.acceptance.dsl.condition.net.NetConditions; @@ -37,7 +36,6 @@ import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.AccountTransactions; import org.hyperledger.besu.tests.acceptance.dsl.transaction.admin.AdminTransactions; import org.hyperledger.besu.tests.acceptance.dsl.transaction.bft.BftTransactions; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions; import org.hyperledger.besu.tests.acceptance.dsl.transaction.contract.ContractTransactions; import org.hyperledger.besu.tests.acceptance.dsl.transaction.debug.DebugTransactions; import org.hyperledger.besu.tests.acceptance.dsl.transaction.eth.EthTransactions; @@ -84,8 +82,6 @@ public class AcceptanceTestBase { protected final AdminConditions admin; protected final AdminTransactions adminTransactions; protected final Blockchain blockchain; - protected final CliqueConditions clique; - protected final CliqueTransactions cliqueTransactions; protected final Cluster cluster; protected final ContractVerifier contractVerifier; protected final ContractTransactions contractTransactions; @@ -113,14 +109,12 @@ protected AcceptanceTestBase() { ethTransactions = new EthTransactions(); accounts = new Accounts(ethTransactions); adminTransactions = new AdminTransactions(); - cliqueTransactions = new CliqueTransactions(); bftTransactions = new BftTransactions(); accountTransactions = new AccountTransactions(accounts); permissioningTransactions = new PermissioningTransactions(); contractTransactions = new ContractTransactions(); minerTransactions = new MinerTransactions(); blockchain = new Blockchain(ethTransactions); - clique = new CliqueConditions(ethTransactions, cliqueTransactions); eth = new EthConditions(ethTransactions); bft = new BftConditions(bftTransactions); login = new LoginConditions(); diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/bft/AwaitValidatorSetChange.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/bft/AwaitValidatorSetChange.java index 85c448eff92..21dfca41d67 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/bft/AwaitValidatorSetChange.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/bft/AwaitValidatorSetChange.java @@ -15,7 +15,6 @@ package org.hyperledger.besu.tests.acceptance.dsl.condition.bft; import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions.LATEST; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.tests.acceptance.dsl.WaitUtils; @@ -40,6 +39,7 @@ public void verify(final Node node) { WaitUtils.waitFor( 60, () -> - assertThat(node.execute(bft.createGetValidators(LATEST))).isNotEqualTo(initialSigners)); + assertThat(node.execute(bft.createGetValidators("latest"))) + .isNotEqualTo(initialSigners)); } } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/bft/BftConditions.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/bft/BftConditions.java index 8439bce81c9..b10c62d0ade 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/bft/BftConditions.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/bft/BftConditions.java @@ -14,8 +14,6 @@ */ package org.hyperledger.besu.tests.acceptance.dsl.condition.bft; -import static org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions.LATEST; - import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; @@ -56,7 +54,7 @@ private Address[] validatorAddresses(final BesuNode[] validators) { } public Condition awaitValidatorSetChange(final Node node) { - return new AwaitValidatorSetChange(node.execute(bft.createGetValidators(LATEST)), bft); + return new AwaitValidatorSetChange(node.execute(bft.createGetValidators("latest")), bft); } public Condition noProposals() { diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/AwaitSignerSetChange.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/AwaitSignerSetChange.java deleted file mode 100644 index f332b387c0d..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/AwaitSignerSetChange.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.clique; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions.LATEST; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.tests.acceptance.dsl.WaitUtils; -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions; - -import java.util.List; - -public class AwaitSignerSetChange implements Condition { - - private final CliqueTransactions clique; - private final List

initialSigners; - - public AwaitSignerSetChange(final List
initialSigners, final CliqueTransactions clique) { - this.initialSigners = initialSigners; - this.clique = clique; - } - - @Override - public void verify(final Node node) { - WaitUtils.waitFor( - 60, - () -> - assertThat(node.execute(clique.createGetSigners(LATEST))).isNotEqualTo(initialSigners)); - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/CliqueConditions.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/CliqueConditions.java deleted file mode 100644 index 4490a2b2a2e..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/CliqueConditions.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.clique; - -import static java.util.Collections.emptyList; -import static org.hyperledger.besu.datatypes.Hash.fromHexString; -import static org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions.LATEST; - -import org.hyperledger.besu.config.CliqueConfigOptions; -import org.hyperledger.besu.config.GenesisConfig; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.condition.blockchain.ExpectBlockNotCreated; -import org.hyperledger.besu.tests.acceptance.dsl.condition.clique.ExpectNonceVote.CLIQUE_NONCE_VOTE; -import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; -import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.eth.EthTransactions; - -import java.math.BigInteger; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -import com.google.common.collect.ImmutableMap; -import org.web3j.protocol.core.DefaultBlockParameter; - -public class CliqueConditions { - - private final EthTransactions eth; - private final CliqueTransactions clique; - - public CliqueConditions(final EthTransactions eth, final CliqueTransactions clique) { - this.eth = eth; - this.clique = clique; - } - - public Condition validatorsEqual(final BesuNode... validators) { - return new ExpectValidators(clique, validatorAddresses(validators)); - } - - public Condition validatorsAtBlockEqual(final String blockNumber, final BesuNode... validators) { - return new ExpectValidatorsAtBlock(clique, blockNumber, validatorAddresses(validators)); - } - - public Condition validatorsAtBlockHashFromBlockNumberEqual( - final Node node, final long blockNumber, final BesuNode... validators) { - final DefaultBlockParameter blockParameter = - DefaultBlockParameter.valueOf(BigInteger.valueOf(blockNumber)); - final String blockHash = node.execute(eth.block(blockParameter)).getHash(); - return new ExpectValidatorsAtBlockHash( - clique, fromHexString(blockHash), validatorAddresses(validators)); - } - - public ProposalsConfig proposalsEqual() { - return new ProposalsConfig(clique); - } - - public Condition noProposals() { - return new ExpectProposals(clique, ImmutableMap.of()); - } - - public Condition nonceVoteEquals(final CLIQUE_NONCE_VOTE clique_nonce_vote) { - return new ExpectNonceVote(eth, clique_nonce_vote); - } - - public Condition noNewBlockCreated(final BesuNode node) { - final int blockPeriodSeconds = cliqueBlockPeriod(node); - final int blockPeriodWait = blockPeriodSeconds * 1000; - return new ExpectBlockNotCreated(eth, blockPeriodWait, blockPeriodWait); - } - - public Condition awaitSignerSetChange(final Node node) { - return new AwaitSignerSetChange(node.execute(clique.createGetSigners(LATEST)), clique); - } - - private int cliqueBlockPeriod(final BesuNode node) { - final String config = node.getGenesisConfigProvider().create(emptyList()).get(); - final GenesisConfig genesisConfig = GenesisConfig.fromConfig(config); - final CliqueConfigOptions cliqueConfigOptions = - genesisConfig.getConfigOptions().getCliqueConfigOptions(); - return cliqueConfigOptions.getBlockPeriodSeconds(); - } - - private Address[] validatorAddresses(final BesuNode[] validators) { - return Arrays.stream(validators).map(BesuNode::getAddress).sorted().toArray(Address[]::new); - } - - public Condition blockIsCreatedByProposer(final BesuNode proposer) { - return new ExpectedBlockHasProposer(eth, proposer.getAddress()); - } - - public static class ProposalsConfig { - - private final Map proposals = new HashMap<>(); - private final CliqueTransactions clique; - - public ProposalsConfig(final CliqueTransactions clique) { - this.clique = clique; - } - - public ProposalsConfig addProposal(final BesuNode node) { - proposals.put(node, true); - return this; - } - - public ProposalsConfig removeProposal(final BesuNode node) { - proposals.put(node, false); - return this; - } - - public Condition build() { - final Map proposalsAsAddress = - this.proposals.entrySet().stream() - .collect(Collectors.toMap(p -> p.getKey().getAddress(), Map.Entry::getValue)); - return new ExpectProposals(clique, proposalsAsAddress); - } - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectNonceVote.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectNonceVote.java deleted file mode 100644 index 1e8f4b8a99f..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectNonceVote.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.clique; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.tests.acceptance.dsl.condition.clique.ExpectNonceVote.CLIQUE_NONCE_VOTE.AUTH; - -import org.hyperledger.besu.tests.acceptance.dsl.WaitUtils; -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.eth.EthTransactions; - -public class ExpectNonceVote implements Condition { - private static final String NONCE_AUTH = "0xffffffffffffffff"; - private static final String NONCE_DROP = "0x0000000000000000"; - private final EthTransactions eth; - private final String expectedNonce; - - public enum CLIQUE_NONCE_VOTE { - AUTH, - DROP - } - - public ExpectNonceVote(final EthTransactions eth, final CLIQUE_NONCE_VOTE vote) { - this.eth = eth; - this.expectedNonce = vote == AUTH ? NONCE_AUTH : NONCE_DROP; - } - - @Override - public void verify(final Node node) { - WaitUtils.waitFor( - () -> assertThat(node.execute(eth.block()).getNonceRaw()).isEqualTo(expectedNonce)); - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectProposals.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectProposals.java deleted file mode 100644 index 715c3501672..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectProposals.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.clique; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.tests.acceptance.dsl.WaitUtils; -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions; - -import java.util.Map; - -public class ExpectProposals implements Condition { - private final CliqueTransactions clique; - private final Map proposers; - - public ExpectProposals(final CliqueTransactions clique, final Map proposers) { - this.clique = clique; - this.proposers = proposers; - } - - @Override - public void verify(final Node node) { - WaitUtils.waitFor( - () -> assertThat(node.execute(clique.createProposals())).isEqualTo(proposers)); - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidators.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidators.java deleted file mode 100644 index 4d25745333b..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidators.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.clique; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions.LATEST; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.tests.acceptance.dsl.WaitUtils; -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions; - -public class ExpectValidators implements Condition { - private final CliqueTransactions clique; - private final Address[] validators; - - public ExpectValidators(final CliqueTransactions clique, final Address... validators) { - this.clique = clique; - this.validators = validators; - } - - @Override - public void verify(final Node node) { - WaitUtils.waitFor( - () -> - assertThat(node.execute(clique.createGetSigners(LATEST))).containsExactly(validators)); - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidatorsAtBlock.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidatorsAtBlock.java deleted file mode 100644 index 6482dd13e6c..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidatorsAtBlock.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.clique; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.tests.acceptance.dsl.WaitUtils; -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions; - -public class ExpectValidatorsAtBlock implements Condition { - private final CliqueTransactions clique; - private final String blockParameter; - private final Address[] validators; - - public ExpectValidatorsAtBlock( - final CliqueTransactions clique, final String blockNumber, final Address... validators) { - this.clique = clique; - this.blockParameter = blockNumber; - this.validators = validators; - } - - @Override - public void verify(final Node node) { - WaitUtils.waitFor( - () -> - assertThat(node.execute(clique.createGetSigners(blockParameter))) - .containsExactly(validators)); - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidatorsAtBlockHash.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidatorsAtBlockHash.java deleted file mode 100644 index f9d3e26289d..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectValidatorsAtBlockHash.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.clique; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.tests.acceptance.dsl.WaitUtils; -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions; - -public class ExpectValidatorsAtBlockHash implements Condition { - private final CliqueTransactions clique; - private final Hash blockHash; - private final Address[] validators; - - public ExpectValidatorsAtBlockHash( - final CliqueTransactions clique, final Hash blockHash, final Address... validators) { - this.clique = clique; - this.blockHash = blockHash; - this.validators = validators; - } - - @Override - public void verify(final Node node) { - WaitUtils.waitFor( - () -> - assertThat(node.execute(clique.createGetSignersAtHash(blockHash))) - .containsExactly(validators)); - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectedBlockHasProposer.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectedBlockHasProposer.java deleted file mode 100644 index 4abba67a0d5..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/clique/ExpectedBlockHasProposer.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.clique; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.consensus.clique.CliqueBlockHeaderFunctions; -import org.hyperledger.besu.consensus.clique.CliqueExtraData; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.tests.acceptance.dsl.BlockUtils; -import org.hyperledger.besu.tests.acceptance.dsl.WaitUtils; -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.eth.EthTransactions; - -import org.web3j.protocol.core.methods.response.EthBlock.Block; - -public class ExpectedBlockHasProposer implements Condition { - private final EthTransactions eth; - private final Address proposer; - - public ExpectedBlockHasProposer(final EthTransactions eth, final Address proposer) { - this.eth = eth; - this.proposer = proposer; - } - - @Override - public void verify(final Node node) { - WaitUtils.waitFor(() -> assertThat(proposerAddress(node)).isEqualTo(proposer)); - } - - private Address proposerAddress(final Node node) { - final Block block = node.execute(eth.block()); - final BlockHeader blockHeader = - BlockUtils.createBlockHeader(block, new CliqueBlockHeaderFunctions()); - final CliqueExtraData cliqueExtraData = CliqueExtraData.decode(blockHeader); - return cliqueExtraData.getProposerAddress(); - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java index 97f739ce872..0fc96e6c720 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java @@ -45,7 +45,6 @@ import org.hyperledger.besu.tests.acceptance.dsl.transaction.admin.AdminRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.bft.BftRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.bft.ConsensusType; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.debug.DebugRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.login.LoginRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.miner.MinerRequestFactory; @@ -496,7 +495,6 @@ public NodeRequests nodeRequests() { new NodeRequests( web3jService, new JsonRpc2_0Web3j(web3jService, 2000, Async.defaultExecutorService()), - new CliqueRequestFactory(web3jService), new BftRequestFactory(web3jService, bftType), new PermissioningJsonRpcRequestFactory(web3jService), new AdminRequestFactory(web3jService), diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/NodeConfigurationFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/NodeConfigurationFactory.java index c2e82c0f835..e2f5b2d444f 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/NodeConfigurationFactory.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/NodeConfigurationFactory.java @@ -17,7 +17,6 @@ import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ADMIN; -import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.CLIQUE; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.IBFT; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.MINER; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.QBFT; @@ -48,12 +47,6 @@ public Optional createGenesisConfigForValidators( return genesisConfigProvider.create(nodes); } - public JsonRpcConfiguration createJsonRpcWithCliqueEnabledConfig(final Set extraRpcApis) { - final var enabledApis = new HashSet<>(extraRpcApis); - enabledApis.add(CLIQUE.name()); - return createJsonRpcWithRpcApiEnabledConfig(enabledApis.toArray(String[]::new)); - } - public JsonRpcConfiguration createJsonRpcWithIbft2EnabledConfig(final boolean minerEnabled) { return minerEnabled ? createJsonRpcWithRpcApiEnabledConfig(IBFT.name(), MINER.name()) diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/NodeRequests.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/NodeRequests.java index e5bddf8e9bd..f36fded86a6 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/NodeRequests.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/NodeRequests.java @@ -16,7 +16,6 @@ import org.hyperledger.besu.tests.acceptance.dsl.transaction.admin.AdminRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.bft.BftRequestFactory; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.debug.DebugRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.login.LoginRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.miner.MinerRequestFactory; @@ -34,7 +33,6 @@ public class NodeRequests { private final Web3jService web3jService; private final Web3j netEth; - private final CliqueRequestFactory clique; private final BftRequestFactory bft; private final PermissioningJsonRpcRequestFactory perm; private final AdminRequestFactory admin; @@ -49,7 +47,6 @@ public class NodeRequests { public NodeRequests( final Web3jService web3jService, final Web3j netEth, - final CliqueRequestFactory clique, final BftRequestFactory bft, final PermissioningJsonRpcRequestFactory perm, final AdminRequestFactory admin, @@ -62,7 +59,6 @@ public NodeRequests( final PluginsRequestFactory plugins) { this.web3jService = web3jService; this.netEth = netEth; - this.clique = clique; this.bft = bft; this.perm = perm; this.admin = admin; @@ -83,10 +79,6 @@ public Web3j net() { return netEth; } - public CliqueRequestFactory clique() { - return clique; - } - public BftRequestFactory bft() { return bft; } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueDiscard.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueDiscard.java deleted file mode 100644 index 5888913760b..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueDiscard.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.transaction.clique; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; - -import java.io.IOException; - -import org.web3j.protocol.core.Response; - -public class CliqueDiscard implements Transaction { - private final String address; - - public CliqueDiscard(final String address) { - this.address = address; - } - - @Override - public Boolean execute(final NodeRequests node) { - try { - final Response result = node.clique().cliqueDiscard(address).send(); - assertThat(result).isNotNull(); - assertThat(result.hasError()).isFalse(); - return result.getResult(); - } catch (final IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueGetSigners.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueGetSigners.java deleted file mode 100644 index 481d6bd3460..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueGetSigners.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.transaction.clique; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; - -import java.io.IOException; -import java.util.List; - -public class CliqueGetSigners implements Transaction> { - private final String blockNumber; - - public CliqueGetSigners(final String blockNumber) { - this.blockNumber = blockNumber; - } - - @Override - public List
execute(final NodeRequests node) { - try { - final CliqueRequestFactory.SignersBlockResponse result = - node.clique().cliqueGetSigners(blockNumber).send(); - assertThat(result).isNotNull(); - assertThat(result.hasError()).isFalse(); - return result.getResult(); - } catch (final IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueGetSignersAtHash.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueGetSignersAtHash.java deleted file mode 100644 index 31a1e8872cc..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueGetSignersAtHash.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.transaction.clique; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; - -import java.io.IOException; -import java.util.List; - -public class CliqueGetSignersAtHash implements Transaction> { - private final Hash hash; - - public CliqueGetSignersAtHash(final Hash hash) { - this.hash = hash; - } - - @Override - public List
execute(final NodeRequests node) { - try { - final CliqueRequestFactory.SignersBlockResponse result = - node.clique().cliqueGetSignersAtHash(hash).send(); - assertThat(result).isNotNull(); - assertThat(result.hasError()).isFalse(); - return result.getResult(); - } catch (final IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueProposals.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueProposals.java deleted file mode 100644 index 634af775be9..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueProposals.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.transaction.clique; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; - -import java.io.IOException; -import java.util.Map; - -public class CliqueProposals implements Transaction> { - - @Override - public Map execute(final NodeRequests node) { - try { - final CliqueRequestFactory.ProposalsResponse result = node.clique().cliqueProposals().send(); - assertThat(result).isNotNull(); - assertThat(result.hasError()).isFalse(); - return result.getResult(); - } catch (final IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliquePropose.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliquePropose.java deleted file mode 100644 index 69462e29de6..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliquePropose.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.transaction.clique; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; - -import java.io.IOException; - -public class CliquePropose implements Transaction { - private final String address; - private final boolean auth; - - public CliquePropose(final String address, final boolean auth) { - this.address = address; - this.auth = auth; - } - - @Override - public Boolean execute(final NodeRequests node) { - try { - final CliqueRequestFactory.ProposeResponse result = - node.clique().cliquePropose(address, auth).send(); - assertThat(result).isNotNull(); - assertThat(result.hasError()).isFalse(); - return result.getResult(); - } catch (final IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueRequestFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueRequestFactory.java deleted file mode 100644 index a25b568eae0..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueRequestFactory.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.transaction.clique; - -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Hash; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import org.web3j.protocol.Web3jService; -import org.web3j.protocol.core.Request; -import org.web3j.protocol.core.Response; - -public class CliqueRequestFactory { - - public static class ProposeResponse extends Response {} - - public static class DiscardResponse extends Response {} - - public static class SignersBlockResponse extends Response> {} - - public static class ProposalsResponse extends Response> {} - - private final Web3jService web3jService; - - public CliqueRequestFactory(final Web3jService web3jService) { - this.web3jService = web3jService; - } - - Request cliquePropose(final String address, final Boolean auth) { - return new Request<>( - "clique_propose", - Arrays.asList(address, auth.toString()), - web3jService, - ProposeResponse.class); - } - - Request cliqueDiscard(final String address) { - return new Request<>( - "clique_discard", singletonList(address), web3jService, DiscardResponse.class); - } - - Request cliqueProposals() { - return new Request<>("clique_proposals", emptyList(), web3jService, ProposalsResponse.class); - } - - Request cliqueGetSigners(final String blockNumber) { - return new Request<>( - "clique_getSigners", singletonList(blockNumber), web3jService, SignersBlockResponse.class); - } - - Request cliqueGetSignersAtHash(final Hash hash) { - return new Request<>( - "clique_getSignersAtHash", - singletonList(hash.toString()), - web3jService, - SignersBlockResponse.class); - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueTransactions.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueTransactions.java deleted file mode 100644 index 9df60f969a9..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/clique/CliqueTransactions.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.tests.acceptance.dsl.transaction.clique; - -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; - -public class CliqueTransactions { - public static final String LATEST = "latest"; - - public CliquePropose createRemoveProposal(final BesuNode node) { - return propose(node.getAddress().toString(), false); - } - - public CliquePropose createAddProposal(final BesuNode node) { - return propose(node.getAddress().toString(), true); - } - - private CliquePropose propose(final String address, final boolean auth) { - return new CliquePropose(address, auth); - } - - public CliqueProposals createProposals() { - return new CliqueProposals(); - } - - public CliqueGetSigners createGetSigners(final String blockNumber) { - return new CliqueGetSigners(blockNumber); - } - - public CliqueGetSignersAtHash createGetSignersAtHash(final Hash blockHash) { - return new CliqueGetSignersAtHash(blockHash); - } - - public CliqueDiscard createDiscardProposal(final BesuNode node) { - return new CliqueDiscard(node.getAddress().toString()); - } -} diff --git a/app/src/main/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilder.java b/app/src/main/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilder.java index 93374af51b0..564709ed034 100644 --- a/app/src/main/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilder.java +++ b/app/src/main/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilder.java @@ -24,14 +24,12 @@ import org.hyperledger.besu.consensus.clique.blockcreation.CliqueBlockScheduler; import org.hyperledger.besu.consensus.clique.blockcreation.CliqueMinerExecutor; import org.hyperledger.besu.consensus.clique.blockcreation.CliqueMiningCoordinator; -import org.hyperledger.besu.consensus.clique.jsonrpc.CliqueJsonRpcMethods; import org.hyperledger.besu.consensus.common.BlockInterface; import org.hyperledger.besu.consensus.common.EpochManager; import org.hyperledger.besu.consensus.common.ForksSchedule; import org.hyperledger.besu.consensus.common.validator.blockbased.BlockValidatorProvider; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.api.jsonrpc.methods.JsonRpcMethods; import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; @@ -69,14 +67,6 @@ protected void prepForBuild() { forksSchedule = CliqueForksSchedulesFactory.create(genesisConfigOptions); } - @Override - protected JsonRpcMethods createAdditionalJsonRpcMethodFactory( - final ProtocolContext protocolContext, - final ProtocolSchedule protocolSchedule, - final MiningConfiguration miningConfiguration) { - return new CliqueJsonRpcMethods(protocolContext, protocolSchedule, miningConfiguration); - } - @Override protected MiningCoordinator createMiningCoordinator( final ProtocolSchedule protocolSchedule, @@ -152,7 +142,7 @@ protected void validateContext(final ProtocolContext context) { @Override protected PluginServiceFactory createAdditionalPluginServices( final Blockchain blockchain, final ProtocolContext protocolContext) { - return new CliqueQueryPluginServiceFactory(blockchain, nodeKey); + return new NoopPluginServiceFactory(); } @Override diff --git a/app/src/main/java/org/hyperledger/besu/controller/CliqueQueryPluginServiceFactory.java b/app/src/main/java/org/hyperledger/besu/controller/CliqueQueryPluginServiceFactory.java deleted file mode 100644 index 9d9a2a901c8..00000000000 --- a/app/src/main/java/org/hyperledger/besu/controller/CliqueQueryPluginServiceFactory.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.controller; - -import org.hyperledger.besu.consensus.clique.CliqueBlockInterface; -import org.hyperledger.besu.consensus.common.BlockInterface; -import org.hyperledger.besu.consensus.common.PoaQueryServiceImpl; -import org.hyperledger.besu.cryptoservices.NodeKey; -import org.hyperledger.besu.ethereum.chain.Blockchain; -import org.hyperledger.besu.plugin.services.query.PoaQueryService; -import org.hyperledger.besu.services.BesuPluginContextImpl; - -/** The Clique query plugin service factory. */ -public class CliqueQueryPluginServiceFactory implements PluginServiceFactory { - - private final Blockchain blockchain; - private final NodeKey nodeKey; - - /** - * Instantiates a new Clique query plugin service factory. - * - * @param blockchain the blockchain - * @param nodeKey the node key - */ - public CliqueQueryPluginServiceFactory(final Blockchain blockchain, final NodeKey nodeKey) { - this.blockchain = blockchain; - this.nodeKey = nodeKey; - } - - @Override - public void appendPluginServices(final BesuPluginContextImpl besuContext) { - final BlockInterface blockInterface = new CliqueBlockInterface(); - final PoaQueryServiceImpl service = - new PoaQueryServiceImpl(blockInterface, blockchain, nodeKey); - besuContext.addService(PoaQueryService.class, service); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/CliqueJsonRpcMethods.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/CliqueJsonRpcMethods.java deleted file mode 100644 index 4e1e75f54c8..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/CliqueJsonRpcMethods.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc; - -import org.hyperledger.besu.consensus.clique.CliqueBlockInterface; -import org.hyperledger.besu.consensus.clique.CliqueContext; -import org.hyperledger.besu.consensus.clique.jsonrpc.methods.CliqueGetSignerMetrics; -import org.hyperledger.besu.consensus.clique.jsonrpc.methods.CliqueGetSigners; -import org.hyperledger.besu.consensus.clique.jsonrpc.methods.CliqueGetSignersAtHash; -import org.hyperledger.besu.consensus.clique.jsonrpc.methods.CliqueProposals; -import org.hyperledger.besu.consensus.clique.jsonrpc.methods.Discard; -import org.hyperledger.besu.consensus.clique.jsonrpc.methods.Propose; -import org.hyperledger.besu.consensus.common.EpochManager; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.consensus.common.validator.blockbased.BlockValidatorProvider; -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.chain.MutableBlockchain; -import org.hyperledger.besu.ethereum.core.MiningConfiguration; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; - -import java.util.Map; - -/** The Clique json rpc methods. */ -public class CliqueJsonRpcMethods extends ApiGroupJsonRpcMethods { - private final ProtocolContext context; - private final ProtocolSchedule protocolSchedule; - private final MiningConfiguration miningConfiguration; - - /** - * Instantiates a new Clique json rpc methods. - * - * @param context the protocol context - * @param protocolSchedule the protocol schedule - * @param miningConfiguration the mining parameters - */ - public CliqueJsonRpcMethods( - final ProtocolContext context, - final ProtocolSchedule protocolSchedule, - final MiningConfiguration miningConfiguration) { - this.context = context; - this.protocolSchedule = protocolSchedule; - this.miningConfiguration = miningConfiguration; - } - - @Override - protected String getApiGroup() { - return RpcApis.CLIQUE.name(); - } - - @Override - protected Map create() { - final MutableBlockchain blockchain = context.getBlockchain(); - final WorldStateArchive worldStateArchive = context.getWorldStateArchive(); - final BlockchainQueries blockchainQueries = - new BlockchainQueries(protocolSchedule, blockchain, worldStateArchive, miningConfiguration); - final ValidatorProvider validatorProvider = - context.getConsensusContext(CliqueContext.class).getValidatorProvider(); - - // Must create our own voteTallyCache as using this would pollute the main voteTallyCache - final ValidatorProvider readOnlyValidatorProvider = - createValidatorProvider(context, blockchain); - - return mapOf( - new CliqueGetSigners(blockchainQueries, readOnlyValidatorProvider), - new CliqueGetSignersAtHash(blockchainQueries, readOnlyValidatorProvider), - new Propose(validatorProvider), - new Discard(validatorProvider), - new CliqueProposals(validatorProvider), - new CliqueGetSignerMetrics( - readOnlyValidatorProvider, new CliqueBlockInterface(), blockchainQueries)); - } - - private ValidatorProvider createValidatorProvider( - final ProtocolContext context, final MutableBlockchain blockchain) { - final EpochManager epochManager = - context.getConsensusContext(CliqueContext.class).getEpochManager(); - final CliqueBlockInterface cliqueBlockInterface = new CliqueBlockInterface(); - return BlockValidatorProvider.nonForkingValidatorProvider( - blockchain, epochManager, cliqueBlockInterface); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignerMetrics.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignerMetrics.java deleted file mode 100644 index 514ab0a4e60..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignerMetrics.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import org.hyperledger.besu.consensus.common.BlockInterface; -import org.hyperledger.besu.consensus.common.jsonrpc.AbstractGetSignerMetricsMethod; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; -import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; - -/** The Clique get signer metrics. */ -public class CliqueGetSignerMetrics extends AbstractGetSignerMetricsMethod - implements JsonRpcMethod { - - /** - * Instantiates a new Clique get signer metrics. - * - * @param validatorProvider the validator provider - * @param blockInterface the block interface - * @param blockchainQueries the blockchain queries - */ - public CliqueGetSignerMetrics( - final ValidatorProvider validatorProvider, - final BlockInterface blockInterface, - final BlockchainQueries blockchainQueries) { - super(validatorProvider, blockInterface, blockchainQueries); - } - - @Override - public String getName() { - return RpcMethod.CLIQUE_GET_SIGNER_METRICS.getMethodName(); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSigners.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSigners.java deleted file mode 100644 index 057fb30ee5e..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSigners.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -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.JsonRpcMethod; -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.JsonRpcErrorResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; -import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; -import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; -import org.hyperledger.besu.ethereum.core.BlockHeader; - -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -/** The Clique get signers. */ -public class CliqueGetSigners implements JsonRpcMethod { - private final BlockchainQueries blockchainQueries; - private final ValidatorProvider validatorProvider; - - /** - * Instantiates a new Clique get signers. - * - * @param blockchainQueries the blockchain queries - * @param validatorProvider the validator provider - */ - public CliqueGetSigners( - final BlockchainQueries blockchainQueries, final ValidatorProvider validatorProvider) { - this.blockchainQueries = blockchainQueries; - this.validatorProvider = validatorProvider; - } - - @Override - public String getName() { - return RpcMethod.CLIQUE_GET_SIGNERS.getMethodName(); - } - - @Override - public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { - final Optional blockHeader = determineBlockHeader(requestContext); - return blockHeader - .map(validatorProvider::getValidatorsAfterBlock) - .map(addresses -> addresses.stream().map(Objects::toString).collect(Collectors.toList())) - .map( - addresses -> new JsonRpcSuccessResponse(requestContext.getRequest().getId(), addresses)) - .orElse( - new JsonRpcErrorResponse( - requestContext.getRequest().getId(), RpcErrorType.INTERNAL_ERROR)); - } - - private Optional determineBlockHeader(final JsonRpcRequestContext request) { - final Optional blockParameter; - try { - blockParameter = request.getOptionalParameter(0, BlockParameter.class); - } catch (JsonRpcParameterException e) { - throw new InvalidJsonRpcParameters( - "Invalid block parameter (index 0)", RpcErrorType.INVALID_BLOCK_PARAMS, e); - } - final long latest = blockchainQueries.headBlockNumber(); - final long blockNumber = blockParameter.map(b -> b.getNumber().orElse(latest)).orElse(latest); - return blockchainQueries.blockByNumber(blockNumber).map(BlockWithMetadata::getHeader); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHash.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHash.java deleted file mode 100644 index 908ff9f4123..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHash.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -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.JsonRpcMethod; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; -import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; -import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; -import org.hyperledger.besu.ethereum.core.BlockHeader; - -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -/** The Clique get signers at hash. */ -public class CliqueGetSignersAtHash implements JsonRpcMethod { - private final BlockchainQueries blockchainQueries; - private final ValidatorProvider validatorProvider; - - /** - * Instantiates a new Clique get signers at hash. - * - * @param blockchainQueries the blockchain queries - * @param validatorProvider the validator provider - */ - public CliqueGetSignersAtHash( - final BlockchainQueries blockchainQueries, final ValidatorProvider validatorProvider) { - this.blockchainQueries = blockchainQueries; - this.validatorProvider = validatorProvider; - } - - @Override - public String getName() { - return RpcMethod.CLIQUE_GET_SIGNERS_AT_HASH.getMethodName(); - } - - @Override - public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { - final Optional blockHeader = determineBlockHeader(requestContext); - return blockHeader - .map(validatorProvider::getValidatorsAfterBlock) - .map(addresses -> addresses.stream().map(Objects::toString).collect(Collectors.toList())) - .map( - addresses -> new JsonRpcSuccessResponse(requestContext.getRequest().getId(), addresses)) - .orElse( - new JsonRpcErrorResponse( - requestContext.getRequest().getId(), RpcErrorType.INTERNAL_ERROR)); - } - - private Optional determineBlockHeader(final JsonRpcRequestContext request) { - final Hash hash; - try { - hash = request.getRequiredParameter(0, Hash.class); - } catch (JsonRpcParameterException e) { - throw new InvalidJsonRpcParameters( - "Invalid block hash parameter (index 0)", RpcErrorType.INVALID_BLOCK_HASH_PARAMS, e); - } - return blockchainQueries.blockByHash(hash).map(BlockWithMetadata::getHeader); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueProposals.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueProposals.java deleted file mode 100644 index 02a266c4364..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueProposals.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import org.hyperledger.besu.consensus.common.jsonrpc.AbstractVoteProposerMethod; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; - -/** The Clique proposals. */ -public class CliqueProposals extends AbstractVoteProposerMethod implements JsonRpcMethod { - - /** - * Instantiates a new Clique proposals. - * - * @param validatorProvider the validator provider - */ - public CliqueProposals(final ValidatorProvider validatorProvider) { - super(validatorProvider); - } - - @Override - public String getName() { - return RpcMethod.CLIQUE_GET_PROPOSALS.getMethodName(); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/Discard.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/Discard.java deleted file mode 100644 index da3214b1ef0..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/Discard.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import static com.google.common.base.Preconditions.checkState; - -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.datatypes.Address; -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.JsonRpcMethod; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; - -/** The Discard Json RPC method. */ -public class Discard implements JsonRpcMethod { - private final ValidatorProvider validatorProvider; - - /** - * Instantiates a new Discard. - * - * @param validatorProvider the validator provider - */ - public Discard(final ValidatorProvider validatorProvider) { - this.validatorProvider = validatorProvider; - } - - @Override - public String getName() { - return RpcMethod.CLIQUE_DISCARD.getMethodName(); - } - - @Override - public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { - checkState( - validatorProvider.getVoteProviderAtHead().isPresent(), "Clique requires a vote provider"); - final Address address; - try { - address = requestContext.getRequiredParameter(0, Address.class); - } catch (JsonRpcParameterException e) { - throw new InvalidJsonRpcParameters( - "Invalid address parameter (index 0)", RpcErrorType.INVALID_ADDRESS_PARAMS, e); - } - validatorProvider.getVoteProviderAtHead().get().discardVote(address); - return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), true); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/Propose.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/Propose.java deleted file mode 100644 index 26ab19c9882..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/Propose.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import static com.google.common.base.Preconditions.checkState; - -import org.hyperledger.besu.consensus.clique.CliqueBlockInterface; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.datatypes.Address; -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.JsonRpcMethod; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; - -/** The Propose Json Rpc method. */ -public class Propose implements JsonRpcMethod { - private final ValidatorProvider validatorProvider; - - /** - * Instantiates a new Propose. - * - * @param validatorProvider the validator provider - */ - public Propose(final ValidatorProvider validatorProvider) { - this.validatorProvider = validatorProvider; - } - - @Override - public String getName() { - return RpcMethod.CLIQUE_PROPOSE.getMethodName(); - } - - @Override - public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { - checkState( - validatorProvider.getVoteProviderAtHead().isPresent(), "Clique requires a vote provider"); - final Address address; - try { - address = requestContext.getRequiredParameter(0, Address.class); - } catch (JsonRpcParameterException e) { - throw new InvalidJsonRpcParameters( - "Invalid address parameter (index 0)", RpcErrorType.INVALID_ADDRESS_PARAMS, e); - } - final Boolean auth; - try { - auth = requestContext.getRequiredParameter(1, Boolean.class); - } catch (JsonRpcParameterException e) { - throw new InvalidJsonRpcParameters( - "Invalid auth parameter (index 1)", RpcErrorType.INVALID_PROPOSAL_PARAMS, e); - } - if (address.equals(CliqueBlockInterface.NO_VOTE_SUBJECT)) { - return new JsonRpcErrorResponse( - requestContext.getRequest().getId(), RpcErrorType.INVALID_REQUEST); - } - - if (auth) { - validatorProvider.getVoteProviderAtHead().get().authVote(address); - } else { - validatorProvider.getVoteProviderAtHead().get().dropVote(address); - } - // Return true regardless, the vote is always recorded - return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), true); - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignerMetricsTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignerMetricsTest.java deleted file mode 100644 index 310b3b1017d..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignerMetricsTest.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.AdditionalMatchers.lt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.consensus.common.BlockInterface; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.datatypes.Address; -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.exception.InvalidJsonRpcParameters; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.SignerMetricResult; -import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.stream.LongStream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class CliqueGetSignerMetricsTest { - - private static final Address[] VALIDATORS = { - Address.fromHexString("0x1"), Address.fromHexString("0x2"), Address.fromHexString("0x3") - }; - - private final String CLIQUE_METHOD = "clique_getSignerMetrics"; - private final String JSON_RPC_VERSION = "2.0"; - private CliqueGetSignerMetrics method; - - private ValidatorProvider validatorProvider; - private BlockchainQueries blockchainQueries; - private BlockInterface blockInterface; - - @BeforeEach - public void setup() { - validatorProvider = mock(ValidatorProvider.class); - blockchainQueries = mock(BlockchainQueries.class); - blockInterface = mock(BlockInterface.class); - method = new CliqueGetSignerMetrics(validatorProvider, blockInterface, blockchainQueries); - } - - @Test - public void returnsCorrectMethodName() { - assertThat(method.getName()).isEqualTo(CLIQUE_METHOD); - } - - @Test - public void exceptionWhenInvalidStartBlockSupplied() { - assertThatThrownBy(() -> method.response(requestWithParams("INVALID"))) - .isInstanceOf(InvalidJsonRpcParameters.class) - .hasMessageContaining("Invalid start block parameter (index 0)"); - } - - @Test - public void exceptionWhenInvalidEndBlockSupplied() { - assertThatThrownBy(() -> method.response(requestWithParams("1", "INVALID"))) - .isInstanceOf(InvalidJsonRpcParameters.class) - .hasMessageContaining("Invalid end block parameter (index 1)"); - } - - @Test - @SuppressWarnings("unchecked") - public void getSignerMetricsWhenNoParams() { - - final long startBlock = 1L; - final long endBlock = 3L; - - when(blockchainQueries.headBlockNumber()).thenReturn(endBlock); - - final List signerMetricResultList = new ArrayList<>(); - - LongStream.range(startBlock, endBlock) - .forEach(value -> signerMetricResultList.add(generateBlock(value))); - - signerMetricResultList.add(new SignerMetricResult(VALIDATORS[0])); // missing validator - - final JsonRpcRequestContext request = requestWithParams(); - - final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); - - assertThat((Collection) response.getResult()) - .containsExactlyInAnyOrderElementsOf(signerMetricResultList); - } - - @Test - @SuppressWarnings("unchecked") - public void getSignerMetrics() { - - final long startBlock = 1L; - final long endBlock = 5L; - - when(blockchainQueries.headBlockNumber()).thenReturn(endBlock); - - final List signerMetricResultList = new ArrayList<>(); - - // sign a first block with keypairs number 1 - final SignerMetricResult signerMetricResultFirstKeyPairs = generateBlock(startBlock); - signerMetricResultList.add(signerMetricResultFirstKeyPairs); - // sign a second block with keypairs number 2 - final SignerMetricResult signerMetricResultSecondKeyPairs = generateBlock(startBlock + 1); - signerMetricResultList.add(signerMetricResultSecondKeyPairs); - // sign a third block with keypairs number 3 - final SignerMetricResult signerMetricResultThirdKeyPairs = generateBlock(startBlock + 2); - signerMetricResultList.add(signerMetricResultThirdKeyPairs); - // sign the last block with the keypairs number 1 - generateBlock(startBlock + 3); - signerMetricResultFirstKeyPairs.setLastProposedBlockNumber(startBlock + 3); - signerMetricResultFirstKeyPairs.incrementeNbBlock(); - - final JsonRpcRequestContext request = requestWithParams(); - - final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); - - assertThat((Collection) response.getResult()) - .containsExactlyInAnyOrderElementsOf(signerMetricResultList); - } - - @Test - @SuppressWarnings("unchecked") - public void getSignerMetricsWhenThereAreFewerBlocksThanTheDefaultRange() { - final long startBlock = 0L; - final long headBlock = 2L; - - final List signerMetricResultList = new ArrayList<>(); - - when(blockchainQueries.headBlockNumber()).thenReturn(headBlock); - - LongStream.range(startBlock, headBlock) - .forEach(value -> signerMetricResultList.add(generateBlock(value))); - - signerMetricResultList.add(new SignerMetricResult(VALIDATORS[2])); // missing validator - - final JsonRpcRequestContext request = requestWithParams(); - - final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); - - // verify getBlockHeaderByNumber is not called with negative values - verify(blockchainQueries, never()).getBlockHeaderByNumber(lt(0L)); - - assertThat((Collection) response.getResult()) - .containsExactlyInAnyOrderElementsOf(signerMetricResultList); - } - - @Test - @SuppressWarnings("unchecked") - public void getSignerMetricsWithLatest() { - - final long startBlock = 1L; - final long endBlock = 3L; - - final List signerMetricResultList = new ArrayList<>(); - - when(blockchainQueries.headBlockNumber()).thenReturn(endBlock); - - LongStream.range(startBlock, endBlock) - .forEach(value -> signerMetricResultList.add(generateBlock(value))); - - signerMetricResultList.add(new SignerMetricResult(VALIDATORS[0])); // missing validator - - final JsonRpcRequestContext request = requestWithParams(String.valueOf(startBlock), "latest"); - - final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); - - assertThat((Collection) response.getResult()) - .containsExactlyInAnyOrderElementsOf(signerMetricResultList); - } - - @Test - @SuppressWarnings("unchecked") - public void getSignerMetricsWithPending() { - - final long startBlock = 1L; - final long endBlock = 3L; - - final List signerMetricResultList = new ArrayList<>(); - - when(blockchainQueries.headBlockNumber()).thenReturn(endBlock); - - LongStream.range(startBlock, endBlock) - .forEach(value -> signerMetricResultList.add(generateBlock(value))); - - signerMetricResultList.add(new SignerMetricResult(VALIDATORS[0])); // missing validator - - final JsonRpcRequestContext request = requestWithParams(String.valueOf(startBlock), "pending"); - - final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); - - assertThat((Collection) response.getResult()) - .containsExactlyInAnyOrderElementsOf(signerMetricResultList); - } - - @Test - @SuppressWarnings("unchecked") - public void getSignerMetricsWithEarliest() { - - final long startBlock = 0L; - final long endBlock = 3L; - - final List signerMetricResultList = new ArrayList<>(); - - when(blockchainQueries.headBlockNumber()).thenReturn(endBlock); - - LongStream.range(startBlock, endBlock) - .forEach(value -> signerMetricResultList.add(generateBlock(value))); - - final JsonRpcRequestContext request = requestWithParams("earliest", String.valueOf(endBlock)); - - final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); - - assertThat((Collection) response.getResult()) - .containsExactlyInAnyOrderElementsOf(signerMetricResultList); - } - - private JsonRpcRequestContext requestWithParams(final Object... params) { - return new JsonRpcRequestContext(new JsonRpcRequest(JSON_RPC_VERSION, CLIQUE_METHOD, params)); - } - - private SignerMetricResult generateBlock(final long number) { - - final Address proposerAddressBlock = VALIDATORS[(int) (number % VALIDATORS.length)]; - - final BlockHeader header = new BlockHeaderTestFixture().number(number).buildHeader(); - - when(blockchainQueries.getBlockHeaderByNumber(number)).thenReturn(Optional.of(header)); - when(blockInterface.getProposerOfBlock(header)).thenReturn(proposerAddressBlock); - - when(validatorProvider.getValidatorsAfterBlock(header)).thenReturn((Arrays.asList(VALIDATORS))); - - final SignerMetricResult signerMetricResult = new SignerMetricResult(proposerAddressBlock); - signerMetricResult.incrementeNbBlock(); - signerMetricResult.setLastProposedBlockNumber(number); - - return signerMetricResult; - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHashTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHashTest.java deleted file mode 100644 index 3699ccf6eed..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHashTest.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import static java.util.Arrays.asList; -import static java.util.stream.Collectors.toList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.datatypes.Address.fromHexString; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.consensus.clique.CliqueBlockHeaderFunctions; -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.exception.InvalidJsonRpcParameters; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; -import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; -import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; -import org.hyperledger.besu.ethereum.api.query.TransactionWithMetadata; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; - -import java.util.List; -import java.util.Optional; - -import org.apache.tuweni.bytes.Bytes; -import org.assertj.core.api.AssertionsForClassTypes; -import org.bouncycastle.util.encoders.Hex; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -public class CliqueGetSignersAtHashTest { - - private CliqueGetSignersAtHash method; - private BlockHeader blockHeader; - private List
validators; - private List validatorsAsStrings; - - @Mock private BlockchainQueries blockchainQueries; - @Mock private BlockWithMetadata blockWithMetadata; - @Mock private ValidatorProvider validatorProvider; - - public static final String BLOCK_HASH = - "0xe36a3edf0d8664002a72ef7c5f8e271485e7ce5c66455a07cb679d855818415f"; - - @BeforeEach - public void setup() { - method = new CliqueGetSignersAtHash(blockchainQueries, validatorProvider); - - final byte[] genesisBlockExtraData = - Hex.decode( - "52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); - - blockHeader = - new BlockHeaderTestFixture() - .blockHeaderFunctions(new CliqueBlockHeaderFunctions()) - .extraData(Bytes.wrap(genesisBlockExtraData)) - .buildHeader(); - - validators = - asList( - fromHexString("0x42eb768f2244c8811c63729a21a3569731535f06"), - fromHexString("0x7ffc57839b00206d1ad20c69a1981b489f772031"), - fromHexString("0xb279182d99e65703f0076e4812653aab85fca0f0")); - validatorsAsStrings = validators.stream().map(Object::toString).collect(toList()); - } - - @Test - public void returnsMethodName() { - assertThat(method.getName()).isEqualTo("clique_getSignersAtHash"); - } - - @Test - @SuppressWarnings("unchecked") - public void failsWhenNoParam() { - final JsonRpcRequestContext request = - new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "clique_getSignersAtHash", new Object[] {})); - - final Throwable thrown = AssertionsForClassTypes.catchThrowable(() -> method.response(request)); - - assertThat(thrown) - .isInstanceOf(InvalidJsonRpcParameters.class) - .hasMessage("Invalid block hash parameter (index 0)"); - } - - @Test - @SuppressWarnings("unchecked") - public void returnsValidatorsForBlockHash() { - final JsonRpcRequestContext request = - new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "clique_getSignersAtHash", new Object[] {BLOCK_HASH})); - - when(blockchainQueries.blockByHash(Hash.fromHexString(BLOCK_HASH))) - .thenReturn(Optional.of(blockWithMetadata)); - when(blockWithMetadata.getHeader()).thenReturn(blockHeader); - when(validatorProvider.getValidatorsAfterBlock(blockHeader)).thenReturn(validators); - - final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); - assertThat(response.getResult()).isEqualTo(validatorsAsStrings); - } - - @Test - public void failsOnInvalidBlockHash() { - final JsonRpcRequestContext request = - new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {BLOCK_HASH})); - - when(blockchainQueries.blockByHash(Hash.fromHexString(BLOCK_HASH))) - .thenReturn(Optional.empty()); - - final JsonRpcErrorResponse response = (JsonRpcErrorResponse) method.response(request); - assertThat(response.getErrorType()).isEqualTo(RpcErrorType.INTERNAL_ERROR); - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersTest.java deleted file mode 100644 index 8cb6fd11039..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueGetSignersTest.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import static java.util.Arrays.asList; -import static java.util.stream.Collectors.toList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.datatypes.Address.fromHexString; -import static org.mockito.Mockito.when; - -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.JsonRpcErrorResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; -import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; -import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; -import org.hyperledger.besu.ethereum.api.query.TransactionWithMetadata; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; - -import java.util.List; -import java.util.Optional; - -import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -public class CliqueGetSignersTest { - private CliqueGetSigners method; - private BlockHeader blockHeader; - private List
validators; - private List validatorAsStrings; - - @Mock private BlockchainQueries blockchainQueries; - @Mock private ValidatorProvider validatorProvider; - @Mock private BlockWithMetadata blockWithMetadata; - - @BeforeEach - public void setup() { - method = new CliqueGetSigners(blockchainQueries, validatorProvider); - - final String genesisBlockExtraData = - "52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - final Bytes bufferToInject = Bytes.fromHexString(genesisBlockExtraData); - final BlockHeaderTestFixture blockHeaderTestFixture = new BlockHeaderTestFixture(); - blockHeader = blockHeaderTestFixture.extraData(bufferToInject).buildHeader(); - - validators = - asList( - fromHexString("0x42eb768f2244c8811c63729a21a3569731535f06"), - fromHexString("0x7ffc57839b00206d1ad20c69a1981b489f772031"), - fromHexString("0xb279182d99e65703f0076e4812653aab85fca0f0")); - validatorAsStrings = validators.stream().map(Object::toString).collect(toList()); - } - - @Test - public void returnsMethodName() { - assertThat(method.getName()).isEqualTo("clique_getSigners"); - } - - @Test - @SuppressWarnings("unchecked") - public void returnsValidatorsWhenNoParam() { - final JsonRpcRequestContext request = - new JsonRpcRequestContext(new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {})); - - when(blockchainQueries.headBlockNumber()).thenReturn(3065995L); - when(blockchainQueries.blockByNumber(3065995L)).thenReturn(Optional.of(blockWithMetadata)); - when(blockWithMetadata.getHeader()).thenReturn(blockHeader); - when(validatorProvider.getValidatorsAfterBlock(blockHeader)).thenReturn(validators); - - final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); - assertThat(response.getResult()).isEqualTo(validatorAsStrings); - } - - @Test - @SuppressWarnings("unchecked") - public void returnsValidatorsForBlockNumber() { - final JsonRpcRequestContext request = - new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {"0x2EC88B"})); - - when(blockchainQueries.blockByNumber(3065995L)).thenReturn(Optional.of(blockWithMetadata)); - when(blockWithMetadata.getHeader()).thenReturn(blockHeader); - when(validatorProvider.getValidatorsAfterBlock(blockHeader)).thenReturn(validators); - - final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); - assertThat(response.getResult()).isEqualTo(validatorAsStrings); - } - - @Test - public void failsOnInvalidBlockNumber() { - final JsonRpcRequestContext request = - new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {"0x1234"})); - - when(blockchainQueries.blockByNumber(4660)).thenReturn(Optional.empty()); - - final JsonRpcErrorResponse response = (JsonRpcErrorResponse) method.response(request); - assertThat(response.getErrorType()).isEqualTo(RpcErrorType.INTERNAL_ERROR); - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueProposalsTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueProposalsTest.java deleted file mode 100644 index cdfbeb6299b..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/CliqueProposalsTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.consensus.common.jsonrpc.AbstractVoteProposerMethod; -import org.hyperledger.besu.consensus.common.jsonrpc.AbstractVoteProposerMethodTest; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class CliqueProposalsTest extends AbstractVoteProposerMethodTest { - - private CliqueProposals method; - - @Override - protected AbstractVoteProposerMethod getMethod() { - return method; - } - - @Override - protected String getMethodName() { - return "clique_proposals"; - } - - @BeforeEach - public void setup() { - method = new CliqueProposals(getValidatorProvider()); - } - - @Test - public void returnsCorrectMethodName() { - assertThat(method.getName()).isEqualTo(getMethodName()); - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/DiscardTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/DiscardTest.java deleted file mode 100644 index 80220a3f04a..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/DiscardTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; - -import org.hyperledger.besu.consensus.common.BlockInterface; -import org.hyperledger.besu.consensus.common.EpochManager; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.consensus.common.validator.VoteType; -import org.hyperledger.besu.consensus.common.validator.blockbased.BlockValidatorProvider; -import org.hyperledger.besu.datatypes.Address; -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.exception.InvalidJsonRpcParameters; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.chain.Blockchain; -import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -public class DiscardTest { - private final String JSON_RPC_VERSION = "2.0"; - private final String METHOD = "clique_discard"; - - private ValidatorProvider validatorProvider; - - @BeforeEach - public void setup() { - final Blockchain blockchain = mock(Blockchain.class); - final EpochManager epochManager = mock(EpochManager.class); - final BlockInterface blockInterface = mock(BlockInterface.class); - validatorProvider = - BlockValidatorProvider.nonForkingValidatorProvider( - blockchain, epochManager, blockInterface); - } - - @Test - public void discardEmpty() { - final Discard discard = new Discard(validatorProvider); - final Address a0 = Address.fromHexString("0"); - - final JsonRpcResponse response = discard.response(requestWithParams(a0)); - - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a0)).isNull(); - } - - @Test - public void discardAuth() { - final Discard discard = new Discard(validatorProvider); - final Address a0 = Address.fromHexString("0"); - - validatorProvider.getVoteProviderAtHead().get().authVote(a0); - - final JsonRpcResponse response = discard.response(requestWithParams(a0)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a0)).isNull(); - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - } - - @Test - public void discardDrop() { - final Discard discard = new Discard(validatorProvider); - final Address a0 = Address.fromHexString("0"); - - validatorProvider.getVoteProviderAtHead().get().dropVote(a0); - - final JsonRpcResponse response = discard.response(requestWithParams(a0)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a0)).isNull(); - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - } - - @Test - public void discardIsolation() { - final Discard discard = new Discard(validatorProvider); - final Address a0 = Address.fromHexString("0"); - final Address a1 = Address.fromHexString("1"); - - validatorProvider.getVoteProviderAtHead().get().authVote(a0); - validatorProvider.getVoteProviderAtHead().get().authVote(a1); - - final JsonRpcResponse response = discard.response(requestWithParams(a0)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a0)).isNull(); - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a1)) - .isEqualTo(VoteType.ADD); - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - } - - @Test - public void discardWithoutAddress() { - final Discard discard = new Discard(validatorProvider); - - assertThatThrownBy(() -> discard.response(requestWithParams())) - .hasMessage("Invalid address parameter (index 0)") - .isInstanceOf(InvalidJsonRpcParameters.class); - } - - private JsonRpcRequestContext requestWithParams(final Object... params) { - return new JsonRpcRequestContext(new JsonRpcRequest(JSON_RPC_VERSION, METHOD, params)); - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/ProposeTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/ProposeTest.java deleted file mode 100644 index 7e7d7bf7704..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/jsonrpc/methods/ProposeTest.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.jsonrpc.methods; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -import org.hyperledger.besu.consensus.common.BlockInterface; -import org.hyperledger.besu.consensus.common.EpochManager; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.consensus.common.validator.VoteType; -import org.hyperledger.besu.consensus.common.validator.blockbased.BlockValidatorProvider; -import org.hyperledger.besu.datatypes.Address; -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.JsonRpcErrorResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; -import org.hyperledger.besu.ethereum.chain.Blockchain; -import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class ProposeTest { - private final String JSON_RPC_VERSION = "2.0"; - private final String METHOD = "clique_propose"; - private ValidatorProvider validatorProvider; - - @BeforeEach - public void setup() { - final Blockchain blockchain = mock(Blockchain.class); - final EpochManager epochManager = mock(EpochManager.class); - final BlockInterface blockInterface = mock(BlockInterface.class); - validatorProvider = - BlockValidatorProvider.nonForkingValidatorProvider( - blockchain, epochManager, blockInterface); - } - - @Test - public void testAuth() { - final Propose propose = new Propose(validatorProvider); - final Address a1 = Address.fromHexString("1"); - - final JsonRpcResponse response = propose.response(requestWithParams(a1, true)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a1)) - .isEqualTo(VoteType.ADD); - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - } - - @Test - public void testAuthWithAddressZeroResultsInError() { - final Propose propose = new Propose(validatorProvider); - final Address a0 = Address.fromHexString("0"); - - final JsonRpcResponse response = propose.response(requestWithParams(a0, true)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a0)).isNull(); - assertThat(response.getType()).isEqualTo(RpcResponseType.ERROR); - final JsonRpcErrorResponse errorResponse = (JsonRpcErrorResponse) response; - assertThat(errorResponse.getErrorType()).isEqualTo(RpcErrorType.INVALID_REQUEST); - } - - @Test - public void testDrop() { - final Propose propose = new Propose(validatorProvider); - final Address a1 = Address.fromHexString("1"); - - final JsonRpcResponse response = propose.response(requestWithParams(a1, false)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a1)) - .isEqualTo(VoteType.DROP); - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - } - - @Test - public void testDropWithAddressZeroResultsInError() { - final Propose propose = new Propose(validatorProvider); - final Address a0 = Address.fromHexString("0"); - - final JsonRpcResponse response = propose.response(requestWithParams(a0, false)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a0)).isNull(); - assertThat(response.getType()).isEqualTo(RpcResponseType.ERROR); - final JsonRpcErrorResponse errorResponse = (JsonRpcErrorResponse) response; - assertThat(errorResponse.getErrorType()).isEqualTo(RpcErrorType.INVALID_REQUEST); - } - - @Test - public void testRepeatAuth() { - final Propose propose = new Propose(validatorProvider); - final Address a1 = Address.fromHexString("1"); - - validatorProvider.getVoteProviderAtHead().get().authVote(a1); - final JsonRpcResponse response = propose.response(requestWithParams(a1, true)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a1)) - .isEqualTo(VoteType.ADD); - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - } - - @Test - public void testRepeatDrop() { - final Propose propose = new Propose(validatorProvider); - final Address a1 = Address.fromHexString("1"); - - validatorProvider.getVoteProviderAtHead().get().dropVote(a1); - final JsonRpcResponse response = propose.response(requestWithParams(a1, false)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a1)) - .isEqualTo(VoteType.DROP); - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - } - - @Test - public void testChangeToAuth() { - final Propose propose = new Propose(validatorProvider); - final Address a1 = Address.fromHexString("1"); - - validatorProvider.getVoteProviderAtHead().get().dropVote(a1); - final JsonRpcResponse response = propose.response(requestWithParams(a1, true)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a1)) - .isEqualTo(VoteType.ADD); - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - } - - @Test - public void testChangeToDrop() { - final Propose propose = new Propose(validatorProvider); - final Address a0 = Address.fromHexString("1"); - - validatorProvider.getVoteProviderAtHead().get().authVote(a0); - final JsonRpcResponse response = propose.response(requestWithParams(a0, false)); - - assertThat(validatorProvider.getVoteProviderAtHead().get().getProposals().get(a0)) - .isEqualTo(VoteType.DROP); - assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS); - final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; - assertThat(successResponse.getResult()).isEqualTo(true); - } - - private JsonRpcRequestContext requestWithParams(final Object... params) { - return new JsonRpcRequestContext(new JsonRpcRequest(JSON_RPC_VERSION, METHOD, params)); - } -} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcApis.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcApis.java index a90134dac22..c426e03dedd 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcApis.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcApis.java @@ -30,7 +30,6 @@ public enum RpcApis { TXPOOL, TRACE, PLUGINS, - CLIQUE, IBFT, ENGINE, QBFT, diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java index f813e59ac31..679f1be52b1 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java @@ -26,12 +26,6 @@ public enum RpcMethod { ADMIN_GENERATE_LOG_BLOOM_CACHE("admin_generateLogBloomCache"), ADMIN_LOGS_REPAIR_CACHE("admin_logsRepairCache"), ADMIN_LOGS_REMOVE_CACHE("admin_logsRemoveCache"), - CLIQUE_DISCARD("clique_discard"), - CLIQUE_GET_SIGNERS("clique_getSigners"), - CLIQUE_GET_SIGNERS_AT_HASH("clique_getSignersAtHash"), - CLIQUE_GET_PROPOSALS("clique_proposals"), - CLIQUE_PROPOSE("clique_propose"), - CLIQUE_GET_SIGNER_METRICS("clique_getSignerMetrics"), DEBUG_ACCOUNT_AT("debug_accountAt"), DEBUG_ACCOUNT_RANGE("debug_accountRange"), DEBUG_METRICS("debug_metrics"), From d414b9c64dd483e7a86527570a3fb3142b238656 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 11 Mar 2026 12:42:42 +0100 Subject: [PATCH 24/77] Implement EIP-7975: eth/70 - partial block receipt lists (#9910) Signed-off-by: Fabio Di Fabio Co-authored-by: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + .../api/jsonrpc/eth/eth_protocolVersion.json | 2 +- .../besu/ethereum/eth/EthProtocol.java | 11 +- .../besu/ethereum/eth/EthProtocolVersion.java | 3 +- .../eth/manager/EthProtocolManager.java | 12 + .../besu/ethereum/eth/manager/EthServer.java | 125 +++- .../ProtocolViolationException.java | 35 + .../peertask/PeerTaskValidationResponse.java | 3 +- .../task/GetSyncReceiptsFromPeerTask.java | 188 ++++- .../messages/GetPaginatedReceiptsMessage.java | 64 ++ .../eth/messages/GetReceiptsMessage.java | 31 +- .../messages/PaginatedReceiptsMessage.java | 62 ++ .../eth/messages/ReceiptsMessage.java | 24 +- .../sync/common/DownloadSyncReceiptsStep.java | 51 +- .../eth/manager/EthProtocolManagerTest.java | 10 +- .../ethereum/eth/manager/EthServerTest.java | 252 ++++++- .../task/GetSyncReceiptsFromPeerTaskTest.java | 671 ++++++++++++------ .../GetPaginatedReceiptsMessageTest.java | 95 +++ .../eth/messages/GetReceiptsMessageTest.java | 2 +- .../PaginatedReceiptsMessageTest.java | 110 +++ .../common/DownloadSyncReceiptsStepTest.java | 258 +++++-- .../ethereum/p2p/rlpx/wire/MessageData.java | 9 +- .../rlpx/wire/messages/DisconnectMessage.java | 2 + .../hyperledger/besu/ethereum/rlp/RLP.java | 2 +- 24 files changed, 1651 insertions(+), 372 deletions(-) create mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/exceptions/ProtocolViolationException.java create mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java create mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessage.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessageTest.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessageTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eaf22bbb71..391367f5894 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Plugin API: Allow the registration of multiple PluginTransactionPoolValidatorFactory [#9964](https://github.com/hyperledger/besu/pull/9964) - Add `-Pcases` case name filtering to JMH benchmark suite [#9982](https://github.com/hyperledger/besu/pull/9982) - Use JDK SHA-256 provider to leverage hardware SHA-NI instructions instead of BouncyCastle [#9924](https://github.com/hyperledger/besu/pull/9924) +- Support [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975): eth/70 - partial block receipt lists ## 26.2.0 diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json index 42359634b44..2fe0c58d880 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json @@ -8,7 +8,7 @@ "response": { "jsonrpc": "2.0", "id": 2, - "result": "0x45" + "result": "0x46" }, "statusCode": 200 } \ No newline at end of file diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java index 146d0fa3ae3..f2b4a4ba141 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java @@ -24,13 +24,14 @@ /** * Eth protocol messages as defined in Ethereum Wire Protocol - * (ETH)} + * (ETH) */ public class EthProtocol implements SubProtocol { public static final String NAME = "eth"; private static final EthProtocol INSTANCE = new EthProtocol(); public static final Capability ETH68 = Capability.create(NAME, EthProtocolVersion.V68); public static final Capability ETH69 = Capability.create(NAME, EthProtocolVersion.V69); + public static final Capability ETH70 = Capability.create(NAME, EthProtocolVersion.V70); public static final BitSet REQUEST_ID_MESSAGES; static { @@ -52,7 +53,7 @@ public class EthProtocol implements SubProtocol { } // Latest version of the Eth protocol - public static final Capability LATEST = ETH69; + public static final Capability LATEST = ETH70; public static boolean requestIdCompatible(final int code) { return REQUEST_ID_MESSAGES.get(code); @@ -67,7 +68,7 @@ public String getName() { public int messageSpace(final int protocolVersion) { return switch (protocolVersion) { case EthProtocolVersion.V68 -> 17; - case EthProtocolVersion.V69 -> 18; + case EthProtocolVersion.V69, EthProtocolVersion.V70 -> 18; default -> 0; }; } @@ -106,4 +107,8 @@ public static EthProtocol get() { public static boolean isEth69Compatible(final Capability capability) { return NAME.equals(capability.getName()) && capability.getVersion() >= ETH69.getVersion(); } + + public static boolean isEth70Compatible(final Capability capability) { + return NAME.equals(capability.getName()) && capability.getVersion() >= ETH70.getVersion(); + } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java index ed35849e98d..555334df314 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java @@ -27,6 +27,7 @@ public class EthProtocolVersion { public static final int V68 = 68; public static final int V69 = 69; + public static final int V70 = 70; /** eth/68 */ private static final List eth68Messages = @@ -76,7 +77,7 @@ public class EthProtocolVersion { public static List getSupportedMessages(final int protocolVersion) { return switch (protocolVersion) { case EthProtocolVersion.V68 -> eth68Messages; - case EthProtocolVersion.V69 -> eth69Messages; + case EthProtocolVersion.V69, EthProtocolVersion.V70 -> eth69Messages; default -> Collections.emptyList(); }; } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java index 0b245610fba..86143925ca2 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java @@ -23,6 +23,7 @@ import org.hyperledger.besu.ethereum.core.Difficulty; import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; +import org.hyperledger.besu.ethereum.eth.manager.exceptions.ProtocolViolationException; import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; import org.hyperledger.besu.ethereum.eth.messages.StatusMessage; import org.hyperledger.besu.ethereum.eth.peervalidation.PeerValidator; @@ -175,6 +176,7 @@ private List calculateCapabilities( capabilities.add(EthProtocol.ETH68); capabilities.add(EthProtocol.ETH69); + capabilities.add(EthProtocol.ETH70); capabilities.removeIf(cap -> cap.getVersion() > ethProtocolConfiguration.getMaxEthCapability()); capabilities.removeIf(cap -> cap.getVersion() < ethProtocolConfiguration.getMinEthCapability()); @@ -321,6 +323,16 @@ public void processMessage(final Capability capability, final Message message) { ethPeer.disconnect( DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); + } catch (final ProtocolViolationException e) { + LOG.atDebug() + .setMessage("Received invalid message {} ({}), disconnecting: {}, {}") + .addArgument(messageData::getData) + .addArgument(e::getReason) + .addArgument(ethPeer::toString) + .addArgument(e::toString) + .log(); + + ethPeer.disconnect(e.getReason()); } maybeResponseData.ifPresent( responseData -> { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java index fb9663664fd..0578a4c9c97 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java @@ -14,6 +14,8 @@ */ package org.hyperledger.besu.ethereum.eth.manager; +import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_FIRST_BLOCK_RECEIPT_INDEX; + import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockBody; @@ -26,15 +28,18 @@ import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; +import org.hyperledger.besu.ethereum.eth.manager.exceptions.ProtocolViolationException; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetNodeDataMessage; +import org.hyperledger.besu.ethereum.eth.messages.GetPaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetPooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.NodeDataMessage; +import org.hyperledger.besu.ethereum.eth.messages.PaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.PooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; @@ -96,13 +101,22 @@ private void registerResponseConstructors() { maxMessageSize)); ethMessages.registerResponseConstructor( EthProtocolMessages.GET_RECEIPTS, - (peer, messageData, capability) -> - constructGetReceiptsResponse( + (peer, messageData, capability) -> { + if (EthProtocol.isEth70Compatible(capability)) { + return constructGetPaginatedReceiptsResponse( + peer, blockchain, messageData, ethereumWireProtocolConfiguration.getMaxGetReceipts(), - maxMessageSize, - capability)); + maxMessageSize); + } + return constructGetReceiptsResponse( + blockchain, + messageData, + ethereumWireProtocolConfiguration.getMaxGetReceipts(), + maxMessageSize, + capability); + }); ethMessages.registerResponseConstructor( EthProtocolMessages.GET_NODE_DATA, (peer, messageData, capability) -> @@ -226,18 +240,18 @@ static MessageData constructGetReceiptsResponse( final int maxMessageSize, final Capability cap) { final GetReceiptsMessage getReceipts = GetReceiptsMessage.readFrom(message); - final Iterable hashes = getReceipts.hashes(); + final Iterable blockHashes = getReceipts.blockHashes(); int responseSizeEstimate = RLP.MAX_PREFIX_SIZE; final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); rlp.startList(); int count = 0; - for (final Hash hash : hashes) { + for (final Hash blockHash : blockHashes) { if (count >= requestLimit) { break; } count++; - final Optional> maybeReceipts = blockchain.getTxReceipts(hash); + final Optional> maybeReceipts = blockchain.getTxReceipts(blockHash); if (maybeReceipts.isEmpty()) { continue; } @@ -265,6 +279,103 @@ static MessageData constructGetReceiptsResponse( return ReceiptsMessage.createUnsafe(rlp.encoded()); } + static MessageData constructGetPaginatedReceiptsResponse( + final EthPeer peer, + final Blockchain blockchain, + final MessageData message, + final int requestLimit, + final int maxMessageSize) { + final GetPaginatedReceiptsMessage getPaginatedReceipts = + GetPaginatedReceiptsMessage.readFrom(message); + final List requestedBlockHashes = getPaginatedReceipts.blockHashes(); + final List blockHashes; + if (requestedBlockHashes.size() > requestLimit) { + LOG.atDebug() + .setMessage( + "Requested receipts for {} blocks, more than allowed max number of {}, ignoring extra blocks") + .addArgument(requestedBlockHashes::size) + .addArgument(requestLimit) + .log(); + blockHashes = requestedBlockHashes.subList(0, requestLimit); + } else { + blockHashes = requestedBlockHashes; + } + + final var blockReceiptsRLPs = new ArrayList(blockHashes.size()); + + int skipBefore = getPaginatedReceipts.firstBlockReceiptIndex(); + LOG.trace( + "Paginated receipt request for {} blocks with first block receipt index {}", + blockHashes.size(), + skipBefore); + int responseSizeEstimate = RLP.MAX_PREFIX_SIZE; + boolean lastBlockIncomplete = false; + + for (final Hash blockHash : blockHashes) { + final Optional> maybeReceipts = blockchain.getTxReceipts(blockHash); + if (maybeReceipts.isEmpty()) { + LOG.debug( + "Invalid request from peer {}, block {} does not exists, returning", peer, blockHash); + break; + } + + final List blockReceipts = maybeReceipts.get(); + final List requestedReceipts; + + if (skipBefore > blockReceipts.size()) { + throw new ProtocolViolationException( + "Invalid request from peer %s, firstBlockReceiptIndex %d is greater than or equal the receipt count of %d for block %s" + .formatted(peer, skipBefore, blockReceipts.size(), blockHash), + INVALID_FIRST_BLOCK_RECEIPT_INDEX); + } + + if (skipBefore > 0) { + requestedReceipts = blockReceipts.subList(skipBefore, blockReceipts.size()); + skipBefore = 0; + } else { + requestedReceipts = blockReceipts; + } + + final BytesValueRLPOutput encodedBlockReceipts = new BytesValueRLPOutput(); + encodedBlockReceipts.startList(); + + for (final TransactionReceipt receipt : requestedReceipts) { + final BytesValueRLPOutput encodedReceipt = new BytesValueRLPOutput(); + TransactionReceiptEncoder.writeTo( + receipt, + encodedReceipt, + TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + if (responseSizeEstimate + encodedReceipt.encodedSize() + RLP.MAX_PREFIX_SIZE + > maxMessageSize) { + lastBlockIncomplete = true; + break; + } + responseSizeEstimate += encodedReceipt.encodedSize(); + encodedBlockReceipts.writeRaw(encodedReceipt.encoded()); + } + + encodedBlockReceipts.endList(); + blockReceiptsRLPs.add(encodedBlockReceipts); + if (lastBlockIncomplete) { + break; + } + } + + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + rlp.writeLongScalar(lastBlockIncomplete ? 1 : 0); + rlp.startList(); + blockReceiptsRLPs.forEach(r -> rlp.writeRaw(r.encoded())); + rlp.endList(); + + final Bytes encodedResponse = rlp.encoded(); + LOG.trace( + "Returning paginated receipts for {} blocks, with last block incomplete {}, enconded size {}", + blockHashes.size(), + lastBlockIncomplete, + encodedResponse.size()); + return PaginatedReceiptsMessage.createUnsafe(encodedResponse, lastBlockIncomplete); + } + static MessageData constructGetPooledTransactionsResponse( final TransactionPool transactionPool, final EthPeer peer, diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/exceptions/ProtocolViolationException.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/exceptions/ProtocolViolationException.java new file mode 100644 index 00000000000..d3ae3ef1388 --- /dev/null +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/exceptions/ProtocolViolationException.java @@ -0,0 +1,35 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.eth.manager.exceptions; + +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage; + +public class ProtocolViolationException extends RuntimeException { + private final DisconnectMessage.DisconnectReason reason; + + public ProtocolViolationException(final String message) { + this(message, DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL); + } + + public ProtocolViolationException( + final String message, final DisconnectMessage.DisconnectReason reason) { + super(message); + this.reason = reason; + } + + public DisconnectMessage.DisconnectReason getReason() { + return reason; + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java index 9c0fae1a8bb..5693f506b77 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java @@ -24,7 +24,8 @@ public enum PeerTaskValidationResponse { RESULTS_DO_NOT_MATCH_QUERY(null, true), NON_SEQUENTIAL_HEADERS_RETURNED( DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL_NON_SEQUENTIAL_HEADERS, true), - RESULTS_VALID_AND_GOOD(null, false); + RESULTS_VALID_AND_GOOD(null, false), + INVALID_RECEIPT_RETURNED(DisconnectMessage.DisconnectReason.INVALID_RECEIPT_RECEIVED, true); private final Optional disconnectReason; private final boolean recordUselessResponse; diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java index ffcc0713b2c..c419f1195c9 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java @@ -28,7 +28,9 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.MalformedRlpFromPeerException; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTask; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskValidationResponse; +import org.hyperledger.besu.ethereum.eth.messages.GetPaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; +import org.hyperledger.besu.ethereum.eth.messages.PaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; @@ -36,6 +38,7 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import org.hyperledger.besu.ethereum.rlp.RLPException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -44,33 +47,32 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes; +import org.jspecify.annotations.Nullable; -public class GetSyncReceiptsFromPeerTask - implements PeerTask>> { - - private final List requestedBlocks; +public class GetSyncReceiptsFromPeerTask implements PeerTask { + private final Request request; + protected final ProtocolSchedule protocolSchedule; private final List requestedHeaders; private final long requiredBlockchainHeight; private final boolean isPoS; private final SyncTransactionReceiptEncoder syncTransactionReceiptEncoder; public GetSyncReceiptsFromPeerTask( - final List blocks, + final Request request, final ProtocolSchedule protocolSchedule, final SyncTransactionReceiptEncoder syncTransactionReceiptEncoder) { - checkArgument(!blocks.isEmpty(), "Requested block list must not be empty"); - this.requestedBlocks = blocks; - this.requestedHeaders = blocks.stream().map(SyncBlock::getHeader).toList(); + checkArgument(!request.blocks.isEmpty(), "Requested block list must not be empty"); + this.request = request; + this.protocolSchedule = protocolSchedule; + this.requestedHeaders = request.blocks.stream().map(SyncBlock::getHeader).toList(); - // calculate the minimum required blockchain height a peer will need to be able to fulfil this - // request requiredBlockchainHeight = - requestedHeaders.stream() + this.requestedHeaders.stream() .mapToLong(BlockHeader::getNumber) .max() .orElse(BlockHeader.GENESIS_BLOCK_NUMBER); - isPoS = protocolSchedule.anyMatch((ps) -> ps.spec().isPoS()); + isPoS = protocolSchedule.anyMatch(ps -> ps.spec().isPoS()); this.syncTransactionReceiptEncoder = syncTransactionReceiptEncoder; } @@ -81,47 +83,123 @@ public SubProtocol getSubProtocol() { @Override public MessageData getRequestMessage(final Set agreedCapabilities) { - return GetReceiptsMessage.create(requestedHeaders.stream().map(BlockHeader::getHash).toList()); + final List blockHashes = requestedHeaders.stream().map(BlockHeader::getHash).toList(); + return agreedCapabilities.stream().anyMatch(EthProtocol::isEth70Compatible) + ? GetPaginatedReceiptsMessage.create(blockHashes, request.firstBlockPartialReceipts.size()) + : GetReceiptsMessage.create(blockHashes); } @Override - public Map> processResponse( + public Response processResponse( final MessageData messageData, final Set agreedCapabilities) throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { if (messageData == null) { throw new InvalidPeerTaskResponseException("Null message data"); } + if (agreedCapabilities.stream().anyMatch(EthProtocol::isEth70Compatible)) { + return processPaginatedResponse(messageData); + } + return processNotPaginatedResponse(messageData); + } + + private Response processNotPaginatedResponse(final MessageData messageData) + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(messageData); try { final List> receivedBlocks = receiptsMessage.syncReceipts(); - if (receivedBlocks.size() > requestedBlocks.size()) { + if (receivedBlocks.size() > request.size()) { throw new InvalidPeerTaskResponseException("Too many result returned"); } - final Map> response = + final Map> receiptsByBlock = HashMap.newHashMap(receivedBlocks.size()); for (int i = 0; i < receivedBlocks.size(); i++) { - response.put(requestedBlocks.get(i), receivedBlocks.get(i)); + receiptsByBlock.put(request.blocks.get(i), receivedBlocks.get(i)); } - return response; + return new Response(receiptsByBlock, List.of()); } catch (RLPException e) { // indicates a malformed or unexpected RLP result from the peer throw new MalformedRlpFromPeerException(e, messageData.getData()); } } + private Response processPaginatedResponse(final MessageData messageData) + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { + final PaginatedReceiptsMessage paginatedReceiptsMessage = + PaginatedReceiptsMessage.readFrom(messageData); + try { + final List> receivedReceipts = + paginatedReceiptsMessage.syncReceipts(); + + if (receivedReceipts.isEmpty()) { + throw new InvalidPeerTaskResponseException("No result returned"); + } + + if (receivedReceipts.size() > request.size()) { + throw new InvalidPeerTaskResponseException("Too many result returned"); + } + + final List> cumulativeReceivedReceipts = + completeFirstBlock(receivedReceipts); + + final int endIndex; + final List lastBlockPartialReceipts; + if (paginatedReceiptsMessage.lastBlockIncomplete()) { + endIndex = cumulativeReceivedReceipts.size() - 1; + lastBlockPartialReceipts = cumulativeReceivedReceipts.getLast(); + } else { + endIndex = cumulativeReceivedReceipts.size(); + lastBlockPartialReceipts = List.of(); + } + + final Map> receiptsByBlock = + HashMap.newHashMap(cumulativeReceivedReceipts.size()); + + for (int i = 0; i < endIndex; i++) { + receiptsByBlock.put(request.blocks.get(i), cumulativeReceivedReceipts.get(i)); + } + + return new Response(receiptsByBlock, lastBlockPartialReceipts); + } catch (RLPException e) { + // indicates a malformed or unexpected RLP result from the peer + throw new MalformedRlpFromPeerException(e, messageData.getData()); + } + } + + private List> completeFirstBlock( + final List> receivedReceipts) { + if (request.firstBlockPartialReceipts.isEmpty()) { + // nothing to integrate returning as is + return receivedReceipts; + } + + // add new receipts to the already present ones + final List cumulativeReceiptsForFirstBlock = + new ArrayList<>( + request.firstBlockPartialReceipts.size() + receivedReceipts.getFirst().size()); + cumulativeReceiptsForFirstBlock.addAll(request.firstBlockPartialReceipts); + cumulativeReceiptsForFirstBlock.addAll(receivedReceipts.getFirst()); + + // replace first list of receipts with the new cumulative list + final List> cumulativeReceipts = + new ArrayList<>(receivedReceipts.size()); + cumulativeReceipts.add(cumulativeReceiptsForFirstBlock); + cumulativeReceipts.addAll(receivedReceipts.subList(1, receivedReceipts.size())); + return cumulativeReceipts; + } + @Override public Predicate getPeerRequirementFilter() { return (ethPeer) -> isPoS || ethPeer.estimatedChainHeight() >= requiredBlockchainHeight; } @Override - public PeerTaskValidationResponse validateResult( - final Map> result) { + public PeerTaskValidationResponse validateResult(final Response result) { if (result.isEmpty()) { return PeerTaskValidationResponse.NO_RESULTS_RETURNED; } - for (final Map.Entry> entry : result.entrySet()) { + for (final Map.Entry> entry : + result.completeReceiptsByBlock.entrySet()) { final SyncBlock requestedBlock = entry.getKey(); final List receivedReceiptsForBlock = entry.getValue(); @@ -136,9 +214,52 @@ public PeerTaskValidationResponse validateResult( } } + if (!result.lastBlockPartialReceipts().isEmpty()) { + final PeerTaskValidationResponse tooManyResultsReturned = validatePartialReceiptList(result); + if (tooManyResultsReturned != null) return tooManyResultsReturned; + } + return PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD; } + private @Nullable PeerTaskValidationResponse validatePartialReceiptList(final Response result) { + final SyncBlock lastBlockReceived = request.blocks.get(result.completeReceiptsByBlock().size()); + final List lastBlockPartialReceipts = result.lastBlockPartialReceipts(); + + if (lastBlockPartialReceipts.size() > lastBlockReceived.getBody().getTransactionCount()) { + return PeerTaskValidationResponse.TOO_MANY_RESULTS_RETURNED; + } + + final long txGasLimitUpperBound = calculateTxGasLimitUpperBound(lastBlockReceived); + + // check that each receipt in partial list is within size bounds + long cumulativeReceiptSize = 0; + for (int i = 0; i < lastBlockPartialReceipts.size(); i++) { + final SyncTransactionReceipt receipt = lastBlockPartialReceipts.get(i); + final long receiptSize = receipt.getRlpBytes().size(); + if (receiptSize > txGasLimitUpperBound / 8) { + return PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED; + } + cumulativeReceiptSize += receiptSize; + if (cumulativeReceiptSize > lastBlockReceived.getHeader().getGasLimit() / 8) { + return PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED; + } + } + return null; + } + + private long calculateTxGasLimitUpperBound(final SyncBlock lastBlockReceived) { + // to avoid having to deserialize the tx to get the actual gas limit we use an upper bound, + // for everything before Osaka we use the block gas limit of 45M and for Osaka onward + // we can use the max gas limit allowed per tx as specified by the protocol schedule + return Math.min( + protocolSchedule + .getByBlockHeader(lastBlockReceived.getHeader()) + .getGasLimitCalculator() + .transactionGasLimitCap(), + 45_000_000L); + } + private boolean receiptsRootMatches( final BlockHeader blockHeader, final List receipts) { final Hash calculatedReceiptsRoot = @@ -160,6 +281,29 @@ private boolean receiptsRootMatches( @VisibleForTesting public List getRequestedBlocks() { - return requestedBlocks; + return request.blocks(); + } + + public record Request( + List blocks, List firstBlockPartialReceipts) { + public boolean isEmpty() { + return blocks.isEmpty(); + } + + public int size() { + return blocks.size(); + } + } + + public record Response( + Map> completeReceiptsByBlock, + List lastBlockPartialReceipts) { + public boolean isEmpty() { + return completeReceiptsByBlock.isEmpty() && lastBlockPartialReceipts.isEmpty(); + } + + public int completeCount() { + return completeReceiptsByBlock.size(); + } } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java new file mode 100644 index 00000000000..b43b7eff53d --- /dev/null +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java @@ -0,0 +1,64 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.eth.messages; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; +import org.hyperledger.besu.ethereum.rlp.RLPInput; + +import java.util.List; + +import org.apache.tuweni.bytes.Bytes; + +public final class GetPaginatedReceiptsMessage extends GetReceiptsMessage { + private final int firstBlockReceiptIndex; + + private GetPaginatedReceiptsMessage( + final Bytes data, final List blockHashes, final int firstBlockReceiptIndex) { + super(data, blockHashes); + this.firstBlockReceiptIndex = firstBlockReceiptIndex; + } + + public static GetPaginatedReceiptsMessage readFrom(final MessageData message) { + if (message instanceof GetPaginatedReceiptsMessage) { + return (GetPaginatedReceiptsMessage) message; + } + final int code = message.getCode(); + if (code != EthProtocolMessages.GET_RECEIPTS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a GetReceipts.", code)); + } + final RLPInput input = new BytesValueRLPInput(message.getData(), false); + final int firstBlockReceiptIndex = input.readIntScalar(); + final List blockHashes = parseBlockHashes(input); + return new GetPaginatedReceiptsMessage(message.getData(), blockHashes, firstBlockReceiptIndex); + } + + public static GetPaginatedReceiptsMessage create( + final List blockHashes, final int firstBlockReceiptIndex) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.writeIntScalar(firstBlockReceiptIndex); + tmp.startList(); + blockHashes.forEach(hash -> tmp.writeBytes(hash.getBytes())); + tmp.endList(); + return new GetPaginatedReceiptsMessage(tmp.encoded(), blockHashes, firstBlockReceiptIndex); + } + + public int firstBlockReceiptIndex() { + return firstBlockReceiptIndex; + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java index a8a0b7e601f..4a70434e920 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java @@ -26,7 +26,13 @@ import org.apache.tuweni.bytes.Bytes; -public final class GetReceiptsMessage extends AbstractMessageData { +public class GetReceiptsMessage extends AbstractMessageData { + private final List blockHashes; + + protected GetReceiptsMessage(final Bytes data, final List blockHashes) { + super(data); + this.blockHashes = blockHashes; + } public static GetReceiptsMessage readFrom(final MessageData message) { if (message instanceof GetReceiptsMessage) { @@ -37,19 +43,16 @@ public static GetReceiptsMessage readFrom(final MessageData message) { throw new IllegalArgumentException( String.format("Message has code %d and thus is not a GetReceipts.", code)); } - return new GetReceiptsMessage(message.getData()); + final RLPInput input = new BytesValueRLPInput(message.getData(), false); + return new GetReceiptsMessage(message.getData(), parseBlockHashes(input)); } - public static GetReceiptsMessage create(final Iterable hashes) { + public static GetReceiptsMessage create(final List blockHashes) { final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); tmp.startList(); - hashes.forEach(hash -> tmp.writeBytes(hash.getBytes())); + blockHashes.forEach(hash -> tmp.writeBytes(hash.getBytes())); tmp.endList(); - return new GetReceiptsMessage(tmp.encoded()); - } - - private GetReceiptsMessage(final Bytes data) { - super(data); + return new GetReceiptsMessage(tmp.encoded(), blockHashes); } @Override @@ -57,10 +60,12 @@ public int getCode() { return EthProtocolMessages.GET_RECEIPTS; } - public List hashes() { - final RLPInput input = new BytesValueRLPInput(data, false); - input.enterList(); - final List hashes = new ArrayList<>(); + public List blockHashes() { + return blockHashes; + } + + protected static List parseBlockHashes(final RLPInput input) { + final List hashes = new ArrayList<>(input.enterList()); while (!input.isEndOfCurrentList()) { hashes.add(Hash.wrap(input.readBytes32())); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessage.java new file mode 100644 index 00000000000..e03b758814c --- /dev/null +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessage.java @@ -0,0 +1,62 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.eth.messages; + +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; +import org.hyperledger.besu.ethereum.rlp.RLPInput; + +import org.apache.tuweni.bytes.Bytes; + +public final class PaginatedReceiptsMessage extends ReceiptsMessage { + private Boolean lastBlockIncomplete; + + private PaginatedReceiptsMessage(final Bytes data, final Boolean lastBlockIncomplete) { + super(data); + this.lastBlockIncomplete = lastBlockIncomplete; + } + + public static PaginatedReceiptsMessage readFrom(final MessageData message) { + if (message instanceof PaginatedReceiptsMessage) { + return (PaginatedReceiptsMessage) message; + } + final int code = message.getCode(); + if (code != EthProtocolMessages.RECEIPTS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a ReceiptsMessage.", code)); + } + + return new PaginatedReceiptsMessage(message.getData(), null); + } + + public static PaginatedReceiptsMessage createUnsafe( + final Bytes data, final boolean lastBlockIncomplete) { + return new PaginatedReceiptsMessage(data, lastBlockIncomplete); + } + + public boolean lastBlockIncomplete() { + if (lastBlockIncomplete == null) { + deserialize(); + } + return lastBlockIncomplete; + } + + @Override + protected void deserialize() { + final RLPInput input = new BytesValueRLPInput(data, false); + lastBlockIncomplete = input.readLongScalar() == 1; + deserializeReceiptLists(input); + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java index 7e2eb3a4c39..70350fc3ff1 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java @@ -26,7 +26,7 @@ import org.apache.tuweni.bytes.Bytes; -public final class ReceiptsMessage extends AbstractMessageData { +public class ReceiptsMessage extends AbstractMessageData { /** * This default decoder instance is used for performance reasons to avoid creating a new decoder * for every ReceiptsMessage @@ -34,7 +34,9 @@ public final class ReceiptsMessage extends AbstractMessageData { private static final SyncTransactionReceiptDecoder DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER = new SyncTransactionReceiptDecoder(); - private ReceiptsMessage(final Bytes data) { + private List> syncReceiptsByBlock; + + protected ReceiptsMessage(final Bytes data) { super(data); } @@ -67,9 +69,19 @@ public int getCode() { } public List> syncReceipts() { + if (syncReceiptsByBlock == null) { + deserialize(); + } + return syncReceiptsByBlock; + } + + protected void deserialize() { final RLPInput input = new BytesValueRLPInput(data, false); - input.enterList(); - final List> receiptsForBodies = new ArrayList<>(); + deserializeReceiptLists(input); + } + + protected void deserializeReceiptLists(final RLPInput input) { + final List> receiptsByBlock = new ArrayList<>(input.enterList()); while (input.nextIsList()) { final int setSize = input.enterList(); final List receiptSet = new ArrayList<>(setSize); @@ -79,9 +91,9 @@ public List> syncReceipts() { input.nextIsList() ? input.currentListAsBytes() : input.readBytes())); } input.leaveList(); - receiptsForBodies.add(receiptSet); + receiptsByBlock.add(receiptSet); } input.leaveList(); - return receiptsForBodies; + syncReceiptsByBlock = receiptsByBlock; } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStep.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStep.java index 3b0b55b7257..458b600424d 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStep.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStep.java @@ -28,6 +28,8 @@ 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.GetSyncReceiptsFromPeerTask; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Request; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Response; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import java.time.Duration; @@ -77,13 +79,21 @@ public DownloadSyncReceiptsStep( public CompletableFuture> apply(final List blocks) { final int currTaskId = taskSequence.incrementAndGet(); final List blocksToRequest = prepareRequest(blocks); + final List firstBlockPartialReceipts = new ArrayList<>(); final Map> receiptsByRootHash = HashMap.newHashMap(blocksToRequest.size()); final AtomicBoolean cancelled = new AtomicBoolean(false); return ethScheduler .scheduleServiceTask( - () -> downloadReceipts(currTaskId, 0, blocksToRequest, receiptsByRootHash, cancelled)) + () -> + downloadReceipts( + currTaskId, + 0, + blocksToRequest, + firstBlockPartialReceipts, + receiptsByRootHash, + cancelled)) .thenApply(receipts -> combineBlocksAndReceipts(blocks, receipts)) .orTimeout(timeoutDuration.toMillis(), TimeUnit.MILLISECONDS) .whenComplete( @@ -139,6 +149,7 @@ private CompletableFuture>> downloadRecei final int currTaskId, final int prevIterations, final List blocksToRequest, + final List firstBlockPartialReceipts, final Map> receiptsByRootHash, final AtomicBoolean cancelled) { @@ -148,25 +159,28 @@ private CompletableFuture>> downloadRecei ++iteration; LOG.atTrace() - .setMessage("[{}:{}] Requesting receipts for {} blocks (initial {}): {}") + .setMessage( + "[{}:{}] Requesting receipts for {} blocks, partial receipts fetched for first block {} (initial {}): {}") .addArgument(currTaskId) .addArgument(iteration) .addArgument(blocksToRequest::size) + .addArgument(firstBlockPartialReceipts::size) .addArgument(initialBlockCount) .addArgument(() -> formatBlockDetails(blocksToRequest)) .log(); final GetSyncReceiptsFromPeerTask task = new GetSyncReceiptsFromPeerTask( - blocksToRequest, protocolSchedule, syncTransactionReceiptEncoder); + new Request(blocksToRequest, firstBlockPartialReceipts), + protocolSchedule, + syncTransactionReceiptEncoder); - final PeerTaskExecutorResult>> getReceiptsResult = - peerTaskExecutor.execute(task); + final PeerTaskExecutorResult getReceiptsResult = peerTaskExecutor.execute(task); final PeerTaskExecutorResponseCode responseCode = getReceiptsResult.responseCode(); if (responseCode == SUCCESS) { - final Map> receiptsByBlock = + final Response response = getReceiptsResult .result() .orElseThrow( @@ -174,10 +188,23 @@ private CompletableFuture>> downloadRecei new IllegalStateException( "Task validation failure, it must flag empty result as failure")); + final Map> receiptsByBlock = + response.completeReceiptsByBlock(); + final List lastBlockPartialReceipts = + response.lastBlockPartialReceipts(); + + firstBlockPartialReceipts.clear(); + if (!lastBlockPartialReceipts.isEmpty()) { + firstBlockPartialReceipts.addAll(lastBlockPartialReceipts); + } + LOG.atTrace() - .setMessage("[{}:{}] Received response for {} blocks (requested {}, initial {}): {}") + .setMessage( + "[{}:{}] Received complete response for {} blocks, last block partial receipts {}, completed blocks {} (requested {}, initial {}): {}") .addArgument(currTaskId) .addArgument(iteration) + .addArgument(response::completeCount) + .addArgument(lastBlockPartialReceipts::size) .addArgument(receiptsByBlock::size) .addArgument(blocksToRequest::size) .addArgument(initialBlockCount) @@ -185,11 +212,10 @@ private CompletableFuture>> downloadRecei .log(); receiptsByBlock.forEach( - (syncBlock, syncTransactionReceipts) -> - receiptsByRootHash.put( - syncBlock.getHeader().getReceiptsRoot(), syncTransactionReceipts)); - - blocksToRequest.removeAll(receiptsByBlock.keySet()); + (block, receipts) -> { + receiptsByRootHash.put(block.getHeader().getReceiptsRoot(), receipts); + blocksToRequest.remove(block); + }); } else { LOG.atTrace() .setMessage( @@ -212,6 +238,7 @@ private CompletableFuture>> downloadRecei currTaskId, passIterations, blocksToRequest, + firstBlockPartialReceipts, receiptsByRootHash, cancelled)), RETRY_DELAY); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java index bb0bfce03c3..3571f054c19 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java @@ -945,7 +945,7 @@ public void respondToGetReceipts() throws ExecutionException, InterruptedExcepti // Run test final PeerConnection peer = setupPeer(ethManager, onSend); - ethManager.processMessage(EthProtocol.LATEST, new DefaultMessage(peer, messageData)); + ethManager.processMessage(EthProtocol.ETH69, new DefaultMessage(peer, messageData)); done.get(); } } @@ -999,7 +999,7 @@ public void respondToGetReceiptsWithinLimits() throws ExecutionException, Interr // Run test final PeerConnection peer = setupPeer(ethManager, onSend); - ethManager.processMessage(EthProtocol.LATEST, new DefaultMessage(peer, messageData)); + ethManager.processMessage(EthProtocol.ETH69, new DefaultMessage(peer, messageData)); done.get(); } } @@ -1045,7 +1045,7 @@ public void respondToGetReceiptsPartial() throws ExecutionException, Interrupted // Run test final PeerConnection peer = setupPeer(ethManager, onSend); - ethManager.processMessage(EthProtocol.LATEST, new DefaultMessage(peer, messageData)); + ethManager.processMessage(EthProtocol.ETH69, new DefaultMessage(peer, messageData)); done.get(); } } @@ -1294,8 +1294,8 @@ public void transactionMessagesGoToTheCorrectExecutor() { @Test public void shouldUseRightCapabilityDependingOnSyncMode() { - assertHighestCapability(SyncMode.SNAP, EthProtocol.ETH69); - assertHighestCapability(SyncMode.FULL, EthProtocol.ETH69); + assertHighestCapability(SyncMode.SNAP, EthProtocol.LATEST); + assertHighestCapability(SyncMode.FULL, EthProtocol.LATEST); } @Test diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java index 877c640dbc5..67cf81fe3e9 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java @@ -16,6 +16,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -34,14 +35,17 @@ import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; import org.hyperledger.besu.ethereum.eth.ImmutableEthProtocolConfiguration; +import org.hyperledger.besu.ethereum.eth.manager.exceptions.ProtocolViolationException; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetNodeDataMessage; +import org.hyperledger.besu.ethereum.eth.messages.GetPaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetPooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.NodeDataMessage; +import org.hyperledger.besu.ethereum.eth.messages.PaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.PooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; @@ -261,7 +265,7 @@ public void shouldLimitTxReceiptsByMessageSize() { serializeReceiptsList( expectedResults, TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION)); - final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.LATEST); + final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH69); assertThat(result).contains(expectedMsg); } @@ -285,7 +289,7 @@ public void shouldLimitTxReceiptsByCount() { serializeReceiptsList( expectedResults, TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION)); - final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.LATEST); + final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH69); assertThat(result).contains(expectedMsg); } @@ -336,6 +340,220 @@ public void shouldLimitTransactionsByCount() { assertThat(result).contains(expectedMsg); } + @Test + public void shouldReturnAllPaginatedReceiptsForKnownBlocks() { + // Use explicit receipts to guarantee each block has at least 1 receipt: blocks with 0 + // receipts from blockSequence would trigger skipBefore(0) >= size(0) → ProtocolViolation. + final Hash hash0 = dataGenerator.hash(); + final Hash hash1 = dataGenerator.hash(); + final Hash hash2 = dataGenerator.hash(); + final List receipts0 = List.of(dataGenerator.receipt()); + final List receipts1 = List.of(dataGenerator.receipt()); + final List receipts2 = List.of(dataGenerator.receipt()); + when(blockchain.getTxReceipts(hash0)).thenReturn(Optional.of(receipts0)); + when(blockchain.getTxReceipts(hash1)).thenReturn(Optional.of(receipts1)); + when(blockchain.getTxReceipts(hash2)).thenReturn(Optional.of(receipts2)); + setupEthServer(); + + final List hashes = List.of(hash0, hash1, hash2); + final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 0); + + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(List.of(receipts0, receipts1, receipts2), false), false); + + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldLimitPaginatedReceiptsByCount() { + final int limit = 6; + final int blockCount = 10; + // Explicit receipts (1 per block) to avoid 0-receipt blocks from blockSequence. + final List hashes = new ArrayList<>(); + final List> allReceipts = new ArrayList<>(); + for (int i = 0; i < blockCount; i++) { + final Hash hash = dataGenerator.hash(); + final List receipts = List.of(dataGenerator.receipt()); + hashes.add(hash); + allReceipts.add(receipts); + when(blockchain.getTxReceipts(hash)).thenReturn(Optional.of(receipts)); + } + setupEthServer(b -> b.maxGetReceipts(limit)); + + final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 0); + + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(allReceipts.subList(0, limit), false), false); + + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldLimitPaginatedReceiptsByMessageSize() { + // Use explicit receipts (not blockSequence) to avoid blocks with 0 transactions, which would + // be silently included in the response without setting lastBlockIncomplete, causing an + // extra empty-list block entry and making the expected vs actual sizes diverge. + final Hash block0Hash = dataGenerator.hash(); + final Hash block1Hash = dataGenerator.hash(); + final TransactionReceipt receipt0 = dataGenerator.receipt(); + final TransactionReceipt receipt1 = dataGenerator.receipt(); + when(blockchain.getTxReceipts(block0Hash)).thenReturn(Optional.of(List.of(receipt0))); + when(blockchain.getTxReceipts(block1Hash)).thenReturn(Optional.of(List.of(receipt1))); + + // Size limit: exactly fits receipt0 from block 0 but not receipt1 from block 1. + // The server check is: responseSizeEstimate + receiptSize + MAX_PREFIX_SIZE > maxMessageSize. + // With sizeLimit = 2*MAX_PREFIX + size(receipt0): + // receipt0 check: MAX_PREFIX + size(receipt0) + MAX_PREFIX = sizeLimit → passes (not >) + // receipt1 check: MAX_PREFIX + size(receipt0) + size(receipt1) + MAX_PREFIX > sizeLimit + // = size(receipt1) > 0 → always true → lastBlockIncomplete = true + final int sizeLimit = 2 * RLP.MAX_PREFIX_SIZE + calculatePaginatedReceiptEncodedSize(receipt0); + setupEthServer(b -> b.maxMessageSize(sizeLimit)); + + final List hashes = List.of(block0Hash, block1Hash); + final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 0); + + // block 0: receipt0 fits; block 1: starts but receipt1 doesn't fit → empty list + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(List.of(List.of(receipt0), List.of()), true), true); + + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldPaginateWithFirstBlockReceiptIndex() { + // Create receipts directly rather than deriving them from blockSequence, which can produce + // blocks with 0 transactions (and thus 0 receipts), causing subList(firstIndex, 0) to throw. + final Hash firstBlockHash = dataGenerator.hash(); + final Hash secondBlockHash = dataGenerator.hash(); + final List firstBlockReceipts = + List.of(dataGenerator.receipt(), dataGenerator.receipt()); + final List secondBlockReceipts = + List.of(dataGenerator.receipt(), dataGenerator.receipt()); + when(blockchain.getTxReceipts(firstBlockHash)).thenReturn(Optional.of(firstBlockReceipts)); + when(blockchain.getTxReceipts(secondBlockHash)).thenReturn(Optional.of(secondBlockReceipts)); + setupEthServer(); + + final int firstIndex = 1; + // Use List.of to guarantee ordering: firstBlockHash is always the block that gets paginated + final List hashes = List.of(firstBlockHash, secondBlockHash); + final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, firstIndex); + final EthMessage ethMsg = new EthMessage(ethPeer, msg); + + // firstIndex skips the first receipt of the first block only; subsequent blocks are unaffected + final List> expectedReceipts = + List.of( + firstBlockReceipts.subList(firstIndex, firstBlockReceipts.size()), secondBlockReceipts); + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(expectedReceipts, false), false); + + final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldReturnCollectedReceiptsUpToUnknownBlockInPaginatedRequest() { + // Setup one known block followed by an unknown one + final Hash knownHash = dataGenerator.hash(); + final TransactionReceipt receipt = dataGenerator.receipt(); + when(blockchain.getTxReceipts(knownHash)).thenReturn(Optional.of(List.of(receipt))); + final Hash unknownHash = dataGenerator.hash(); + when(blockchain.getTxReceipts(unknownHash)).thenReturn(Optional.empty()); + setupEthServer(); + + final GetPaginatedReceiptsMessage msg = + GetPaginatedReceiptsMessage.create(List.of(knownHash, unknownHash), 0); + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + + // Server stops at the unknown block and returns what was collected before it + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(List.of(List.of(receipt)), false), false); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldThrowProtocolViolationForInvalidFirstBlockReceiptIndex() { + // Block has 2 receipts; firstBlockReceiptIndex = 3 triggers skipBefore(3) > size(2) → throws + final Hash blockHash = dataGenerator.hash(); + final List receipts = + List.of(dataGenerator.receipt(), dataGenerator.receipt()); + when(blockchain.getTxReceipts(blockHash)).thenReturn(Optional.of(receipts)); + setupEthServer(); + + final GetPaginatedReceiptsMessage msg = + GetPaginatedReceiptsMessage.create(List.of(blockHash), 3); + + assertThatThrownBy(() -> ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70)) + .isInstanceOf(ProtocolViolationException.class); + } + + @Test + public void shouldTreatFirstBlockReceiptIndexEqualToSizeAsValidAndReturnEmptyFirstBlock() { + // skipBefore == blockReceipts.size() is valid (condition is strictly >): + // the first block contributes an empty receipt list and subsequent blocks are returned + // normally. + final Hash block0Hash = dataGenerator.hash(); + final Hash block1Hash = dataGenerator.hash(); + final List block0Receipts = + List.of(dataGenerator.receipt(), dataGenerator.receipt()); + final List block1Receipts = List.of(dataGenerator.receipt()); + when(blockchain.getTxReceipts(block0Hash)).thenReturn(Optional.of(block0Receipts)); + when(blockchain.getTxReceipts(block1Hash)).thenReturn(Optional.of(block1Receipts)); + setupEthServer(); + + // firstBlockReceiptIndex == size(block0) == 2: skip all receipts → empty list for block0 + final GetPaginatedReceiptsMessage msg = + GetPaginatedReceiptsMessage.create(List.of(block0Hash, block1Hash), 2); + + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(List.of(List.of(), block1Receipts), false), false); + + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldTruncateResponseWhenFirstBlockIndexAndMessageSizeLimitInteract() { + // Covers the interaction between firstBlockReceiptIndex > 0 (skipping receipts in block 0) + // and the message size limit cutting off block 1 mid-way ("double pagination"). + final Hash block0Hash = dataGenerator.hash(); + final Hash block1Hash = dataGenerator.hash(); + final TransactionReceipt r0 = dataGenerator.receipt(); // skipped via firstBlockReceiptIndex + final TransactionReceipt r1 = dataGenerator.receipt(); // included from block 0 + final TransactionReceipt r2 = dataGenerator.receipt(); // would be from block 1, doesn't fit + when(blockchain.getTxReceipts(block0Hash)).thenReturn(Optional.of(List.of(r0, r1))); + when(blockchain.getTxReceipts(block1Hash)).thenReturn(Optional.of(List.of(r2))); + + // Size limit: fits only r1 (from block 0 after skipping r0); r2 from block 1 won't fit. + final int sizeLimit = 2 * RLP.MAX_PREFIX_SIZE + calculatePaginatedReceiptEncodedSize(r1); + setupEthServer(b -> b.maxMessageSize(sizeLimit)); + + final GetPaginatedReceiptsMessage msg = + GetPaginatedReceiptsMessage.create(List.of(block0Hash, block1Hash), 1); + + // block 0: r0 skipped, r1 fits; block 1: starts but r2 doesn't fit → empty list + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(List.of(List.of(r1), List.of()), true), true); + + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + private void setupEthServer() { setupEthServer(Function.identity()); } @@ -428,4 +646,34 @@ private int calculateRlpEncodedSize(final List receipts) { rlp.endList(); return rlp.encodedSize(); } + + private int calculatePaginatedReceiptEncodedSize(final TransactionReceipt receipt) { + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + TransactionReceiptEncoder.writeTo( + receipt, rlp, TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + return rlp.encodedSize(); + } + + private Bytes serializePaginatedReceiptsList( + final List> receipts, final boolean lastBlockIncomplete) { + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + rlp.writeLongScalar(lastBlockIncomplete ? 1 : 0); + rlp.startList(); + for (final List blockReceipts : receipts) { + final BytesValueRLPOutput encodedBlockReceipts = new BytesValueRLPOutput(); + encodedBlockReceipts.startList(); + for (final TransactionReceipt receipt : blockReceipts) { + final BytesValueRLPOutput encodedReceipt = new BytesValueRLPOutput(); + TransactionReceiptEncoder.writeTo( + receipt, + encodedReceipt, + TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + encodedBlockReceipts.writeRaw(encodedReceipt.encoded()); + } + encodedBlockReceipts.endList(); + rlp.writeRaw(encodedBlockReceipts.encoded()); + } + rlp.endList(); + return rlp.encoded(); + } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java index 1d733f342a0..f8da0991440 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java @@ -14,16 +14,20 @@ */ package org.hyperledger.besu.ethereum.eth.manager.peertask.task; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hyperledger.besu.ethereum.eth.core.Utils.receiptToSyncReceipt; import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.GasLimitCalculator; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.Difficulty; import org.hyperledger.besu.ethereum.core.SyncBlock; @@ -41,7 +45,10 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.InvalidPeerTaskResponseException; import org.hyperledger.besu.ethereum.eth.manager.peertask.MalformedRlpFromPeerException; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskValidationResponse; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Request; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Response; import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; +import org.hyperledger.besu.ethereum.eth.messages.GetPaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.mainnet.BodyValidation; @@ -49,30 +56,42 @@ import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.rlp.SimpleNoCopyRlpEncoder; -import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.IntStream; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; +import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; public class GetSyncReceiptsFromPeerTaskTest { - private static final Set AGREED_CAPABILITIES = Set.of(EthProtocol.ETH69); + private static final Set AGREED_CAPABILITIES_ETH69 = Set.of(EthProtocol.ETH69); + private static final Set AGREED_CAPABILITIES_LATEST = Set.of(EthProtocol.LATEST); private static ProtocolSchedule protocolSchedule; @BeforeAll public static void setup() { protocolSchedule = mock(ProtocolSchedule.class); final ProtocolSpec protocolSpec = mock(ProtocolSpec.class); + final GasLimitCalculator gasLimitCalculator = mock(GasLimitCalculator.class); + when(gasLimitCalculator.transactionGasLimitCap()).thenReturn(Long.MAX_VALUE); + when(protocolSpec.getGasLimitCalculator()).thenReturn(gasLimitCalculator); when(protocolSpec.isPoS()).thenReturn(false); when(protocolSchedule.getByBlockHeader(Mockito.any())).thenReturn(protocolSpec); when(protocolSchedule.anyMatch(Mockito.any())).thenReturn(false); @@ -80,133 +99,227 @@ public static void setup() { @Test public void testGetSubProtocol() { - final BlockHeader blockHeader = mockBlockHeader(1); - final TransactionReceipt receipt = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader.getReceiptsRoot()).thenReturn(BodyValidation.receiptsRoot(List.of(receipt))); - final SyncBlockBody syncBlockBody = mock(SyncBlockBody.class); - when(syncBlockBody.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock = new SyncBlock(blockHeader, syncBlockBody); - - final var task = createTask(List.of(syncBlock), protocolSchedule); + final var task = + createTask(new Request(List.of(mockBlock(1, 1).block), List.of()), protocolSchedule); assertEquals(EthProtocol.get(), task.getSubProtocol()); } @Test - public void testGetRequestMessage() { - final BlockHeader blockHeader1 = mockBlockHeader(1); - final TransactionReceipt receipt1_forBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - final TransactionReceipt receipt2_forBlock1 = - new TransactionReceipt(1, 321, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receipt1_forBlock1, receipt2_forBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(2); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - BlockHeader blockHeader2 = mockBlockHeader(2); - TransactionReceipt receiptForBlock2 = - new TransactionReceipt(1, 456, Collections.emptyList(), Optional.empty()); - when(blockHeader2.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock2))); - final SyncBlockBody syncBlockBody2 = mock(SyncBlockBody.class); - when(syncBlockBody2.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock2 = new SyncBlock(blockHeader2, syncBlockBody2); - - BlockHeader blockHeader3 = mockBlockHeader(3); - TransactionReceipt receiptForBlock3 = - new TransactionReceipt(1, 789, Collections.emptyList(), Optional.empty()); - when(blockHeader3.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock3))); - final SyncBlockBody syncBlockBody3 = mock(SyncBlockBody.class); - when(syncBlockBody3.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock3 = new SyncBlock(blockHeader3, syncBlockBody3); - - final List blocks = List.of(syncBlock1, syncBlock2, syncBlock3); - - final var task = createTask(blocks, protocolSchedule); - - final var messageData = task.getRequestMessage(AGREED_CAPABILITIES); - final var getReceiptsMessage = GetReceiptsMessage.readFrom(messageData); + public void testGetRequestMessageETH69() { + final List mockedBlocks = + List.of(mockBlock(1, 2), mockBlock(2, 1), mockBlock(3, 1)); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(mockedBlocks.stream().map(MockedBlock::block).toList(), List.of()), + protocolSchedule); + + final MessageData messageData = task.getRequestMessage(AGREED_CAPABILITIES_ETH69); + final GetReceiptsMessage getReceiptsMessage = GetReceiptsMessage.readFrom(messageData); assertEquals(EthProtocolMessages.GET_RECEIPTS, getReceiptsMessage.getCode()); - List hashesInMessage = getReceiptsMessage.hashes(); - List expectedHashes = blocks.stream().map(SyncBlock::getHash).toList(); + final List hashesInMessage = getReceiptsMessage.blockHashes(); + final List expectedHashes = + mockedBlocks.stream() + .map(MockedBlock::block) + .map(SyncBlock::getHeader) + .map(BlockHeader::getHash) + .toList(); assertThat(expectedHashes).containsExactlyElementsOf(hashesInMessage); } @Test - public void testParseResponseWithNullResponseMessage() { - final BlockHeader blockHeader = mockBlockHeader(1); - final TransactionReceipt receipt = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader.getReceiptsRoot()).thenReturn(BodyValidation.receiptsRoot(List.of(receipt))); - final SyncBlockBody syncBlockBody = mock(SyncBlockBody.class); - when(syncBlockBody.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock = new SyncBlock(blockHeader, syncBlockBody); + public void testGetRequestMessageLatest() { + final List mockedBlocks = + List.of(mockBlock(1, 2), mockBlock(2, 1), mockBlock(3, 1)); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request( + mockedBlocks.stream().map(MockedBlock::block).toList(), + List.of(toResponseReceipt(mockedBlocks.getFirst().receipts.getFirst()))), + protocolSchedule); + + final MessageData messageData = task.getRequestMessage(AGREED_CAPABILITIES_LATEST); + final GetPaginatedReceiptsMessage getReceiptsMessage = + GetPaginatedReceiptsMessage.readFrom(messageData); + + assertEquals(EthProtocolMessages.GET_RECEIPTS, getReceiptsMessage.getCode()); - final var task = createTask(List.of(syncBlock), protocolSchedule); - Assertions.assertThrows( - InvalidPeerTaskResponseException.class, - () -> task.processResponse(null, AGREED_CAPABILITIES)); + final List hashesInMessage = getReceiptsMessage.blockHashes(); + final List expectedHashes = + mockedBlocks.stream() + .map(MockedBlock::block) + .map(SyncBlock::getHeader) + .map(BlockHeader::getHash) + .toList(); + + assertThat(expectedHashes).containsExactlyElementsOf(hashesInMessage); + + assertThat(getReceiptsMessage.firstBlockReceiptIndex()).isEqualTo(1); + } + + @Test + public void testParseResponseWithNullResponseMessage() { + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(mockBlock(1, 2).block), List.of()), protocolSchedule); + assertThrows( + InvalidPeerTaskResponseException.class, () -> task.processResponse(null, Set.of())); } @Test public void testParseResponse() throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { - BlockHeader blockHeader1 = mockBlockHeader(1); - TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - BlockHeader blockHeader2 = mockBlockHeader(2); - TransactionReceipt receiptForBlock2 = - new TransactionReceipt(1, 456, Collections.emptyList(), Optional.empty()); - when(blockHeader2.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock2))); - final SyncBlockBody syncBlockBody2 = mock(SyncBlockBody.class); - when(syncBlockBody2.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock2 = new SyncBlock(blockHeader2, syncBlockBody2); - - BlockHeader blockHeader3 = mockBlockHeader(3); - TransactionReceipt receiptForBlock3 = - new TransactionReceipt(1, 789, Collections.emptyList(), Optional.empty()); - when(blockHeader3.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock3))); - final SyncBlockBody syncBlockBody3 = mock(SyncBlockBody.class); - when(syncBlockBody3.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock3 = new SyncBlock(blockHeader3, syncBlockBody3); - - final var task = createTask(List.of(syncBlock1, syncBlock2, syncBlock3), protocolSchedule); - - ReceiptsMessage receiptsMessage = + final List mockedBlocks = + List.of(mockBlock(1, 1), mockBlock(2, 1), mockBlock(3, 1), mockBlock(4, 0)); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(mockedBlocks.stream().map(MockedBlock::block).toList(), List.of()), + protocolSchedule); + + final ReceiptsMessage receiptsMessage = ReceiptsMessage.createUnsafe( serializeReceiptsList( - List.of( - List.of(receiptForBlock1), - List.of(receiptForBlock2), - List.of(receiptForBlock3)), + mockedBlocks.stream().map(MockedBlock::receipts).toList(), TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); + final MessageData rawMsg = + new RawMessage(EthProtocolMessages.RECEIPTS, receiptsMessage.getData()); + + final Response response = task.processResponse(rawMsg, AGREED_CAPABILITIES_ETH69); + + assertThat(response.completeReceiptsByBlock().values()) + .usingElementComparator(this::receiptsComparator) + .containsExactlyInAnyOrder( + toResponseReceipts(mockedBlocks.get(0).receipts), + toResponseReceipts(mockedBlocks.get(1).receipts), + toResponseReceipts(mockedBlocks.get(2).receipts), + toResponseReceipts(mockedBlocks.get(3).receipts)); + } + + /** Builds a MessageData in eth/70 wire format: {@code } */ + private MessageData buildEth70ReceiptsMessage( + final List> receiptsByBlock, final boolean lastBlockIncomplete) { + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + rlp.writeLongScalar(lastBlockIncomplete ? 1 : 0); + final Bytes serializedReceiptsList = + serializeReceiptsList( + receiptsByBlock, TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION); + return new RawMessage( + EthProtocolMessages.RECEIPTS, Bytes.concatenate(rlp.encoded(), serializedReceiptsList)); + } + + @Test + public void testParseResponseWithEth70PaginatedLastBlockIncomplete() + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { + // Block 1 has 2 receipts (complete), block 2 has 3 receipts but only 1 is returned (partial) + final MockedBlock block1 = mockBlock(1, 2); + final MockedBlock block2 = mockBlock(2, 3); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(block1.block, block2.block), List.of()), protocolSchedule); + + // Server returns block1 fully and 1 receipt from block2 (lastBlockIncomplete=true) + final MessageData receiptsMessage = + buildEth70ReceiptsMessage( + List.of(block1.receipts, List.of(block2.receipts.getFirst())), true); + + final Response response = task.processResponse(receiptsMessage, AGREED_CAPABILITIES_LATEST); + + // Block1 is complete → in completeReceiptsByBlock + assertThat(response.completeReceiptsByBlock()).hasSize(1); + assertThat(response.completeReceiptsByBlock()).containsKey(block1.block); + + // Block2 is partial → in lastBlockPartialReceipts + assertThat(response.lastBlockPartialReceipts()).hasSize(1); + assertThat(response.lastBlockPartialReceipts().getFirst().getRlpBytes()) + .isEqualTo(toResponseReceipt(block2.receipts.getFirst()).getRlpBytes()); + } + + @Test + public void testParseResponseWithEth70AllReceiptsCompleteLastBlockIncompleteFalse() + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { + final MockedBlock block1 = mockBlock(1, 1); + final MockedBlock block2 = mockBlock(2, 2); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(block1.block, block2.block), List.of()), protocolSchedule); + + // Server returns both blocks fully (lastBlockIncomplete=false) + final MessageData receiptsMessage = + buildEth70ReceiptsMessage(List.of(block1.receipts, block2.receipts), false); + + final Response response = task.processResponse(receiptsMessage, AGREED_CAPABILITIES_LATEST); + + assertThat(response.lastBlockPartialReceipts()).isEmpty(); + assertThat(response.completeReceiptsByBlock()).containsOnlyKeys(block1.block, block2.block); + } + + @Test + public void testParseResponseCombinesPartialReceiptsFromPreviousRequest() + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { + // Block has 3 receipts; previous request delivered receipts[0], now we get receipts[1..2] + final MockedBlock block = mockBlock(1, 3); + + final List alreadyFetched = + List.of(toResponseReceipt(block.receipts.getFirst())); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(block.block), alreadyFetched), protocolSchedule); + + // Server returns receipts[1..2] (eth/70 format, lastBlockIncomplete=false) + final MessageData receiptsMessage = + buildEth70ReceiptsMessage(List.of(block.receipts.subList(1, 3)), false); + + final Response response = task.processResponse(receiptsMessage, AGREED_CAPABILITIES_LATEST); + + // completeFirstBlock() should prepend alreadyFetched and produce all 3 receipts + assertThat(response.lastBlockPartialReceipts()).isEmpty(); + assertThat(response.completeReceiptsByBlock()).containsKey(block.block); + assertThat(response.completeReceiptsByBlock().get(block.block)).hasSize(3); + } + + @Test + public void testParseResponseWithEth70LastBlockIncompleteTrueAndEmptyListThrows() { + final MockedBlock block = mockBlock(1, 2); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(block.block), List.of()), protocolSchedule); + + // Malicious server sends lastBlockIncomplete=1 but empty receipt list + final MessageData receiptsMessage = buildEth70ReceiptsMessage(List.of(), true); + + assertThatThrownBy(() -> task.processResponse(receiptsMessage, AGREED_CAPABILITIES_LATEST)) + .isInstanceOf(InvalidPeerTaskResponseException.class); + } + + @Test + public void testParseResponseFailsWhenReceiptsForTooManyBlocksAreReturned() { + final List mockedBlocks = + List.of(mockBlock(1, 1), mockBlock(2, 1), mockBlock(3, 1)); - final var response = task.processResponse(receiptsMessage, AGREED_CAPABILITIES); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(mockedBlocks.stream().map(MockedBlock::block).toList(), List.of()), + protocolSchedule); - assertThat(response).hasSize(3); - assertThat(response.get(syncBlock1)) - .usingElementComparator(Utils::compareSyncReceipts) - .containsExactly(toResponseReceipt(receiptForBlock1)); - assertThat(response.get(syncBlock2)) - .usingElementComparator(Utils::compareSyncReceipts) - .containsExactly(toResponseReceipt(receiptForBlock2)); - assertThat(response.get(syncBlock3)) - .usingElementComparator(Utils::compareSyncReceipts) - .containsExactly(toResponseReceipt(receiptForBlock3)); + final MockedBlock extraMockedBlock = mockBlock(4, 1); + + final ReceiptsMessage receiptsMessage = + ReceiptsMessage.createUnsafe( + serializeReceiptsList( + Stream.concat(mockedBlocks.stream(), Stream.of(extraMockedBlock)) + .map(MockedBlock::receipts) + .toList(), + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); + final MessageData rawMsg = + new RawMessage(EthProtocolMessages.RECEIPTS, receiptsMessage.getData()); + + assertThatThrownBy(() -> task.processResponse(rawMsg, AGREED_CAPABILITIES_ETH69)) + .isInstanceOf(InvalidPeerTaskResponseException.class) + .hasMessageContaining("Too many result returned"); } @ParameterizedTest @@ -214,26 +327,12 @@ public void testParseResponse() public void testGetPeerRequirementFilter(final boolean isPoS) { reset(protocolSchedule); when(protocolSchedule.anyMatch(any())).thenReturn(isPoS); + final List mockedBlocks = List.of(mockBlock(1, 1), mockBlock(2, 1)); - BlockHeader blockHeader1 = mockBlockHeader(1); - TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - BlockHeader blockHeader2 = mockBlockHeader(2); - TransactionReceipt receiptForBlock2 = - new TransactionReceipt(1, 456, Collections.emptyList(), Optional.empty()); - when(blockHeader2.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock2))); - final SyncBlockBody syncBlockBody2 = mock(SyncBlockBody.class); - when(syncBlockBody2.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock2 = new SyncBlock(blockHeader2, syncBlockBody2); - - final var task = createTask(List.of(syncBlock1, syncBlock2), protocolSchedule); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(mockedBlocks.stream().map(MockedBlock::block).toList(), List.of()), + protocolSchedule); EthPeer failForShortChainHeight = mockPeer(1); EthPeer successfulCandidate = mockPeer(5); @@ -246,148 +345,207 @@ public void testGetPeerRequirementFilter(final boolean isPoS) { task.getPeerRequirementFilter().test(EthPeerImmutableAttributes.from(successfulCandidate))); } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void validateResultFailsWhenNoResultAreReturned( + final boolean hasFirstBlockPartialReceipts) { + final MockedBlock mockedBlock = mockBlock(1, 1); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request( + List.of(mockedBlock.block), + hasFirstBlockPartialReceipts + ? toResponseReceipts(mockedBlock.receipts) + : emptyList()), + protocolSchedule); + + assertEquals( + PeerTaskValidationResponse.NO_RESULTS_RETURNED, + task.validateResult(new Response(Map.of(), List.of()))); + } + + static List validateResultProvider() { + return List.of( + Arguments.of(false, false), + Arguments.of(false, true), + Arguments.of(true, false), + Arguments.of(true, true)); + } + + @ParameterizedTest + @MethodSource("validateResultProvider") + public void testValidateResultForFullSuccess( + final boolean hasFirstBlockPartialReceipts, final boolean lastBlockIncomplete) { + final MockedBlock mockedBlock = mockBlock(1, 1); + final MockedBlock lastMockedBlock = mockBlock(2, 3); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request( + List.of(mockedBlock.block, lastMockedBlock.block), + hasFirstBlockPartialReceipts + ? List.of(toResponseReceipt(lastMockedBlock.receipts.getFirst())) + : emptyList()), + protocolSchedule); + + final List expectedLastBlockPartialReceipts = + lastBlockIncomplete + ? toResponseReceipts(lastMockedBlock.receipts).subList(0, 1) + : List.of(); + + final Map> expectedCompletedBlocks = new HashMap<>(); + expectedCompletedBlocks.put(mockedBlock.block, toResponseReceipts(mockedBlock.receipts)); + if (!lastBlockIncomplete) { + expectedCompletedBlocks.put( + lastMockedBlock.block, toResponseReceipts(lastMockedBlock.receipts)); + } + + assertEquals( + PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD, + task.validateResult( + new Response(expectedCompletedBlocks, expectedLastBlockPartialReceipts))); + } + @Test - public void validateResultFailsWhenNoResultAreReturned() { - final BlockHeader blockHeader = mockBlockHeader(1); - final TransactionReceipt receiptForBlock = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock))); - final SyncBlockBody syncBlockBody = mock(SyncBlockBody.class); - when(syncBlockBody.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock = new SyncBlock(blockHeader, syncBlockBody); + public void validateResultFailsReceiptRootDoesNotMatch() { + final MockedBlock mockedRequestedBlock = mockBlock(1, 1); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(mockedRequestedBlock.block), List.of()), protocolSchedule); - final var task = createTask(List.of(syncBlock), protocolSchedule); + final List anotherBlockReceipts = mockBlock(2, 1).receipts; - assertEquals(PeerTaskValidationResponse.NO_RESULTS_RETURNED, task.validateResult(Map.of())); + assertEquals( + PeerTaskValidationResponse.RESULTS_DO_NOT_MATCH_QUERY, + task.validateResult( + new Response( + // for the requested block, receipts returned are from another block + Map.of(mockedRequestedBlock.block, toResponseReceipts(anotherBlockReceipts)), + List.of()))); } @Test - public void testValidateResultForFullSuccess() { - final BlockHeader blockHeader = mockBlockHeader(1); - final TransactionReceipt receiptForBlock = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock))); - final SyncBlockBody syncBlockBody = mock(SyncBlockBody.class); - when(syncBlockBody.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock = new SyncBlock(blockHeader, syncBlockBody); - - final var task = createTask(List.of(syncBlock), protocolSchedule); + public void validateResultSuccessWhenPartialBlockIsIncomplete() { + final MockedBlock mockedBlock = mockBlock(1, 3); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request( + List.of(mockedBlock.block), + List.of(toResponseReceipt(mockedBlock.receipts.getFirst()))), + protocolSchedule); assertEquals( PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD, - task.validateResult(Map.of(syncBlock, List.of(toResponseReceipt(receiptForBlock))))); + task.validateResult( + new Response(Map.of(), toResponseReceipts(mockedBlock.receipts).subList(0, 2)))); } @Test - public void testParseResponseForInvalidResponse() { - // Too many block-lists in the response (4 for 3 requested blocks) must be rejected at parse - // time by processResponse, not deferred to validateResult. - final BlockHeader blockHeader1 = mockBlockHeader(1); - final TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - final BlockHeader blockHeader2 = mockBlockHeader(2); - final TransactionReceipt receiptForBlock2 = - new TransactionReceipt(1, 456, Collections.emptyList(), Optional.empty()); - when(blockHeader2.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock2))); - final SyncBlockBody syncBlockBody2 = mock(SyncBlockBody.class); - when(syncBlockBody2.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock2 = new SyncBlock(blockHeader2, syncBlockBody2); - - final BlockHeader blockHeader3 = mockBlockHeader(3); - final TransactionReceipt receiptForBlock3 = - new TransactionReceipt(1, 789, Collections.emptyList(), Optional.empty()); - when(blockHeader3.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock3))); - final SyncBlockBody syncBlockBody3 = mock(SyncBlockBody.class); - when(syncBlockBody3.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock3 = new SyncBlock(blockHeader3, syncBlockBody3); - - final var task = createTask(List.of(syncBlock1, syncBlock2, syncBlock3), protocolSchedule); + public void validateResultFailsWhenPartialReceiptSizeExceedsTxGasLimitBound() { + // txGasLimitCap=800 → per-receipt threshold = 100 bytes; a 101-byte receipt must be rejected + final MockedBlock block = mockBlockWithGasLimit(1, 1, 30_000_000L); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(List.of(block.block), List.of()), + createProtocolScheduleWithTxGasLimitCap(800L)); - final ReceiptsMessage receiptsMessage = - ReceiptsMessage.createUnsafe( - serializeReceiptsList( - List.of( - List.of(receiptForBlock1), - List.of(receiptForBlock2), - List.of(receiptForBlock3), - List.of( - new TransactionReceipt( - 1, 101112, Collections.emptyList(), Optional.empty()))), - TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); + final SyncTransactionReceipt oversizedReceipt = + new SyncTransactionReceipt(Bytes.of(new byte[101])); - Assertions.assertThrows( - InvalidPeerTaskResponseException.class, - () -> task.processResponse(receiptsMessage, AGREED_CAPABILITIES)); + assertEquals( + PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED, + task.validateResult(new Response(Map.of(), List.of(oversizedReceipt)))); } @Test - public void validateResultFailsWhenTooManyBlocksReturned() { - // A block with 1 transaction that receives 2 receipts must be rejected. - final BlockHeader blockHeader1 = mockBlockHeader(1); - final TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - final TransactionReceipt extraReceipt = - new TransactionReceipt(1, 321, Collections.emptyList(), Optional.empty()); - - final var task = createTask(List.of(syncBlock1), protocolSchedule); + public void validateResultFailsWhenCumulativePartialReceiptSizeExceedsBlockGasLimitBound() { + // blockGasLimit=800 → cumulative threshold = 100 bytes; two 60-byte receipts (total 120) must + // be rejected even though each individually is within the per-receipt bound + final MockedBlock block = mockBlockWithGasLimit(1, 2, 800L); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(List.of(block.block), List.of()), + createProtocolScheduleWithTxGasLimitCap(Long.MAX_VALUE)); + + final List partialReceipts = + List.of( + new SyncTransactionReceipt(Bytes.of(new byte[60])), + new SyncTransactionReceipt(Bytes.of(new byte[60]))); assertEquals( - PeerTaskValidationResponse.TOO_MANY_RESULTS_RETURNED, - task.validateResult( - Map.of( - syncBlock1, - List.of(toResponseReceipt(receiptForBlock1), toResponseReceipt(extraReceipt))))); + PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED, + task.validateResult(new Response(Map.of(), partialReceipts))); } @Test - public void validateResultFailsReceiptRootDoesNotMatch() { - final BlockHeader blockHeader1 = mockBlockHeader(1); - final TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); + public void validateResultPassesWhenPartialReceiptSizesAreWithinBounds() { + // txGasLimitCap=800 → per-receipt threshold=100; blockGasLimit=800 → cumulative threshold=100 + // Two 40-byte receipts: each 40 < 100, cumulative 80 < 100 → valid + final MockedBlock block = mockBlockWithGasLimit(1, 2, 800L); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(List.of(block.block), List.of()), + createProtocolScheduleWithTxGasLimitCap(800L)); + + final List partialReceipts = + List.of( + new SyncTransactionReceipt(Bytes.of(new byte[40])), + new SyncTransactionReceipt(Bytes.of(new byte[40]))); - final TransactionReceipt returnedReceiptForBlock1 = - new TransactionReceipt(1, 321, Collections.emptyList(), Optional.empty()); + assertEquals( + PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD, + task.validateResult(new Response(Map.of(), partialReceipts))); + } - final var task = createTask(List.of(syncBlock1), protocolSchedule); + @Test + public void validateResultChecksAllPartialReceiptsEvenWhenFirstRequestBlockIsComplete() { + // Regression test: when the first block in the request becomes complete and the partial + // block is a later one, the size loop must start from index 0 (not + // firstBlockPartialReceipts.size()) so none of the later block's receipts are skipped. + final MockedBlock blockA = mockBlock(1, 1); + final MockedBlock blockB = mockBlockWithGasLimit(2, 1, 800L); + final SyncTransactionReceipt receiptForA = toResponseReceipt(blockA.receipts.getFirst()); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(List.of(blockA.block, blockB.block), List.of(receiptForA)), + createProtocolScheduleWithTxGasLimitCap(800L)); + + // blockA is complete (receipt root matches), blockB has a single oversized partial receipt + final SyncTransactionReceipt oversizedReceiptForB = + new SyncTransactionReceipt(Bytes.of(new byte[101])); assertEquals( - PeerTaskValidationResponse.RESULTS_DO_NOT_MATCH_QUERY, + PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED, task.validateResult( - Map.of(syncBlock1, List.of(toResponseReceipt(returnedReceiptForBlock1))))); + new Response( + Map.of(blockA.block, List.of(receiptForA)), List.of(oversizedReceiptForB)))); } - private static BlockHeader mockBlockHeader(final long blockNumber) { + private static BlockHeader mockBlockHeader( + final long blockNumber, final List receipts) { BlockHeader blockHeader = mock(BlockHeader.class); when(blockHeader.getNumber()).thenReturn(blockNumber); - // second to last hex digit indicates the blockNumber, last hex digit indicates the usage of the - // hash + // second to last hex digit indicates the blockNumber, + // last hex digit indicates the usage of the hash when(blockHeader.getHash()) .thenReturn(Hash.fromHexString(StringUtils.repeat("00", 31) + blockNumber + "1")); + when(blockHeader.getReceiptsRoot()).thenReturn(BodyValidation.receiptsRoot(receipts)); + when(blockHeader.getGasLimit()).thenReturn(30_000_000L); return blockHeader; } + private static List mockTransactionReceipts( + final long blockNumber, final int count) { + + return IntStream.rangeClosed(1, count) + .mapToObj( + i -> new TransactionReceipt(1, 123L * i + blockNumber, emptyList(), Optional.empty())) + .toList(); + } + private EthPeer mockPeer(final long chainHeight) { EthPeer ethPeer = mock(EthPeer.class); ChainState chainState = mock(ChainState.class); @@ -402,12 +560,61 @@ private EthPeer mockPeer(final long chainHeight) { } private GetSyncReceiptsFromPeerTask createTask( - final List blocks, final ProtocolSchedule protocolSchedule) { + final Request request, final ProtocolSchedule protocolSchedule) { return new GetSyncReceiptsFromPeerTask( - blocks, protocolSchedule, new SyncTransactionReceiptEncoder(new SimpleNoCopyRlpEncoder())); + request, protocolSchedule, new SyncTransactionReceiptEncoder(new SimpleNoCopyRlpEncoder())); } private SyncTransactionReceipt toResponseReceipt(final TransactionReceipt receipt) { return receiptToSyncReceipt(receipt, TransactionReceiptEncodingConfiguration.DEFAULT); } + + private List toResponseReceipts(final List receipts) { + return receipts.stream().map(this::toResponseReceipt).toList(); + } + + private int receiptsComparator( + final List receipts1, final List receipts2) { + if (receipts1.size() != receipts2.size()) { + return receipts1.size() - receipts2.size(); + } + for (int i = 0; i < receipts1.size(); i++) { + if (Utils.compareSyncReceipts(receipts1.get(i), receipts2.get(i)) != 0) { + // quick tiebreak since we are not interested in the order here + return receipts1.hashCode() - receipts2.hashCode(); + } + } + return 0; + } + + private MockedBlock mockBlock(final long number, final int txCount) { + final SyncBlockBody body = mock(SyncBlockBody.class); + when(body.getTransactionCount()).thenReturn(txCount); + final List receipts = mockTransactionReceipts(number, txCount); + return new MockedBlock(new SyncBlock(mockBlockHeader(number, receipts), body), receipts); + } + + private MockedBlock mockBlockWithGasLimit( + final long number, final int txCount, final long blockGasLimit) { + final SyncBlockBody body = mock(SyncBlockBody.class); + when(body.getTransactionCount()).thenReturn(txCount); + final List receipts = mockTransactionReceipts(number, txCount); + final BlockHeader header = mockBlockHeader(number, receipts); + when(header.getGasLimit()).thenReturn(blockGasLimit); + return new MockedBlock(new SyncBlock(header, body), receipts); + } + + private ProtocolSchedule createProtocolScheduleWithTxGasLimitCap(final long txGasLimitCap) { + final ProtocolSchedule ps = mock(ProtocolSchedule.class); + final ProtocolSpec spec = mock(ProtocolSpec.class); + final GasLimitCalculator calc = mock(GasLimitCalculator.class); + when(calc.transactionGasLimitCap()).thenReturn(txGasLimitCap); + when(spec.getGasLimitCalculator()).thenReturn(calc); + when(spec.isPoS()).thenReturn(false); + when(ps.getByBlockHeader(any())).thenReturn(spec); + when(ps.anyMatch(any())).thenReturn(false); + return ps; + } + + private record MockedBlock(SyncBlock block, List receipts) {} } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessageTest.java new file mode 100644 index 00000000000..d4b8e65180b --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessageTest.java @@ -0,0 +1,95 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.eth.messages; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.BlockDataGenerator; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public final class GetPaginatedReceiptsMessageTest { + + @Test + public void roundTripTestWithZeroIndex() { + roundTripTest(0); + } + + @Test + public void roundTripTestWithNonZeroIndex() { + roundTripTest(5); + } + + private void roundTripTest(final int firstBlockReceiptIndex) { + // Generate some hashes + final BlockDataGenerator gen = new BlockDataGenerator(1); + final List hashes = new ArrayList<>(); + final int hashCount = 20; + for (int i = 0; i < hashCount; ++i) { + hashes.add(gen.hash()); + } + + // Perform round-trip transformation + // Create message, copy it to a generic message, then read back into a GetPaginatedReceipts + // message + final MessageData initialMessage = + GetPaginatedReceiptsMessage.create(hashes, firstBlockReceiptIndex); + final MessageData raw = + new RawMessage(EthProtocolMessages.GET_RECEIPTS, initialMessage.getData()); + final GetPaginatedReceiptsMessage message = GetPaginatedReceiptsMessage.readFrom(raw); + + // Read data back out after round trip and check they match originals. + Assertions.assertThat(message.firstBlockReceiptIndex()).isEqualTo(firstBlockReceiptIndex); + final Iterator readData = message.blockHashes().iterator(); + for (int i = 0; i < hashCount; ++i) { + Assertions.assertThat(readData.next()).isEqualTo(hashes.get(i)); + } + Assertions.assertThat(readData.hasNext()).isFalse(); + } + + @Test + public void readFromReturnsSameInstanceIfAlreadyCorrectType() { + final BlockDataGenerator gen = new BlockDataGenerator(1); + final GetPaginatedReceiptsMessage original = + GetPaginatedReceiptsMessage.create(List.of(gen.hash()), 0); + + final GetPaginatedReceiptsMessage result = GetPaginatedReceiptsMessage.readFrom(original); + + Assertions.assertThat(result).isSameAs(original); + } + + @Test + public void readFromThrowsOnWrongMessageCode() { + final BlockDataGenerator gen = new BlockDataGenerator(1); + final MessageData wrongCodeMessage = + new RawMessage(EthProtocolMessages.GET_BLOCK_HEADERS, gen.hash().getBytes()); + + Assertions.assertThatThrownBy(() -> GetPaginatedReceiptsMessage.readFrom(wrongCodeMessage)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getCodeReturnsGetReceipts() { + final GetPaginatedReceiptsMessage message = GetPaginatedReceiptsMessage.create(List.of(), 0); + + Assertions.assertThat(message.getCode()).isEqualTo(EthProtocolMessages.GET_RECEIPTS); + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java index bb3de01f1ea..7f347b235e3 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java @@ -47,7 +47,7 @@ public void roundTripTest() { final GetReceiptsMessage message = GetReceiptsMessage.readFrom(raw); // Read hashes back out after round trip and check they match originals. - final Iterator readData = message.hashes().iterator(); + final Iterator readData = message.blockHashes().iterator(); for (int i = 0; i < hashCount; ++i) { Assertions.assertThat(readData.next()).isEqualTo(hashes.get(i)); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessageTest.java new file mode 100644 index 00000000000..57671663cf3 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessageTest.java @@ -0,0 +1,110 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.eth.messages; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.ethereum.core.BlockDataGenerator; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; +import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public final class PaginatedReceiptsMessageTest { + + static List testDeserializeFromWireWithLastBlockIncomplete() { + return List.of(Arguments.of(0, false), Arguments.of(1, true)); + } + + @ParameterizedTest + @MethodSource("testDeserializeFromWireWithLastBlockIncomplete") + public void testDeserializeFromWireWithLastBlockIncomplete( + final int value, final boolean expected) { + final BlockDataGenerator gen = new BlockDataGenerator(1); + final BytesValueRLPOutput blockRlp = new BytesValueRLPOutput(); + blockRlp.startList(); + TransactionReceiptEncoder.writeTo( + gen.receipt(), + blockRlp, + TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + blockRlp.endList(); + + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeLongScalar(value); + out.startList(); + out.writeRaw(blockRlp.encoded()); + out.endList(); + + final PaginatedReceiptsMessage decoded = + PaginatedReceiptsMessage.readFrom( + new RawMessage(EthProtocolMessages.RECEIPTS, out.encoded())); + assertThat(decoded.lastBlockIncomplete()).isEqualTo(expected); + } + + @Test + public void testCreateUnsafePreservesLastBlockIncomplete() { + final BlockDataGenerator gen = new BlockDataGenerator(1); + final List> receipts = List.of(List.of(gen.receipt())); + + final BytesValueRLPOutput blockReceipts = new BytesValueRLPOutput(); + blockReceipts.startList(); + receipts + .getFirst() + .forEach( + r -> + TransactionReceiptEncoder.writeTo( + r, + blockReceipts, + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); + blockReceipts.endList(); + + final BytesValueRLPOutput messageData = new BytesValueRLPOutput(); + messageData.writeLongScalar(0); + messageData.startList(); + messageData.writeRaw(blockReceipts.encoded()); + messageData.endList(); + + // createUnsafe stores the flag directly without re-parsing + final PaginatedReceiptsMessage messageComplete = + PaginatedReceiptsMessage.createUnsafe(messageData.encoded(), false); + assertThat(messageComplete.lastBlockIncomplete()).isFalse(); + + final PaginatedReceiptsMessage messageIncomplete = + PaginatedReceiptsMessage.createUnsafe(messageData.encoded(), true); + assertThat(messageIncomplete.lastBlockIncomplete()).isTrue(); + } + + @Test + public void testMinimalEncoding() { + // Test minimal encoding without actual receipts to isolate the flag logic + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeLongScalar(1); // Flag = true + out.startList(); // Empty list of blocks + out.endList(); + + final PaginatedReceiptsMessage message = + PaginatedReceiptsMessage.readFrom( + new RawMessage(EthProtocolMessages.RECEIPTS, out.encoded())); + assertThat(message.lastBlockIncomplete()).isTrue(); + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStepTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStepTest.java index 20914dedf33..0826db1ee62 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStepTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStepTest.java @@ -44,6 +44,7 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutor; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutorResult; import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Response; import org.hyperledger.besu.ethereum.mainnet.DefaultProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; @@ -89,25 +90,29 @@ public void setUp() { @Test public void shouldDownloadReceiptsForBlocksWithTransactions() throws ExecutionException, InterruptedException { - // skip genesis block, since we do not need to retrieve receipt for it final List blockWithTxs = gen.blockSequence(3).subList(1, 3); - final var receiptsPerBlock = + final List> returnedReceiptsByBlock = blockWithTxs.stream() .map(gen::receipts) .map(rs -> receiptsToSyncReceipts(rs, ETH69_RECEIPT_CONFIGURATION)) .toList(); final var syncBlocks = blocksToSyncBlocks(blockWithTxs); - final Map> returnedReceiptsByBlock = new HashMap<>(); - for (int i = 0; i < syncBlocks.size(); i++) { - returnedReceiptsByBlock.put(syncBlocks.get(i), receiptsPerBlock.get(i)); - } - // Mock the peer task executor to return receipts for both blocks - final var executorResult = - new PeerTaskExecutorResult<>(Optional.of(returnedReceiptsByBlock), SUCCESS, emptyList()); + final PeerTaskExecutorResult executorResult = + new PeerTaskExecutorResult<>( + Optional.of( + new Response( + Map.of( + syncBlocks.get(0), + returnedReceiptsByBlock.get(0), + syncBlocks.get(1), + returnedReceiptsByBlock.get(1)), + List.of())), + SUCCESS, + emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(executorResult); @@ -120,7 +125,7 @@ public void shouldDownloadReceiptsForBlocksWithTransactions() assertThat(blocksWithReceipts).hasSize(2); for (int i = 0; i < blocksWithReceipts.size(); i++) { assertThat(blocksWithReceipts.get(i).getBlock()).isEqualTo(syncBlocks.get(i)); - assertThat(blocksWithReceipts.get(i).getReceipts()).isEqualTo(receiptsPerBlock.get(i)); + assertThat(blocksWithReceipts.get(i).getReceipts()).isEqualTo(returnedReceiptsByBlock.get(i)); } // Verify the task was executed once @@ -140,21 +145,23 @@ public void shouldSkipDownloadForBlocksWithEmptyReceiptsRoot() gen.setBlockOptionsSupplier(() -> BlockOptions.create().hasTransactions(true)); final Block block3_withTxs = gen.blockSequence(block2_withoutTxs, 1).getFirst(); - final var blocks = List.of(block1_withTxs, block2_withoutTxs, block3_withTxs); + final List blocks = List.of(block1_withTxs, block2_withoutTxs, block3_withTxs); // we must not request receipt for block2, so only return receipts for the 2 blocks with txs - final var receiptsForBlock1 = + final List receiptsForBlock1 = receiptsToSyncReceipts(gen.receipts(block1_withTxs), ETH69_RECEIPT_CONFIGURATION); - final var receiptsForBlock3 = + final List receiptsForBlock3 = receiptsToSyncReceipts(gen.receipts(block3_withTxs), ETH69_RECEIPT_CONFIGURATION); - final var syncBlocks = blocksToSyncBlocks(blocks); + final List syncBlocks = blocksToSyncBlocks(blocks); - final Map> returnedReceiptsByBlock = - Map.of(syncBlocks.get(0), receiptsForBlock1, syncBlocks.get(2), receiptsForBlock3); + final Response taskResponse = + new Response( + Map.of(syncBlocks.get(0), receiptsForBlock1, syncBlocks.get(2), receiptsForBlock3), + List.of()); - final var executorResult = - new PeerTaskExecutorResult<>(Optional.of(returnedReceiptsByBlock), SUCCESS, emptyList()); + final PeerTaskExecutorResult executorResult = + new PeerTaskExecutorResult<>(Optional.of(taskResponse), SUCCESS, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(executorResult); @@ -179,6 +186,120 @@ public void shouldSkipDownloadForBlocksWithEmptyReceiptsRoot() verify(peerTaskExecutor, times(1)).execute(any(GetSyncReceiptsFromPeerTask.class)); } + @Test + public void shouldHandlePartialReceiptsFromFirstBlock() + throws ExecutionException, InterruptedException { + + // Given: blocks with 3 transactions each, excluding genesis block + gen.setBlockOptionsSupplier( + () -> BlockOptions.create().hasTransactions(true).transactionCount(3)); + final List blocks = gen.blockSequence(3).subList(1, 3); + + final List syncBlocks = blocksToSyncBlocks(blocks); + + final List> returnedReceiptsByBlock = + blocks.stream() + .map(gen::receipts) + .map(rs -> receiptsToSyncReceipts(rs, ETH69_RECEIPT_CONFIGURATION)) + .toList(); + + // First call returns partial receipts for first block + final List firstBlockReceipts = returnedReceiptsByBlock.getFirst(); + final List secondBlockReceipts = returnedReceiptsByBlock.get(1); + final List partialReceipts = + firstBlockReceipts.subList(0, firstBlockReceipts.size() / 2); + + final PeerTaskExecutorResult firstExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(Map.of(), partialReceipts)), SUCCESS, emptyList()); + + // Second call returns combined receipts (partial + remaining) for first block and second block + // Note: processResponse in AbstractGetReceiptsFromPeerTask combines firstBlockPartialReceipts + // with newly received receipts, so the result contains the full receipt list + final PeerTaskExecutorResult secondExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of( + new Response( + Map.of( + syncBlocks.getFirst(), + firstBlockReceipts, + syncBlocks.get(1), + secondBlockReceipts), + List.of())), + SUCCESS, + emptyList()); + + when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) + .thenReturn(firstExecutorResult) + .thenReturn(secondExecutorResult); + + // When: downloading receipts + final CompletableFuture> result = + downloadSyncReceiptsStep.apply(syncBlocks); + + // Then: should return blocks with complete receipts + final List blocksWithReceipts = result.get(); + assertThat(blocksWithReceipts).hasSize(2); + assertThat(blocksWithReceipts.get(0).getReceipts()).isEqualTo(firstBlockReceipts); + assertThat(blocksWithReceipts.get(1).getReceipts()).isEqualTo(secondBlockReceipts); + + // Verify the task was executed twice + verify(peerTaskExecutor, times(2)).execute(any(GetSyncReceiptsFromPeerTask.class)); + } + + @Test + public void shouldHandlePartialReceiptsFromBlockAdvanced() + throws ExecutionException, InterruptedException { + + // Given: block with 3 transactions + gen.setBlockOptionsSupplier( + () -> BlockOptions.create().hasTransactions(true).transactionCount(3)); + final Block block = gen.block(); + + final List syncBlocks = blocksToSyncBlocks(List.of(block)); + + final List returnedReceipts = + receiptsToSyncReceipts(gen.receipts(block), ETH69_RECEIPT_CONFIGURATION); + + // Receipts for the block are split in three responses + // Note: processResponse combines partial receipts, so each result contains cumulative receipts + final List firstCallReturnedReceipts = returnedReceipts.subList(0, 1); + final List secondCallReturnedReceipts = returnedReceipts.subList(0, 2); + final List thirdCallReturnedReceipts = returnedReceipts; + + final PeerTaskExecutorResult firstExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(Map.of(), firstCallReturnedReceipts)), SUCCESS, emptyList()); + + final PeerTaskExecutorResult secondExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(Map.of(), secondCallReturnedReceipts)), SUCCESS, emptyList()); + + final PeerTaskExecutorResult thirdExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of( + new Response(Map.of(syncBlocks.getFirst(), thirdCallReturnedReceipts), List.of())), + SUCCESS, + emptyList()); + + when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) + .thenReturn(firstExecutorResult) + .thenReturn(secondExecutorResult) + .thenReturn(thirdExecutorResult); + + // When: downloading receipts + final CompletableFuture> result = + downloadSyncReceiptsStep.apply(syncBlocks); + + // Then: should return block with complete receipts + final List blocksWithReceipts = result.get(); + assertThat(blocksWithReceipts).hasSize(1); + assertThat(blocksWithReceipts.getFirst().getReceipts()).isEqualTo(returnedReceipts); + + // Verify the task was executed twice + verify(peerTaskExecutor, times(3)).execute(any(GetSyncReceiptsFromPeerTask.class)); + } + @Test public void shouldRetryUntilAllReceiptsDownloaded() throws ExecutionException, InterruptedException { @@ -186,26 +307,35 @@ public void shouldRetryUntilAllReceiptsDownloaded() final List blocks = gen.blockSequence(4).subList(1, 4); final List syncBlocks = blocksToSyncBlocks(blocks); - final var receiptsPerBlock = + final List> receiptsPerBlock = blocks.stream() .map(gen::receipts) .map(rs -> receiptsToSyncReceipts(rs, ETH69_RECEIPT_CONFIGURATION)) .toList(); // First call returns first block only - final var firstExecutorResult = + final PeerTaskExecutorResult firstExecutorResult = new PeerTaskExecutorResult<>( - Optional.of(Map.of(syncBlocks.get(0), receiptsPerBlock.get(0))), SUCCESS, emptyList()); + Optional.of( + new Response(Map.of(syncBlocks.get(0), receiptsPerBlock.get(0)), List.of())), + SUCCESS, + emptyList()); // Second call returns second block only - final var secondExecutorResult = + final PeerTaskExecutorResult secondExecutorResult = new PeerTaskExecutorResult<>( - Optional.of(Map.of(syncBlocks.get(1), receiptsPerBlock.get(1))), SUCCESS, emptyList()); + Optional.of( + new Response(Map.of(syncBlocks.get(1), receiptsPerBlock.get(1)), List.of())), + SUCCESS, + emptyList()); // Third call returns third block only - final var thirdExecutorResult = + final PeerTaskExecutorResult thirdExecutorResult = new PeerTaskExecutorResult<>( - Optional.of(Map.of(syncBlocks.get(2), receiptsPerBlock.get(2))), SUCCESS, emptyList()); + Optional.of( + new Response(Map.of(syncBlocks.get(2), receiptsPerBlock.get(2)), List.of())), + SUCCESS, + emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(firstExecutorResult) @@ -288,13 +418,13 @@ public void shouldRetryAfterSingleFailureAndEventuallySucceed() } // First call returns failure (e.g., peer disconnected) - final var failureResult = - new PeerTaskExecutorResult>>( - Optional.empty(), PEER_DISCONNECTED, emptyList()); + final PeerTaskExecutorResult failureResult = + new PeerTaskExecutorResult<>(Optional.empty(), PEER_DISCONNECTED, emptyList()); // Second call returns success with all receipts - final var successResult = - new PeerTaskExecutorResult<>(Optional.of(returnedReceiptsByBlock), SUCCESS, emptyList()); + final PeerTaskExecutorResult successResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(returnedReceiptsByBlock, emptyList())), SUCCESS, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(failureResult) @@ -330,20 +460,19 @@ public void shouldRetryMultipleTimesAfterConsecutiveFailures() .toList(); // First three calls return different failures - final var timeoutResult = - new PeerTaskExecutorResult>>( - Optional.empty(), TIMEOUT, emptyList()); - final var noPeerResult = - new PeerTaskExecutorResult>>( - Optional.empty(), NO_PEER_AVAILABLE, emptyList()); - final var disconnectedResult = - new PeerTaskExecutorResult>>( - Optional.empty(), PEER_DISCONNECTED, emptyList()); + final PeerTaskExecutorResult timeoutResult = + new PeerTaskExecutorResult<>(Optional.empty(), TIMEOUT, emptyList()); + final PeerTaskExecutorResult noPeerResult = + new PeerTaskExecutorResult<>(Optional.empty(), NO_PEER_AVAILABLE, emptyList()); + final PeerTaskExecutorResult disconnectedResult = + new PeerTaskExecutorResult<>(Optional.empty(), PEER_DISCONNECTED, emptyList()); // Fourth call returns success - final var successResult = + final PeerTaskExecutorResult successResult = new PeerTaskExecutorResult<>( - Optional.of(Map.of(syncBlocks.getFirst(), receiptsPerBlock.getFirst())), + Optional.of( + new Response( + Map.of(syncBlocks.getFirst(), receiptsPerBlock.getFirst()), List.of())), SUCCESS, emptyList()); @@ -361,6 +490,7 @@ public void shouldRetryMultipleTimesAfterConsecutiveFailures() final List blocksWithReceipts = result.get(); assertThat(blocksWithReceipts).hasSize(1); assertThat(blocksWithReceipts.getFirst().getBlock()).isEqualTo(syncBlocks.getFirst()); + assertThat(blocksWithReceipts.getFirst().getReceipts()).isEqualTo(receiptsPerBlock.getFirst()); // Verify the task was executed 4 times (3 failures + 1 success) @@ -374,9 +504,8 @@ public void shouldThrowIllegalStateExceptionWhenSuccessWithEmptyResult() { final List syncBlocks = blocksToSyncBlocks(blocks); // Mock returns SUCCESS but with empty Optional (should never happen in practice) - final var invalidResult = - new PeerTaskExecutorResult>>( - Optional.empty(), SUCCESS, emptyList()); + final PeerTaskExecutorResult invalidResult = + new PeerTaskExecutorResult<>(Optional.empty(), SUCCESS, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(invalidResult); @@ -418,16 +547,17 @@ public void shouldDeduplicateBlocksWithSameReceiptRoot() final List syncBlocks = blocksToSyncBlocks(blocks); // Only 2 unique receipt requests should be made (for block1 and block3) - final var receiptsForBlock1 = + final List receiptsForBlock1 = receiptsToSyncReceipts(gen.receipts(block1), ETH69_RECEIPT_CONFIGURATION); - final var receiptsForBlock3 = + final List receiptsForBlock3 = receiptsToSyncReceipts(gen.receipts(block3), ETH69_RECEIPT_CONFIGURATION); final Map> returnedReceiptsByBlock = Map.of(syncBlocks.get(0), receiptsForBlock1, syncBlocks.get(2), receiptsForBlock3); - final var executorResult = - new PeerTaskExecutorResult<>(Optional.of(returnedReceiptsByBlock), SUCCESS, emptyList()); + final PeerTaskExecutorResult executorResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(returnedReceiptsByBlock, List.of())), SUCCESS, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(executorResult); @@ -463,34 +593,37 @@ public void shouldHandlePartialSuccessThenFailureThenSuccess() final List blocks = gen.blockSequence(5).subList(1, 5); final List syncBlocks = blocksToSyncBlocks(blocks); - final var allReceiptsPerBlock = + final List> allReceiptsPerBlock = blocks.stream() .map(gen::receipts) .map(rs -> receiptsToSyncReceipts(rs, ETH69_RECEIPT_CONFIGURATION)) .toList(); // First call returns partial success (first 2 blocks only) - final var firstSuccessResult = + final PeerTaskExecutorResult firstSuccessResult = new PeerTaskExecutorResult<>( Optional.of( - Map.of( - syncBlocks.get(0), allReceiptsPerBlock.get(0), - syncBlocks.get(1), allReceiptsPerBlock.get(1))), + new Response( + Map.of( + syncBlocks.get(0), allReceiptsPerBlock.get(0), + syncBlocks.get(1), allReceiptsPerBlock.get(1)), + List.of())), SUCCESS, emptyList()); // Second call for remaining blocks returns failure - final var failureResult = - new PeerTaskExecutorResult>>( - Optional.empty(), TIMEOUT, emptyList()); + final PeerTaskExecutorResult failureResult = + new PeerTaskExecutorResult<>(Optional.empty(), TIMEOUT, emptyList()); // Third call (retry) returns the remaining 2 blocks successfully - final var secondSuccessResult = + final PeerTaskExecutorResult secondSuccessResult = new PeerTaskExecutorResult<>( Optional.of( - Map.of( - syncBlocks.get(2), allReceiptsPerBlock.get(2), - syncBlocks.get(3), allReceiptsPerBlock.get(3))), + new Response( + Map.of( + syncBlocks.get(2), allReceiptsPerBlock.get(2), + syncBlocks.get(3), allReceiptsPerBlock.get(3)), + List.of())), SUCCESS, emptyList()); @@ -542,9 +675,8 @@ public void shouldTimeoutAfterConfiguredDuration() throws Exception { Duration.ofMillis(100)); // Mock continuous failures that would retry indefinitely without timeout - final var failureResult = - new PeerTaskExecutorResult>>( - Optional.empty(), TIMEOUT, emptyList()); + final PeerTaskExecutorResult failureResult = + new PeerTaskExecutorResult<>(Optional.empty(), TIMEOUT, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(failureResult); diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/MessageData.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/MessageData.java index 35a45c5015d..37f2c278ff2 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/MessageData.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/MessageData.java @@ -20,6 +20,7 @@ import java.math.BigInteger; import java.util.AbstractMap; +import java.util.ArrayList; import java.util.Map; import org.apache.tuweni.bytes.Bytes; @@ -63,9 +64,13 @@ default Map.Entry unwrapMessageData() { final RLPInput messageDataRLP = RLP.input(getData()); messageDataRLP.enterList(); final BigInteger requestId = messageDataRLP.readBigIntegerScalar(); - final Bytes message = messageDataRLP.readAsRlp().raw(); + final var params = new ArrayList(); + while (!messageDataRLP.isEndOfCurrentList()) { + params.add(messageDataRLP.readAsRlp().raw()); + } messageDataRLP.leaveList(); - return new AbstractMap.SimpleImmutableEntry<>(requestId, new RawMessage(getCode(), message)); + return new AbstractMap.SimpleImmutableEntry<>( + requestId, new RawMessage(getCode(), Bytes.concatenate(params))); } /** diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java index c5011e17d63..bb007f58ea8 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java @@ -143,6 +143,8 @@ public enum DisconnectReason { UNEXPECTED_ID((byte) 0x09, "Unexpected ID"), LOCAL_IDENTITY((byte) 0x0a, "Local identity"), TIMEOUT((byte) 0x0b, "Timeout"), + INVALID_RECEIPT_RECEIVED((byte) 0x0f, "Invalid receipt received"), + INVALID_FIRST_BLOCK_RECEIPT_INDEX((byte) 0x0f, "Invalid first block receipt index"), SUBPROTOCOL_TRIGGERED((byte) 0x10, "Sub protocol triggered"), SUBPROTOCOL_TRIGGERED_MISMATCHED_NETWORK((byte) 0x10, "Mismatched network id"), SUBPROTOCOL_TRIGGERED_MISMATCHED_FORKID((byte) 0x10, "Mismatched fork id"), diff --git a/ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLP.java b/ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLP.java index b3c18749ddb..964f6406fcd 100644 --- a/ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLP.java +++ b/ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLP.java @@ -30,7 +30,7 @@ private RLP() {} public static final Bytes EMPTY_LIST; - // RLP encoding requires payloads to be less thatn 2^64 bytes in length + // RLP encoding requires payloads to be less than 2^64 bytes in length // As a result, the longest RLP strings will have a prefix composed of 1 byte encoding the type // of string followed by at most 8 bytes describing the length of the string public static final int MAX_PREFIX_SIZE = 9; From d091417f3031c745f4087a5c773cb38cc222ab50 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 11 Mar 2026 15:03:20 +0100 Subject: [PATCH 25/77] Remove pre-eth/68 transaction announcement support and limit pooled tx requests by size (#9990) Signed-off-by: Fabio Di Fabio --- CHANGELOG.md | 1 + .../TransactionAnnouncementDecoder.java | 39 +----- ...dGetPooledTransactionsFromPeerFetcher.java | 32 ++++- .../NewPooledTransactionHashesMessage.java | 8 +- ...oledTransactionHashesMessageProcessor.java | 25 ++-- .../transactions/TransactionAnnouncement.java | 49 +------ .../transactions/TransactionPoolFactory.java | 9 +- ...PooledTransactionsFromPeerFetcherTest.java | 129 +++++++++++++----- ...NewPooledTransactionHashesMessageTest.java | 9 +- ...TransactionHashesMessageProcessorTest.java | 83 +++-------- ...ledTransactionHashesMessageSenderTest.java | 23 ++-- .../TransactionPoolFactoryTest.java | 3 +- 12 files changed, 189 insertions(+), 221 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 391367f5894..83302dee4fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Add `-Pcases` case name filtering to JMH benchmark suite [#9982](https://github.com/hyperledger/besu/pull/9982) - Use JDK SHA-256 provider to leverage hardware SHA-NI instructions instead of BouncyCastle [#9924](https://github.com/hyperledger/besu/pull/9924) - Support [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975): eth/70 - partial block receipt lists +- Limit pooled tx requests by size and remove pre-eth/68 transaction announcement support [#9990](https://github.com/besu-eth/besu/pull/9990) ## 26.2.0 diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/encoding/TransactionAnnouncementDecoder.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/encoding/TransactionAnnouncementDecoder.java index 2e91386aa58..2d08cbaa96a 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/encoding/TransactionAnnouncementDecoder.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/encoding/TransactionAnnouncementDecoder.java @@ -16,7 +16,6 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.TransactionType; -import org.hyperledger.besu.ethereum.eth.EthProtocolVersion; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAnnouncement; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.rlp.RLPException; @@ -24,9 +23,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; - -import org.apache.tuweni.bytes.Bytes; public class TransactionAnnouncementDecoder { @@ -44,24 +40,7 @@ public interface Decoder { * @return the correct decoder */ public static Decoder getDecoder(final Capability capability) { - if (capability.getVersion() >= EthProtocolVersion.V68) { - return TransactionAnnouncementDecoder::decodeForEth68; - } else { - return TransactionAnnouncementDecoder::decodeForEth66; - } - } - - /** - * Decode the list of transactions in the NewPooledTransactionHashesMessage - * - * @param input input used to decode the NewPooledTransactionHashesMessage before Eth/68 - *

format: [hash_0: B_32, hash_1: B_32, ...] - * @return the list of TransactionAnnouncement decoded from the message. Only hash is present. - * size and type will return an Optional.empty() - */ - private static List decodeForEth66(final RLPInput input) { - final List hashes = input.readList(rlp -> Hash.wrap(rlp.readBytes32())); - return hashes.stream().map(TransactionAnnouncement::new).collect(Collectors.toList()); + return TransactionAnnouncementDecoder::decodeForEth68; } /** @@ -84,21 +63,7 @@ private static List decodeForEth68(final RLPInput input types.add(transactionType); } - List sizes = - input.readList( - in -> { - // for backward compatibility with previous Besu implementation be lenient and support - // also unsigned int with leading zeros. - // ToDo: this could be replaced with the simpler `RLPInput::readUnsignedIntScalar` - // after some months it has been released, since most of the Besus - // will be using the new implementation. - final Bytes intBytes = in.readBytes(); - if (intBytes.size() > 4) { - throw new RLPException( - "Expected max 4 bytes for unsigned int, but got " + intBytes.size() + " bytes"); - } - return intBytes.toLong(); - }); + final List sizes = input.readList(RLPInput::readUnsignedIntScalar); final List hashes = input.readList(rlp -> Hash.wrap(rlp.readBytes32())); input.leaveList(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcher.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcher.java index 5c9e144038e..5b7f9bcbacb 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcher.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcher.java @@ -24,6 +24,7 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutorResult; import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetPooledTransactionsFromPeerTask; import org.hyperledger.besu.ethereum.eth.transactions.PeerTransactionTracker; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionAnnouncement; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; @@ -51,7 +52,8 @@ public class BufferedGetPooledTransactionsFromPeerFetcher { private final String metricLabel; private final ScheduledFuture scheduledFuture; private final EthPeer peer; - private final Queue txAnnounces; + private final Queue txAnnounces; + private final int maxTransactionsMessageSize; public BufferedGetPooledTransactionsFromPeerFetcher( final EthContext ethContext, @@ -59,6 +61,7 @@ public BufferedGetPooledTransactionsFromPeerFetcher( final EthPeer peer, final TransactionPool transactionPool, final PeerTransactionTracker transactionTracker, + final int maxTransactionsMessageSize, final TransactionPoolMetrics metrics, final String metricLabel) { this.ethContext = ethContext; @@ -70,6 +73,7 @@ public BufferedGetPooledTransactionsFromPeerFetcher( this.metricLabel = metricLabel; this.txAnnounces = Queues.synchronizedQueue(EvictingQueue.create(DEFAULT_MAX_PENDING_TRANSACTIONS)); + this.maxTransactionsMessageSize = maxTransactionsMessageSize; } public ScheduledFuture getScheduledFuture() { @@ -124,20 +128,36 @@ public void requestTransactions() { } } - public void addHashes(final Collection hashes) { - txAnnounces.addAll(hashes); + public void addAnnouncements(final Collection announcements) { + txAnnounces.addAll(announcements); } private List getTxHashesToRetrieve() { final List toRetrieve = new ArrayList<>(MAX_HASHES); int discarded = 0; + long cumulativeSize = 0; while (toRetrieve.size() < MAX_HASHES && !txAnnounces.isEmpty()) { - final Hash txHashAnnounced = txAnnounces.poll(); - if (!transactionTracker.hasSeenTransaction(txHashAnnounced)) { - toRetrieve.add(txHashAnnounced); + final TransactionAnnouncement txAnnounced = txAnnounces.peek(); + if (!transactionTracker.hasSeenTransaction(txAnnounced.hash())) { + if (cumulativeSize + txAnnounced.size() > maxTransactionsMessageSize) { + // defense in case maxTransactionsMessageSize is set too small + // this avoids an infinite loop if the first announcement is oversized + if (txAnnounced.size() > maxTransactionsMessageSize) { + LOG.warn( + "maxTransactionsMessageSize ({} bytes) is set too small to fetch tx announcement {}", + maxTransactionsMessageSize, + txAnnounced); + txAnnounces.remove(); + } + // max size reached + break; + } + toRetrieve.add(txAnnounced.hash()); + cumulativeSize += txAnnounced.size(); } else { discarded++; } + txAnnounces.remove(); } final int alreadySeenCount = discarded; diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/NewPooledTransactionHashesMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/NewPooledTransactionHashesMessage.java index 6056e7dab1c..3a81fc06d25 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/NewPooledTransactionHashesMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/NewPooledTransactionHashesMessage.java @@ -17,7 +17,6 @@ import static org.hyperledger.besu.ethereum.eth.encoding.TransactionAnnouncementDecoder.getDecoder; import static org.hyperledger.besu.ethereum.eth.encoding.TransactionAnnouncementEncoder.getEncoder; -import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAnnouncement; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.AbstractMessageData; @@ -67,15 +66,10 @@ public static NewPooledTransactionHashesMessage readFrom( return new NewPooledTransactionHashesMessage(message.getData(), capability); } - @VisibleForTesting - public List pendingTransactions() { + public List pendingTransactionAnnouncements() { if (pendingTransactions == null) { pendingTransactions = getDecoder(capability).decode(RLP.input(data)); } return pendingTransactions; } - - public List pendingTransactionHashes() { - return pendingTransactions().stream().map(TransactionAnnouncement::getHash).toList(); - } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessor.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessor.java index 7938900a03e..69f13822e23 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessor.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessor.java @@ -16,7 +16,6 @@ import static java.time.Instant.now; -import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.eth.manager.EthContext; import org.hyperledger.besu.ethereum.eth.manager.EthPeer; import org.hyperledger.besu.ethereum.eth.manager.task.BufferedGetPooledTransactionsFromPeerFetcher; @@ -48,13 +47,15 @@ public class NewPooledTransactionHashesMessageProcessor { private final TransactionPoolConfiguration transactionPoolConfiguration; private final EthContext ethContext; private final TransactionPoolMetrics metrics; + private final int maxTransactionsMessageSize; public NewPooledTransactionHashesMessageProcessor( final PeerTransactionTracker transactionTracker, final TransactionPool transactionPool, final TransactionPoolConfiguration transactionPoolConfiguration, final EthContext ethContext, - final TransactionPoolMetrics metrics) { + final TransactionPoolMetrics metrics, + final int maxTransactionsMessageSize) { this.transactionTracker = transactionTracker; this.transactionPool = transactionPool; this.transactionPoolConfiguration = transactionPoolConfiguration; @@ -62,6 +63,7 @@ public NewPooledTransactionHashesMessageProcessor( this.metrics = metrics; metrics.initExpiredMessagesCounter(METRIC_LABEL); this.scheduledTasks = new ConcurrentHashMap<>(); + this.maxTransactionsMessageSize = maxTransactionsMessageSize; } void processNewPooledTransactionHashesMessage( @@ -76,12 +78,12 @@ void processNewPooledTransactionHashesMessage( } else { LOG.atTrace() .setMessage( - "Ignoring expired transactions message: peer={}, latency={}, queuedAt={}, keepAlive={}, hashes={}") + "Ignoring expired transactions message: peer={}, latency={}, queuedAt={}, keepAlive={}, announcements={}") .addArgument(peer) .addArgument(latency) .addArgument(queueAt) .addArgument(keepAlive) - .addArgument(transactionsMessage::pendingTransactionHashes) + .addArgument(transactionsMessage::pendingTransactionAnnouncements) .log(); metrics.incrementExpiredMessages(METRIC_LABEL); } @@ -90,12 +92,14 @@ void processNewPooledTransactionHashesMessage( private void processNewPooledTransactionHashesMessage( final EthPeer peer, final NewPooledTransactionHashesMessage transactionsMessage) { try { - final List incomingTransactionHashes = transactionsMessage.pendingTransactionHashes(); + final List incomingTransactionAnnouncements = + transactionsMessage.pendingTransactionAnnouncements(); LOG.atTrace() - .setMessage("Received pooled transaction hashes message: peer={}, incoming hashes={}") + .setMessage( + "Received pooled transaction hashes message: peer={}, incoming announcements={}") .addArgument(peer) - .addArgument(incomingTransactionHashes) + .addArgument(incomingTransactionAnnouncements) .log(); final BufferedGetPooledTransactionsFromPeerFetcher bufferedTask = @@ -120,13 +124,14 @@ private void processNewPooledTransactionHashesMessage( peer, transactionPool, transactionTracker, + maxTransactionsMessageSize, metrics, METRIC_LABEL); }); - bufferedTask.addHashes( - incomingTransactionHashes.stream() - .filter(hash -> transactionPool.getTransactionByHash(hash).isEmpty()) + bufferedTask.addAnnouncements( + incomingTransactionAnnouncements.stream() + .filter(ann -> transactionPool.getTransactionByHash(ann.hash()).isEmpty()) .toList()); } catch (final RLPException ex) { if (peer != null) { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionAnnouncement.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionAnnouncement.java index 8a5aeb28490..b0af58d2689 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionAnnouncement.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionAnnouncement.java @@ -22,20 +22,8 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class TransactionAnnouncement { - private final Hash hash; - private final Optional type; - private final Optional size; - - public TransactionAnnouncement(final Hash hash) { - this.hash = checkNotNull(hash, "Hash cannot be null"); - this.type = Optional.empty(); - this.size = Optional.empty(); - } +public record TransactionAnnouncement(Hash hash, TransactionType type, Long size) { public TransactionAnnouncement(final Transaction transaction) { this( checkNotNull(transaction, "Transaction cannot be null").getHash(), @@ -45,20 +33,8 @@ public TransactionAnnouncement(final Transaction transaction) { public TransactionAnnouncement(final Hash hash, final TransactionType type, final Long size) { this.hash = checkNotNull(hash, "Hash cannot be null"); - this.type = Optional.of(checkNotNull(type, "Type cannot be null")); - this.size = Optional.of(checkNotNull(size, "Size cannot be null")); - } - - public Hash getHash() { - return hash; - } - - public Optional getType() { - return type; - } - - public Optional getSize() { - return size; + this.type = checkNotNull(type, "Type cannot be null"); + this.size = checkNotNull(size, "Size cannot be null"); } public static List create( @@ -74,23 +50,4 @@ public static List create( } return transactions; } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final TransactionAnnouncement that = (TransactionAnnouncement) o; - return Objects.equals(size, that.size) - && Objects.equals(type, that.type) - && Objects.equals(hash, that.hash); - } - - @Override - public int hashCode() { - return Objects.hash(hash, size, type); - } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactory.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactory.java index b100437eaa8..de5b7376bf1 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactory.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactory.java @@ -84,7 +84,8 @@ public static TransactionPool createTransactionPool( transactionsMessageSender, newPooledTransactionHashesMessageSender, blobCache, - miningConfiguration); + miningConfiguration, + ethProtocolConfiguration); } static TransactionPool createTransactionPool( @@ -99,7 +100,8 @@ static TransactionPool createTransactionPool( final TransactionsMessageSender transactionsMessageSender, final NewPooledTransactionHashesMessageSender newPooledTransactionHashesMessageSender, final BlobCache blobCache, - final MiningConfiguration miningConfiguration) { + final MiningConfiguration miningConfiguration, + final EthProtocolConfiguration ethProtocolConfiguration) { final TransactionPool transactionPool = new TransactionPool( @@ -139,7 +141,8 @@ static TransactionPool createTransactionPool( transactionPool, transactionPoolConfiguration, ethContext, - metrics), + metrics, + ethProtocolConfiguration.getMaxTransactionsMessageSize()), transactionPoolConfiguration.getUnstable().getTxMessageKeepAliveSeconds()); subscribeTransactionHandlers( diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcherTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcherTest.java index 2ae4aa8340c..be815b1889b 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcherTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcherTest.java @@ -28,6 +28,7 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; import org.hyperledger.besu.ethereum.eth.manager.EthContext; import org.hyperledger.besu.ethereum.eth.manager.EthPeer; import org.hyperledger.besu.ethereum.eth.manager.EthPeers; @@ -35,7 +36,9 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutor; 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.GetPooledTransactionsFromPeerTask; import org.hyperledger.besu.ethereum.eth.transactions.PeerTransactionTracker; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionAnnouncement; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; @@ -45,6 +48,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -52,6 +56,7 @@ 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.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -90,6 +95,7 @@ public void setup() { ethPeer, transactionPool, transactionTracker, + EthProtocolConfiguration.DEFAULT_MAX_TRANSACTIONS_MESSAGE_SIZE, new TransactionPoolMetrics(metricsSystem), "new_pooled_transaction_hashes"); } @@ -99,25 +105,18 @@ public void requestTransactionShouldStartTaskWhenUnknownTransaction() { final Transaction transaction = generator.transaction(); final List taskResult = List.of(transaction); final PeerTaskExecutorResult> peerTaskResult = - new PeerTaskExecutorResult>( + new PeerTaskExecutorResult<>( Optional.of(taskResult), PeerTaskExecutorResponseCode.SUCCESS, List.of(ethPeer)); when(peerTaskExecutor.executeAgainstPeer( - any( - org.hyperledger.besu.ethereum.eth.manager.peertask.task - .GetPooledTransactionsFromPeerTask.class), - eq(ethPeer))) + any(GetPooledTransactionsFromPeerTask.class), eq(ethPeer))) .thenReturn(peerTaskResult); - fetcher.addHashes(List.of(transaction.getHash())); + fetcher.addAnnouncements(List.of(new TransactionAnnouncement(transaction))); fetcher.requestTransactions(); verify(peerTaskExecutor) - .executeAgainstPeer( - any( - org.hyperledger.besu.ethereum.eth.manager.peertask.task - .GetPooledTransactionsFromPeerTask.class), - eq(ethPeer)); + .executeAgainstPeer(any(GetPooledTransactionsFromPeerTask.class), eq(ethPeer)); verifyNoMoreInteractions(peerTaskExecutor); verify(transactionPool, times(1)).addRemoteTransactions(taskResult); @@ -126,29 +125,25 @@ public void requestTransactionShouldStartTaskWhenUnknownTransaction() { @Test public void requestTransactionShouldSplitRequestIntoSeveralTasks() { - final Map transactionsByHash = + final Map transactionsByAnnouncement = IntStream.range(0, 257) .mapToObj(unused -> generator.transaction()) - .collect(Collectors.toMap((t) -> t.getHash(), (t) -> t)); - fetcher.addHashes(transactionsByHash.keySet()); + .collect(Collectors.toMap(TransactionAnnouncement::new, Function.identity())); + final Map transactionsByHash = + transactionsByAnnouncement.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().hash(), Map.Entry::getValue)); + fetcher.addAnnouncements(transactionsByAnnouncement.keySet()); when(peerTaskExecutor.executeAgainstPeer( - any( - org.hyperledger.besu.ethereum.eth.manager.peertask.task - .GetPooledTransactionsFromPeerTask.class), - eq(ethPeer))) + any(GetPooledTransactionsFromPeerTask.class), eq(ethPeer))) .thenAnswer( (invocationOnMock) -> { - org.hyperledger.besu.ethereum.eth.manager.peertask.task - .GetPooledTransactionsFromPeerTask - task = - invocationOnMock.getArgument( - 0, - org.hyperledger.besu.ethereum.eth.manager.peertask.task - .GetPooledTransactionsFromPeerTask.class); + GetPooledTransactionsFromPeerTask task = + invocationOnMock.getArgument(0, GetPooledTransactionsFromPeerTask.class); + List resultTransactions = task.getHashes().stream().map(transactionsByHash::get).toList(); - return new PeerTaskExecutorResult>( + return new PeerTaskExecutorResult<>( Optional.of(resultTransactions), PeerTaskExecutorResponseCode.SUCCESS, List.of(ethPeer)); @@ -157,11 +152,77 @@ public void requestTransactionShouldSplitRequestIntoSeveralTasks() { fetcher.requestTransactions(); verify(peerTaskExecutor, times(2)) - .executeAgainstPeer( - any( - org.hyperledger.besu.ethereum.eth.manager.peertask.task - .GetPooledTransactionsFromPeerTask.class), - eq(ethPeer)); + .executeAgainstPeer(any(GetPooledTransactionsFromPeerTask.class), eq(ethPeer)); + verifyNoMoreInteractions(peerTaskExecutor); + } + + @Test + public void requestTransactionShouldSplitRequestWhenCumulativeSizeExceedsLimit() { + // DEFAULT_MAX_TRANSACTIONS_MESSAGE_SIZE = 1 MB (1,048,576 bytes). + // The inner check is: if (cumulative + txSize > limit) break. + // With 2 announcements of 600 KB each: + // ann1: 0 + 600KB ≤ 1MB → added (cumulative = 600KB) + // ann2: 600KB + 600KB > 1MB → break, ann2 is not removed + // First batch = [ann1]. ann2 stays queued and is returned as the second batch. + final long largeSize = 600L * 1024; + final List announcements = + IntStream.range(0, 2) + .mapToObj(unused -> generator.transaction()) + .map(tx -> new TransactionAnnouncement(tx.getHash(), tx.getType(), largeSize)) + .toList(); + + when(peerTaskExecutor.executeAgainstPeer( + any(GetPooledTransactionsFromPeerTask.class), eq(ethPeer))) + .thenReturn( + new PeerTaskExecutorResult<>( + Optional.of(List.of()), PeerTaskExecutorResponseCode.SUCCESS, List.of(ethPeer))); + + fetcher.addAnnouncements(announcements); + fetcher.requestTransactions(); + + final ArgumentCaptor taskCaptor = + ArgumentCaptor.forClass(GetPooledTransactionsFromPeerTask.class); + + verify(peerTaskExecutor, times(2)).executeAgainstPeer(taskCaptor.capture(), eq(ethPeer)); + verifyNoMoreInteractions(peerTaskExecutor); + + assertThat(taskCaptor.getAllValues().stream().map(GetPooledTransactionsFromPeerTask::getHashes)) + .containsExactly( + List.of(announcements.get(0).hash()), List.of(announcements.get(1).hash())); + } + + @Test + public void requestTransactionShouldDiscardOversizedAnnouncementAndNotLoopForever() { + // An announcement whose individual size exceeds the limit would be stuck at the head of the + // queue forever (peek → size check fails → break → peek again → ...). + // The fix: if txSize > limit, discard it (remove) before breaking. + final long oversizedSize = EthProtocolConfiguration.DEFAULT_MAX_TRANSACTIONS_MESSAGE_SIZE + 1L; + final Transaction oversizedTx = generator.transaction(); + final Transaction normalTx = generator.transaction(); + + final TransactionAnnouncement oversized = + new TransactionAnnouncement(oversizedTx.getHash(), oversizedTx.getType(), oversizedSize); + final TransactionAnnouncement normal = new TransactionAnnouncement(normalTx); + + fetcher.addAnnouncements(List.of(oversized, normal)); + + when(peerTaskExecutor.executeAgainstPeer( + any(GetPooledTransactionsFromPeerTask.class), eq(ethPeer))) + .thenReturn( + new PeerTaskExecutorResult<>( + Optional.of(List.of()), PeerTaskExecutorResponseCode.SUCCESS, List.of(ethPeer))); + + // First call: oversized is discarded then break → no request is made, method returns normally + fetcher.requestTransactions(); + verifyNoInteractions(peerTaskExecutor); + + // Second call: oversized is gone; normal tx is now at the head and gets fetched + fetcher.requestTransactions(); + + final ArgumentCaptor taskCaptor = + ArgumentCaptor.forClass(GetPooledTransactionsFromPeerTask.class); + verify(peerTaskExecutor, times(1)).executeAgainstPeer(taskCaptor.capture(), eq(ethPeer)); + assertThat(taskCaptor.getValue().getHashes()).containsExactly(normal.hash()); verifyNoMoreInteractions(peerTaskExecutor); } @@ -169,10 +230,10 @@ public void requestTransactionShouldSplitRequestIntoSeveralTasks() { public void requestTransactionShouldNotStartTaskWhenTransactionAlreadySeen() { final Transaction transaction = generator.transaction(); - final Hash hash = transaction.getHash(); - transactionTracker.markTransactionHashesAsSeen(ethPeer, List.of(hash)); + final TransactionAnnouncement announcement = new TransactionAnnouncement(transaction); + transactionTracker.markTransactionHashesAsSeen(ethPeer, List.of(announcement.hash())); - fetcher.addHashes(List.of(hash)); + fetcher.addAnnouncements(List.of(announcement)); fetcher.requestTransactions(); verifyNoInteractions(peerTaskExecutor); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/NewPooledTransactionHashesMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/NewPooledTransactionHashesMessageTest.java index d64ab02b5fa..7cb36baa211 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/NewPooledTransactionHashesMessageTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/NewPooledTransactionHashesMessageTest.java @@ -16,12 +16,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.hyperledger.besu.ethereum.core.Transaction.toHashList; -import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.eth.EthProtocol; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionAnnouncement; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; import java.util.List; @@ -37,8 +36,10 @@ public void roundTripNewPooledTransactionHashesMessage() { final NewPooledTransactionHashesMessage msg = NewPooledTransactionHashesMessage.create(transactions, EthProtocol.LATEST); assertThat(msg.getCode()).isEqualTo(EthProtocolMessages.NEW_POOLED_TRANSACTION_HASHES); - final List pendingHashes = msg.pendingTransactionHashes(); - assertThat(pendingHashes).isEqualTo(toHashList(transactions)); + final List pendingAnnouncements = + msg.pendingTransactionAnnouncements(); + assertThat(pendingAnnouncements) + .isEqualTo(transactions.stream().map(TransactionAnnouncement::new).toList()); } @Test diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessorTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessorTest.java index 4e0d26029f2..01aada53d39 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessorTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessorTest.java @@ -35,6 +35,7 @@ import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.eth.EthProtocol; +import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; import org.hyperledger.besu.ethereum.eth.encoding.TransactionAnnouncementDecoder; import org.hyperledger.besu.ethereum.eth.encoding.TransactionAnnouncementEncoder; import org.hyperledger.besu.ethereum.eth.manager.EthContext; @@ -100,7 +101,8 @@ public void setup() { transactionPool, transactionPoolConfiguration, ethContext, - new TransactionPoolMetrics(metricsSystem)); + new TransactionPoolMetrics(metricsSystem), + EthProtocolConfiguration.DEFAULT_MAX_TRANSACTIONS_MESSAGE_SIZE); when(ethContext.getScheduler()).thenReturn(ethScheduler); } @@ -226,7 +228,8 @@ void shouldCreateAndDecodeForEth68() { final NewPooledTransactionHashesMessage message = NewPooledTransactionHashesMessage.create(transactionList, EthProtocol.ETH68); - final List announcementList = message.pendingTransactions(); + final List announcementList = + message.pendingTransactionAnnouncements(); assertThat(announcementList).containsExactlyElementsOf(expectedTransactions); } @@ -271,72 +274,28 @@ void shouldDecodeBytesCorrectly_Eth68() { getDecoder(EthProtocol.ETH68).decode(RLP.input(bytes)); final TransactionAnnouncement frontier = announcementList.get(0); - assertThat(frontier.getHash()) + assertThat(frontier.hash()) .isEqualTo( Hash.fromHexString( "0x0000000000000000000000000000000000000000000000000000000000000001")); - assertThat(frontier.getType()).hasValue(TransactionType.FRONTIER); - assertThat(frontier.getSize()).hasValue(1L); + assertThat(frontier.type()).isEqualTo(TransactionType.FRONTIER); + assertThat(frontier.size()).isEqualTo(1L); final TransactionAnnouncement accessList = announcementList.get(1); - assertThat(accessList.getHash()) + assertThat(accessList.hash()) .isEqualTo( Hash.fromHexString( "0x0000000000000000000000000000000000000000000000000000000000000002")); - assertThat(accessList.getType()).hasValue(TransactionType.ACCESS_LIST); - assertThat(accessList.getSize()).hasValue(2L); + assertThat(accessList.type()).isEqualTo(TransactionType.ACCESS_LIST); + assertThat(accessList.size()).isEqualTo(2L); final TransactionAnnouncement eip1559 = announcementList.get(2); - assertThat(eip1559.getHash()) + assertThat(eip1559.hash()) .isEqualTo( Hash.fromHexString( "0x0000000000000000000000000000000000000000000000000000000000000003")); - assertThat(eip1559.getType()).hasValue(TransactionType.EIP1559); - assertThat(eip1559.getSize()).hasValue(3L); - } - - @Test - void shouldDecodeBytesCorrectly_PreviousImplementations_Eth68() { - /* - * [ - * "0x0000102"] - * ["0x00000001","0x00000002","0x00000003"], - * ["0x0000000000000000000000000000000000000000000000000000000000000001", - * "0x0000000000000000000000000000000000000000000000000000000000000002", - * "0x0000000000000000000000000000000000000000000000000000000000000003"] - * ] - */ - - final Bytes bytes = - Bytes.fromHexString( - "0xf87983000102cf840000000184000000028400000003f863a00000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000000003"); - - final List announcementList = - getDecoder(EthProtocol.ETH68).decode(RLP.input(bytes)); - - final TransactionAnnouncement frontier = announcementList.get(0); - assertThat(frontier.getHash()) - .isEqualTo( - Hash.fromHexString( - "0x0000000000000000000000000000000000000000000000000000000000000001")); - assertThat(frontier.getType()).hasValue(TransactionType.FRONTIER); - assertThat(frontier.getSize()).hasValue(1L); - - final TransactionAnnouncement accessList = announcementList.get(1); - assertThat(accessList.getHash()) - .isEqualTo( - Hash.fromHexString( - "0x0000000000000000000000000000000000000000000000000000000000000002")); - assertThat(accessList.getType()).hasValue(TransactionType.ACCESS_LIST); - assertThat(accessList.getSize()).hasValue(2L); - - final TransactionAnnouncement eip1559 = announcementList.get(2); - assertThat(eip1559.getHash()) - .isEqualTo( - Hash.fromHexString( - "0x0000000000000000000000000000000000000000000000000000000000000003")); - assertThat(eip1559.getType()).hasValue(TransactionType.EIP1559); - assertThat(eip1559.getSize()).hasValue(3L); + assertThat(eip1559.type()).isEqualTo(TransactionType.EIP1559); + assertThat(eip1559.size()).isEqualTo(3L); } @Test @@ -355,9 +314,9 @@ void shouldEncodeAndDecodeTransactionAnnouncement_Eth68() { for (final Transaction transaction : list) { final TransactionAnnouncement announcement = announcementList.get(list.indexOf(transaction)); - assertThat(announcement.getHash()).isEqualTo(transaction.getHash()); - assertThat(announcement.getType()).hasValue(transaction.getType()); - assertThat(announcement.getSize()).hasValue((long) transaction.getSizeForAnnouncement()); + assertThat(announcement.hash()).isEqualTo(transaction.getHash()); + assertThat(announcement.type()).isEqualTo(transaction.getType()); + assertThat(announcement.size()).isEqualTo(transaction.getSizeForAnnouncement()); } } @@ -423,15 +382,13 @@ void shouldThrowRLPExceptionWhenSizeSizeGreaterThanFourBytes() { TransactionAnnouncementDecoder.getDecoder(EthProtocol.ETH68) .decode(RLP.input(invalidMessageBytes))) .isInstanceOf(RLPException.class) - .hasMessageContaining("Expected max 4 bytes for unsigned int, but got 5 bytes"); + .hasMessageContaining( + "Cannot read a unsigned int scalar, expecting a maximum of 4 bytes but current element is 5 bytes long"); } @Test void shouldThrowNullPointerIfArgumentsAreNull() { final Hash hash = Hash.hash(Bytes.random(32)); - assertThatThrownBy(() -> new TransactionAnnouncement((Hash) null)) - .isInstanceOf(NullPointerException.class) - .hasMessage("Hash cannot be null"); assertThatThrownBy(() -> new TransactionAnnouncement(null, TransactionType.EIP1559, 0L)) .isInstanceOf(NullPointerException.class) @@ -445,7 +402,7 @@ void shouldThrowNullPointerIfArgumentsAreNull() { .isInstanceOf(NullPointerException.class) .hasMessage("Size cannot be null"); - assertThatThrownBy(() -> new TransactionAnnouncement((Transaction) null)) + assertThatThrownBy(() -> new TransactionAnnouncement(null)) .isInstanceOf(NullPointerException.class) .hasMessage("Transaction cannot be null"); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageSenderTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageSenderTest.java index 308b6e15ea0..f89dfbb7147 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageSenderTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageSenderTest.java @@ -16,7 +16,6 @@ import static com.google.common.collect.Sets.newHashSet; import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.ethereum.core.Transaction.toHashList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; @@ -25,7 +24,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.eth.EthProtocol; @@ -115,8 +113,9 @@ public void shouldSendTransactionsInBatchesWithLimit() throws Exception { .hasSize(2) .allMatch( message -> message.getCode() == EthProtocolMessages.NEW_POOLED_TRANSACTION_HASHES); - final Set firstBatch = getTransactionsFromMessage(sentMessages.get(0)); - final Set secondBatch = getTransactionsFromMessage(sentMessages.get(1)); + final Set firstBatch = getTransactionsFromMessage(sentMessages.get(0)); + final Set secondBatch = + getTransactionsFromMessage(sentMessages.get(1)); final int expectedFirstBatchSize = 4096, expectedSecondBatchSize = 1904, toleranceDelta = 0; assertThat(firstBatch) @@ -127,23 +126,27 @@ public void shouldSendTransactionsInBatchesWithLimit() throws Exception { expectedSecondBatchSize - toleranceDelta, expectedSecondBatchSize + toleranceDelta); assertThat(Sets.union(firstBatch, secondBatch)) - .containsExactlyInAnyOrderElementsOf(toHashList(transactions)); + .containsExactlyInAnyOrderElementsOf( + transactions.stream().map(TransactionAnnouncement::new).toList()); } private MessageData transactionsMessageContaining(final Transaction... transactions) { return argThat( message -> { - final Set actualSentTransactions = getTransactionsFromMessage(message); - final Set expectedTransactions = - newHashSet(toHashList(Arrays.asList(transactions))); + final Set actualSentTransactions = + getTransactionsFromMessage(message); + final Set expectedTransactions = + Arrays.stream(transactions) + .map(TransactionAnnouncement::new) + .collect(Collectors.toSet()); return message.getCode() == EthProtocolMessages.NEW_POOLED_TRANSACTION_HASHES && actualSentTransactions.equals(expectedTransactions); }); } - private Set getTransactionsFromMessage(final MessageData message) { + private Set getTransactionsFromMessage(final MessageData message) { final NewPooledTransactionHashesMessage transactionsMessage = NewPooledTransactionHashesMessage.readFrom(message, EthProtocol.LATEST); - return newHashSet(transactionsMessage.pendingTransactionHashes()); + return newHashSet(transactionsMessage.pendingTransactionAnnouncements()); } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactoryTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactoryTest.java index 9bcd06f4fef..c940cf232ef 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactoryTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactoryTest.java @@ -413,7 +413,8 @@ private TransactionPool createTransactionPool( transactionsMessageSender, newPooledTransactionHashesMessageSender, new BlobCache(), - MiningConfiguration.newDefault()); + MiningConfiguration.newDefault(), + EthProtocolConfiguration.DEFAULT); } private TransactionPool createAndEnableTransactionPool( From b92544f17f8092a8405cc2d85e54ecebf4e23091 Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Wed, 11 Mar 2026 15:02:29 +0000 Subject: [PATCH 26/77] Fix addMod case in Modulus256 (#10001) Fixes case in addMod for 256bit modulus, changes some strange logic in 64bit modulus for reduceNormalised with UInt256 and UInt257 dividends and refactor of unit tests to use parameterized tests. Signed-off-by: Luis Pinto --- .../org/hyperledger/besu/evm/UInt256.java | 84 ++-- .../org/hyperledger/besu/evm/UInt256Test.java | 447 ++++++------------ 2 files changed, 171 insertions(+), 360 deletions(-) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java index 83df96c3af1..55568420dd5 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java @@ -1054,48 +1054,24 @@ private long reduceStep(final long v1, final long v0, final long inv) { private UInt256 reduceNormalised(final UInt256 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); - if ((v.u4 | v.u3) == 0 - && Long.compareUnsigned(v.u3, u0) <= 0 - && Long.compareUnsigned(v.u2, u0) <= 0) { + if ((v.u4 | v.u3) == 0 && Long.compareUnsigned(v.u2, u0) < 0) { long r; - if (v.u2 != 0 || Long.compareUnsigned(v.u1, u0) > 0) { - r = (Long.compareUnsigned(v.u2, u0) >= 0) ? v.u2 - u0 : v.u2; - r = reduceStep(r, v.u1, inv); + if (v.u2 != 0 || Long.compareUnsigned(v.u1, u0) >= 0) { + r = reduceStep(v.u2, v.u1, inv); r = reduceStep(r, v.u0, inv); } else { - r = (Long.compareUnsigned(v.u1, u0) >= 0) ? v.u1 - u0 : v.u1; - r = reduceStep(r, v.u0, inv); + r = reduceStep(v.u1, v.u0, inv); } return UInt256.fromLong(r >>> shift); } - return reduceNormalisedSlowPathUInt256(v, shift, inv); - } - - private UInt256 reduceNormalisedSlowPathUInt256( - final UInt320 v, final int shift, final long inv) { - long r; - if (v.u4 != 0 || Long.compareUnsigned(v.u3, u0) > 0) { - r = (Long.compareUnsigned(v.u4, u0) >= 0) ? v.u4 - u0 : v.u4; - r = reduceStep(r, v.u3, inv); - r = reduceStep(r, v.u2, inv); - r = reduceStep(r, v.u1, inv); - r = reduceStep(r, v.u0, inv); - } else { - r = (Long.compareUnsigned(v.u3, u0) >= 0) ? v.u3 - u0 : v.u3; - r = reduceStep(r, v.u2, inv); - r = reduceStep(r, v.u1, inv); - r = reduceStep(r, v.u0, inv); - } - return UInt256.fromLong(r >>> shift); + return reduceNormalisedSlowPath(v, shift, inv); } private UInt256 reduceNormalised(final UInt257 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); - if ((v.u4 | v.u3) == 0 - && Long.compareUnsigned(v.u3, u0) <= 0 - && Long.compareUnsigned(v.u2, u0) <= 0) { + if ((v.u4 | v.u3) == 0 && Long.compareUnsigned(v.u2, u0) < 0) { long r; - if (v.u2 != 0 || Long.compareUnsigned(v.u1, u0) > 0) { + if (v.u2 != 0 || Long.compareUnsigned(v.u1, u0) >= 0) { r = reduceStep(v.u2, v.u1, inv); r = reduceStep(r, v.u0, inv); } else { @@ -1103,13 +1079,19 @@ private UInt256 reduceNormalised(final UInt257 that, final int shift, final long } return UInt256.fromLong(r >>> shift); } - return reduceNormalisedSlowPathUInt257(v, shift, inv); + return reduceNormalisedSlowPath(v, shift, inv); } - private UInt256 reduceNormalisedSlowPathUInt257( - final UInt320 v, final int shift, final long inv) { + private UInt256 reduceNormalisedSlowPath(final UInt320 v, final int shift, final long inv) { long r; - if (v.u4 != 0 || Long.compareUnsigned(v.u3, u0) > 0) { + if (Long.compareUnsigned(v.u4, u0) >= 0) { + r = reduceStep(0, v.u4, inv); + r = reduceStep(r, v.u3, inv); + r = reduceStep(r, v.u2, inv); + r = reduceStep(r, v.u1, inv); + r = reduceStep(r, v.u0, inv); + + } else if (v.u4 != 0 || Long.compareUnsigned(v.u3, u0) >= 0) { r = reduceStep(v.u4, v.u3, inv); r = reduceStep(r, v.u2, inv); r = reduceStep(r, v.u1, inv); @@ -1242,7 +1224,7 @@ private UInt128 reduceStep(final long v2, final long v1, final long v0, final lo private UInt256 reduceNormalised(final UInt256 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); - if (v.u4 == 0 && Long.compareUnsigned(v.u4, u1) < 0 && Long.compareUnsigned(v.u3, u1) < 0) { + if (v.u4 == 0 && Long.compareUnsigned(v.u3, u1) < 0) { UInt128 r; if (v.u3 != 0 || Long.compareUnsigned(v.u2, u1) >= 0) { r = reduceStep(v.u3, v.u2, v.u1, inv); @@ -1257,7 +1239,7 @@ private UInt256 reduceNormalised(final UInt256 that, final int shift, final long private UInt256 reduceNormalised(final UInt257 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); - if (v.u4 == 0 && Long.compareUnsigned(v.u4, u1) < 0 && Long.compareUnsigned(v.u3, u1) < 0) { + if (v.u4 == 0 && Long.compareUnsigned(v.u3, u1) < 0) { UInt128 r; if (v.u3 != 0 || Long.compareUnsigned(v.u2, u1) >= 0) { r = reduceStep(v.u3, v.u2, v.u1, inv); @@ -1488,10 +1470,7 @@ private UInt256 reduceNormalisedSlowPath(final UInt320 v, final int shift, final private UInt256 reduceNormalised(final UInt448 that, final int shift, final long inv) { UInt512 v = that.shiftLeftWide(shift); - if ((v.u7 | v.u6 | v.u5) == 0 - && Long.compareUnsigned(v.u6, u2) < 0 - && Long.compareUnsigned(v.u5, u2) < 0 - && Long.compareUnsigned(v.u4, u2) < 0) { + if ((v.u7 | v.u6 | v.u5) == 0 && Long.compareUnsigned(v.u4, u2) < 0) { UInt192 r; if (v.u4 != 0 || Long.compareUnsigned(v.u3, u2) >= 0) { r = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); @@ -1706,20 +1685,31 @@ private UInt256 reduceStep( private UInt256 reduceNormalised(final UInt256 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); - return reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv).shiftRight(shift); + UInt256 r; + if (Long.compareUnsigned(v.u4, u3) >= 0) { + r = reduceStep(0, v.u4, v.u3, v.u2, v.u1, inv); + r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u0, inv); + } else { + r = reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv); + } + return r.shiftRight(shift); } private UInt256 reduceNormalised(final UInt257 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); - return reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv).shiftRight(shift); + UInt256 r; + if (Long.compareUnsigned(v.u4, u3) >= 0) { + r = reduceStep(0, v.u4, v.u3, v.u2, v.u1, inv); + r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u0, inv); + } else { + r = reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv); + } + return r.shiftRight(shift); } private UInt256 reduceNormalised(final UInt512 that, final int shift, final long inv) { UInt576 v = that.shiftLeftWide(shift); - if ((v.u8 | v.u7 | v.u6) == 0 - && Long.compareUnsigned(v.u7, u3) < 0 - && Long.compareUnsigned(v.u6, u3) < 0 - && Long.compareUnsigned(v.u5, u3) < 0) { + if ((v.u8 | v.u7 | v.u6) == 0 && Long.compareUnsigned(v.u5, u3) < 0) { UInt256 r; if (v.u5 != 0 || Long.compareUnsigned(v.u4, u3) >= 0) { r = reduceStep(v.u5, v.u4, v.u3, v.u2, v.u1, inv); diff --git a/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java b/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java index e296cf22085..5ce51d311f7 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java @@ -19,13 +19,17 @@ import java.math.BigInteger; import java.util.Arrays; import java.util.Random; +import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class UInt256Test { - static final int SAMPLE_SIZE = 3; + static final int SAMPLE_SIZE = 1_000; private Bytes32 bigIntTo32B(final BigInteger y) { byte[] a = y.toByteArray(); @@ -38,8 +42,8 @@ private Bytes32 bigIntTo32B(final BigInteger x, final int sign) { byte[] a = new byte[32]; Arrays.fill(a, (byte) 0xFF); byte[] b = x.toByteArray(); - System.arraycopy(b, 0, a, 32 - b.length, b.length); - if (a.length > 32) return Bytes32.wrap(a, a.length - 32); + final int length = Math.min(32, b.length); + System.arraycopy(b, 0, a, 32 - length, length); return Bytes32.leftPad(Bytes.wrap(a)); } @@ -191,10 +195,11 @@ public void bigModWithExtraCarry() { assertThat(remainder).isEqualTo(expected); } - @Test - public void modA() { - BigInteger big_number = new BigInteger("0000000067e36864", 16); - BigInteger big_modulus = new BigInteger("001fff", 16); + @ParameterizedTest + @MethodSource("modTestCases") + public void mod(final String dividend, final String divisor) { + BigInteger big_number = new BigInteger(dividend, 16); + BigInteger big_modulus = new BigInteger(divisor, 16); UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); @@ -202,148 +207,42 @@ public void modA() { assertThat(remainder).isEqualTo(expected); } - @Test - public void modB() { - BigInteger big_number = new BigInteger("022b1c8c1227a00000", 16); - BigInteger big_modulus = new BigInteger("038d7ea4c68000", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modC() { - BigInteger big_number = new BigInteger("1000000000000000000000000000000000000000000000000", 16); - BigInteger big_modulus = new BigInteger("ff00000000000000", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modD() { - BigInteger big_number = new BigInteger("ff00000000000000000000000000000000", 16); - BigInteger big_modulus = new BigInteger("100000000000000000000000000000000", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modE() { - BigInteger big_number = new BigInteger("ff00000000000000000000000000000000", 16); - BigInteger big_modulus = new BigInteger("100000000000000000000000000000001", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modF() { - BigInteger big_number = new BigInteger("1000000000000000000000000000000000000000000000000", 16); - BigInteger big_modulus = new BigInteger("ff000000000000000000000000000000", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modG() { - BigInteger big_number = new BigInteger("1000000000000000000000000000000000000000000000000", 16); - BigInteger big_modulus = new BigInteger("100000000000000000000000000000001", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modH() { - BigInteger big_number = - new BigInteger("000000000000000000ff00000000000000000000000000000000000000000000", 16); - BigInteger big_modulus = - new BigInteger("0000000000000000000000000000000000fe0000000000000000000000000001", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modI() { - // modulus 128 with overflow case - BigInteger big_number = new BigInteger("020000000000000000000000000000000000", 16); - BigInteger big_modulus = new BigInteger("02000000000000000000", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modJ() { - // modulus 128 with overflow case -> 2 add back in quotient estimate div2by1. - BigInteger big_number = new BigInteger("10000000000000000010000000000000000", 16); - BigInteger big_modulus = new BigInteger("200000000000000ff", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modK() { - // modulus 128 with overflow case -> 2 add back in quotient estimate div2by1. - BigInteger big_number = - new BigInteger("ff000000000000000000000000000000000000000000000000000000", 16); - BigInteger big_modulus = - new BigInteger("1000000000000000000000002000000000000000000000000", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modL() { - // modulus 128 with overflow case -> 2 add back in quotient estimate div2by1. - BigInteger big_number = new BigInteger("800000000000000080", 16); - BigInteger big_modulus = new BigInteger("80", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modGeneralState() { - BigInteger big_number = new BigInteger("cea0c5cc171fa61277e5604a3bc8aef4de3d3882", 16); - BigInteger big_modulus = new BigInteger("7dae7454bb193b1c28e64a6a935bc3", 16); - UInt256 number = UInt256.fromBytesBE(big_number.toByteArray()); - UInt256 modulus = UInt256.fromBytesBE(big_modulus.toByteArray()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(number.mod(modulus).toBytesBE())); - Bytes32 expected = Bytes32.leftPad(Bytes.wrap(big_number.mod(big_modulus).toByteArray())); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void modDiv8Mod8() { + public static Stream modTestCases() { + return Stream.of( + Arguments.of("0000000067e36864", "001fff"), + Arguments.of("022b1c8c1227a00000", "038d7ea4c68000"), + Arguments.of("1000000000000000000000000000000000000000000000000", "ff00000000000000"), + Arguments.of("ff00000000000000000000000000000000", "100000000000000000000000000000000"), + Arguments.of("ff00000000000000000000000000000000", "100000000000000000000000000000001"), + Arguments.of( + "1000000000000000000000000000000000000000000000000", + "ff000000000000000000000000000000"), + Arguments.of( + "1000000000000000000000000000000000000000000000000", + "100000000000000000000000000000001"), + Arguments.of( + "000000000000000000ff00000000000000000000000000000000000000000000", + "0000000000000000000000000000000000fe0000000000000000000000000001"), + Arguments.of("020000000000000000000000000000000000", "02000000000000000000"), + Arguments.of("10000000000000000010000000000000000", "200000000000000ff"), + Arguments.of( + "ff000000000000000000000000000000000000000000000000000000", + "1000000000000000000000002000000000000000000000000"), + Arguments.of("800000000000000080", "80"), + Arguments.of("cea0c5cc171fa61277e5604a3bc8aef4de3d3882", "7dae7454bb193b1c28e64a6a935bc3"), + // mulSubOverflow - addBack bugs + // Modulus192 path (b.u3==0, b.u2!=0) + Arguments.of( + "7effffff8000000000000000000000000000000000000000d900000000000001", + "7effffff800000007effffff800000008000ff0000010000"), + // Modulus128 path (b.u3==0, b.u2==0, b.u1!=0) + Arguments.of( + "7effffff800000000000000000000000d900000000000001", + "7effffff800000007fffffffffffffff")); + } + + @Test + public void modRandom() { final Random random = new Random(41335); for (int i = 0; i < SAMPLE_SIZE; i++) { final byte[] a = new byte[32]; @@ -373,15 +272,20 @@ public void modDiv8Mod8() { BigInteger.ZERO.compareTo(big_modulus) == 0 ? Bytes32.ZERO : bigIntTo32B(big_number.mod(big_modulus)); - assertThat(remainder).isEqualTo(expected); + assertThat(remainder) + .withFailMessage( + String.format( + "Failure detected:\n%s.MOD(%s)\n", number.toHexString(), modulus.toHexString())) + .isEqualTo(expected); } } - @Test - public void referenceTest459() { - BigInteger xbig = new BigInteger("000000010000000000000000000000000000000000000000", 16); - BigInteger ybig = new BigInteger("0000c350", 16); - BigInteger mbig = new BigInteger("000003e8", 16); + @ParameterizedTest + @MethodSource("addModTestCases") + public void addMod(final String a, final String b, final String modulus) { + BigInteger xbig = new BigInteger(a, 16); + BigInteger ybig = new BigInteger(b, 16); + BigInteger mbig = new BigInteger(modulus, 16); UInt256 x = UInt256.fromBytesBE(xbig.toByteArray()); UInt256 y = UInt256.fromBytesBE(ybig.toByteArray()); UInt256 m = UInt256.fromBytesBE(mbig.toByteArray()); @@ -391,32 +295,27 @@ public void referenceTest459() { assertThat(remainder).isEqualTo(expected); } - @Test - public void ExecutionSpecStateTest_453() { - byte[] xArr = - new byte[] { - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -2 - }; - byte[] mArr = - new byte[] { - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 - }; - BigInteger xbig = new BigInteger(1, xArr); - BigInteger ybig = new BigInteger(1, xArr); - BigInteger mbig = new BigInteger(1, mArr); - UInt256 x = UInt256.fromBytesBE(xArr); - UInt256 y = UInt256.fromBytesBE(xArr); - UInt256 m = UInt256.fromBytesBE(mArr); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(x.addMod(y, m).toBytesBE())); - Bytes32 expected = - BigInteger.ZERO.compareTo(mbig) == 0 ? Bytes32.ZERO : bigIntTo32B(xbig.add(ybig).mod(mbig)); - assertThat(remainder).isEqualTo(expected); + public static Stream addModTestCases() { + return Stream.of( + // reference tests + Arguments.of("000000010000000000000000000000000000000000000000", "0000c350", "000003e8"), + Arguments.of( + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + // reduceNormalised bugs + Arguments.of( + "62d900c9700000000000000000023f00bc1814ff00000000000000ca22300806", + "ffffffffffffffffb4fffff4befff4f4f4d4f4f504f4f4bef5f5100b0bf4f5f6", + "13464637e8bdc0e53b895d7b79348a784"), + Arguments.of( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "80008e949e9e9ec0cf4f4d4f4f4f41523410af5f20b0b7606f4d4f439f5f6000", + "1800000000000000080000000000000017ffffffffffffffd")); } @Test - public void addMod() { + public void addModRandom() { final Random random = new Random(42); for (int i = 0; i < SAMPLE_SIZE; i++) { int aSize = random.nextInt(1, 33); @@ -439,85 +338,62 @@ public void addMod() { BigInteger.ZERO.compareTo(cInt) == 0 ? Bytes32.ZERO : bigIntTo32B(aInt.add(bInt).mod(cInt)); - assertThat(remainder).isEqualTo(expected); + assertThat(remainder) + .withFailMessage( + String.format( + "Failure detected:\n%s.ADDMOD(%s, %s)\n", + a.toHexString(), b.toHexString(), c.toHexString())) + .isEqualTo(expected); } } - @Test - public void mulMod_Modulus256_mulSubOverflow() { - Bytes modBytes = - Bytes.fromHexString("0x0000000000000001000000000000000000000000000000000000000000000001"); - Bytes aBytes = - Bytes.fromHexString("0x0000000000000001000000000000000000000000000000000000000000000000"); - Bytes bBytes = - Bytes.fromHexString("0x0000000000000001000000000000000000000000000000000000000000000000"); + @ParameterizedTest + @MethodSource("mulModTestCases") + public void mulMod(final String a, final String b, final String modulus) { + Bytes aBytes = Bytes.fromHexString(a); + Bytes bBytes = Bytes.fromHexString(b); + Bytes modBytes = Bytes.fromHexString(modulus); BigInteger aInt = new BigInteger(1, aBytes.toArrayUnsafe()); BigInteger bInt = new BigInteger(1, bBytes.toArrayUnsafe()); BigInteger mInt = new BigInteger(1, modBytes.toArrayUnsafe()); - UInt256 a = UInt256.fromBytesBE(aBytes.toArrayUnsafe()); - UInt256 b = UInt256.fromBytesBE(bBytes.toArrayUnsafe()); + UInt256 x = UInt256.fromBytesBE(aBytes.toArrayUnsafe()); + UInt256 y = UInt256.fromBytesBE(bBytes.toArrayUnsafe()); UInt256 m = UInt256.fromBytesBE(modBytes.toArrayUnsafe()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(a.mulMod(b, m).toBytesBE())); + Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(x.mulMod(y, m).toBytesBE())); Bytes32 expected = bigIntTo32B(aInt.multiply(bInt).mod(mInt)); assertThat(remainder).isEqualTo(expected); } - @Test - public void mulMod_ExecutionSpecStateTest_104() { - Bytes value0 = - Bytes.fromHexString("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"); - Bytes value1 = - Bytes.fromHexString("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"); - Bytes value2 = - Bytes.fromHexString("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"); - BigInteger aInt = new BigInteger(1, value0.toArrayUnsafe()); - BigInteger bInt = new BigInteger(1, value1.toArrayUnsafe()); - BigInteger cInt = new BigInteger(1, value2.toArrayUnsafe()); - UInt256 a = UInt256.fromBytesBE(value0.toArrayUnsafe()); - UInt256 b = UInt256.fromBytesBE(value1.toArrayUnsafe()); - UInt256 c = UInt256.fromBytesBE(value2.toArrayUnsafe()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(a.mulMod(b, c).toBytesBE())); - Bytes32 expected = bigIntTo32B(aInt.multiply(bInt).mod(cInt)); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void mulMod_ExecutionSpecStateTest_457() { - Bytes value0 = - Bytes.fromHexString("0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff"); - Bytes value1 = - Bytes.fromHexString("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"); - Bytes value2 = - Bytes.fromHexString("0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff"); - BigInteger aInt = new BigInteger(1, value0.toArrayUnsafe()); - BigInteger bInt = new BigInteger(1, value1.toArrayUnsafe()); - BigInteger cInt = new BigInteger(1, value2.toArrayUnsafe()); - UInt256 a = UInt256.fromBytesBE(value0.toArrayUnsafe()); - UInt256 b = UInt256.fromBytesBE(value1.toArrayUnsafe()); - UInt256 c = UInt256.fromBytesBE(value2.toArrayUnsafe()); - Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(a.mulMod(b, c).toBytesBE())); - Bytes32 expected = bigIntTo32B(aInt.multiply(bInt).mod(cInt)); - assertThat(remainder).isEqualTo(expected); - - value0 = - Bytes.fromHexString("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"); - value1 = - Bytes.fromHexString("0xffffffffffffffffffffffffb195148ca348dc57a7331852b390ccefa7b0c18b"); - value2 = - Bytes.fromHexString("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"); - aInt = new BigInteger(1, value0.toArrayUnsafe()); - bInt = new BigInteger(1, value1.toArrayUnsafe()); - cInt = new BigInteger(1, value2.toArrayUnsafe()); - a = UInt256.fromBytesBE(value0.toArrayUnsafe()); - b = UInt256.fromBytesBE(value1.toArrayUnsafe()); - c = UInt256.fromBytesBE(value2.toArrayUnsafe()); - remainder = Bytes32.leftPad(Bytes.wrap(a.mulMod(b, c).toBytesBE())); - expected = bigIntTo32B(aInt.multiply(bInt).mod(cInt)); - assertThat(remainder).isEqualTo(expected); - } - - @Test - public void mulMod() { + public static Stream mulModTestCases() { + return Stream.of( + // reference tests + Arguments.of( + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"), + Arguments.of( + "0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + "0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + "0xffffffffffffffffffffffffb195148ca348dc57a7331852b390ccefa7b0c18b", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"), + // mulSubOverflow bugs + Arguments.of( + "0x0000000000000001000000000000000000000000000000000000000000000001", + "0x0000000000000001000000000000000000000000000000000000000000000000", + "0x0000000000000001000000000000000000000000000000000000000000000000"), + // mulSubOverflow - addBack bugs + // Modulus256 path (b.u3!=0) via mulMod + Arguments.of( + "0x7effffff8000000000000000000000000000000000000000d900000000000001", + "0x010000000000000000", + "0x7effffff800000007effffff800000008000ff00000100007effffff80000000")); + } + + @Test + public void mulModRandom() { final Random random = new Random(123); for (int i = 0; i < SAMPLE_SIZE; i++) { int aSize = random.nextInt(1, 33); @@ -540,75 +416,17 @@ public void mulMod() { BigInteger.ZERO.compareTo(cInt) == 0 ? Bytes32.ZERO : bigIntTo32B(aInt.multiply(bInt).mod(cInt)); - assertThat(remainder).isEqualTo(expected); + assertThat(remainder) + .withFailMessage( + String.format( + "Failure detected:\n%s.MULMOD(%s, %s)\n", + a.toHexString(), b.toHexString(), c.toHexString())) + .isEqualTo(expected); } } @Test - public void addModTestReduceNormalisedTopLimb() { - UInt256 a = - UInt256.fromBytesBE( - new BigInteger("62d900c9700000000000000000023f00bc1814ff00000000000000ca22300806", 16) - .toByteArray()); - UInt256 b = - UInt256.fromBytesBE( - new BigInteger("ffffffffffffffffb4fffff4befff4f4f4d4f4f504f4f4bef5f5100b0bf4f5f6", 16) - .toByteArray()); - UInt256 m = - UInt256.fromBytesBE(new BigInteger("13464637e8bdc0e53b895d7b79348a784", 16).toByteArray()); - BigInteger A = new BigInteger(1, a.toBytesBE()); - BigInteger B = new BigInteger(1, b.toBytesBE()); - BigInteger M = new BigInteger(1, m.toBytesBE()); - BigInteger expected = A.add(B).mod(M); - assertThat(new BigInteger(1, a.addMod(b, m).toBytesBE())).isEqualTo(expected); - } - - @Test - public void mulSubOverflowWithAddBackBug() { - // When the dividend's leading limb equals the modulus's leading limb, the trial quotient - // overflows and is clamped to 2^64-1. Verify correctness for each Modulus size. - - // Modulus192 path (b.u3==0, b.u2!=0) - UInt256 a1 = - UInt256.fromBytesBE( - new BigInteger("7effffff8000000000000000000000000000000000000000d900000000000001", 16) - .toByteArray()); - UInt256 b1 = - UInt256.fromBytesBE( - new BigInteger("7effffff800000007effffff800000008000ff0000010000", 16).toByteArray()); - BigInteger expected1 = new BigInteger("7effffff800000007dff00feffff0001d901fe0000020001", 16); - assertThat(new BigInteger(1, a1.mod(b1).toBytesBE())).isEqualTo(expected1); - - // Modulus128 path (b.u3==0, b.u2==0, b.u1!=0) - UInt256 a2 = - UInt256.fromBytesBE( - new BigInteger("7effffff800000000000000000000000d900000000000001", 16).toByteArray()); - UInt256 b2 = - UInt256.fromBytesBE(new BigInteger("7effffff800000007fffffffffffffff", 16).toByteArray()); - BigInteger aBI2 = new BigInteger(1, a2.toBytesBE()); - BigInteger bBI2 = new BigInteger(1, b2.toBytesBE()); - BigInteger expected2 = aBI2.mod(bBI2); - assertThat(new BigInteger(1, a2.mod(b2).toBytesBE())).isEqualTo(expected2); - - // Modulus256 path (b.u3!=0) via mulMod - UInt256 a3 = - UInt256.fromBytesBE( - new BigInteger("7effffff8000000000000000000000000000000000000000d900000000000001", 16) - .toByteArray()); - UInt256 x3 = UInt256.fromBytesBE(new BigInteger("10000000000000000", 16).toByteArray()); // 2^64 - UInt256 m3 = - UInt256.fromBytesBE( - new BigInteger("7effffff800000007effffff800000008000ff00000100007effffff80000000", 16) - .toByteArray()); - BigInteger aBI3 = new BigInteger(1, a3.toBytesBE()); - BigInteger xBI3 = new BigInteger(1, x3.toBytesBE()); - BigInteger mBI3 = new BigInteger(1, m3.toBytesBE()); - BigInteger expected3 = aBI3.multiply(xBI3).mod(mBI3); - assertThat(new BigInteger(1, a3.mulMod(x3, m3).toBytesBE())).isEqualTo(expected3); - } - - @Test - public void signedMod() { + public void signedModRandom() { final Random random = new Random(432); for (int i = 0; i < SAMPLE_SIZE; i++) { int aSize = random.nextInt(1, 33); @@ -631,7 +449,7 @@ public void signedMod() { BigInteger bInt = b.isNegative() ? new BigInteger(bArray) : new BigInteger(1, bArray); Bytes32 remainder = Bytes32.leftPad(Bytes.wrap(r.toBytesBE())); Bytes32 expected; - BigInteger rem = BigInteger.ZERO; + BigInteger rem; if (BigInteger.ZERO.compareTo(bInt) == 0) expected = Bytes32.ZERO; else { rem = aInt.abs().mod(bInt.abs()); @@ -642,7 +460,10 @@ public void signedMod() { expected = bigIntTo32B(rem, 1); } } - assertThat(remainder).isEqualTo(expected); + assertThat(remainder) + .withFailMessage( + String.format("Failure detected:\n%s.SMOD(%s)\n", a.toHexString(), b.toHexString())) + .isEqualTo(expected); } } } From 881471b21ef11ddff4f7272b25d04f259619e2d0 Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Wed, 11 Mar 2026 17:10:22 +0000 Subject: [PATCH 27/77] Use VarHandle in UInt256::fromBytesBE and uInt256::toBytesBE (#9976) Signed-off-by: Luis Pinto --- .../operations/BinaryOperationBenchmark.java | 2 +- .../operations/TernaryOperationBenchmark.java | 2 +- .../org/hyperledger/besu/evm/UInt256.java | 78 +++++++++++-------- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/BinaryOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/BinaryOperationBenchmark.java index 77cac0ad110..cefcc40b559 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/BinaryOperationBenchmark.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/BinaryOperationBenchmark.java @@ -32,7 +32,7 @@ import org.openjdk.jmh.infra.Blackhole; @State(Scope.Thread) -@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) @OutputTimeUnit(value = TimeUnit.NANOSECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @BenchmarkMode(Mode.AverageTime) diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/TernaryOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/TernaryOperationBenchmark.java index 21e1cdeee64..6b300f97e7b 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/TernaryOperationBenchmark.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/TernaryOperationBenchmark.java @@ -32,7 +32,7 @@ import org.openjdk.jmh.infra.Blackhole; @State(Scope.Thread) -@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) @OutputTimeUnit(value = TimeUnit.NANOSECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @BenchmarkMode(Mode.AverageTime) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java index 55568420dd5..a4dc23dae2f 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java @@ -14,7 +14,10 @@ */ package org.hyperledger.besu.evm; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; import java.math.BigInteger; +import java.nio.ByteOrder; import java.util.Arrays; /** @@ -60,6 +63,11 @@ public record UInt256(long u3, long u2, long u1, long u0) { // Fixed number of bits per limb. private static final int N_BITS_PER_LIMB = 64; + private static final int N_BYTES_PER_LIMB = 8; + + private static final VarHandle LONG_BE = + MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.BIG_ENDIAN); + // -------------------------------------------------------------------------- // endregion @@ -73,27 +81,45 @@ public record UInt256(long u3, long u2, long u1, long u0) { * @return Big-endian UInt256 represented by the bytes. */ public static UInt256 fromBytesBE(final byte[] bytes) { - if (bytes.length == 0) return ZERO; - long u3 = 0; - long u2 = 0; - long u1 = 0; - long u0 = 0; - int b = bytes.length - 1; // Index in bytes array - for (int shift = 0; shift < 64 && b >= 0; b--, shift += 8) { - u0 |= ((bytes[b] & 0xFFL) << shift); - } - for (int shift = 0; shift < 64 && b >= 0; b--, shift += 8) { - u1 |= ((bytes[b] & 0xFFL) << shift); - } - for (int shift = 0; shift < 64 && b >= 0; b--, shift += 8) { - u2 |= ((bytes[b] & 0xFFL) << shift); + if (bytes.length == 0) { + return ZERO; } - for (int shift = 0; shift < 64 && b >= 0; b--, shift += 8) { - u3 |= ((bytes[b] & 0xFFL) << shift); + if (bytes.length < 8) { + return fromBytesSingleLimb(bytes); } + int prevIndex = bytes.length; + int nextIndex = prevIndex - 8; + final long u0 = getLong(bytes, nextIndex, prevIndex); + prevIndex = nextIndex; + + nextIndex = Math.max(0, prevIndex - 8); + final long u1 = getLong(bytes, nextIndex, prevIndex); + prevIndex = nextIndex; + + nextIndex = Math.max(0, prevIndex - 8); + final long u2 = getLong(bytes, nextIndex, prevIndex); + prevIndex = nextIndex; + + nextIndex = Math.max(0, bytes.length - BYTESIZE); + final long u3 = getLong(bytes, nextIndex, prevIndex); + return new UInt256(u3, u2, u1, u0); } + private static long getLong(final byte[] bytes, final int from, final int to) { + int shift = (N_BYTES_PER_LIMB + from - to) * 8; + final long value = (long) LONG_BE.get(bytes, from); + return shift == N_BITS_PER_LIMB ? 0L : value >>> shift; + } + + private static UInt256 fromBytesSingleLimb(final byte[] bytes) { + long value = 0; + for (int i = bytes.length - 1, shift = 0; i >= 0; i--, shift += 8) { + value |= ((bytes[i] & 0xFFL) << shift); + } + return new UInt256(0, 0, 0, value); + } + /** * Instantiates a new UInt256 from an int. * @@ -162,25 +188,13 @@ public long longValue() { */ public byte[] toBytesBE() { byte[] result = new byte[BYTESIZE]; - longIntoBytes(result, 0, u3); - longIntoBytes(result, 8, u2); - longIntoBytes(result, 16, u1); - longIntoBytes(result, 24, u0); + LONG_BE.set(result, 0, u3); + LONG_BE.set(result, 8, u2); + LONG_BE.set(result, 16, u1); + LONG_BE.set(result, 24, u0); return result; } - // Helper method to write 8 bytes from big-endian int - private static void longIntoBytes(final byte[] bytes, final int offset, final long value) { - bytes[offset] = (byte) (value >>> 56); - bytes[offset + 1] = (byte) (value >>> 48); - bytes[offset + 2] = (byte) (value >>> 40); - bytes[offset + 3] = (byte) (value >>> 32); - bytes[offset + 4] = (byte) (value >>> 24); - bytes[offset + 5] = (byte) (value >>> 16); - bytes[offset + 6] = (byte) (value >>> 8); - bytes[offset + 7] = (byte) value; - } - /** * Convert to BigInteger. * From bc5e6774b1d41e0f1110e33966fe5be5569f8dfb Mon Sep 17 00:00:00 2001 From: Matilda-Clerke Date: Thu, 12 Mar 2026 12:24:41 +1100 Subject: [PATCH 28/77] Update --bootnodes description and throw exception is mixed enode and ENR bootnodes are present (#9955) * Update --bootnodes description and throw exception is mixed enode and ENR bootnodes are present Signed-off-by: Matilda Clerke * Fix BesuCommandTest Signed-off-by: Matilda Clerke * Revert changes to BesuCommand Signed-off-by: Matilda Clerke * Revert changes to BesuCommand Signed-off-by: Matilda Clerke * Apply suggestion from @macfarla Co-authored-by: Sally MacFarlane Signed-off-by: Matilda-Clerke --------- Signed-off-by: Matilda Clerke Signed-off-by: Matilda-Clerke Co-authored-by: Sally MacFarlane --- .../hyperledger/besu/cli/options/P2PDiscoveryOptions.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/hyperledger/besu/cli/options/P2PDiscoveryOptions.java b/app/src/main/java/org/hyperledger/besu/cli/options/P2PDiscoveryOptions.java index b90e9e44b43..3f280921bdc 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/options/P2PDiscoveryOptions.java +++ b/app/src/main/java/org/hyperledger/besu/cli/options/P2PDiscoveryOptions.java @@ -88,9 +88,10 @@ public P2PDiscoveryOptions() {} // NOTE: we have no control over default value here. @CommandLine.Option( names = {"--bootnodes"}, - paramLabel = "", + paramLabel = "|", description = - "Comma separated enode URLs for P2P discovery bootstrap. " + "Comma separated enode or ENR URLs for P2P discovery bootstrap. " + + "Must be either all enode URLs (discovery V4) or all ENR URLs (discovery V5). " + "Default is a predefined list.", split = ",", arity = "0..*") From 151ac57776ccf15e72c86237d1c32ae9e6952d43 Mon Sep 17 00:00:00 2001 From: Matilda-Clerke Date: Thu, 12 Mar 2026 12:47:51 +1100 Subject: [PATCH 29/77] Remove Peer Task System feature toggle from DownloadBodiesStep (#9952) * Remove Peer Task System feature toggle from DownloadBodiesStep Signed-off-by: Matilda Clerke * Fix FullSyncChainDownloaderForkTest Signed-off-by: Matilda Clerke * Remove invalid test Signed-off-by: Matilda Clerke * spotless Signed-off-by: Matilda Clerke * Fix FullSyncChainDownloaderTotalTerminalDifficultyTest Signed-off-by: Matilda Clerke * Remove unneeded old code Signed-off-by: Matilda Clerke * Fix infinite loop in CompleteBlocksWithPeerTask and add unit test Signed-off-by: Matilda Clerke --------- Signed-off-by: Matilda Clerke Signed-off-by: Matilda-Clerke --- .../ethereum/eth/sync/DownloadBodiesStep.java | 40 +-- .../FullSyncDownloadPipelineFactory.java | 2 +- .../eth/sync/tasks/CompleteBlocksTask.java | 104 ------- .../tasks/CompleteBlocksWithPeerTask.java | 6 + .../FullSyncChainDownloaderForkTest.java | 13 +- .../fullsync/FullSyncChainDownloaderTest.java | 68 ----- ...DownloaderTotalTerminalDifficultyTest.java | 6 + .../sync/tasks/CompleteBlocksTaskTest.java | 276 ------------------ .../tasks/CompleteBlocksWithPeerTaskTest.java | 41 ++- 9 files changed, 71 insertions(+), 485 deletions(-) delete mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTask.java delete mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadBodiesStep.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadBodiesStep.java index d65d8e82407..f0df314145a 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadBodiesStep.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadBodiesStep.java @@ -17,10 +17,8 @@ import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.eth.manager.EthContext; -import org.hyperledger.besu.ethereum.eth.sync.tasks.CompleteBlocksTask; import org.hyperledger.besu.ethereum.eth.sync.tasks.CompleteBlocksWithPeerTask; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.plugin.services.MetricsSystem; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -31,39 +29,23 @@ public class DownloadBodiesStep private final ProtocolSchedule protocolSchedule; private final EthContext ethContext; - private final MetricsSystem metricsSystem; - private final SynchronizerConfiguration synchronizerConfiguration; - public DownloadBodiesStep( - final ProtocolSchedule protocolSchedule, - final EthContext ethContext, - final SynchronizerConfiguration synchronizerConfiguration, - final MetricsSystem metricsSystem) { + public DownloadBodiesStep(final ProtocolSchedule protocolSchedule, final EthContext ethContext) { this.protocolSchedule = protocolSchedule; this.ethContext = ethContext; - this.synchronizerConfiguration = synchronizerConfiguration; - this.metricsSystem = metricsSystem; } @Override public CompletableFuture> apply(final List blockHeaders) { - if (synchronizerConfiguration.isPeerTaskSystemEnabled()) { - return ethContext - .getScheduler() - .scheduleServiceTask(() -> getBodiesWithPeerTaskSystem(blockHeaders)); - } else { - return CompleteBlocksTask.forHeaders( - protocolSchedule, ethContext, blockHeaders, metricsSystem) - .run(); - } - } - - private CompletableFuture> getBodiesWithPeerTaskSystem( - final List headers) { - - final CompleteBlocksWithPeerTask completeBlocksWithPeerTask = - new CompleteBlocksWithPeerTask(protocolSchedule, headers, ethContext.getPeerTaskExecutor()); - final List blocks = completeBlocksWithPeerTask.retrieveBlocksFromPeers(); - return CompletableFuture.completedFuture(blocks); + return ethContext + .getScheduler() + .scheduleServiceTask( + () -> { + final CompleteBlocksWithPeerTask completeBlocksWithPeerTask = + new CompleteBlocksWithPeerTask( + protocolSchedule, blockHeaders, ethContext.getPeerTaskExecutor()); + final List blocks = completeBlocksWithPeerTask.retrieveBlocksFromPeers(); + return CompletableFuture.completedFuture(blocks); + }); } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncDownloadPipelineFactory.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncDownloadPipelineFactory.java index 635e53db237..228d9dde2b5 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncDownloadPipelineFactory.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncDownloadPipelineFactory.java @@ -106,7 +106,7 @@ public Pipeline createDownloadPipelineForSyncTarget( metricsSystem); final RangeHeadersValidationStep validateHeadersJoinUpStep = new RangeHeadersValidationStep(); final DownloadBodiesStep downloadBodiesStep = - new DownloadBodiesStep(protocolSchedule, ethContext, syncConfig, metricsSystem); + new DownloadBodiesStep(protocolSchedule, ethContext); final ExtractTxSignaturesStep extractTxSignaturesStep = new ExtractTxSignaturesStep(); final FullImportBlockStep importBlockStep = new FullImportBlockStep( diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTask.java deleted file mode 100644 index 45b632624dd..00000000000 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTask.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.ethereum.eth.sync.tasks; - -import static java.util.concurrent.CompletableFuture.completedFuture; - -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.eth.manager.EthContext; -import org.hyperledger.besu.ethereum.eth.manager.EthPeer; -import org.hyperledger.besu.ethereum.eth.manager.task.AbstractPeerTask; -import org.hyperledger.besu.ethereum.eth.manager.task.GetBodiesFromPeerTask; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.plugin.services.MetricsSystem; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CompleteBlocksTask extends AbstractCompleteBlocksTask { - - private static final Logger LOG = LoggerFactory.getLogger(CompleteBlocksTask.class); - - private CompleteBlocksTask( - final ProtocolSchedule protocolSchedule, - final EthContext ethContext, - final List headers, - final int maxRetries, - final MetricsSystem metricsSystem) { - super(protocolSchedule, ethContext, headers, maxRetries, metricsSystem); - } - - public static CompleteBlocksTask forHeaders( - final ProtocolSchedule protocolSchedule, - final EthContext ethContext, - final List headers, - final int maxRetries, - final MetricsSystem metricsSystem) { - return new CompleteBlocksTask(protocolSchedule, ethContext, headers, maxRetries, metricsSystem); - } - - public static CompleteBlocksTask forHeaders( - final ProtocolSchedule protocolSchedule, - final EthContext ethContext, - final List headers, - final MetricsSystem metricsSystem) { - return new CompleteBlocksTask( - protocolSchedule, ethContext, headers, DEFAULT_RETRIES, metricsSystem); - } - - @Override - Block createEmptyBlock(final BlockHeader header) { - return new Block( - header, - new BlockBody( - Collections.emptyList(), - Collections.emptyList(), - isWithdrawalsEnabled(header) - ? Optional.of(Collections.emptyList()) - : Optional.empty())); - } - - @Override - CompletableFuture> requestBodies(final Optional assignedPeer) { - final List incompleteHeaders = incompleteHeaders(); - if (incompleteHeaders.isEmpty()) { - return completedFuture(Collections.emptyList()); - } - LOG.debug( - "Requesting bodies to complete {} blocks, starting with {}.", - incompleteHeaders.size(), - incompleteHeaders.getFirst().getNumber()); - return executeSubTask( - () -> { - final GetBodiesFromPeerTask task = - GetBodiesFromPeerTask.forHeaders( - protocolSchedule, ethContext, incompleteHeaders, metricsSystem); - assignedPeer.ifPresent(task::assignPeer); - return task.run().thenApply(AbstractPeerTask.PeerTaskResult::getResult); - }); - } - - @Override - long getBlockNumber(final Block block) { - return block.getHeader().getNumber(); - } -} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksWithPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksWithPeerTask.java index 70e17ca1fc2..09c1ec36676 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksWithPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksWithPeerTask.java @@ -119,6 +119,12 @@ public List retrieveBlocksFromPeers() { headersToGet.removeFirst(); nextIndex = findNextIndex(nextIndex + 1); }); + } else { + throw new RuntimeException( + "Unable to retrieve blocks for block numbers: " + + headersToGet.getFirst().getNumber() + + " to " + + headersToGet.getLast().getNumber()); } } return List.of(result); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderForkTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderForkTest.java index 41095ec42b8..0ad111dfa49 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderForkTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderForkTest.java @@ -15,7 +15,6 @@ package org.hyperledger.besu.ethereum.eth.sync.fullsync; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.chain.Blockchain; @@ -29,6 +28,8 @@ import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.manager.RespondingEthPeer; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutor; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetBodiesFromPeerTask; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetBodiesFromPeerTaskExecutorAnswer; import org.hyperledger.besu.ethereum.eth.sync.ChainDownloader; import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration; import org.hyperledger.besu.ethereum.eth.sync.state.SyncState; @@ -43,6 +44,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; public class FullSyncChainDownloaderForkTest { @@ -51,6 +53,7 @@ public class FullSyncChainDownloaderForkTest { protected EthContext ethContext; protected ProtocolContext protocolContext; private SyncState syncState; + private PeerTaskExecutor peerTaskExecutor; private BlockchainSetupUtil localBlockchainSetup; protected MutableBlockchain localBlockchain; @@ -67,6 +70,7 @@ public void setupTest() throws IOException { protocolSchedule = localBlockchainSetup.getProtocolSchedule(); protocolContext = localBlockchainSetup.getProtocolContext(); + peerTaskExecutor = Mockito.mock(PeerTaskExecutor.class); ethProtocolManager = EthProtocolManagerTestBuilder.builder() .setProtocolSchedule(protocolSchedule) @@ -75,9 +79,14 @@ public void setupTest() throws IOException { .setWorldStateArchive(localBlockchainSetup.getWorldArchive()) .setTransactionPool(localBlockchainSetup.getTransactionPool()) .setEthereumWireProtocolConfiguration(EthProtocolConfiguration.DEFAULT) + .setPeerTaskExecutor(peerTaskExecutor) .build(); ethContext = ethProtocolManager.ethContext(); syncState = new SyncState(protocolContext.getBlockchain(), ethContext.getEthPeers()); + + Mockito.when(peerTaskExecutor.execute(Mockito.any(GetBodiesFromPeerTask.class))) + .thenAnswer( + new GetBodiesFromPeerTaskExecutorAnswer(otherBlockchain, ethContext.getEthPeers())); } @AfterEach @@ -95,7 +104,7 @@ private ChainDownloader downloader(final SynchronizerConfiguration syncConfig) { metricsSystem, SyncTerminationCondition.never(), SyncDurationMetrics.NO_OP_SYNC_DURATION_METRICS, - mock(PeerTaskExecutor.class)); + peerTaskExecutor); } private ChainDownloader downloader() { diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderTest.java index 7f6982afc52..d0598322c27 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderTest.java @@ -47,7 +47,6 @@ import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration; import org.hyperledger.besu.ethereum.eth.sync.state.SyncState; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import org.hyperledger.besu.metrics.SyncDurationMetrics; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.MetricsSystem; @@ -363,73 +362,6 @@ public void choosesBestPeerAsSyncTarget_byTdAndHeight(final DataStorageFormat st assertThat(syncState.syncTarget().get().peer()).isEqualTo(peerB.getEthPeer()); } - @ParameterizedTest - @ArgumentsSource(FullSyncChainDownloaderTestArguments.class) - public void recoversFromSyncTargetDisconnect(final DataStorageFormat storageFormat) { - setupTest(storageFormat); - localBlockchainSetup.importFirstBlocks(2); - final long localChainHeadAtStart = localBlockchain.getChainHeadBlockNumber(); - otherBlockchainSetup.importAllBlocks(); - final long targetBlock = otherBlockchain.getChainHeadBlockNumber(); - // Sanity check - assertThat(targetBlock).isGreaterThan(localBlockchain.getChainHeadBlockNumber()); - - final SynchronizerConfiguration syncConfig = - syncConfigBuilder().downloaderChainSegmentSize(5).downloaderHeadersRequestSize(3).build(); - final ChainDownloader downloader = downloader(syncConfig); - - final long bestPeerChainHead = otherBlockchain.getChainHeadBlockNumber(); - final RespondingEthPeer bestPeer = - EthProtocolManagerTestUtil.createPeer(ethProtocolManager, otherBlockchain); - final long secondBestPeerChainHead = bestPeerChainHead - 3; - final Blockchain shorterChain = createShortChain(otherBlockchain, secondBestPeerChainHead); - final RespondingEthPeer secondBestPeer = - EthProtocolManagerTestUtil.createPeer(ethProtocolManager, shorterChain); - final RespondingEthPeer.Responder bestResponder = - RespondingEthPeer.blockchainResponder(otherBlockchain); - final RespondingEthPeer.Responder secondBestResponder = - RespondingEthPeer.blockchainResponder(shorterChain); - downloader.start(); - - // Process through sync target selection - bestPeer.respondWhileOtherThreadsWork(bestResponder, () -> !syncState.syncTarget().isPresent()); - - assertThat(syncState.syncTarget()).isPresent(); - assertThat(syncState.syncTarget().get().peer()).isEqualTo(bestPeer.getEthPeer()); - - // The next message should be for checkpoint headers from the sync target - Awaitility.waitAtMost(10, TimeUnit.SECONDS) - .until(() -> bestPeer.peekNextOutgoingRequest().isPresent()); - - // Process through the first import - await() - .atMost(10, TimeUnit.SECONDS) - .pollInterval(100, TimeUnit.MILLISECONDS) - .untilAsserted( - () -> { - if (!bestPeer.respond(bestResponder)) { - secondBestPeer.respond(secondBestResponder); - } - assertThat(localBlockchain.getChainHeadBlockNumber()) - .isNotEqualTo(localChainHeadAtStart); - }); - - // Sanity check that we haven't already passed the second best peer - assertThat(localBlockchain.getChainHeadBlockNumber()).isLessThan(secondBestPeerChainHead); - - // Disconnect peer - ethProtocolManager.handleDisconnect( - bestPeer.getPeerConnection(), DisconnectReason.TOO_MANY_PEERS, true); - - // Downloader should recover and sync to next best peer, but it may stall - // for 10 seconds first (by design). - secondBestPeer.respondWhileOtherThreadsWork( - secondBestResponder, - () -> localBlockchain.getChainHeadBlockNumber() != secondBestPeerChainHead); - - assertThat(localBlockchain.getChainHeadBlockNumber()).isEqualTo(secondBestPeerChainHead); - } - @ParameterizedTest @ArgumentsSource(FullSyncChainDownloaderTestArguments.class) public void requestsCheckpointsFromSyncTarget(final DataStorageFormat storageFormat) { diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderTotalTerminalDifficultyTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderTotalTerminalDifficultyTest.java index 88cfb9cae1e..22312716f67 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderTotalTerminalDifficultyTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncChainDownloaderTotalTerminalDifficultyTest.java @@ -30,6 +30,8 @@ import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.manager.RespondingEthPeer; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutor; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetBodiesFromPeerTask; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetBodiesFromPeerTaskExecutorAnswer; import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetHeadersFromPeerTask; import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetHeadersFromPeerTaskExecutorAnswer; import org.hyperledger.besu.ethereum.eth.sync.ChainDownloader; @@ -109,6 +111,10 @@ public void setupTest(final DataStorageFormat storageFormat) { peerTaskExecutor.executeAgainstPeer( Mockito.any(GetHeadersFromPeerTask.class), Mockito.any(EthPeer.class))) .thenAnswer(headersAnswer); + + Mockito.when(peerTaskExecutor.execute(Mockito.any(GetBodiesFromPeerTask.class))) + .thenAnswer( + new GetBodiesFromPeerTaskExecutorAnswer(otherBlockchain, ethContext.getEthPeers())); } @AfterEach diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java deleted file mode 100644 index 0e83fdbf48d..00000000000 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.ethereum.eth.sync.tasks; - -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.GWei; -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.ethereum.chain.Blockchain; -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.Withdrawal; -import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManagerTestUtil; -import org.hyperledger.besu.ethereum.eth.manager.RespondingEthPeer; -import org.hyperledger.besu.ethereum.eth.manager.ethtaskutils.RetryingMessageTaskTest; -import org.hyperledger.besu.ethereum.eth.manager.exceptions.MaxRetriesReachedException; -import org.hyperledger.besu.ethereum.eth.manager.task.EthTask; -import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; -import org.hyperledger.besu.ethereum.mainnet.BodyValidation; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; -import org.hyperledger.besu.ethereum.mainnet.WithdrawalsProcessor; -import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; -import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; - -import org.apache.tuweni.units.bigints.UInt64; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -public class CompleteBlocksTaskTest extends RetryingMessageTaskTest> { - - @Override - protected List generateDataToBeRequested() { - return generateDataToBeRequested(3); - } - - protected List generateDataToBeRequested(final int nbBlock) { - // Setup data to be requested and expected response - final List blocks = new ArrayList<>(nbBlock); - for (long i = 0; i < nbBlock; i++) { - final BlockHeader header = blockchain.getBlockHeader(10 + i).get(); - final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); - blocks.add(new Block(header, body)); - } - return blocks; - } - - @Override - protected CompleteBlocksTask createTask(final List requestedData) { - final List headersToComplete = - requestedData.stream().map(Block::getHeader).collect(Collectors.toList()); - return CompleteBlocksTask.forHeaders( - protocolSchedule, ethContext, headersToComplete, maxRetries, new NoOpMetricsSystem()); - } - - @Test - public void shouldCompleteWithoutPeersWhenAllBlocksAreEmpty() { - final BlockHeader header1 = new BlockHeaderTestFixture().number(1).buildHeader(); - final BlockHeader header2 = new BlockHeaderTestFixture().number(2).buildHeader(); - final BlockHeader header3 = new BlockHeaderTestFixture().number(3).buildHeader(); - - final Block block1 = new Block(header1, BlockBody.empty()); - final Block block2 = new Block(header2, BlockBody.empty()); - final Block block3 = new Block(header3, BlockBody.empty()); - - final List blocks = asList(block1, block2, block3); - final EthTask> task = createTask(blocks); - assertThat(task.run()).isCompletedWithValue(blocks); - } - - @Test - public void shouldCreateWithdrawalsAwareEmptyBlock_whenWithdrawalsAreEnabled() { - final ProtocolSchedule mockProtocolSchedule = Mockito.mock(ProtocolSchedule.class); - final ProtocolSpec mockParisSpec = Mockito.mock(ProtocolSpec.class); - final ProtocolSpec mockShanghaiSpec = Mockito.mock(ProtocolSpec.class); - final WithdrawalsProcessor mockWithdrawalsProcessor = Mockito.mock(WithdrawalsProcessor.class); - - final BlockHeader header1 = - new BlockHeaderTestFixture().number(1).withdrawalsRoot(null).buildHeader(); - final BlockHeader header2 = - new BlockHeaderTestFixture().number(2).withdrawalsRoot(Hash.EMPTY_TRIE_HASH).buildHeader(); - - when(mockProtocolSchedule.getByBlockHeader((eq(header1)))).thenReturn(mockParisSpec); - when(mockParisSpec.getWithdrawalsProcessor()).thenReturn(Optional.empty()); - when(mockProtocolSchedule.getByBlockHeader((eq(header2)))).thenReturn(mockShanghaiSpec); - when(mockShanghaiSpec.getWithdrawalsProcessor()) - .thenReturn(Optional.of(mockWithdrawalsProcessor)); - - final Block block1 = - new Block( - header1, - new BlockBody(Collections.emptyList(), Collections.emptyList(), Optional.empty())); - final Block block2 = - new Block( - header2, - new BlockBody( - Collections.emptyList(), - Collections.emptyList(), - Optional.of(Collections.emptyList()))); - - final List expectedBlocks = asList(block1, block2); - final EthTask> task = - CompleteBlocksTask.forHeaders( - mockProtocolSchedule, - ethContext, - List.of(header1, header2), - maxRetries, - new NoOpMetricsSystem()); - assertThat(task.run()).isCompletedWithValue(expectedBlocks); - } - - @Test - public void shouldCompleteBlockThatOnlyContainsWithdrawals_whenWithdrawalsAreEnabled() { - final ProtocolSchedule mockProtocolSchedule = Mockito.mock(ProtocolSchedule.class); - final ProtocolSpec mockParisSpec = Mockito.mock(ProtocolSpec.class); - final ProtocolSpec mockShanghaiSpec = Mockito.mock(ProtocolSpec.class); - final WithdrawalsProcessor mockWithdrawalsProcessor = Mockito.mock(WithdrawalsProcessor.class); - - final Withdrawal withdrawal = - new Withdrawal(UInt64.ONE, UInt64.ONE, Address.fromHexString("0x1"), GWei.ONE); - final List withdrawals = List.of(withdrawal); - final Hash withdrawalsRoot = BodyValidation.withdrawalsRoot(withdrawals); - - final BlockHeader header1 = new BlockHeaderTestFixture().number(1).buildHeader(); - final BlockHeader header2 = - new BlockHeaderTestFixture().number(2).withdrawalsRoot(withdrawalsRoot).buildHeader(); - final BlockHeader header3 = - new BlockHeaderTestFixture().number(3).withdrawalsRoot(Hash.EMPTY_TRIE_HASH).buildHeader(); - - final Block block1 = new Block(header1, BlockBody.empty()); - final Block block2 = - new Block( - header2, - new BlockBody( - Collections.emptyList(), Collections.emptyList(), Optional.of(withdrawals))); - final Block block3 = - new Block( - header3, - new BlockBody( - Collections.emptyList(), - Collections.emptyList(), - Optional.of(Collections.emptyList()))); - final List expected = asList(block1, block2, block3); - - final RespondingEthPeer respondingPeer = - EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); - final RespondingEthPeer.Responder responder = responderForFakeBlocks(expected); - - when(mockProtocolSchedule.getByBlockHeader((eq(header1)))).thenReturn(mockParisSpec); - when(mockParisSpec.getWithdrawalsProcessor()).thenReturn(Optional.empty()); - when(mockProtocolSchedule.getByBlockHeader((eq(header3)))).thenReturn(mockShanghaiSpec); - when(mockShanghaiSpec.getWithdrawalsProcessor()) - .thenReturn(Optional.of(mockWithdrawalsProcessor)); - - final EthTask> task = - CompleteBlocksTask.forHeaders( - mockProtocolSchedule, - ethContext, - List.of(header1, header2, header3), - maxRetries, - new NoOpMetricsSystem()); - - final CompletableFuture> runningTask = task.run(); - - assertThat(runningTask).isNotDone(); - respondingPeer.respond(responder); - assertThat(runningTask).isCompletedWithValue(expected); - } - - private RespondingEthPeer.Responder responderForFakeBlocks(final List blocks) { - final Blockchain mockBlockchain = spy(blockchain); - for (Block block : blocks) { - when(mockBlockchain.getBlockBody(block.getHash())).thenReturn(Optional.of(block.getBody())); - } - - return RespondingEthPeer.blockchainResponder(mockBlockchain); - } - - @SuppressWarnings("unchecked") - @Test - public void shouldReduceTheBlockSegmentSizeAfterEachRetry() { - final RespondingEthPeer respondingPeer = - EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); - - final List requestedData = generateDataToBeRequested(10); - - final CompleteBlocksTask task = createTask(requestedData); - final CompletableFuture> future = task.run(); - - final List messageCollector = new ArrayList<>(4); - - peerCountToTimeout.set(4); - // after 3 timeouts a peer is disconnected, so we need another peer to reach 4 retries - respondingPeer.respondTimes( - RespondingEthPeer.wrapResponderWithCollector( - RespondingEthPeer.emptyResponder(), messageCollector), - 3); - final RespondingEthPeer respondingPeer2 = - EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); - respondingPeer2.respond( - RespondingEthPeer.wrapResponderWithCollector( - RespondingEthPeer.emptyResponder(), messageCollector)); - - assertThat(batchSize(messageCollector.get(0))).isEqualTo(10); - assertThat(batchSize(messageCollector.get(1))).isEqualTo(5); - assertThat(batchSize(messageCollector.get(2))).isEqualTo(4); - assertThat(batchSize(messageCollector.get(3))).isEqualTo(3); - assertThat(future.isCompletedExceptionally()).isTrue(); - assertThatThrownBy(future::get).hasCauseInstanceOf(MaxRetriesReachedException.class); - } - - @SuppressWarnings("unchecked") - @Test - public void shouldNotReduceTheBlockSegmentSizeIfOnlyOneBlockNeeded() { - final RespondingEthPeer respondingPeer = - EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); - - final List requestedData = generateDataToBeRequested(1); - - final EthTask> task = createTask(requestedData); - final CompletableFuture> future = task.run(); - - final List messageCollector = new ArrayList<>(4); - - peerCountToTimeout.set(4); - // after 3 timeouts a peer is disconnected, so we need another peer to reach 4 retries - respondingPeer.respondTimes( - RespondingEthPeer.wrapResponderWithCollector( - RespondingEthPeer.emptyResponder(), messageCollector), - 3); - final RespondingEthPeer respondingPeer2 = - EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); - respondingPeer2.respond( - RespondingEthPeer.wrapResponderWithCollector( - RespondingEthPeer.emptyResponder(), messageCollector)); - - assertThat(batchSize(messageCollector.get(0))).isEqualTo(1); - assertThat(batchSize(messageCollector.get(1))).isEqualTo(1); - assertThat(batchSize(messageCollector.get(2))).isEqualTo(1); - assertThat(batchSize(messageCollector.get(3))).isEqualTo(1); - assertThat(future.isCompletedExceptionally()).isTrue(); - assertThatThrownBy(future::get).hasCauseInstanceOf(MaxRetriesReachedException.class); - } - - private long batchSize(final MessageData msg) { - GetBlockBodiesMessage getBlockBodiesMessage = GetBlockBodiesMessage.readFrom(msg); - return getBlockBodiesMessage.hashes().spliterator().getExactSizeIfKnown(); - } -} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksWithPeerTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksWithPeerTaskTest.java index 9407b4e365c..51b154f39e2 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksWithPeerTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksWithPeerTaskTest.java @@ -31,6 +31,7 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutor; 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.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.WithdrawalsProcessor; @@ -39,14 +40,19 @@ import java.util.List; import java.util.Optional; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; public class CompleteBlocksWithPeerTaskTest { - @BeforeAll - public static void setUp() {} + private long blockNumber; + + @BeforeEach + public void setUp() { + blockNumber = 1; + } @Test public void shouldFailWhenEmptyHeaders() { @@ -207,6 +213,29 @@ public void shouldRequestMoreBodiesUntilFinished() { assertThat(BlockHeader.hasEmptyBlock(blocks.get(3).getHeader())).isTrue(); } + @Test + public void shouldThrowExceptionWhenNoPeersAvailable() { + final ProtocolSchedule protocolSchedule = getProtocolScheduleMock(); + final PeerTaskExecutor peerTaskExecutor = mock(PeerTaskExecutor.class); + final BlockHeader header1 = getNonEmptyBlockHeaderMock("0x01", "0x02"); + final BlockHeader header2 = getNonEmptyBlockHeaderMock("0x03", "0x05"); + + Mockito.when(peerTaskExecutor.execute(any(GetBodiesFromPeerTask.class))) + .thenReturn( + new PeerTaskExecutorResult<>( + Optional.empty(), + PeerTaskExecutorResponseCode.NO_PEER_AVAILABLE, + Collections.emptyList())); + + CompleteBlocksWithPeerTask task = + new CompleteBlocksWithPeerTask( + protocolSchedule, List.of(header1, header2), peerTaskExecutor); + Assertions.assertThrows( + RuntimeException.class, + task::retrieveBlocksFromPeers, + "Unable to retrieve blocks for block numbers: 1 to 2"); + } + private static ProtocolSchedule getProtocolScheduleMock() { final ProtocolSchedule protocolSchedule = mock(ProtocolSchedule.class); final ProtocolSpec protocolSpec = mock(ProtocolSpec.class); @@ -216,17 +245,19 @@ private static ProtocolSchedule getProtocolScheduleMock() { return protocolSchedule; } - private static BlockHeader getEmptyBlockHeaderMock() { + private BlockHeader getEmptyBlockHeaderMock() { final BlockHeader blockHeader = mock(BlockHeader.class); + when(blockHeader.getNumber()).thenReturn(blockNumber++); when(blockHeader.getTransactionsRoot()).thenReturn(Hash.EMPTY_TRIE_HASH); when(blockHeader.getOmmersHash()).thenReturn(Hash.EMPTY_LIST_HASH); when(blockHeader.getWithdrawalsRoot()).thenReturn(Optional.empty()); return blockHeader; } - private static BlockHeader getNonEmptyBlockHeaderMock( + private BlockHeader getNonEmptyBlockHeaderMock( final String transactionsRootHexString, final String ommersHash) { final BlockHeader blockHeader = mock(BlockHeader.class); + when(blockHeader.getNumber()).thenReturn(blockNumber++); when(blockHeader.getTransactionsRoot()) .thenReturn(Hash.fromHexStringLenient(transactionsRootHexString)); when(blockHeader.getOmmersHash()).thenReturn(Hash.fromHexStringLenient(ommersHash)); From 8a948b4893ea8c1905cd0d04fda25dc74ae6b868 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Thu, 12 Mar 2026 13:13:57 +1000 Subject: [PATCH 30/77] Support IPv6 subnets in --net-restrict (#10028) * Replace commons-net SubnetUtils with IPAddress library for IPv6 subnet support Apache Commons Net SubnetUtils only supports IPv4 CIDR notation, causing --net-restrict to fail at CLI parse time for IPv6 subnets (e.g. fd00::/64). Replace it with the IPAddress library (com.github.seancfoley:ipaddress) which supports both IPv4 and IPv6 CIDR notation natively. Closes #10026 Signed-off-by: Usman Saleem * Address PR review: rename SubnetInfoConverter and add mixed IPv4+IPv6 test - Rename SubnetInfoConverter to SubnetCidrConverter to match new IPAddress return type - Add mixed IPv4+IPv6 test case ("127.0.0.0/24,fd00::/64") to BesuCommandTest parameterized net-restrict tests Signed-off-by: Usman Saleem * Add changelog entry for IPv6 --net-restrict support Signed-off-by: Usman Saleem --------- Signed-off-by: Usman Saleem --- CHANGELOG.md | 1 + app/build.gradle | 2 +- .../org/hyperledger/besu/RunnerBuilder.java | 6 +-- .../cli/converter/SubnetCidrConverter.java | 41 +++++++++++++++++++ .../cli/converter/SubnetInfoConverter.java | 36 ---------------- .../besu/cli/options/P2PDiscoveryOptions.java | 8 ++-- .../hyperledger/besu/cli/BesuCommandTest.java | 28 +++++++------ .../besu/cli/CommandTestAbstract.java | 4 +- ...Test.java => SubnetCidrConverterTest.java} | 35 +++++++++++----- ethereum/p2p/build.gradle | 2 +- .../discovery/P2PDiscoveryConfiguration.java | 4 +- .../p2p/permissions/PeerPermissionSubnet.java | 24 +++++++---- .../PeerPermissionsSubnetTest.java | 38 ++++++++++++----- gradle/verification-metadata.xml | 11 +++++ platform/build.gradle | 4 +- 15 files changed, 150 insertions(+), 94 deletions(-) create mode 100644 app/src/main/java/org/hyperledger/besu/cli/converter/SubnetCidrConverter.java delete mode 100644 app/src/main/java/org/hyperledger/besu/cli/converter/SubnetInfoConverter.java rename app/src/test/java/org/hyperledger/besu/cli/converter/{SubnetInfoConverterTest.java => SubnetCidrConverterTest.java} (50%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83302dee4fc..49e3b77e978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ ### Additions and Improvements - Add IPv6 dual-stack support for DiscV5 peer discovery (enabled via `--Xv5-discovery-enabled`): new `--p2p-host-ipv6`, `--p2p-interface-ipv6`, and `--p2p-port-ipv6` CLI options enable a second UDP discovery socket; `--p2p-ipv6-outbound-enabled` controls whether IPv6 is preferred for outbound connections when a peer advertises both address families [#9763](https://github.com/hyperledger/besu/pull/9763); RLPx now also binds a second TCP socket on the IPv6 interface so IPv6-only peers can establish connections [#9873](https://github.com/hyperledger/besu/pull/9873) +- `--net-restrict` now supports IPv6 CIDR notation (e.g. `fd00::/64`) in addition to IPv4, enabling subnet-based peer filtering in IPv6 and dual-stack deployments [#10028](https://github.com/besu-eth/besu/pull/10028) - Stop EngineQosTimer as part of shutdown [#9903](https://github.com/hyperledger/besu/pull/9903) - Add `--max-blobs-per-transaction` CLI option to configure the maximum number of blobs per transaction [#9912](https://github.com/hyperledger/besu/pull/9912) - Add `--max-blobs-per-block` CLI option to configure the maximum number of blobs per block when block building [#9983](https://github.com/hyperledger/besu/pull/9983) diff --git a/app/build.gradle b/app/build.gradle index 149348e6e74..7c17787b020 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -71,7 +71,7 @@ dependencies { implementation 'com.google.guava:guava' implementation 'com.google.dagger:dagger' implementation 'com.graphql-java:graphql-java' - implementation 'commons-net:commons-net' + implementation 'com.github.seancfoley:ipaddress' implementation 'info.picocli:picocli' implementation 'io.vertx:vertx-core' implementation 'io.vertx:vertx-web' diff --git a/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java b/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java index 0901658cd24..ce505942858 100644 --- a/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java +++ b/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java @@ -143,9 +143,9 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import graphql.GraphQL; +import inet.ipaddr.IPAddress; import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; -import org.apache.commons.net.util.SubnetUtils.SubnetInfo; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; import org.slf4j.Logger; @@ -194,7 +194,7 @@ public class RunnerBuilder { private RpcEndpointServiceImpl rpcEndpointServiceImpl; private JsonRpcIpcConfiguration jsonRpcIpcConfiguration; private Optional enodeDnsConfiguration; - private List allowedSubnets = new ArrayList<>(); + private List allowedSubnets = new ArrayList<>(); private boolean poaDiscoveryRetryBootnodes = true; private TransactionValidatorServiceImpl transactionValidatorService; @@ -610,7 +610,7 @@ public RunnerBuilder enodeDnsConfiguration(final EnodeDnsConfiguration enodeDnsC * @param allowedSubnets the allowedSubnets * @return the runner builder */ - public RunnerBuilder allowedSubnets(final List allowedSubnets) { + public RunnerBuilder allowedSubnets(final List allowedSubnets) { this.allowedSubnets = allowedSubnets; return this; } diff --git a/app/src/main/java/org/hyperledger/besu/cli/converter/SubnetCidrConverter.java b/app/src/main/java/org/hyperledger/besu/cli/converter/SubnetCidrConverter.java new file mode 100644 index 00000000000..c422c610509 --- /dev/null +++ b/app/src/main/java/org/hyperledger/besu/cli/converter/SubnetCidrConverter.java @@ -0,0 +1,41 @@ +/* + * 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.cli.converter; + +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; +import picocli.CommandLine; + +/** Converts CIDR notation strings to IPAddress prefix blocks. Supports both IPv4 and IPv6. */ +public class SubnetCidrConverter implements CommandLine.ITypeConverter { + /** Default Constructor. */ + public SubnetCidrConverter() {} + + /** + * Converts an IP address with CIDR notation into an IPAddress prefix block. + * + * @param value The IP address with CIDR notation (e.g. "192.168.1.0/24" or "fd00::/64"). + * @return the IPAddress prefix block + */ + @Override + public IPAddress convert(final String value) { + final IPAddressString addrString = new IPAddressString(value); + if (!addrString.isValid() || addrString.getNetworkPrefixLength() == null) { + throw new CommandLine.TypeConversionException( + "Invalid CIDR notation: " + value + ". Expected format: /"); + } + return addrString.getAddress().toPrefixBlock(); + } +} diff --git a/app/src/main/java/org/hyperledger/besu/cli/converter/SubnetInfoConverter.java b/app/src/main/java/org/hyperledger/besu/cli/converter/SubnetInfoConverter.java deleted file mode 100644 index 9ad9db9d14e..00000000000 --- a/app/src/main/java/org/hyperledger/besu/cli/converter/SubnetInfoConverter.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.cli.converter; - -import org.apache.commons.net.util.SubnetUtils; -import org.apache.commons.net.util.SubnetUtils.SubnetInfo; -import picocli.CommandLine; - -/** The SubnetInfo converter for CLI options. */ -public class SubnetInfoConverter implements CommandLine.ITypeConverter { - /** Default Constructor. */ - public SubnetInfoConverter() {} - - /** - * Converts an IP addresses with CIDR notation into SubnetInfo - * - * @param value The IP addresses with CIDR notation. - * @return the SubnetInfo - */ - @Override - public SubnetInfo convert(final String value) { - return new SubnetUtils(value).getInfo(); - } -} diff --git a/app/src/main/java/org/hyperledger/besu/cli/options/P2PDiscoveryOptions.java b/app/src/main/java/org/hyperledger/besu/cli/options/P2PDiscoveryOptions.java index 3f280921bdc..9a7a293d76e 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/options/P2PDiscoveryOptions.java +++ b/app/src/main/java/org/hyperledger/besu/cli/options/P2PDiscoveryOptions.java @@ -16,7 +16,7 @@ import org.hyperledger.besu.cli.DefaultCommandValues; import org.hyperledger.besu.cli.converter.PercentageConverter; -import org.hyperledger.besu.cli.converter.SubnetInfoConverter; +import org.hyperledger.besu.cli.converter.SubnetCidrConverter; import org.hyperledger.besu.cli.util.CommandLineUtils; import org.hyperledger.besu.ethereum.p2p.discovery.P2PDiscoveryConfiguration; import org.hyperledger.besu.ethereum.p2p.peers.EnodeURLImpl; @@ -34,7 +34,7 @@ import java.util.stream.Collectors; import com.google.common.net.InetAddresses; -import org.apache.commons.net.util.SubnetUtils; +import inet.ipaddr.IPAddress; import org.apache.tuweni.bytes.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -257,10 +257,10 @@ public InetAddress autoDiscoverDefaultIP() { names = {"--net-restrict"}, arity = "1..*", split = ",", - converter = SubnetInfoConverter.class, + converter = SubnetCidrConverter.class, description = "Comma-separated list of allowed IP subnets (e.g., '192.168.1.0/24,10.0.0.0/8').") - private List allowedSubnets; + private List allowedSubnets; @Override public P2PDiscoveryConfiguration toDomainObject() { diff --git a/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index b4132438cfc..9a09100996b 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -103,6 +103,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.google.common.base.Splitter; import com.google.common.io.Resources; import io.vertx.core.json.JsonObject; import org.apache.tuweni.bytes.Bytes; @@ -1315,22 +1316,23 @@ public void parsesValidSyncMinPeersOption() { assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } - @Test - public void netRestrictParsedCorrectly() { - final String subnet1 = "127.0.0.1/24"; - final String subnet2 = "10.0.0.1/24"; - parseCommand("--net-restrict", String.join(",", subnet1, subnet2)); + @ParameterizedTest + @ValueSource( + strings = {"127.0.0.0/24,10.0.0.0/24", "fd00::/64,fe80::/10", "127.0.0.0/24,fd00::/64"}) + public void netRestrictParsedCorrectly(final String subnets) { + parseCommand("--net-restrict", subnets); verify(mockRunnerBuilder).allowedSubnets(allowedSubnetsArgumentCaptor.capture()); - assertThat(allowedSubnetsArgumentCaptor.getValue().size()).isEqualTo(2); - assertThat(allowedSubnetsArgumentCaptor.getValue().get(0).getCidrSignature()) - .isEqualTo(subnet1); - assertThat(allowedSubnetsArgumentCaptor.getValue().get(1).getCidrSignature()) - .isEqualTo(subnet2); + final List expected = Splitter.on(',').splitToList(subnets); + assertThat(allowedSubnetsArgumentCaptor.getValue()).hasSize(expected.size()); + for (int i = 0; i < expected.size(); i++) { + assertThat(allowedSubnetsArgumentCaptor.getValue().get(i).toString()) + .isEqualTo(expected.get(i)); + } } - @Test - public void netRestrictInvalidShouldFail() { - final String subnet = "127.0.0.1/abc"; + @ParameterizedTest + @ValueSource(strings = {"127.0.0.1/abc", "abc", ""}) + public void netRestrictInvalidShouldFail(final String subnet) { parseCommand("--net-restrict", subnet); verifyNoInteractions(mockRunnerBuilder); assertThat(commandErrorOutput.toString(UTF_8)) diff --git a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index 91e8e3b3c46..6a07d1d965a 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -114,12 +114,12 @@ import java.util.function.Function; import java.util.function.Supplier; +import inet.ipaddr.IPAddress; import io.opentelemetry.api.GlobalOpenTelemetry; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; -import org.apache.commons.net.util.SubnetUtils.SubnetInfo; import org.apache.commons.text.StringEscapeUtils; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -276,7 +276,7 @@ public abstract class CommandTestAbstract { @Captor protected ArgumentCaptor apiConfigurationCaptor; @Captor protected ArgumentCaptor ethstatsOptionsArgumentCaptor; - @Captor protected ArgumentCaptor> allowedSubnetsArgumentCaptor; + @Captor protected ArgumentCaptor> allowedSubnetsArgumentCaptor; @BeforeEach public void initMocks() throws Exception { diff --git a/app/src/test/java/org/hyperledger/besu/cli/converter/SubnetInfoConverterTest.java b/app/src/test/java/org/hyperledger/besu/cli/converter/SubnetCidrConverterTest.java similarity index 50% rename from app/src/test/java/org/hyperledger/besu/cli/converter/SubnetInfoConverterTest.java rename to app/src/test/java/org/hyperledger/besu/cli/converter/SubnetCidrConverterTest.java index aaeb4536142..e4c0f49d829 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/converter/SubnetInfoConverterTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/converter/SubnetCidrConverterTest.java @@ -15,45 +15,58 @@ package org.hyperledger.besu.cli.converter; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -import org.apache.commons.net.util.SubnetUtils.SubnetInfo; +import inet.ipaddr.IPAddress; import org.junit.jupiter.api.Test; +import picocli.CommandLine; -public class SubnetInfoConverterTest { +public class SubnetCidrConverterTest { @Test void testCreateIpRestrictionHandlerWithValidSubnets() { String subnet = "192.168.1.0/24"; - assertThat(parseSubnetRules(subnet).getCidrSignature()).isEqualTo(subnet); + IPAddress result = parseSubnetRules(subnet); + assertThat(result.toString()).isEqualTo(subnet); + } + + @Test + void testCreateIpRestrictionHandlerWithValidIpv6Subnet() { + IPAddress result = parseSubnetRules("fd00::/64"); + assertThat(result.toString()).isEqualTo("fd00::/64"); } @Test void testCreateIpRestrictionHandlerWithInvalidSubnet() { - assertThrows(IllegalArgumentException.class, () -> parseSubnetRules("abc")); + assertThatThrownBy(() -> parseSubnetRules("abc")) + .isInstanceOf(CommandLine.TypeConversionException.class); } @Test void testCreateIpRestrictionHandlerMissingCIDR() { - assertThrows(IllegalArgumentException.class, () -> parseSubnetRules("192.168.1.0")); + assertThatThrownBy(() -> parseSubnetRules("192.168.1.0")) + .isInstanceOf(CommandLine.TypeConversionException.class); } @Test void testCreateIpRestrictionHandlerBigCIDR() { - assertThrows(IllegalArgumentException.class, () -> parseSubnetRules("192.168.1.0:25")); + assertThatThrownBy(() -> parseSubnetRules("192.168.1.0:25")) + .isInstanceOf(CommandLine.TypeConversionException.class); } @Test void testCreateIpRestrictionHandlerWithInvalidCIDR() { - assertThrows(IllegalArgumentException.class, () -> parseSubnetRules("192.168.1.0/abc")); + assertThatThrownBy(() -> parseSubnetRules("192.168.1.0/abc")) + .isInstanceOf(CommandLine.TypeConversionException.class); } @Test void testCreateIpRestrictionHandlerWithEmptyString() { - assertThrows(IllegalArgumentException.class, () -> parseSubnetRules("")); + assertThatThrownBy(() -> parseSubnetRules("")) + .isInstanceOf(CommandLine.TypeConversionException.class); } - private SubnetInfo parseSubnetRules(final String subnet) { - return new SubnetInfoConverter().convert(subnet); + private IPAddress parseSubnetRules(final String subnet) { + return new SubnetCidrConverter().convert(subnet); } } diff --git a/ethereum/p2p/build.gradle b/ethereum/p2p/build.gradle index 8f599bceddb..cd14a720cb2 100644 --- a/ethereum/p2p/build.gradle +++ b/ethereum/p2p/build.gradle @@ -56,7 +56,7 @@ dependencies { implementation 'org.jetbrains.kotlin:kotlin-stdlib' implementation 'org.owasp.encoder:encoder' implementation 'org.xerial.snappy:snappy-java' - implementation 'commons-net:commons-net' + implementation 'com.github.seancfoley:ipaddress' annotationProcessor "org.immutables:value" implementation "org.immutables:value-annotations" diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/P2PDiscoveryConfiguration.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/P2PDiscoveryConfiguration.java index 0a4c3b06073..8a0ef672905 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/P2PDiscoveryConfiguration.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/P2PDiscoveryConfiguration.java @@ -20,7 +20,7 @@ import java.util.List; import java.util.Optional; -import org.apache.commons.net.util.SubnetUtils; +import inet.ipaddr.IPAddress; import org.apache.tuweni.bytes.Bytes; public record P2PDiscoveryConfiguration( @@ -37,7 +37,7 @@ public record P2PDiscoveryConfiguration( Percentage maxRemoteConnectionsPercentage, Boolean randomPeerPriority, Collection bannedNodeIds, - List allowedSubnets, + List allowedSubnets, Boolean poaDiscoveryRetryBootnodes, List bootNodes, String discoveryDnsUrl, diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/permissions/PeerPermissionSubnet.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/permissions/PeerPermissionSubnet.java index 720cd4dd609..bc24564b654 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/permissions/PeerPermissionSubnet.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/permissions/PeerPermissionSubnet.java @@ -18,7 +18,8 @@ import java.util.List; -import org.apache.commons.net.util.SubnetUtils.SubnetInfo; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +30,8 @@ * allows for the configuration of permitted subnets and uses these configurations to determine * whether a peer should be allowed or denied access based on its IP address. * + *

Supports both IPv4 and IPv6 subnets. + * *

Note: If no subnets are specified, all peers are considered permitted by default. * * @see PeerPermissions @@ -36,15 +39,15 @@ public class PeerPermissionSubnet extends PeerPermissions { private static final Logger LOG = LoggerFactory.getLogger(PeerPermissionSubnet.class); - private final List allowedSubnets; + private final List allowedSubnets; /** * Constructs a new {@code PeerPermissionSubnet} instance with specified allowed subnets. * - * @param allowedSubnets A list of {@link SubnetInfo} objects representing the subnets that are - * allowed to interact with the local node. Cannot be {@code null}. + * @param allowedSubnets A list of {@link IPAddress} prefix blocks representing the subnets that + * are allowed to interact with the local node. Cannot be {@code null}. */ - public PeerPermissionSubnet(final List allowedSubnets) { + public PeerPermissionSubnet(final List allowedSubnets) { this.allowedSubnets = allowedSubnets; } @@ -66,9 +69,14 @@ public boolean isPermitted(final Peer localNode, final Peer remotePeer, final Ac if (allowedSubnets == null || allowedSubnets.isEmpty()) { return true; } - String remotePeerHostAddress = remotePeer.getEnodeURL().getIpAsString(); - for (SubnetInfo subnet : allowedSubnets) { - if (subnet.isInRange(remotePeerHostAddress)) { + final String remotePeerHostAddress = remotePeer.getEnodeURL().getIpAsString(); + final IPAddress remoteAddress = new IPAddressString(remotePeerHostAddress).getAddress(); + if (remoteAddress == null) { + LOG.trace("Could not parse peer address: {}", remotePeerHostAddress); + return false; + } + for (final IPAddress subnet : allowedSubnets) { + if (subnet.contains(remoteAddress)) { return true; } } diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/permissions/PeerPermissionsSubnetTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/permissions/PeerPermissionsSubnetTest.java index 185858e658e..4659edfadc6 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/permissions/PeerPermissionsSubnetTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/permissions/PeerPermissionsSubnetTest.java @@ -23,31 +23,31 @@ import java.util.List; -import org.apache.commons.net.util.SubnetUtils; -import org.apache.commons.net.util.SubnetUtils.SubnetInfo; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; import org.junit.jupiter.api.Test; public class PeerPermissionsSubnetTest { - private final Peer remoteNode = createPeer(); + private final Peer remoteNode = createPeer("127.0.0.1"); @Test public void peerInSubnetRangeShouldBePermitted() { - List allowedSubnets = List.of(subnet("127.0.0.0/24")); + List allowedSubnets = List.of(subnet("127.0.0.0/24")); PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(allowedSubnets); checkPermissions(peerPermissionSubnet, remoteNode, true); } @Test public void peerInAtLeastOneSubnetRangeShouldBePermitted() { - List allowedSubnets = List.of(subnet("127.0.0.0/24"), subnet("10.0.0.1/24")); + List allowedSubnets = List.of(subnet("127.0.0.0/24"), subnet("10.0.0.1/24")); PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(allowedSubnets); checkPermissions(peerPermissionSubnet, remoteNode, true); } @Test public void peerOutSubnetRangeShouldNotBePermitted() { - List allowedSubnets = List.of(subnet("10.0.0.0/24")); + List allowedSubnets = List.of(subnet("10.0.0.0/24")); PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(allowedSubnets); checkPermissions(peerPermissionSubnet, remoteNode, false); } @@ -58,23 +58,39 @@ public void peerShouldBePermittedIfNoSubnets() { checkPermissions(peerPermissionSubnet, remoteNode, true); } + @Test + public void ipv6PeerInSubnetRangeShouldBePermitted() { + Peer ipv6Peer = createPeer("fd00::1"); + List allowedSubnets = List.of(subnet("fd00::/64")); + PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(allowedSubnets); + checkPermissions(peerPermissionSubnet, ipv6Peer, true); + } + + @Test + public void ipv6PeerOutSubnetRangeShouldNotBePermitted() { + Peer ipv6Peer = createPeer("fe80::1"); + List allowedSubnets = List.of(subnet("fd00::/64")); + PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(allowedSubnets); + checkPermissions(peerPermissionSubnet, ipv6Peer, false); + } + private void checkPermissions( final PeerPermissions peerPermissions, final Peer remotePeer, final boolean expectedResult) { for (Action action : Action.values()) { - assertThat(peerPermissions.isPermitted(createPeer(), remotePeer, action)) + assertThat(peerPermissions.isPermitted(createPeer("127.0.0.1"), remotePeer, action)) .isEqualTo(expectedResult); } } - private SubnetInfo subnet(final String subnet) { - return new SubnetUtils(subnet).getInfo(); + private IPAddress subnet(final String cidr) { + return new IPAddressString(cidr).getAddress().toPrefixBlock(); } - private Peer createPeer() { + private Peer createPeer(final String ip) { return DefaultPeer.fromEnodeURL( EnodeURLImpl.builder() .nodeId(Peer.randomId()) - .ipAddress("127.0.0.1") + .ipAddress(ip) .discoveryAndListeningPorts(EnodeURLImpl.DEFAULT_LISTENING_PORT) .build()); } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 10264c16e9e..9315d954fed 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -567,6 +567,14 @@ + + + + + + + + @@ -1669,6 +1677,9 @@ + + + diff --git a/platform/build.gradle b/platform/build.gradle index 6070878141f..b362b07c390 100644 --- a/platform/build.gradle +++ b/platform/build.gradle @@ -94,9 +94,9 @@ dependencies { api 'com.squareup.okhttp3:okhttp:4.12.0' - api 'commons-io:commons-io:2.21.0' + api 'com.github.seancfoley:ipaddress:5.6.1' - api 'commons-net:commons-net:3.12.0' + api 'commons-io:commons-io:2.21.0' api 'dnsjava:dnsjava:3.6.4' From 772c3789454e8e8b12047f09fdd76e39ac12d4f4 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Thu, 12 Mar 2026 10:04:37 +0100 Subject: [PATCH 31/77] Implement `txpool_status` RPC method (#10002) Signed-off-by: Fabio Di Fabio --- CHANGELOG.md | 1 + .../besu/ethereum/api/jsonrpc/RpcMethod.java | 1 + .../internal/methods/TxPoolStatus.java | 47 +++++++ .../results/TransactionPoolStatusResult.java | 38 ++++++ .../jsonrpc/methods/TxPoolJsonRpcMethods.java | 4 +- .../internal/methods/TxPoolStatusTest.java | 115 ++++++++++++++++++ .../DisabledPendingTransactions.java | 5 + .../eth/transactions/PendingTransactions.java | 4 + .../eth/transactions/TransactionPool.java | 4 + .../AbstractSequentialTransactionsLayer.java | 8 ++ .../eth/transactions/layered/EndLayer.java | 6 + .../layered/LayeredPendingTransactions.java | 5 + .../layered/SparseTransactions.java | 18 +++ .../layered/TransactionsLayer.java | 3 + .../AbstractPendingTransactionsSorter.java | 14 +++ .../sorter/PendingTransactionsForSender.java | 22 ++-- .../LayeredPendingTransactionsTest.java | 73 +++++++++++ .../AbstractPendingTransactionsTestBase.java | 72 +++++++++++ 18 files changed, 428 insertions(+), 12 deletions(-) create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatus.java create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolStatusResult.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatusTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e3b77e978..b8d81f8ea1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Plugin API: Allow the registration of multiple PluginTransactionPoolValidatorFactory [#9964](https://github.com/hyperledger/besu/pull/9964) - Add `-Pcases` case name filtering to JMH benchmark suite [#9982](https://github.com/hyperledger/besu/pull/9982) - Use JDK SHA-256 provider to leverage hardware SHA-NI instructions instead of BouncyCastle [#9924](https://github.com/hyperledger/besu/pull/9924) +- Implement `txpool_status` RPC method [#10002](https://github.com/hyperledger/besu/pull/10002) - Support [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975): eth/70 - partial block receipt lists - Limit pooled tx requests by size and remove pre-eth/68 transaction announcement support [#9990](https://github.com/besu-eth/besu/pull/9990) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java index 679f1be52b1..e8f6bc7ce9e 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java @@ -168,6 +168,7 @@ public enum RpcMethod { TX_POOL_BESU_STATISTICS("txpool_besuStatistics"), TX_POOL_BESU_TRANSACTIONS("txpool_besuTransactions"), TX_POOL_BESU_PENDING_TRANSACTIONS("txpool_besuPendingTransactions"), + TX_POOL_STATUS("txpool_status"), WEB3_CLIENT_VERSION("web3_clientVersion"), WEB3_SHA3("web3_sha3"), PLUGINS_RELOAD_CONFIG("plugins_reloadPluginConfig"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatus.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatus.java new file mode 100644 index 00000000000..0ff46a95359 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatus.java @@ -0,0 +1,47 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; + +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.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPoolStatusResult; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; + +public class TxPoolStatus implements JsonRpcMethod { + + private final TransactionPool transactionPool; + + public TxPoolStatus(final TransactionPool transactionPool) { + this.transactionPool = transactionPool; + } + + @Override + public String getName() { + return RpcMethod.TX_POOL_STATUS.getMethodName(); + } + + @Override + public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), status()); + } + + private TransactionPoolStatusResult status() { + final PendingTransactions.Status status = transactionPool.getStatus(); + return new TransactionPoolStatusResult(status.pendingCount(), status.queuedCount()); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolStatusResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolStatusResult.java new file mode 100644 index 00000000000..d491a1b2fbd --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolStatusResult.java @@ -0,0 +1,38 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public class TransactionPoolStatusResult { + + private final long pending; + private final long queued; + + public TransactionPoolStatusResult(final long pending, final long queued) { + this.pending = pending; + this.queued = queued; + } + + @JsonGetter + public String getPending() { + return Quantity.create(pending); + } + + @JsonGetter + public String getQueued() { + return Quantity.create(queued); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TxPoolJsonRpcMethods.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TxPoolJsonRpcMethods.java index a4ea64101b4..5a62028fce7 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TxPoolJsonRpcMethods.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TxPoolJsonRpcMethods.java @@ -19,6 +19,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolBesuPendingTransactions; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolBesuStatistics; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolBesuTransactions; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolStatus; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; import java.util.Map; @@ -41,6 +42,7 @@ protected Map create() { return mapOf( new TxPoolBesuTransactions(transactionPool), new TxPoolBesuPendingTransactions(transactionPool), - new TxPoolBesuStatistics(transactionPool)); + new TxPoolBesuStatistics(transactionPool), + new TxPoolStatus(transactionPool)); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatusTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatusTest.java new file mode 100644 index 00000000000..68f4862306f --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatusTest.java @@ -0,0 +1,115 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +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.jsonrpc.internal.results.TransactionPoolStatusResult; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class TxPoolStatusTest { + + @Mock private TransactionPool transactionPool; + private TxPoolStatus method; + private static final String JSON_RPC_VERSION = "2.0"; + private static final String TXPOOL_STATUS_METHOD = "txpool_status"; + + @BeforeEach + public void setUp() { + method = new TxPoolStatus(transactionPool); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(TXPOOL_STATUS_METHOD); + } + + @Test + public void shouldReturnZeroCountsWhenPoolIsEmpty() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(0, 0)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0x0"); + assertThat(result.getQueued()).isEqualTo("0x0"); + } + + @Test + public void shouldReturnCorrectCountsWithPendingAndQueuedTransactions() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(10, 7)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0xa"); + assertThat(result.getQueued()).isEqualTo("0x7"); + } + + @Test + public void shouldReturnCorrectCountsWithOnlyPendingTransactions() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(5, 0)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0x5"); + assertThat(result.getQueued()).isEqualTo("0x0"); + } + + @Test + public void shouldReturnCorrectCountsWithOnlyQueuedTransactions() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(0, 3)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0x0"); + assertThat(result.getQueued()).isEqualTo("0x3"); + } + + @Test + public void shouldReturnHexEncodedLargeValues() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(256, 4096)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0x100"); + assertThat(result.getQueued()).isEqualTo("0x1000"); + } + + private JsonRpcRequestContext buildRequest() { + return new JsonRpcRequestContext( + new JsonRpcRequest(JSON_RPC_VERSION, TXPOOL_STATUS_METHOD, new Object[] {})); + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/DisabledPendingTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/DisabledPendingTransactions.java index a0610f679d8..bc7c6db516c 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/DisabledPendingTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/DisabledPendingTransactions.java @@ -117,6 +117,11 @@ public String logStats() { return "Disabled"; } + @Override + public Status getStatus() { + return new Status(0, 0); + } + @Override public Optional restoreBlob(final Transaction transaction) { return Optional.empty(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java index 03bacc92e65..13642cb70dd 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java @@ -73,6 +73,8 @@ void manageBlockAdded( String logStats(); + Status getStatus(); + Optional restoreBlob(Transaction transaction); @FunctionalInterface @@ -80,4 +82,6 @@ interface PendingTransactionsSelector { Map evaluatePendingTransactions( List candidatePendingTransactions); } + + record Status(long pendingCount, long queuedCount) {} } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java index 30a6c2efa21..bceeb502e16 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java @@ -576,6 +576,10 @@ public String logStats() { return pendingTransactions.logStats(); } + public PendingTransactions.Status getStatus() { + return pendingTransactions.getStatus(); + } + @VisibleForTesting Class pendingTransactionsImplementation() { return pendingTransactions.getClass(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractSequentialTransactionsLayer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractSequentialTransactionsLayer.java index cf7195e690f..ca71ee998db 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractSequentialTransactionsLayer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractSequentialTransactionsLayer.java @@ -22,6 +22,7 @@ import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.transactions.BlobCache; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; import org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredRemovalReason.PoolRemovalReason; @@ -123,6 +124,13 @@ public OptionalLong getCurrentNonceFor(final Address sender) { return nextLayer.getCurrentNonceFor(sender); } + @Override + public PendingTransactions.Status getStatus() { + final PendingTransactions.Status nextLayerStatus = nextLayer.getStatus(); + return new PendingTransactions.Status( + nextLayerStatus.pendingCount() + pendingTransactions.size(), nextLayerStatus.queuedCount()); + } + @Override protected void internalNotifyAdded( final NavigableMap senderTxs, diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EndLayer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EndLayer.java index da1731b4256..e0ba12303c5 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EndLayer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EndLayer.java @@ -24,6 +24,7 @@ import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; import org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredRemovalReason.PoolRemovalReason; @@ -183,6 +184,11 @@ public String logStats() { return "Dropped: " + droppedCount; } + @Override + public PendingTransactions.Status getStatus() { + return new PendingTransactions.Status(0, 0); + } + @Override public String logSender(final Address sender) { return ""; diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java index 5baf4a5672c..e514c35a8bb 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java @@ -433,6 +433,11 @@ public synchronized String logStats() { return prioritizedTransactions.logStats(); } + @Override + public synchronized Status getStatus() { + return prioritizedTransactions.getStatus(); + } + @Override public Optional restoreBlob(final Transaction transaction) { return prioritizedTransactions.getBlobCache().restoreBlob(transaction); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java index 59d1c6de36a..78492749566 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java @@ -23,6 +23,7 @@ import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.transactions.BlobCache; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; @@ -216,6 +217,23 @@ public List promote( return promotedTxs; } + @Override + public PendingTransactions.Status getStatus() { + final PendingTransactions.Status nextLayerStatus = nextLayer.getStatus(); + long pendingCount = nextLayerStatus.pendingCount(); + long queueCount = nextLayerStatus.queuedCount(); + for (final Map.Entry entry : gapBySender.entrySet()) { + final Address sender = entry.getKey(); + final int gap = entry.getValue(); + if (gap == 0) { + pendingCount += txsBySender.get(sender).size(); + } else { + queueCount += txsBySender.get(sender).size(); + } + } + return new PendingTransactions.Status(pendingCount, queueCount); + } + private NavigableMap getSequentialSubset( final NavigableMap senderTxs) { long lastSequentialNonce = senderTxs.firstKey(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java index 670ef4fd840..e37c5920579 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java @@ -21,6 +21,7 @@ import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; import org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredRemovalReason.PoolRemovalReason; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; @@ -127,5 +128,7 @@ List promote( String logStats(); + PendingTransactions.Status getStatus(); + String logSender(Address sender); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsSorter.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsSorter.java index 7d8f61886a2..9c4b4a71a40 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsSorter.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsSorter.java @@ -507,6 +507,20 @@ public String logStats() { return "Pending " + pendingTransactions.size(); } + @Override + public Status getStatus() { + long pendingCount = 0; + long queuedCount = 0; + synchronized (lock) { + for (final PendingTransactionsForSender pendingTxsForSender : transactionsBySender.values()) { + final Status accountStatus = pendingTxsForSender.getStatus(); + pendingCount += accountStatus.pendingCount(); + queuedCount += accountStatus.queuedCount(); + } + } + return new Status(pendingCount, queuedCount); + } + @Override public String toTraceLog() { synchronized (lock) { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/PendingTransactionsForSender.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/PendingTransactionsForSender.java index 3d42b2c3d7a..77b0fa82bb9 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/PendingTransactionsForSender.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/PendingTransactionsForSender.java @@ -15,10 +15,10 @@ package org.hyperledger.besu.ethereum.eth.transactions.sorter; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.evm.account.Account; import java.util.List; -import java.util.Map; import java.util.NavigableMap; import java.util.NoSuchElementException; import java.util.Optional; @@ -70,12 +70,16 @@ public void updateSenderAccount(final Optional maybeSenderAccount) { this.maybeSenderAccount = maybeSenderAccount; } - public long getSenderAccountNonce() { - return maybeSenderAccount.map(Account::getNonce).orElse(0L); - } - - public Optional getSenderAccount() { - return maybeSenderAccount; + PendingTransactions.Status getStatus() { + synchronized (pendingTransactions) { + if (nextGap.isPresent()) { + final long gap = nextGap.getAsLong(); + final long accountNonce = maybeSenderAccount.map(Account::getNonce).orElse(0L); + final long nonGapped = Math.max(0, gap - accountNonce); + return new PendingTransactions.Status(nonGapped, transactionCount() - nonGapped); + } + return new PendingTransactions.Status(transactionCount(), 0); + } } private void findGap() { @@ -109,10 +113,6 @@ public OptionalLong maybeNextNonce() { } } - public Optional maybeLastPendingTransaction() { - return Optional.ofNullable(pendingTransactions.lastEntry()).map(Map.Entry::getValue); - } - public int transactionCount() { return pendingTransactions.size(); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java index 96f1508d1f8..e8d8218ca33 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java @@ -46,6 +46,7 @@ import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.RemovalReason; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; @@ -821,6 +822,78 @@ public void returnsCorrectNextNonceWhenAddedTransactionsHaveGaps() { .hasValue(1); } + @Test + public void shouldReturnZeroStatusWhenPoolIsEmpty() { + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isZero(); + assertThat(status.queuedCount()).isZero(); + } + + @Test + public void shouldCountAllTransactionsAsPendingWhenNoncesAreSequential() { + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(1, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(2, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isZero(); + } + + @Test + public void shouldCountTransactionsBeyondNonceGapAsQueued() { + // nonce 0 lands in prioritized/ready (pending), nonce 2 lands in sparse with gap=1 (queued) + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(2, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(1); + assertThat(status.queuedCount()).isEqualTo(1); + } + + @Test + public void shouldAggregatePendingAndQueuedAcrossMultipleSenders() { + // SENDER1: nonces 0, 1 — sequential, all pending + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(1, KEYS1)), Optional.empty()); + // SENDER2: nonces 0, 2 — gap at 1: 1 pending, 1 queued + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS2)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(2, KEYS2)), Optional.empty()); + + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isEqualTo(1); + } + + @Test + public void shouldMoveTxFromQueuedToPendingWhenGapIsFilled() { + // start with a gap: nonce 0 pending, nonce 2 queued + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(2, KEYS1)), Optional.empty()); + + assertThat(pendingTransactions.getStatus().pendingCount()).isEqualTo(1); + assertThat(pendingTransactions.getStatus().queuedCount()).isEqualTo(1); + + // fill the gap: nonce 1 arrives, nonce 2 is promoted from sparse to ready/prioritized + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(1, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isZero(); + } + private TransactionAndAccount[] populateCache(final int numTxs, final long startingNonce) { return populateCache(numTxs, KEYS1, startingNonce, OptionalLong.empty()); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java index 80046f69074..e9f32cd1772 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java @@ -625,6 +625,78 @@ public void shouldNotIncreasePriorityOfTransactionsBecauseOfNonceOrder() { }); } + @Test + public void shouldReturnZeroStatusWhenPoolIsEmpty() { + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isZero(); + assertThat(status.queuedCount()).isZero(); + } + + @Test + public void shouldCountAllTransactionsAsPendingWhenNonceIsSequential() { + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(0, KEYS1)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(1, KEYS1)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(2, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isZero(); + } + + @Test + public void shouldCountTransactionsBeyondNonceGapAsQueued() { + // nonce 0 is pending, nonce 2 is queued (gap at 1) + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(0, KEYS1)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(2, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(1); + assertThat(status.queuedCount()).isEqualTo(1); + } + + @Test + public void shouldAggregatePendingAndQueuedAcrossMultipleSenders() { + // SENDER1: nonces 0, 1 — sequential, all pending + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(0, KEYS1)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(1, KEYS1)), Optional.empty()); + // SENDER2: nonces 0, 2 — gap at 1: 1 pending, 1 queued + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(0, KEYS2)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(2, KEYS2)), Optional.empty()); + + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isEqualTo(1); + } + + @Test + public void shouldUseAccountNonceToComputePendingCountWhenGapIsPresent() { + // account nonce is 2, txs at 2, 3, 5 — gap at 4: 2 pending (nonces 2,3), 1 queued (nonce 5) + final Account sender = mock(Account.class); + when(sender.getNonce()).thenReturn(2L); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(2, KEYS1)), + Optional.of(sender)); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(3, KEYS1)), + Optional.of(sender)); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(5, KEYS1)), + Optional.of(sender)); + + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(2); + assertThat(status.queuedCount()).isEqualTo(1); + } + protected void assertMaximumNonceForSender(final Address sender1, final int i) { assertThat(transactions.getNextNonceForSender(sender1)).isEqualTo(OptionalLong.of(i)); } From 93ca593474e0fd25ec6e4778e90465df21211fee Mon Sep 17 00:00:00 2001 From: Cyrus Date: Thu, 12 Mar 2026 19:33:18 +0530 Subject: [PATCH 32/77] =?UTF-8?q?fix:=20add=20missing=20return=20for=20fai?= =?UTF-8?q?led=20future=20in=20TransactionPool=20SaveRest=E2=80=A6=20(#100?= =?UTF-8?q?20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add missing return for failed future in TransactionPool SaveRestoreManager The serializeAndDedupOperation() method creates a CompletableFuture.failedFuture() when the disk access lock times out, but does not return it. Execution falls through to return CompletableFuture.completedFuture(null), silently swallowing the timeout error. This can cause the transaction pool save/restore to silently fail, leading to loss of all pending transactions during sync state transitions. Signed-off-by: Shridhar Panigrahi * test: add test verifying timeout failure is propagated in SaveRestoreManager Add diskLockTimeoutIsPropagatedNotSwallowed test that acquires the disk access lock to simulate contention, then verifies that loadFromDisk() returns a failed future with TimeoutException rather than silently returning completedFuture(null). Also adds @VisibleForTesting accessors for getSaveRestoreManager() and getDiskAccessLock() to support the test. Signed-off-by: Shridhar Panigrahi * refactor: make SaveRestoreManager lock timeout configurable and simplify test assertion Address PR review feedback: make the disk access lock timeout configurable via a package-private setter so tests can use a short timeout (100ms) instead of blocking for 60 seconds. Simplify the test assertion to use assertThatThrownBy for cleaner style. Signed-off-by: Shridhar Panigrahi * refactor: expose save-restore lock timeout as unstable CLI option Add --Xtx-pool-save-restore-timeout as a hidden unstable option in TransactionPoolOptions, wired through TransactionPoolConfiguration, so SaveRestoreManager reads the timeout from config instead of using a @VisibleForTesting setter. This avoids modifying production code solely for testing and allows operators to tune the timeout for special use cases. Signed-off-by: Shridhar Panigrahi --------- Signed-off-by: Shridhar Panigrahi Signed-off-by: Cyrus --- .../cli/options/TransactionPoolOptions.java | 14 +++++++ .../eth/transactions/TransactionPool.java | 16 +++++++- .../TransactionPoolConfiguration.java | 6 +++ .../TransactionPoolSaveRestoreTest.java | 40 +++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/hyperledger/besu/cli/options/TransactionPoolOptions.java b/app/src/main/java/org/hyperledger/besu/cli/options/TransactionPoolOptions.java index ba3c62915c6..8e8492b24a2 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/options/TransactionPoolOptions.java +++ b/app/src/main/java/org/hyperledger/besu/cli/options/TransactionPoolOptions.java @@ -250,6 +250,8 @@ static class Unstable { private static final String MAX_TRACKED_SEEN_TXS_PER_PEER = "--Xmax-tracked-seen-txs-per-peer"; private static final String PEER_TRACKER_FORGET_EVICTED_TXS_FLAG = "--Xpeer-tracker-forget-evicted-txs"; + private static final String TX_POOL_SAVE_RESTORE_TIMEOUT_FLAG = + "--Xtx-pool-save-restore-timeout"; @CommandLine.Option( names = {TX_MESSAGE_KEEP_ALIVE_SEC_FLAG}, @@ -288,6 +290,16 @@ static class Unstable { arity = "0..1", fallbackValue = "true") private Boolean peerTrackerForgetEvictedTxs; + + @CommandLine.Option( + names = {TX_POOL_SAVE_RESTORE_TIMEOUT_FLAG}, + paramLabel = "", + converter = DurationMillisConverter.class, + hidden = true, + description = + "Timeout in milliseconds for acquiring the disk access lock during txpool save/restore operations (default: ${DEFAULT-VALUE})") + private Duration saveRestoreTimeout = + TransactionPoolConfiguration.Unstable.DEFAULT_SAVE_RESTORE_TIMEOUT; } private TransactionPoolOptions() {} @@ -353,6 +365,7 @@ public static TransactionPoolOptions fromConfig(final TransactionPoolConfigurati config.getUnstable().getMaxTrackedSeenTxsPerPeer(); options.unstableOptions.peerTrackerForgetEvictedTxs = config.getUnstable().getPeerTrackerForgetEvictedTxs(); + options.unstableOptions.saveRestoreTimeout = config.getUnstable().getSaveRestoreTimeout(); return options; } @@ -415,6 +428,7 @@ public TransactionPoolConfiguration toDomainObject() { .peerTrackerForgetEvictedTxs( Optional.ofNullable(unstableOptions.peerTrackerForgetEvictedTxs) .orElse(deriveDefaultPeersTrackerForgetEvictedTxs(txPoolImplementation))) + .saveRestoreTimeout(unstableOptions.saveRestoreTimeout) .build()) .build(); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java index bceeb502e16..bafce8ab92a 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java @@ -585,6 +585,11 @@ Class pendingTransactionsImplementation() { return pendingTransactions.getClass(); } + @VisibleForTesting + SaveRestoreManager getSaveRestoreManager() { + return saveRestoreManager; + } + public interface TransactionBatchAddedListener { void onTransactionsAdded(Collection transactions); @@ -760,6 +765,11 @@ class SaveRestoreManager { new AtomicReference<>(CompletableFuture.completedFuture(null)); private final AtomicBoolean isCancelled = new AtomicBoolean(false); + @VisibleForTesting + Semaphore getDiskAccessLock() { + return diskAccessLock; + } + CompletableFuture saveToDisk(final PendingTransactions pendingTransactionsToSave) { cancelInProgressReadOperation(); return serializeAndDedupOperation( @@ -794,7 +804,9 @@ private CompletableFuture serializeAndDedupOperation( final AtomicReference> operationInProgress) { if (configuration.getEnableSaveRestore()) { try { - if (diskAccessLock.tryAcquire(1, TimeUnit.MINUTES)) { + if (diskAccessLock.tryAcquire( + configuration.getUnstable().getSaveRestoreTimeout().toMillis(), + TimeUnit.MILLISECONDS)) { isCancelled.set(false); operationInProgress.set( @@ -802,7 +814,7 @@ private CompletableFuture serializeAndDedupOperation( .whenComplete((res, err) -> diskAccessLock.release())); return operationInProgress.get(); } else { - CompletableFuture.failedFuture( + return CompletableFuture.failedFuture( new TimeoutException("Timeout waiting for disk access lock")); } } catch (InterruptedException ie) { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolConfiguration.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolConfiguration.java index ffe8ae727aa..1ac692deed7 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolConfiguration.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolConfiguration.java @@ -42,6 +42,7 @@ interface Unstable { int DEFAULT_TX_MSG_KEEP_ALIVE = 60; int DEFAULT_MAX_TRACKED_SEEN_TXS_PER_PEER = 200_000; boolean DEFAULT_PEER_TRACKER_FORGET_EVICTED_TXS = false; + Duration DEFAULT_SAVE_RESTORE_TIMEOUT = Duration.ofMinutes(1); TransactionPoolConfiguration.Unstable DEFAULT = ImmutableTransactionPoolConfiguration.Unstable.builder().build(); @@ -65,6 +66,11 @@ default int getMaxTrackedSeenTxsPerPeer() { default boolean getPeerTrackerForgetEvictedTxs() { return DEFAULT_PEER_TRACKER_FORGET_EVICTED_TXS; } + + @Value.Default + default Duration getSaveRestoreTimeout() { + return DEFAULT_SAVE_RESTORE_TIMEOUT; + } } enum Implementation { diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolSaveRestoreTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolSaveRestoreTest.java index 0f46e1c5d0e..e2a0803fac4 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolSaveRestoreTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolSaveRestoreTest.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.eth.transactions; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; import org.hyperledger.besu.datatypes.Wei; @@ -40,7 +41,9 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -270,4 +273,41 @@ private String transaction2Base64(final Transaction transaction) { TransactionEncoder.encodeRLP(transaction, rlp, EncodingContext.POOLED_TRANSACTION); return rlp.encoded().toBase64String(); } + + @Test + public void diskLockTimeoutIsPropagatedNotSwallowed() throws InterruptedException { + // Create a txpool with save and restore enabled and a short lock timeout + // so the test does not block for 60 seconds + this.transactionPool = + createTransactionPool( + b -> + b.enableSaveRestore(true) + .saveFile(saveFilePath.toFile()) + .unstable( + ImmutableTransactionPoolConfiguration.Unstable.builder() + .saveRestoreTimeout(Duration.ofMillis(100)) + .build())); + + // Acquire the lock to simulate another operation holding it. Using acquire() + // instead of drainPermits() ensures we wait for any in-progress async operation + // (e.g. loadFromDisk triggered by pool creation) to release the permit first. + final var lock = transactionPool.getSaveRestoreManager().getDiskAccessLock(); + lock.acquire(); + + try { + // Call loadFromDisk() directly on the SaveRestoreManager to test + // serializeAndDedupOperation() in isolation, since setDisabled() wraps the + // future with .exceptionally() which swallows the error for logging. + // Before the fix, the failedFuture was not returned and the caller silently got + // completedFuture(null). After the fix, the failed future must be propagated. + final CompletableFuture result = transactionPool.getSaveRestoreManager().loadFromDisk(); + + assertThatThrownBy(result::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(TimeoutException.class); + } finally { + // Always restore the permit so cleanup does not hang + lock.release(); + } + } } From 63c17cbcfa4f412c06842888bec8aca618f95b04 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Thu, 12 Mar 2026 22:43:05 +0100 Subject: [PATCH 33/77] Remove deprecation warning from `--block-txs-selection-max-time` still used in PoS networks (#10037) Signed-off-by: Fabio Di Fabio --- .../besu/cli/options/MiningOptions.java | 20 +++++++------------ .../besu/cli/options/MiningOptionsTest.java | 12 +++++------ .../AbstractBlockTransactionSelectorTest.java | 12 +++++------ ...FeeMarketBlockTransactionSelectorTest.java | 8 ++++---- .../ethereum/core/MiningConfiguration.java | 8 ++++---- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java b/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java index cb4523da662..b5d6d83ec1a 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java +++ b/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java @@ -16,9 +16,9 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.util.Collections.singletonList; -import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_PLUGIN_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POA_BLOCK_TXS_SELECTION_MAX_TIME; +import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.MutableInitValues.DEFAULT_EXTRA_DATA; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.MutableInitValues.DEFAULT_MIN_BLOCK_OCCUPANCY_RATIO; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.MutableInitValues.DEFAULT_MIN_PRIORITY_FEE_PER_GAS; @@ -50,10 +50,6 @@ /** The Mining CLI options. */ public class MiningOptions implements CLIOptions { - - private static final String DEPRECATION_PREFIX = - "Deprecated. PoW consensus is deprecated. See CHANGELOG for alternative options. "; - @Option( names = {"--miner-extra-data"}, description = @@ -91,18 +87,16 @@ public class MiningOptions implements CLIOptions { names = {"--block-txs-selection-max-time"}, converter = PositiveNumberConverter.class, description = - DEPRECATION_PREFIX - + "Specifies the maximum time, in milliseconds, that could be spent selecting transactions to be included in the block." + "Specifies the maximum time, in milliseconds, that could be spent selecting transactions to be included in the block on PoS networks." + " Not compatible with PoA networks, see poa-block-txs-selection-max-time. (default: ${DEFAULT-VALUE})") - private PositiveNumber nonPoaBlockTxsSelectionMaxTime = - DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME; + private PositiveNumber posBlockTxsSelectionMaxTime = DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME; @Option( names = {"--poa-block-txs-selection-max-time"}, converter = PositiveNumberConverter.class, description = "Specifies the maximum time that could be spent selecting transactions to be included in the block, as a percentage of the fixed block time of the PoA network." - + " To be only used on PoA networks, for other networks see block-txs-selection-max-time." + + " To be only used on PoA networks, for PoS networks see block-txs-selection-max-time." + " (default: ${DEFAULT-VALUE})") private PositiveNumber poaBlockTxsSelectionMaxTime = DEFAULT_POA_BLOCK_TXS_SELECTION_MAX_TIME; @@ -268,8 +262,8 @@ static MiningOptions fromConfig(final MiningConfiguration miningConfiguration) { miningOptions.minTransactionGasPrice = miningConfiguration.getMinTransactionGasPrice(); miningOptions.minPriorityFeePerGas = miningConfiguration.getMinPriorityFeePerGas(); miningOptions.minBlockOccupancyRatio = miningConfiguration.getMinBlockOccupancyRatio(); - miningOptions.nonPoaBlockTxsSelectionMaxTime = - miningConfiguration.getNonPoaBlockTxsSelectionMaxTime(); + miningOptions.posBlockTxsSelectionMaxTime = + miningConfiguration.getPosBlockTxsSelectionMaxTime(); miningOptions.poaBlockTxsSelectionMaxTime = miningConfiguration.getPoaBlockTxsSelectionMaxTime(); miningOptions.pluginBlockTxsSelectionMaxTime = @@ -318,7 +312,7 @@ public MiningConfiguration toDomainObject() { .mutableInitValues(updatableInitValuesBuilder.build()) .maxBlobsPerBlock( maxBlobsPerBlock != null ? OptionalInt.of(maxBlobsPerBlock) : OptionalInt.empty()) - .nonPoaBlockTxsSelectionMaxTime(nonPoaBlockTxsSelectionMaxTime) + .posBlockTxsSelectionMaxTime(posBlockTxsSelectionMaxTime) .poaBlockTxsSelectionMaxTime(poaBlockTxsSelectionMaxTime) .pluginBlockTxsSelectionMaxTime(pluginBlockTxsSelectionMaxTime) .unstable( diff --git a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java index fcfd103cb43..d97e9bdbd59 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java @@ -15,9 +15,9 @@ package org.hyperledger.besu.cli.options; import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_PLUGIN_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POA_BLOCK_TXS_SELECTION_MAX_TIME; +import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.Unstable.DEFAULT_POS_BLOCK_CREATION_MAX_TIME; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.verify; @@ -180,8 +180,8 @@ public void blockTxsSelectionMaxTimeDefaultValue() { internalTestSuccess( this::runtimeConfiguration, miningParams -> - assertThat(miningParams.getNonPoaBlockTxsSelectionMaxTime()) - .isEqualTo(DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME)); + assertThat(miningParams.getPosBlockTxsSelectionMaxTime()) + .isEqualTo(DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME)); } @Test @@ -212,7 +212,7 @@ public void blockTxsSelectionMaxTimeRequiresPoSTransition() throws IOException { internalTestSuccess( this::runtimeConfiguration, miningParams -> - assertThat(miningParams.getNonPoaBlockTxsSelectionMaxTime()) + assertThat(miningParams.getPosBlockTxsSelectionMaxTime()) .isEqualTo(PositiveNumber.fromInt(2)), "--genesis-file", genesisFilePoS.toString(), @@ -227,7 +227,7 @@ public void bothBlockTxsSelectionMaxTimeOptionsAllowedWhenPoSTransitionIsPresent internalTestSuccess( this::runtimeConfiguration, miningParams -> { - assertThat(miningParams.getNonPoaBlockTxsSelectionMaxTime()) + assertThat(miningParams.getPosBlockTxsSelectionMaxTime()) .isEqualTo(PositiveNumber.fromInt(2000)); assertThat(miningParams.getPoaBlockTxsSelectionMaxTime()) .isEqualTo(PositiveNumber.fromInt(80)); @@ -250,7 +250,7 @@ public void bothBlockTxsSelectionMaxTimeOptionsAllowedWhenPoSTransitionIsPresent internalTestSuccess( this::runtimeConfiguration, miningParams -> { - assertThat(miningParams.getNonPoaBlockTxsSelectionMaxTime()) + assertThat(miningParams.getPosBlockTxsSelectionMaxTime()) .isEqualTo(PositiveNumber.fromInt(2000)); assertThat(miningParams.getPoaBlockTxsSelectionMaxTime()) .isEqualTo(PositiveNumber.fromInt(80)); diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java index 3fb4dff71da..ddacd2ca8f0 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java @@ -23,7 +23,7 @@ import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER3; import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER4; import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER5; -import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME; +import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.EXECUTION_INTERRUPTED; import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.NONCE_TOO_LOW; import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOCK_SELECTION_TIMEOUT; @@ -165,7 +165,7 @@ public void setup() { transactionSelectionService, Wei.ZERO, MIN_OCCUPANCY_80_PERCENT, - DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME); + DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME); final Block genesisBlock = GenesisState.fromConfig(genesisConfig, protocolSchedule, new CodeCache()).getBlock(); @@ -520,7 +520,7 @@ public void transactionSelectionStopsWhenBlockIsFull() { transactionSelectionService, Wei.ZERO, MIN_OCCUPANCY_100_PERCENT, - DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME), + DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -582,7 +582,7 @@ public void transactionSelectionStopsWhenRemainingGasIsNotEnoughForAnyMoreTransa transactionSelectionService, Wei.ZERO, MIN_OCCUPANCY_100_PERCENT, - DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME), + DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -824,7 +824,7 @@ public PluginTransactionSelector create( transactionSelectionService, Wei.ZERO, MIN_OCCUPANCY_80_PERCENT, - DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME), + DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -1765,7 +1765,7 @@ protected MiningConfiguration createMiningParameters( .minBlockOccupancyRatio(minBlockOccupancyRatio) .build()) .transactionSelectionService(transactionSelectionService) - .nonPoaBlockTxsSelectionMaxTime(txsSelectionMaxTime) + .posBlockTxsSelectionMaxTime(txsSelectionMaxTime) .build(); } diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/LondonFeeMarketBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/LondonFeeMarketBlockTransactionSelectorTest.java index 9e8c0402bee..ff291277d92 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/LondonFeeMarketBlockTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/LondonFeeMarketBlockTransactionSelectorTest.java @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.entry; import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER1; import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER2; -import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME; +import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME; import static org.mockito.Mockito.mock; import org.hyperledger.besu.config.GenesisConfig; @@ -122,7 +122,7 @@ public void eip1559TransactionCurrentGasPriceLessThanMinimumIsSkippedAndKeptInTh transactionSelectionService, Wei.of(6), MIN_OCCUPANCY_80_PERCENT, - DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME), + DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -154,7 +154,7 @@ public void eip1559TransactionCurrentGasPriceGreaterThanMinimumIsSelected() { transactionSelectionService, Wei.of(6), MIN_OCCUPANCY_80_PERCENT, - DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME), + DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -185,7 +185,7 @@ public void eip1559PriorityTransactionCurrentGasPriceLessThanMinimumIsSelected() transactionSelectionService, Wei.of(6), MIN_OCCUPANCY_80_PERCENT, - DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME), + DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java index ab9b393612d..d4f2646b07e 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java @@ -41,7 +41,7 @@ @Value.Immutable @Value.Enclosing public abstract class MiningConfiguration { - public static final PositiveNumber DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME = + public static final PositiveNumber DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME = PositiveNumber.fromInt((int) Duration.ofSeconds(5).toMillis()); public static final PositiveNumber DEFAULT_POA_BLOCK_TXS_SELECTION_MAX_TIME = PositiveNumber.fromInt(75); @@ -168,8 +168,8 @@ public MiningConfiguration setEmptyBlockPeriodSeconds(final int emptyBlockPeriod } @Value.Default - public PositiveNumber getNonPoaBlockTxsSelectionMaxTime() { - return DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME; + public PositiveNumber getPosBlockTxsSelectionMaxTime() { + return DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME; } @Value.Default @@ -214,7 +214,7 @@ public Duration getBlockTxsSelectionMaxTime(final boolean isPoS) { } } - return Duration.ofMillis(getNonPoaBlockTxsSelectionMaxTime().getValue()); + return Duration.ofMillis(getPosBlockTxsSelectionMaxTime().getValue()); } public Duration getPluginTxsSelectionMaxTime(final Duration blockTxsSelectionMaxTime) { From 7bc858d61468d5ce299a44951f881f19c6f17d30 Mon Sep 17 00:00:00 2001 From: Stefan Pingel <16143240+pinges@users.noreply.github.com> Date: Fri, 13 Mar 2026 08:20:18 +1000 Subject: [PATCH 34/77] Change log level to debug (#10023) Signed-off-by: stefan.pingel@consensys.net Co-authored-by: Sally MacFarlane Signed-off-by: Stefan Pingel <16143240+pinges@users.noreply.github.com> --- .../ethereum/eth/sync/snapsync/SnapSyncChainDownloader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloader.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloader.java index 88fff662814..551a4a48c52 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloader.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloader.java @@ -547,7 +547,7 @@ private void handleDownloadError( chainSyncStateStorage.storeState(chainSyncState.get()); if (shouldRetry(error)) { - LOG.warn("Chain sync encountered error, will retry from saved state", error); + LOG.debug("Chain sync encountered error, will retry from saved state", error); // Schedule next attempt without recursion // Use a small delay to avoid tight retry loops From 7191630b0c7784bdd9346406445da598a5f24a26 Mon Sep 17 00:00:00 2001 From: Stefan Pingel <16143240+pinges@users.noreply.github.com> Date: Fri, 13 Mar 2026 09:12:51 +1000 Subject: [PATCH 35/77] Throw RLPException that is handled correctly instead of IllegalArgumentException (#10025) * throw RLPException that is handled correctly instead of IllegalArgumentException Signed-off-by: stefan.pingel@consensys.net Signed-off-by: Stefan Pingel <16143240+pinges@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../TransactionAnnouncementDecoder.java | 4 +++- ...TransactionHashesMessageProcessorTest.java | 20 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/encoding/TransactionAnnouncementDecoder.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/encoding/TransactionAnnouncementDecoder.java index 2d08cbaa96a..efde8de94f5 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/encoding/TransactionAnnouncementDecoder.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/encoding/TransactionAnnouncementDecoder.java @@ -59,7 +59,9 @@ private static List decodeForEth68(final RLPInput input final var transactionType = TransactionType.fromEthSerializedType(b) .orElseThrow( - () -> new IllegalArgumentException("Invalid transaction type %x".formatted(b))); + () -> + new RLPException( + "Invalid transaction type 0x%02x".formatted(Byte.toUnsignedInt(b)))); types.add(transactionType); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessorTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessorTest.java index 01aada53d39..5ceaf60a78f 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessorTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessorTest.java @@ -43,6 +43,7 @@ import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.messages.NewPooledTransactionHashesMessage; import org.hyperledger.besu.ethereum.eth.transactions.NewPooledTransactionHashesMessageProcessor.FetcherCreatorTask; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import org.hyperledger.besu.ethereum.rlp.RLP; import org.hyperledger.besu.ethereum.rlp.RLPException; import org.hyperledger.besu.metrics.StubMetricsSystem; @@ -366,8 +367,8 @@ void shouldThrowRLPExceptionWhenTypeIsInvalid() { () -> TransactionAnnouncementDecoder.getDecoder(EthProtocol.ETH68) .decode(RLP.input(invalidMessageBytes))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Invalid transaction type 7"); + .isInstanceOf(RLPException.class) + .hasMessageContaining("Invalid transaction type 0x07"); } @Test @@ -406,4 +407,19 @@ void shouldThrowNullPointerIfArgumentsAreNull() { .isInstanceOf(NullPointerException.class) .hasMessage("Transaction cannot be null"); } + + @Test + void shouldDisconnectPeerWhenInvalidTransactionType() { + final Bytes invalidMessageBytes = + Bytes.fromHexString( + // ["0x07",["0x00000002"],["0x881699519a25b0e32db9b1ba9981f3fbec93fbc0726c3e096af89e5ada2b1351"]] + "0xe907c58400000002e1a0881699519a25b0e32db9b1ba9981f3fbec93fbc0726c3e096af89e5ada2b1351"); + + final NewPooledTransactionHashesMessage message = + new NewPooledTransactionHashesMessage(invalidMessageBytes, EthProtocol.ETH68); + + messageHandler.processNewPooledTransactionHashesMessage(peer1, message, now(), ofMinutes(1)); + + verify(peer1).disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); + } } From 342c490128664641e5cbdfd4ee12dc3b91556fe7 Mon Sep 17 00:00:00 2001 From: Karim Taam Date: Mon, 16 Mar 2026 11:37:40 +0400 Subject: [PATCH 36/77] remove invalid warning pre glamsterdam (#10049) Signed-off-by: Karim Taam --- .../besu/ethereum/chain/ChainDataPruner.java | 11 +- .../ethereum/chain/ChainDataPrunerTest.java | 173 ++++++++++++++++++ 2 files changed, 181 insertions(+), 3 deletions(-) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/ChainDataPruner.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/ChainDataPruner.java index fbbf39a765f..05737994fb1 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/ChainDataPruner.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/ChainDataPruner.java @@ -72,7 +72,9 @@ private void chainPrunerAction(final BlockAddedEvent event) { final long storedBlockPruningMark = prunerStorage.getChainPruningMark().orElse(blockNumber); final long storedBalPruningMark = prunerStorage.getBalPruningMark().orElse(blockNumber); - validatePruningMarks(blockNumber, storedBlockPruningMark, storedBalPruningMark); + final boolean isBalHashPresent = event.getHeader().getBalHash().isPresent(); + validatePruningMarks( + blockNumber, storedBlockPruningMark, storedBalPruningMark, isBalHashPresent); recordForkBlock(event, blockNumber); if (!event.isNewCanonicalHead()) { @@ -84,14 +86,17 @@ private void chainPrunerAction(final BlockAddedEvent event) { } private void validatePruningMarks( - final long blockNumber, final long storedPruningMark, final long storedBalPruningMark) { + final long blockNumber, + final long storedPruningMark, + final long storedBalPruningMark, + final boolean isBalHashPresent) { if (config.isBlockPruningEnabled() && blockNumber < storedPruningMark) { LOG.warn( "Block number {} is less than pruning mark {} - chain-pruning-blocks-retained may be too small", blockNumber, storedPruningMark); } - if (config.isBalPruningEnabled() && blockNumber < storedBalPruningMark) { + if (config.isBalPruningEnabled() && isBalHashPresent && blockNumber < storedBalPruningMark) { LOG.warn( "Block number {} is less than BAL pruning mark {} - chain-pruning-bals-retained may be too small", blockNumber, diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/ChainDataPrunerTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/ChainDataPrunerTest.java index 9f5c127a2de..6fb3ded3c10 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/ChainDataPrunerTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/ChainDataPrunerTest.java @@ -28,10 +28,17 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import jakarta.validation.constraints.NotNull; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -1379,6 +1386,146 @@ public void balPruningMarkAdvancesAutomaticallyWhenNoBalPresent() { } } + @Test + public void noBalPruningWarningBeforeGlamsterdam() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final BlockchainStorage blockchainStorage = + new KeyValueStoragePrefixedKeyBlockchainStorage( + new InMemoryKeyValueStorage(), + new VariablesKeyValueStorage(new InMemoryKeyValueStorage()), + new MainnetBlockHeaderFunctions(), + false); + final ChainDataPruner chainDataPruner = + new ChainDataPruner( + blockchainStorage, + () -> {}, + new ChainDataPrunerStorage(new InMemoryKeyValueStorage()), + 0, + ChainDataPruner.PruningMode.CHAIN_PRUNING, + new ChainPrunerConfiguration( + ChainDataPruner.ChainPruningStrategy.BAL, + Long.MAX_VALUE, + 256, + Long.MAX_VALUE, + 0, + 0), + new BlockingExecutor()); + Block genesisBlock = gen.genesisBlock(); + final MutableBlockchain blockchain = + DefaultBlockchain.createMutable( + genesisBlock, blockchainStorage, new NoOpMetricsSystem(), 0); + blockchain.observeBlockAdded(chainDataPruner); + + gen.setBlockOptionsSupplier( + () -> BlockDataGenerator.BlockOptions.create().withoutGeneratedBlockAccessList()); + + final List capturedEvents = + withLogCapture( + ChainDataPruner.class, + () -> { + List canonicalChain = gen.blockSequence(genesisBlock, 258); + List forkChain = gen.blockSequence(genesisBlock, 16); + + for (Block blk : canonicalChain) { + blockchain.appendBlock(blk, gen.receipts(blk)); + } + + // Fork blocks at lower heights: blockNumber < storedBalPruningMark, + // but no warning expected because BAL is not activated yet. + for (Block blk : forkChain) { + blockchain.storeBlock(blk, gen.receipts(blk)); + } + }); + + assertThat( + capturedEvents.stream() + .filter(e -> e.getLevel().equals(Level.WARN)) + .map(e -> e.getMessage().getFormattedMessage()) + .anyMatch(msg -> msg.contains("is less than BAL pruning mark"))) + .as("No BAL pruning warning should be emitted for pre-Glamsterdam blocks") + .isFalse(); + } + + @Test + public void balPruningWarningAfterGlamsterdam() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final BlockchainStorage blockchainStorage = + new KeyValueStoragePrefixedKeyBlockchainStorage( + new InMemoryKeyValueStorage(), + new VariablesKeyValueStorage(new InMemoryKeyValueStorage()), + new MainnetBlockHeaderFunctions(), + false); + final ChainDataPruner chainDataPruner = + new ChainDataPruner( + blockchainStorage, + () -> {}, + new ChainDataPrunerStorage(new InMemoryKeyValueStorage()), + 0, + ChainDataPruner.PruningMode.CHAIN_PRUNING, + new ChainPrunerConfiguration( + ChainDataPruner.ChainPruningStrategy.BAL, + Long.MAX_VALUE, + 256, + Long.MAX_VALUE, + 0, + 0), + new BlockingExecutor()); + Block genesisBlock = gen.genesisBlock(); + final MutableBlockchain blockchain = + DefaultBlockchain.createMutable( + genesisBlock, blockchainStorage, new NoOpMetricsSystem(), 0); + blockchain.observeBlockAdded(chainDataPruner); + + gen.setBlockOptionsSupplier( + () -> BlockDataGenerator.BlockOptions.create().withGeneratedBlockAccessList()); + + final List capturedEvents = + withLogCapture( + ChainDataPruner.class, + () -> { + List canonicalChain = + gen.blockSequenceWithAccessList(genesisBlock, 258); + List forkChain = + gen.blockSequenceWithAccessList(genesisBlock, 16); + + for (BlockDataGenerator.BlockWithAccessList blockWithBal : canonicalChain) { + final Block blk = blockWithBal.getBlock(); + blockWithBal + .getBlockAccessList() + .ifPresent( + bal -> { + final BlockchainStorage.Updater updater = blockchainStorage.updater(); + updater.putBlockAccessList(blk.getHash(), bal); + updater.commit(); + }); + blockchain.appendBlock(blk, gen.receipts(blk)); + } + + // Fork blocks with BAL at lower heights (1-16). + // blockNumber < storedBalPruningMark → warning expected. + for (BlockDataGenerator.BlockWithAccessList blockWithBal : forkChain) { + final Block blk = blockWithBal.getBlock(); + blockWithBal + .getBlockAccessList() + .ifPresent( + bal -> { + final BlockchainStorage.Updater updater = blockchainStorage.updater(); + updater.putBlockAccessList(blk.getHash(), bal); + updater.commit(); + }); + blockchain.storeBlock(blk, gen.receipts(blk)); + } + }); + + assertThat( + capturedEvents.stream() + .filter(e -> e.getLevel().equals(Level.WARN)) + .map(e -> e.getMessage().getFormattedMessage()) + .anyMatch(msg -> msg.contains("is less than BAL pruning mark"))) + .as("BAL pruning warning should be emitted for post-Glamsterdam fork blocks") + .isTrue(); + } + @Test public void testPreMergePruningAction() { final BlockDataGenerator gen = new BlockDataGenerator(); @@ -1428,6 +1575,32 @@ public void testPreMergePruningAction() { checkBlocks(blockchain, mergeBlock, blockchain.getChainHeadBlockNumber(), Optional::isPresent); } + /** + * Attaches a temporary log4j appender to the given class's logger, runs the action, and returns + * all captured log events. The appender is properly stopped and removed regardless of outcome. + */ + @SuppressWarnings("BannedMethod") + private static List withLogCapture(final Class loggerClass, final Runnable action) { + final Logger logger = (Logger) LogManager.getLogger(loggerClass); + final List events = new CopyOnWriteArrayList<>(); + final AbstractAppender appender = + new AbstractAppender("test-capture", null, null, false, Property.EMPTY_ARRAY) { + @Override + public void append(final LogEvent event) { + events.add(event.toImmutable()); + } + }; + appender.start(); + logger.addAppender(appender); + try { + action.run(); + } finally { + logger.removeAppender(appender); + appender.stop(); + } + return events; + } + /** * Helper method to check if blocks in a range satisfy a given predicate. * From d210ad4589b20d4bcac6312ce11dcf08d2a68754 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Mon, 16 Mar 2026 11:22:32 +0100 Subject: [PATCH 37/77] Remove Clique block production and mining infrastructure (Phase 3) (#10035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete the blockcreation sub-package (6 production files + 6 test files): CliqueBlockCreator, CliqueBlockMiner, CliqueBlockScheduler, CliqueMinerExecutor, CliqueMiningCoordinator, CliqueProposerSelector, and CliqueMiningTracker. CliqueBesuControllerBuilder.createMiningCoordinator() now returns NoopMiningCoordinator. The overrideMiningConfiguration() override is removed (no longer forcing mining on). The localAddress field is removed as it was only needed for mining setup. CliqueProposerSelector is moved from blockcreation/ to the main clique package since it is still needed by CliqueHelpers.getProposerForBlockAfter(), which is called by CliqueDifficultyCalculator for block validation on Clique→PoS chains. EthStatsService no longer special-cases CliqueMiningCoordinator for isMining reporting; it falls through to the standard miningCoordinator.isMining() path. Signed-off-by: Fabio Di Fabio Co-authored-by: Claude Sonnet 4.6 --- .../genesis/GenesisConfigurationFactory.java | 35 --- .../resources/dev/dev_london.json | 43 --- .../CliqueBesuControllerBuilder.java | 63 +--- .../hyperledger/besu/cli/BesuCommandTest.java | 29 -- .../besu/cli/CommandTestAbstract.java | 10 - .../besu/cli/options/MiningOptionsTest.java | 4 +- .../CliqueBesuControllerBuilderTest.java | 243 --------------- .../TransitionControllerBuilderTest.java | 7 - .../besu/consensus/clique/CliqueHelpers.java | 1 - .../consensus/clique/CliqueMiningTracker.java | 78 ----- .../CliqueProposerSelector.java | 2 +- .../blockcreation/CliqueBlockCreator.java | 149 --------- .../blockcreation/CliqueBlockMiner.java | 95 ------ .../blockcreation/CliqueBlockScheduler.java | 93 ------ .../blockcreation/CliqueMinerExecutor.java | 150 --------- .../CliqueMiningCoordinator.java | 98 ------ .../blockcreation/CliqueBlockCreatorTest.java | 279 ----------------- .../blockcreation/CliqueBlockMinerTest.java | 217 ------------- .../CliqueBlockSchedulerTest.java | 184 ----------- .../CliqueMinerExecutorTest.java | 285 ------------------ .../CliqueMiningCoordinatorTest.java | 265 ---------------- .../CliqueProposerSelectorTest.java | 62 ---- .../besu/ethstats/EthStatsService.java | 8 +- 23 files changed, 6 insertions(+), 2394 deletions(-) delete mode 100644 acceptance-tests/tests/src/acceptanceTest/resources/dev/dev_london.json delete mode 100644 app/src/test/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilderTest.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueMiningTracker.java rename consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/{blockcreation => }/CliqueProposerSelector.java (97%) delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockCreator.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockMiner.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockScheduler.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutor.java delete mode 100644 consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinator.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockCreatorTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockMinerTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockSchedulerTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutorTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java delete mode 100644 consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueProposerSelectorTest.java diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/genesis/GenesisConfigurationFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/genesis/GenesisConfigurationFactory.java index abeeeeddad7..2ae227b02b5 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/genesis/GenesisConfigurationFactory.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/genesis/GenesisConfigurationFactory.java @@ -16,7 +16,6 @@ import static java.util.stream.Collectors.toList; -import org.hyperledger.besu.consensus.clique.CliqueExtraData; import org.hyperledger.besu.consensus.ibft.IbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.datatypes.Address; @@ -44,21 +43,6 @@ private GenesisConfigurationFactory() { throw new IllegalStateException("Utility class"); } - public static Optional createCliqueGenesisConfig( - final Collection validators) { - return createCliqueGenesisConfig(validators, CliqueOptions.DEFAULT); - } - - public static Optional createCliqueGenesisConfig( - final Collection validators, final CliqueOptions cliqueOptions) { - final String template = readGenesisFile("/clique/clique.json.tpl"); - - return updateGenesisExtraData( - validators, - updateGenesisCliqueOptions(template, cliqueOptions), - CliqueExtraData::createGenesisExtraDataString); - } - public static Optional createIbft2GenesisConfig( final Collection validators) { return createIbft2GenesisConfig(validators, "/ibft/ibft.json"); @@ -142,13 +126,6 @@ public static Optional createQbftValidatorContractGenesisConfig( } } - public static Optional createDevLondonGenesisConfig( - final Collection validators) { - final String template = readGenesisFile("/dev/dev_london.json"); - return updateGenesisExtraData( - validators, template, CliqueExtraData::createGenesisExtraDataString); - } - private static Optional updateGenesisExtraData( final Collection validators, final String genesisTemplate, @@ -164,14 +141,6 @@ public static Optional createFromResource(final String resourceName) { return Optional.of(readGenesisFile(resourceName)); } - private static String updateGenesisCliqueOptions( - final String template, final CliqueOptions cliqueOptions) { - return template - .replace("%blockperiodseconds%", String.valueOf(cliqueOptions.blockPeriodSeconds)) - .replace("%epochlength%", String.valueOf(cliqueOptions.epochLength)) - .replace("%createemptyblocks%", String.valueOf(cliqueOptions.createEmptyBlocks)); - } - @SuppressWarnings("UnstableApiUsage") public static String readGenesisFile(final String filepath) { try { @@ -181,8 +150,4 @@ public static String readGenesisFile(final String filepath) { throw new IllegalStateException("Unable to get test genesis config " + filepath); } } - - public record CliqueOptions(int blockPeriodSeconds, int epochLength, boolean createEmptyBlocks) { - public static final CliqueOptions DEFAULT = new CliqueOptions(10, 30000, true); - } } diff --git a/acceptance-tests/tests/src/acceptanceTest/resources/dev/dev_london.json b/acceptance-tests/tests/src/acceptanceTest/resources/dev/dev_london.json deleted file mode 100644 index 709f74a3026..00000000000 --- a/acceptance-tests/tests/src/acceptanceTest/resources/dev/dev_london.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "config":{ - "chainId":20211, - "homesteadBlock":0, - "eip150Block":0, - "eip155Block":0, - "eip158Block":0, - "byzantiumBlock":0, - "constantinopleBlock":0, - "petersburgBlock":0, - "istanbulBlock":0, - "berlinBlock":0, - "londonBlock":0, - "ethash":{ - "fixeddifficulty": 1 - } - }, - "alloc":{ - "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { - "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", - "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", - "balance": "0xad78ebc5ac6200000" - }, - "627306090abaB3A6e1400e9345bC60c78a8BEf57": { - "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", - "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", - "balance": "90000000000000000000000" - }, - "f17f52151EbEF6C7334FAD080c5704D77216b732": { - "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", - "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", - "balance": "90000000000000000000000" - } - }, - "coinbase":"0x0000000000000000000000000000000000000000", - "difficulty":"0x00001", - "extraData":"0x5365706f6c69612c20417468656e732c204174746963612c2047726565636521", - "gasLimit":"0x1c9c380", - "nonce":"0x000000000000000", - "mixhash":"0x0000000000000000000000000000000000000000000000000000000000000000", - "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", - "timestamp":"0x6159af19" -} \ No newline at end of file diff --git a/app/src/main/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilder.java b/app/src/main/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilder.java index 564709ed034..697e08a5e12 100644 --- a/app/src/main/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilder.java +++ b/app/src/main/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilder.java @@ -19,22 +19,17 @@ import org.hyperledger.besu.consensus.clique.CliqueContext; import org.hyperledger.besu.consensus.clique.CliqueForksSchedulesFactory; import org.hyperledger.besu.consensus.clique.CliqueHelpers; -import org.hyperledger.besu.consensus.clique.CliqueMiningTracker; import org.hyperledger.besu.consensus.clique.CliqueProtocolSchedule; -import org.hyperledger.besu.consensus.clique.blockcreation.CliqueBlockScheduler; -import org.hyperledger.besu.consensus.clique.blockcreation.CliqueMinerExecutor; -import org.hyperledger.besu.consensus.clique.blockcreation.CliqueMiningCoordinator; import org.hyperledger.besu.consensus.common.BlockInterface; import org.hyperledger.besu.consensus.common.EpochManager; import org.hyperledger.besu.consensus.common.ForksSchedule; import org.hyperledger.besu.consensus.common.validator.blockbased.BlockValidatorProvider; -import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator; +import org.hyperledger.besu.ethereum.blockcreation.NoopMiningCoordinator; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.MiningConfiguration; -import org.hyperledger.besu.ethereum.core.Util; import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManager; import org.hyperledger.besu.ethereum.eth.sync.state.SyncState; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; @@ -49,7 +44,6 @@ public class CliqueBesuControllerBuilder extends BesuControllerBuilder { private static final Logger LOG = LoggerFactory.getLogger(CliqueBesuControllerBuilder.class); - private Address localAddress; private EpochManager epochManager; private final BlockInterface blockInterface = new CliqueBlockInterface(); private ForksSchedule forksSchedule; @@ -59,7 +53,6 @@ public CliqueBesuControllerBuilder() {} @Override protected void prepForBuild() { - localAddress = Util.publicKeyToAddress(nodeKey.getPublicKey()); final CliqueConfigOptions cliqueConfig = genesisConfigOptions.getCliqueConfigOptions(); final long blocksPerEpoch = cliqueConfig.getEpochLength(); @@ -75,44 +68,7 @@ protected MiningCoordinator createMiningCoordinator( final MiningConfiguration miningConfiguration, final SyncState syncState, final EthProtocolManager ethProtocolManager) { - final CliqueMinerExecutor miningExecutor = - new CliqueMinerExecutor( - protocolContext, - protocolSchedule, - transactionPool, - nodeKey, - miningConfiguration, - new CliqueBlockScheduler( - clock, - protocolContext.getConsensusContext(CliqueContext.class).getValidatorProvider(), - localAddress, - forksSchedule), - epochManager, - forksSchedule, - ethProtocolManager.ethContext().getScheduler()); - final CliqueMiningCoordinator miningCoordinator = - new CliqueMiningCoordinator( - protocolContext.getBlockchain(), - miningExecutor, - syncState, - new CliqueMiningTracker(localAddress, protocolContext)); - - // Update the next block period in seconds according to the transition schedule - protocolContext - .getBlockchain() - .observeBlockAdded( - o -> - miningConfiguration.setBlockPeriodSeconds( - forksSchedule - .getFork(o.getHeader().getNumber() + 1, o.getHeader().getTimestamp()) - .getValue() - .getBlockPeriodSeconds())); - - miningCoordinator.addMinedBlockObserver(ethProtocolManager); - - // Clique mining is implicitly enabled. - miningCoordinator.enable(); - return miningCoordinator; + return new NoopMiningCoordinator(); } @Override @@ -160,19 +116,4 @@ protected CliqueContext createConsensusContext( CliqueHelpers.installCliqueBlockChoiceRule(blockchain, cliqueContext); return cliqueContext; } - - @Override - public void overrideMiningConfiguration(final MiningConfiguration fromCli) { - // Clique ignores CLI mining options and enforces its own requirements: - // - Mining is always enabled (actual block production depends on validator status) - // - Coinbase is always the local validator address (not user-configurable) - // All other CLI configuration values are preserved - fromCli.setMiningEnabled(true); - - final Address localValidatorAddress = - nodeKey != null ? Util.publicKeyToAddress(nodeKey.getPublicKey()) : null; - if (localValidatorAddress != null) { - fromCli.setCoinbase(localValidatorAddress); - } - } } diff --git a/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index 9a09100996b..3ad15d52950 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -2780,21 +2780,6 @@ void chainPruningEnabledWithPOAShouldFailWhenChainPruningBlocksRetainedValueLess assertThat(commandErrorOutput.toString(UTF_8)) .contains( "--Xchain-pruning-blocks-retained must be >= " + CHAIN_DATA_PRUNING_RETAINED_MINIMUM); - commandErrorOutput.reset(); - - // for Clique - genesis.getJsonObject("config").put("clique", new JsonObject().put("epochlength", 10000)); - genesis.getJsonObject("config").remove("ibft2"); - final Path genesisFileClique = createFakeGenesisFile(genesis); - parseCommand( - "--genesis-file", - genesisFileClique.toString(), - "--Xchain-pruning-enabled=ALL", - "--Xchain-pruning-blocks-retained=7200", - "--version-compatibility-protection=false"); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains( - "--Xchain-pruning-blocks-retained must be >= " + CHAIN_DATA_PRUNING_RETAINED_MINIMUM); } @Test @@ -2827,20 +2812,6 @@ void chainPruningEnabledWithPOA() throws IOException { "--version-compatibility-protection=false"); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - - // for Clique - genesis - .getJsonObject("config") - .put("clique", new JsonObject().put("epochlength", CHAIN_DATA_PRUNING_RETAINED_MINIMUM)); - genesis.getJsonObject("config").remove("ibft2"); - final Path genesisFileClique = createFakeGenesisFile(genesis); - parseCommand( - "--genesis-file", - genesisFileClique.toString(), - "--Xchain-pruning-enabled=ALL", - "--Xchain-pruning-blocks-retained=" + CHAIN_DATA_PRUNING_RETAINED_MINIMUM, - "--version-compatibility-protection=false"); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test diff --git a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index 6a07d1d965a..5580990c2a8 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -165,16 +165,6 @@ public abstract class CommandTestAbstract { "ibft2", new JsonObject().put("blockperiodseconds", POA_BLOCK_PERIOD_SECONDS))); - protected static final JsonObject VALID_GENESIS_CLIQUE_POST_LONDON = - (new JsonObject()) - .put( - "config", - new JsonObject() - .put("londonBlock", 0) - .put( - "clique", - new JsonObject().put("blockperiodseconds", POA_BLOCK_PERIOD_SECONDS))); - protected static final JsonObject VALID_GENESIS_CLIQUE_WITH_POS_TRANSITION = (new JsonObject()) .put( diff --git a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java index d97e9bdbd59..c7af2fcda4c 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java @@ -291,7 +291,7 @@ public void poaBlockTxsSelectionMaxTimeOption() throws IOException { @Test public void poaBlockTxsSelectionMaxTimeOptionOver100Percent() throws IOException { - final Path genesisFileClique = createFakeGenesisFile(VALID_GENESIS_CLIQUE_POST_LONDON); + final Path genesisFileIBFT2 = createFakeGenesisFile(VALID_GENESIS_IBFT2_POST_LONDON); internalTestSuccess( this::runtimeConfiguration, miningParams -> { @@ -301,7 +301,7 @@ public void poaBlockTxsSelectionMaxTimeOptionOver100Percent() throws IOException .isEqualTo(Duration.ofSeconds(POA_BLOCK_PERIOD_SECONDS * 2)); }, "--genesis-file", - genesisFileClique.toString(), + genesisFileIBFT2.toString(), "--poa-block-txs-selection-max-time", "200"); } diff --git a/app/src/test/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilderTest.java b/app/src/test/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilderTest.java deleted file mode 100644 index 7cf1ec99644..00000000000 --- a/app/src/test/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilderTest.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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.controller; - -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.mock; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.components.BesuComponent; -import org.hyperledger.besu.config.CheckpointConfigOptions; -import org.hyperledger.besu.config.GenesisConfig; -import org.hyperledger.besu.config.GenesisConfigOptions; -import org.hyperledger.besu.config.ImmutableCliqueConfigOptions; -import org.hyperledger.besu.config.TransitionsConfigOptions; -import org.hyperledger.besu.consensus.clique.CliqueBlockHeaderFunctions; -import org.hyperledger.besu.cryptoservices.NodeKey; -import org.hyperledger.besu.cryptoservices.NodeKeyUtils; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.datatypes.LogsBloomFilter; -import org.hyperledger.besu.datatypes.Wei; -import org.hyperledger.besu.ethereum.api.ImmutableApiConfiguration; -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.Difficulty; -import org.hyperledger.besu.ethereum.core.MiningConfiguration; -import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; -import org.hyperledger.besu.ethereum.eth.sync.SyncMode; -import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; -import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions; -import org.hyperledger.besu.ethereum.p2p.config.NetworkingConfiguration; -import org.hyperledger.besu.ethereum.storage.StorageProvider; -import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStoragePrefixedKeyBlockchainStorage; -import org.hyperledger.besu.ethereum.storage.keyvalue.VariablesKeyValueStorage; -import org.hyperledger.besu.ethereum.trie.forest.storage.ForestWorldStateKeyValueStorage; -import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; -import org.hyperledger.besu.ethereum.worldstate.WorldStatePreimageStorage; -import org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator; -import org.hyperledger.besu.evm.internal.EvmConfiguration; -import org.hyperledger.besu.metrics.ObservableMetricsSystem; -import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; -import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; - -import java.math.BigInteger; -import java.nio.file.Path; -import java.time.Clock; -import java.time.Duration; -import java.util.List; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.collect.Range; -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -public class CliqueBesuControllerBuilderTest { - - private BesuControllerBuilder cliqueBesuControllerBuilder; - - @Mock private GenesisConfig genesisConfig; - @Mock private GenesisConfigOptions genesisConfigOptions; - @Mock private SynchronizerConfiguration synchronizerConfiguration; - @Mock private EthProtocolConfiguration ethProtocolConfiguration; - @Mock private CheckpointConfigOptions checkpointConfigOptions; - @Mock private Clock clock; - @Mock private StorageProvider storageProvider; - @Mock private WorldStatePreimageStorage worldStatePreimageStorage; - private static final BigInteger networkId = BigInteger.ONE; - private static final NodeKey nodeKey = NodeKeyUtils.generate(); - private final TransactionPoolConfiguration poolConfiguration = - TransactionPoolConfiguration.DEFAULT; - private final ObservableMetricsSystem observableMetricsSystem = new NoOpMetricsSystem(); - private final ObjectMapper objectMapper = new ObjectMapper(); - private final MiningConfiguration miningConfiguration = MiningConfiguration.newDefault(); - - @TempDir Path tempDir; - - @BeforeEach - public void setup() throws JsonProcessingException { - // Clique Besu controller setup - final ForestWorldStateKeyValueStorage worldStateKeyValueStorage = - mock(ForestWorldStateKeyValueStorage.class); - final WorldStateStorageCoordinator worldStateStorageCoordinator = - new WorldStateStorageCoordinator(worldStateKeyValueStorage); - - lenient().when(genesisConfig.getParentHash()).thenReturn(Hash.ZERO.getBytes().toHexString()); - lenient().when(genesisConfig.getDifficulty()).thenReturn(Bytes.of(0).toHexString()); - when(genesisConfig.getExtraData()) - .thenReturn( - "0x0000000000000000000000000000000000000000000000000000000000000000b9b81ee349c3807e46bc71aa2632203c5b4620340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); - lenient().when(genesisConfig.getMixHash()).thenReturn(Hash.ZERO.getBytes().toHexString()); - lenient().when(genesisConfig.getNonce()).thenReturn(Long.toHexString(1)); - lenient().when(genesisConfig.getConfigOptions()).thenReturn(genesisConfigOptions); - lenient().when(genesisConfigOptions.getCheckpointOptions()).thenReturn(checkpointConfigOptions); - lenient() - .when(storageProvider.createBlockchainStorage(any(), any(), any())) - .thenReturn( - new KeyValueStoragePrefixedKeyBlockchainStorage( - new InMemoryKeyValueStorage(), - new VariablesKeyValueStorage(new InMemoryKeyValueStorage()), - new MainnetBlockHeaderFunctions(), - false)); - lenient() - .when( - storageProvider.createWorldStateStorageCoordinator( - DataStorageConfiguration.DEFAULT_FOREST_CONFIG)) - .thenReturn(worldStateStorageCoordinator); - lenient().when(worldStateKeyValueStorage.isWorldStateAvailable(any())).thenReturn(true); - lenient() - .when(worldStateKeyValueStorage.updater()) - .thenReturn(mock(ForestWorldStateKeyValueStorage.Updater.class)); - lenient() - .when(worldStatePreimageStorage.updater()) - .thenReturn(mock(WorldStatePreimageStorage.Updater.class)); - lenient() - .when(storageProvider.createWorldStatePreimageStorage()) - .thenReturn(worldStatePreimageStorage); - lenient().when(synchronizerConfiguration.getDownloaderParallelism()).thenReturn(1); - lenient().when(synchronizerConfiguration.getTransactionsParallelism()).thenReturn(1); - lenient().when(synchronizerConfiguration.getComputationParallelism()).thenReturn(1); - lenient().when(synchronizerConfiguration.getSyncMode()).thenReturn(SyncMode.FULL); - - lenient() - .when(synchronizerConfiguration.getBlockPropagationRange()) - .thenReturn(Range.closed(1L, 2L)); - - // clique prepForBuild setup - lenient() - .when(genesisConfigOptions.getCliqueConfigOptions()) - .thenReturn( - ImmutableCliqueConfigOptions.builder() - .epochLength(30) - .createEmptyBlocks(true) - .blockPeriodSeconds(1) - .build()); - - lenient() - .when(ethProtocolConfiguration.getMaxEthCapability()) - .thenReturn(EthProtocolConfiguration.DEFAULT_MAX_CAPABILITY); - - final var jsonTransitions = - (ObjectNode) - objectMapper.readTree( - """ - {"clique": [ - { - "block": 2, - "blockperiodseconds": 2 - } - ]} - """); - - lenient() - .when(genesisConfigOptions.getTransitions()) - .thenReturn(new TransitionsConfigOptions(jsonTransitions)); - - cliqueBesuControllerBuilder = - new CliqueBesuControllerBuilder() - .genesisConfig(genesisConfig) - .synchronizerConfiguration(synchronizerConfiguration) - .ethProtocolConfiguration(ethProtocolConfiguration) - .networkId(networkId) - .miningParameters(miningConfiguration) - .metricsSystem(observableMetricsSystem) - .dataDirectory(tempDir) - .clock(clock) - .transactionPoolConfiguration(poolConfiguration) - .dataStorageConfiguration(DataStorageConfiguration.DEFAULT_FOREST_CONFIG) - .nodeKey(nodeKey) - .storageProvider(storageProvider) - .evmConfiguration(EvmConfiguration.DEFAULT) - .besuComponent(mock(BesuComponent.class)) - .networkConfiguration(NetworkingConfiguration.DEFAULT) - .apiConfiguration(ImmutableApiConfiguration.builder().build()); - } - - @Test - public void miningParametersBlockPeriodSecondsIsUpdatedOnTransition() { - final var besuController = cliqueBesuControllerBuilder.build(); - final var protocolContext = besuController.getProtocolContext(); - final var protocolSchedule = besuController.getProtocolSchedule(); - - final BlockHeader header1 = - new BlockHeader( - protocolContext.getBlockchain().getChainHeadHash(), - Hash.EMPTY_TRIE_HASH, - Address.ZERO, - Hash.EMPTY_TRIE_HASH, - Hash.EMPTY_TRIE_HASH, - Hash.EMPTY_TRIE_HASH, - LogsBloomFilter.builder().build(), - Difficulty.ONE, - 1, - 0, - 0, - 0, - Bytes.EMPTY, - Wei.ZERO, - Bytes32.wrap(Hash.EMPTY.getBytes()), - 0, - null, - null, - null, - null, - null, - null, - null, // slotNumber - new CliqueBlockHeaderFunctions()); - final Block block1 = new Block(header1, BlockBody.empty()); - - protocolContext.getBlockchain().appendBlock(block1, List.of()); - - assertThat(miningConfiguration.getBlockPeriodSeconds()).isNotEmpty().hasValue(2); - assertThat( - miningConfiguration.getBlockTxsSelectionMaxTime( - protocolSchedule.getByBlockHeader(header1).isPoS())) - .isEqualTo(Duration.ofMillis(2000 * 75 / 100)); - } -} diff --git a/app/src/test/java/org/hyperledger/besu/controller/TransitionControllerBuilderTest.java b/app/src/test/java/org/hyperledger/besu/controller/TransitionControllerBuilderTest.java index dc8eee16eb5..1b6e18454b0 100644 --- a/app/src/test/java/org/hyperledger/besu/controller/TransitionControllerBuilderTest.java +++ b/app/src/test/java/org/hyperledger/besu/controller/TransitionControllerBuilderTest.java @@ -123,13 +123,6 @@ public void setup() { miningConfiguration = MiningConfiguration.newDefault(); } - @Test - public void assertCliqueMiningOverridePreMerge() { - assertThat(miningConfiguration.isMiningEnabled()).isFalse(); - var transCoordinator = buildTransitionCoordinator(cliqueBuilder, postMergeBuilder); - assertThat(transCoordinator.isMiningBeforeMerge()).isTrue(); - } - @Test public void assertPoWIsNotMiningPreMerge() { assertThat(miningConfiguration.isMiningEnabled()).isFalse(); diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueHelpers.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueHelpers.java index 362be20a1cc..e0eff0bdf5e 100644 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueHelpers.java +++ b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueHelpers.java @@ -14,7 +14,6 @@ */ package org.hyperledger.besu.consensus.clique; -import org.hyperledger.besu.consensus.clique.blockcreation.CliqueProposerSelector; import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.ethereum.ProtocolContext; diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueMiningTracker.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueMiningTracker.java deleted file mode 100644 index 8f350609dc8..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueMiningTracker.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.core.BlockHeader; - -/** The Clique mining tracker. */ -public class CliqueMiningTracker { - - private final Address localAddress; - private final ProtocolContext protocolContext; - - /** - * Instantiates a new Clique mining tracker. - * - * @param localAddress the local address - * @param protocolContext the protocol context - */ - public CliqueMiningTracker(final Address localAddress, final ProtocolContext protocolContext) { - this.localAddress = localAddress; - this.protocolContext = protocolContext; - } - - /** - * Is next proposer. - * - * @param header the header - * @return the boolean - */ - public boolean isProposerAfter(final BlockHeader header) { - final Address nextProposer = CliqueHelpers.getProposerForBlockAfter(header); - return localAddress.equals(nextProposer); - } - - /** - * Is signer. - * - * @param header the header - * @return the boolean - */ - public boolean isSigner(final BlockHeader header) { - return CliqueHelpers.isSigner(localAddress, protocolContext, header); - } - - /** - * Can make block next round. - * - * @param header the header - * @return the boolean - */ - public boolean canMakeBlockNextRound(final BlockHeader header) { - return CliqueHelpers.addressIsAllowedToProduceNextBlock(localAddress, protocolContext, header); - } - - /** - * Block created locally. - * - * @param header the header - * @return the boolean - */ - public boolean blockCreatedLocally(final BlockHeader header) { - return CliqueHelpers.getProposerOfBlock(header).equals(localAddress); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueProposerSelector.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueProposerSelector.java similarity index 97% rename from consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueProposerSelector.java rename to consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueProposerSelector.java index a6ba67c46d6..058325f4c23 100644 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueProposerSelector.java +++ b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/CliqueProposerSelector.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.consensus.clique.blockcreation; +package org.hyperledger.besu.consensus.clique; import static com.google.common.base.Preconditions.checkNotNull; diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockCreator.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockCreator.java deleted file mode 100644 index 00e54a38b64..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockCreator.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import static com.google.common.base.Preconditions.checkState; - -import org.hyperledger.besu.consensus.clique.CliqueBlockHashing; -import org.hyperledger.besu.consensus.clique.CliqueBlockInterface; -import org.hyperledger.besu.consensus.clique.CliqueContext; -import org.hyperledger.besu.consensus.clique.CliqueExtraData; -import org.hyperledger.besu.consensus.common.EpochManager; -import org.hyperledger.besu.consensus.common.validator.ValidatorVote; -import org.hyperledger.besu.cryptoservices.NodeKey; -import org.hyperledger.besu.datatypes.Hash; -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.BlockHeaderFunctions; -import org.hyperledger.besu.ethereum.core.MiningConfiguration; -import org.hyperledger.besu.ethereum.core.SealableBlockHeader; -import org.hyperledger.besu.ethereum.core.Util; -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.ScheduleBasedBlockHeaderFunctions; - -import java.util.Optional; - -import org.apache.tuweni.bytes.Bytes32; - -/** The Clique block creator. */ -public class CliqueBlockCreator extends AbstractBlockCreator { - - private final NodeKey nodeKey; - private final EpochManager epochManager; - - /** - * Instantiates a new Clique block creator. - * - * @param miningConfiguration the mining parameters - * @param extraDataCalculator the extra data calculator - * @param transactionPool the pending transactions - * @param protocolContext the protocol context - * @param protocolSchedule the protocol schedule - * @param nodeKey the node key - * @param epochManager the epoch manager - * @param ethScheduler the scheduler for asynchronous block creation tasks - */ - public CliqueBlockCreator( - final MiningConfiguration miningConfiguration, - final ExtraDataCalculator extraDataCalculator, - final TransactionPool transactionPool, - final ProtocolContext protocolContext, - final ProtocolSchedule protocolSchedule, - final NodeKey nodeKey, - final EpochManager epochManager, - final EthScheduler ethScheduler) { - super( - miningConfiguration, - (__, ___) -> Util.publicKeyToAddress(nodeKey.getPublicKey()), - extraDataCalculator, - transactionPool, - protocolContext, - protocolSchedule, - ethScheduler); - this.nodeKey = nodeKey; - this.epochManager = epochManager; - } - - /** - * Responsible for signing (hash of) the block (including MixHash and Nonce), and then injecting - * the seal into the extraData. This is called after a suitable set of transactions have been - * identified, and all resulting hashes have been inserted into the passed-in SealableBlockHeader. - * - * @param sealableBlockHeader A block header containing StateRoots, TransactionHashes etc. - * @return The blockhead which is to be added to the block being proposed. - */ - @Override - protected BlockHeader createFinalBlockHeader(final SealableBlockHeader sealableBlockHeader) { - final BlockHeaderFunctions blockHeaderFunctions = - ScheduleBasedBlockHeaderFunctions.create(protocolSchedule); - - final BlockHeaderBuilder builder = - BlockHeaderBuilder.create() - .populateFrom(sealableBlockHeader) - .mixHash(Hash.ZERO) - .blockHeaderFunctions(blockHeaderFunctions); - - final Optional vote = determineCliqueVote(sealableBlockHeader); - final BlockHeaderBuilder builderIncludingProposedVotes = - CliqueBlockInterface.createHeaderBuilderWithVoteHeaders(builder, vote); - final CliqueExtraData sealedExtraData = - constructSignedExtraData(builderIncludingProposedVotes.buildBlockHeader()); - - // Replace the extraData in the BlockHeaderBuilder, and return header. - return builderIncludingProposedVotes.extraData(sealedExtraData.encode()).buildBlockHeader(); - } - - private Optional determineCliqueVote( - final SealableBlockHeader sealableBlockHeader) { - BlockHeader parentHeader = - protocolContext.getBlockchain().getBlockHeader(sealableBlockHeader.getParentHash()).get(); - if (epochManager.isEpochBlock(sealableBlockHeader.getNumber())) { - return Optional.empty(); - } else { - final CliqueContext cliqueContext = protocolContext.getConsensusContext(CliqueContext.class); - checkState( - cliqueContext.getValidatorProvider().getVoteProviderAtHead().isPresent(), - "Clique requires a vote provider"); - return cliqueContext - .getValidatorProvider() - .getVoteProviderAtHead() - .get() - .getVoteAfterBlock(parentHeader, Util.publicKeyToAddress(nodeKey.getPublicKey())); - } - } - - /** - * Produces a CliqueExtraData object with a populated proposerSeal. The signature in the block is - * generated from the Hash of the header (minus proposer and committer seals) and the nodeKeys. - * - * @param headerToSign An almost fully populated header (proposer and committer seals are empty) - * @return Extra data containing the same vanity data and validators as extraData, however - * proposerSeal will also be populated. - */ - private CliqueExtraData constructSignedExtraData(final BlockHeader headerToSign) { - final CliqueExtraData extraData = CliqueExtraData.decode(headerToSign); - final Hash hashToSign = - CliqueBlockHashing.calculateDataHashForProposerSeal(headerToSign, extraData); - return new CliqueExtraData( - extraData.getVanityData(), - nodeKey.sign(Bytes32.wrap(hashToSign.getBytes())), - extraData.getValidators(), - headerToSign); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockMiner.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockMiner.java deleted file mode 100644 index 3b1beadcb58..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockMiner.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import org.hyperledger.besu.config.CliqueConfigOptions; -import org.hyperledger.besu.consensus.clique.CliqueHelpers; -import org.hyperledger.besu.consensus.common.ForksSchedule; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.blockcreation.BlockMiner; -import org.hyperledger.besu.ethereum.blockcreation.DefaultBlockScheduler; -import org.hyperledger.besu.ethereum.chain.MinedBlockObserver; -import org.hyperledger.besu.ethereum.core.Block; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.util.Subscribers; - -import java.util.function.Function; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** The Clique block miner. */ -public class CliqueBlockMiner extends BlockMiner { - private static final Logger LOG = LoggerFactory.getLogger(CliqueBlockMiner.class); - private static final int WAIT_IN_MS_BETWEEN_EMPTY_BUILD_ATTEMPTS = 1_000; - - private final Address localAddress; - private final ForksSchedule forksSchedule; - - /** - * Instantiates a new Clique block miner. - * - * @param blockCreator the block creator - * @param protocolSchedule the protocol schedule - * @param protocolContext the protocol context - * @param observers the observers - * @param scheduler the scheduler - * @param parentHeader the parent header - * @param localAddress the local address - * @param forksSchedule the transitions - */ - public CliqueBlockMiner( - final Function blockCreator, - final ProtocolSchedule protocolSchedule, - final ProtocolContext protocolContext, - final Subscribers observers, - final DefaultBlockScheduler scheduler, - final BlockHeader parentHeader, - final Address localAddress, - final ForksSchedule forksSchedule) { - super(blockCreator, protocolSchedule, protocolContext, observers, scheduler, parentHeader); - this.localAddress = localAddress; - this.forksSchedule = forksSchedule; - } - - @Override - protected boolean mineBlock() throws InterruptedException { - if (CliqueHelpers.addressIsAllowedToProduceNextBlock( - localAddress, protocolContext, parentHeader)) { - return super.mineBlock(); - } - - return true; // terminate mining. - } - - @Override - protected boolean shouldImportBlock(final Block block) throws InterruptedException { - if (forksSchedule - .getFork(block.getHeader().getNumber(), block.getHeader().getTimestamp()) - .getValue() - .getCreateEmptyBlocks()) { - return true; - } - - final boolean isEmpty = block.getBody().getTransactions().isEmpty(); - if (isEmpty) { - LOG.debug("Skipping creating empty block {}", block.toLogString()); - Thread.sleep(WAIT_IN_MS_BETWEEN_EMPTY_BUILD_ATTEMPTS); - } - return !isEmpty; - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockScheduler.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockScheduler.java deleted file mode 100644 index 3f4cfb3a4fa..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockScheduler.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import org.hyperledger.besu.config.CliqueConfigOptions; -import org.hyperledger.besu.consensus.common.ForksSchedule; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.blockcreation.DefaultBlockScheduler; -import org.hyperledger.besu.ethereum.core.BlockHeader; - -import java.time.Clock; -import java.util.Collection; -import java.util.Random; - -import com.google.common.annotations.VisibleForTesting; - -/** The Clique block scheduler. */ -public class CliqueBlockScheduler extends DefaultBlockScheduler { - - private final int OUT_OF_TURN_DELAY_MULTIPLIER_MILLIS = 500; - - private final ValidatorProvider validatorProvider; - private final Address localNodeAddress; - private final Random r = new Random(); - - /** - * Instantiates a new Clique block scheduler. - * - * @param clock the clock - * @param validatorProvider the validator provider - * @param localNodeAddress the local node address - * @param forksSchedule the transitions - */ - public CliqueBlockScheduler( - final Clock clock, - final ValidatorProvider validatorProvider, - final Address localNodeAddress, - final ForksSchedule forksSchedule) { - super( - parentHeader -> - (long) - forksSchedule - .getFork(parentHeader.getNumber() + 1, parentHeader.getTimestamp()) - .getValue() - .getBlockPeriodSeconds(), - 0L, - clock); - this.validatorProvider = validatorProvider; - this.localNodeAddress = localNodeAddress; - } - - @Override - @VisibleForTesting - public BlockCreationTimeResult getNextTimestamp(final BlockHeader parentHeader) { - final BlockCreationTimeResult result = super.getNextTimestamp(parentHeader); - - final long milliSecondsUntilNextBlock = - result.millisecondsUntilValid() + calculateTurnBasedDelay(parentHeader); - - return new BlockCreationTimeResult( - result.timestampForHeader(), Math.max(0, milliSecondsUntilNextBlock)); - } - - private int calculateTurnBasedDelay(final BlockHeader parentHeader) { - final CliqueProposerSelector proposerSelector = new CliqueProposerSelector(validatorProvider); - final Address nextProposer = proposerSelector.selectProposerForNextBlock(parentHeader); - - if (nextProposer.equals(localNodeAddress)) { - return 0; - } - return calculatorOutOfTurnDelay(validatorProvider.getValidatorsAfterBlock(parentHeader)); - } - - private int calculatorOutOfTurnDelay(final Collection

validators) { - final int countSigners = validators.size(); - final double multiplier = (countSigners / 2d) + 1; - final int maxDelay = (int) (multiplier * OUT_OF_TURN_DELAY_MULTIPLIER_MILLIS); - return r.nextInt(maxDelay) + 1; - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutor.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutor.java deleted file mode 100644 index bcdd69d5dd4..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutor.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import org.hyperledger.besu.config.CliqueConfigOptions; -import org.hyperledger.besu.consensus.clique.CliqueContext; -import org.hyperledger.besu.consensus.clique.CliqueExtraData; -import org.hyperledger.besu.consensus.common.ConsensusHelpers; -import org.hyperledger.besu.consensus.common.EpochManager; -import org.hyperledger.besu.consensus.common.ForksSchedule; -import org.hyperledger.besu.cryptoservices.NodeKey; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.blockcreation.AbstractMinerExecutor; -import org.hyperledger.besu.ethereum.blockcreation.DefaultBlockScheduler; -import org.hyperledger.besu.ethereum.chain.MinedBlockObserver; -import org.hyperledger.besu.ethereum.chain.PoWObserver; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.MiningConfiguration; -import org.hyperledger.besu.ethereum.core.Util; -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.util.Subscribers; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Lists; -import org.apache.tuweni.bytes.Bytes; - -/** The Clique miner executor. */ -public class CliqueMinerExecutor extends AbstractMinerExecutor { - - private final Address localAddress; - private final NodeKey nodeKey; - private final EpochManager epochManager; - private final ForksSchedule forksSchedule; - - /** - * Instantiates a new Clique miner executor. - * - * @param protocolContext the protocol context - * @param protocolSchedule the protocol schedule - * @param transactionPool the pending transactions - * @param nodeKey the node key - * @param miningParams the mining params - * @param blockScheduler the block scheduler - * @param epochManager the epoch manager - * @param forksSchedule the clique transitions - * @param ethScheduler the scheduler for asynchronous block creation tasks - */ - public CliqueMinerExecutor( - final ProtocolContext protocolContext, - final ProtocolSchedule protocolSchedule, - final TransactionPool transactionPool, - final NodeKey nodeKey, - final MiningConfiguration miningParams, - final DefaultBlockScheduler blockScheduler, - final EpochManager epochManager, - final ForksSchedule forksSchedule, - final EthScheduler ethScheduler) { - super( - protocolContext, - protocolSchedule, - transactionPool, - miningParams, - blockScheduler, - ethScheduler); - this.nodeKey = nodeKey; - this.localAddress = Util.publicKeyToAddress(nodeKey.getPublicKey()); - this.epochManager = epochManager; - this.forksSchedule = forksSchedule; - } - - @Override - public CliqueBlockMiner createMiner( - final Subscribers observers, - final Subscribers ethHashObservers, - final BlockHeader parentHeader) { - final Function blockCreator = - (header) -> - new CliqueBlockCreator( - miningConfiguration, - this::calculateExtraData, - transactionPool, - protocolContext, - protocolSchedule, - nodeKey, - epochManager, - ethScheduler); - - return new CliqueBlockMiner( - blockCreator, - protocolSchedule, - protocolContext, - observers, - blockScheduler, - parentHeader, - localAddress, - forksSchedule); - } - - @Override - public Optional
getCoinbase() { - return Optional.of(localAddress); - } - - /** - * Calculate extra data bytes. - * - * @param parentHeader the parent header - * @return the bytes - */ - @VisibleForTesting - Bytes calculateExtraData(final BlockHeader parentHeader) { - final List
validators = Lists.newArrayList(); - - final Bytes vanityDataToInsert = - ConsensusHelpers.zeroLeftPad( - miningConfiguration.getExtraData(), CliqueExtraData.EXTRA_VANITY_LENGTH); - // Building ON TOP of canonical head, if the next block is epoch, include validators. - if (epochManager.isEpochBlock(parentHeader.getNumber() + 1)) { - - final Collection
storedValidators = - protocolContext - .getConsensusContext(CliqueContext.class) - .getValidatorProvider() - .getValidatorsAfterBlock(parentHeader); - validators.addAll(storedValidators); - } - - return CliqueExtraData.encodeUnsealed(vanityDataToInsert, validators); - } -} diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinator.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinator.java deleted file mode 100644 index 8320cb242ad..00000000000 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinator.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import org.hyperledger.besu.consensus.clique.CliqueMiningTracker; -import org.hyperledger.besu.ethereum.blockcreation.AbstractMiningCoordinator; -import org.hyperledger.besu.ethereum.chain.Blockchain; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.eth.sync.state.SyncState; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** The type Clique mining coordinator. */ -public class CliqueMiningCoordinator extends AbstractMiningCoordinator { - - private static final Logger LOG = LoggerFactory.getLogger(CliqueMiningCoordinator.class); - - private final CliqueMiningTracker miningTracker; - - /** - * Instantiates a new Clique mining coordinator. - * - * @param blockchain the blockchain - * @param executor the executor - * @param syncState the sync state - * @param miningTracker the mining tracker - */ - public CliqueMiningCoordinator( - final Blockchain blockchain, - final CliqueMinerExecutor executor, - final SyncState syncState, - final CliqueMiningTracker miningTracker) { - super(blockchain, executor, syncState); - this.miningTracker = miningTracker; - } - - @Override - public void onResumeMining() { - if (isSigner()) { - LOG.info("Resuming block production operations"); - } - } - - @Override - public void onPauseMining() { - if (isSigner()) { - LOG.info("Pausing block production while behind chain head"); - } - } - - /** - * Is signer. - * - * @return the boolean - */ - public boolean isSigner() { - return miningTracker.isSigner(blockchain.getChainHeadHeader()); - } - - @Override - protected boolean newChainHeadInvalidatesMiningOperation(final BlockHeader newChainHeadHeader) { - if (currentRunningMiner.isEmpty()) { - return true; - } - - if (miningTracker.blockCreatedLocally(newChainHeadHeader)) { - return true; - } - - return networkBlockBetterThanCurrentMiner(newChainHeadHeader); - } - - private boolean networkBlockBetterThanCurrentMiner(final BlockHeader newChainHeadHeader) { - final BlockHeader parentHeader = currentRunningMiner.get().getParentHeader(); - final long currentMinerTargetHeight = parentHeader.getNumber() + 1; - if (currentMinerTargetHeight < newChainHeadHeader.getNumber()) { - return true; - } - - final boolean nodeIsMining = miningTracker.canMakeBlockNextRound(parentHeader); - final boolean nodeIsInTurn = miningTracker.isProposerAfter(parentHeader); - - return !nodeIsMining || !nodeIsInTurn; - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockCreatorTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockCreatorTest.java deleted file mode 100644 index e9f0884b82f..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockCreatorTest.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryBlockchain; -import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryWorldStateArchive; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.config.GenesisConfig; -import org.hyperledger.besu.consensus.clique.CliqueBlockInterface; -import org.hyperledger.besu.consensus.clique.CliqueContext; -import org.hyperledger.besu.consensus.clique.CliqueExtraData; -import org.hyperledger.besu.consensus.clique.CliqueHelpers; -import org.hyperledger.besu.consensus.clique.CliqueProtocolSchedule; -import org.hyperledger.besu.consensus.clique.TestHelpers; -import org.hyperledger.besu.consensus.common.EpochManager; -import org.hyperledger.besu.consensus.common.ForksSchedule; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.consensus.common.validator.ValidatorVote; -import org.hyperledger.besu.consensus.common.validator.VoteProvider; -import org.hyperledger.besu.consensus.common.validator.VoteType; -import org.hyperledger.besu.crypto.KeyPair; -import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; -import org.hyperledger.besu.cryptoservices.NodeKey; -import org.hyperledger.besu.cryptoservices.NodeKeyUtils; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Wei; -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.chain.BadBlockManager; -import org.hyperledger.besu.ethereum.chain.GenesisState; -import org.hyperledger.besu.ethereum.chain.MutableBlockchain; -import org.hyperledger.besu.ethereum.core.AddressHelpers; -import org.hyperledger.besu.ethereum.core.Block; -import org.hyperledger.besu.ethereum.core.BlockBody; -import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; -import org.hyperledger.besu.ethereum.core.ImmutableMiningConfiguration; -import org.hyperledger.besu.ethereum.core.ImmutableMiningConfiguration.MutableInitValues; -import org.hyperledger.besu.ethereum.core.MiningConfiguration; -import org.hyperledger.besu.ethereum.core.Util; -import org.hyperledger.besu.ethereum.eth.manager.EthContext; -import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; -import org.hyperledger.besu.ethereum.eth.transactions.BlobCache; -import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionBroadcaster; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; -import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter; -import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.CodeCache; -import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; -import org.hyperledger.besu.evm.internal.EvmConfiguration; -import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; -import org.hyperledger.besu.plugin.services.MetricsSystem; -import org.hyperledger.besu.testutil.DeterministicEthScheduler; -import org.hyperledger.besu.testutil.TestClock; - -import java.time.ZoneId; -import java.util.List; -import java.util.Optional; - -import com.google.common.collect.Lists; -import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class CliqueBlockCreatorTest { - - private final NodeKey proposerNodeKey = NodeKeyUtils.generate(); - private final Address proposerAddress = Util.publicKeyToAddress(proposerNodeKey.getPublicKey()); - private final KeyPair otherKeyPair = SignatureAlgorithmFactory.getInstance().generateKeyPair(); - private final List
validatorList = Lists.newArrayList(); - private final MetricsSystem metricsSystem = new NoOpMetricsSystem(); - private final CliqueBlockInterface blockInterface = new CliqueBlockInterface(); - private final EthScheduler ethScheduler = new DeterministicEthScheduler(); - private ProtocolSchedule protocolSchedule; - private final WorldStateArchive stateArchive = createInMemoryWorldStateArchive(); - - private MutableBlockchain blockchain; - private ProtocolContext protocolContext; - private EpochManager epochManager; - private ValidatorProvider validatorProvider; - private VoteProvider voteProvider; - - @BeforeEach - void setup() { - final Address otherAddress = Util.publicKeyToAddress(otherKeyPair.getPublicKey()); - validatorList.add(otherAddress); - - validatorProvider = mock(ValidatorProvider.class); - voteProvider = mock(VoteProvider.class); - when(validatorProvider.getVoteProviderAtHead()).thenReturn(Optional.of(voteProvider)); - when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(validatorList); - - protocolSchedule = - CliqueProtocolSchedule.create( - GenesisConfig.DEFAULT.getConfigOptions(), - new ForksSchedule<>(List.of()), - proposerNodeKey, - false, - EvmConfiguration.DEFAULT, - MiningConfiguration.MINING_DISABLED, - new BadBlockManager(), - false, - BalConfiguration.DEFAULT, - new NoOpMetricsSystem()); - - final CliqueContext cliqueContext = new CliqueContext(validatorProvider, null, blockInterface); - CliqueHelpers.setCliqueContext(cliqueContext); - - final Block genesis = - GenesisState.fromConfig(GenesisConfig.mainnet(), protocolSchedule, new CodeCache()) - .getBlock(); - blockchain = createInMemoryBlockchain(genesis); - protocolContext = - new ProtocolContext.Builder() - .withBlockchain(blockchain) - .withWorldStateArchive(stateArchive) - .withConsensusContext(cliqueContext) - .build(); - epochManager = new EpochManager(10); - - // Add a block above the genesis - final BlockHeaderTestFixture headerTestFixture = new BlockHeaderTestFixture(); - headerTestFixture.number(1).parentHash(genesis.getHeader().getHash()); - final Block emptyBlock = - new Block( - TestHelpers.createCliqueSignedBlockHeader( - headerTestFixture, otherKeyPair, validatorList), - new BlockBody(Lists.newArrayList(), Lists.newArrayList())); - blockchain.appendBlock(emptyBlock, Lists.newArrayList()); - } - - @Test - public void proposerAddressCanBeExtractFromAConstructedBlock() { - - final Bytes extraData = - CliqueExtraData.createWithoutProposerSeal(Bytes.wrap(new byte[32]), validatorList); - - final Address coinbase = AddressHelpers.ofValue(1); - - final MiningConfiguration miningConfiguration = createMiningConfiguration(extraData, coinbase); - - final CliqueBlockCreator blockCreator = - new CliqueBlockCreator( - miningConfiguration, - parent -> extraData, - createTransactionPool(), - protocolContext, - protocolSchedule, - proposerNodeKey, - epochManager, - ethScheduler); - - final Block createdBlock = - blockCreator.createBlock(5L, blockchain.getChainHeadHeader()).getBlock(); - - assertThat(CliqueHelpers.getProposerOfBlock(createdBlock.getHeader())) - .isEqualTo(proposerAddress); - } - - @Test - public void insertsValidVoteIntoConstructedBlock() { - final Bytes extraData = - CliqueExtraData.createWithoutProposerSeal(Bytes.wrap(new byte[32]), validatorList); - final Address a1 = Address.fromHexString("5"); - final Address coinbase = AddressHelpers.ofValue(1); - when(voteProvider.getVoteAfterBlock(any(), any())) - .thenReturn(Optional.of(new ValidatorVote(VoteType.ADD, coinbase, a1))); - - final MiningConfiguration miningConfiguration = createMiningConfiguration(extraData, coinbase); - - final CliqueBlockCreator blockCreator = - new CliqueBlockCreator( - miningConfiguration, - parent -> extraData, - createTransactionPool(), - protocolContext, - protocolSchedule, - proposerNodeKey, - epochManager, - ethScheduler); - - final Block createdBlock = - blockCreator.createBlock(0L, blockchain.getChainHeadHeader()).getBlock(); - assertThat(createdBlock.getHeader().getNonce()).isEqualTo(CliqueBlockInterface.ADD_NONCE); - assertThat(createdBlock.getHeader().getCoinbase()).isEqualTo(a1); - } - - @Test - public void insertsNoVoteWhenAtEpoch() { - // ensure that the next block is epoch - epochManager = new EpochManager(1); - - final Bytes extraData = - CliqueExtraData.createWithoutProposerSeal(Bytes.wrap(new byte[32]), validatorList); - final Address a1 = Address.fromHexString("5"); - final Address coinbase = AddressHelpers.ofValue(1); - - final VoteProvider mockVoteProvider = mock(VoteProvider.class); - when(validatorProvider.getVoteProviderAtHead()).thenReturn(Optional.of(mockVoteProvider)); - when(mockVoteProvider.getVoteAfterBlock(any(), any())) - .thenReturn(Optional.of(new ValidatorVote(VoteType.ADD, coinbase, a1))); - - final MiningConfiguration miningConfiguration = createMiningConfiguration(extraData, coinbase); - - final CliqueBlockCreator blockCreator = - new CliqueBlockCreator( - miningConfiguration, - parent -> extraData, - createTransactionPool(), - protocolContext, - protocolSchedule, - proposerNodeKey, - epochManager, - ethScheduler); - - final Block createdBlock = - blockCreator.createBlock(0L, blockchain.getChainHeadHeader()).getBlock(); - assertThat(createdBlock.getHeader().getNonce()).isEqualTo(CliqueBlockInterface.DROP_NONCE); - assertThat(createdBlock.getHeader().getCoinbase()).isEqualTo(Address.fromHexString("0")); - } - - private TransactionPool createTransactionPool() { - final TransactionPoolConfiguration conf = - ImmutableTransactionPoolConfiguration.builder().txPoolMaxSize(5).build(); - final EthContext ethContext = mock(EthContext.class, RETURNS_DEEP_STUBS); - when(ethContext.getEthPeers().subscribeConnect(any())).thenReturn(1L); - final TransactionPool transactionPool = - new TransactionPool( - () -> - new GasPricePendingTransactionsSorter( - conf, - TestClock.system(ZoneId.systemDefault()), - metricsSystem, - blockchain::getChainHeadHeader), - protocolSchedule, - protocolContext, - mock(TransactionBroadcaster.class), - ethContext, - new TransactionPoolMetrics(metricsSystem), - conf, - new BlobCache()); - transactionPool.setEnabled(); - return transactionPool; - } - - private static MiningConfiguration createMiningConfiguration( - final Bytes extraData, final Address coinbase) { - final MiningConfiguration miningConfiguration = - ImmutableMiningConfiguration.builder() - .mutableInitValues( - MutableInitValues.builder() - .extraData(extraData) - .targetGasLimit(10_000_000L) - .minTransactionGasPrice(Wei.ZERO) - .coinbase(coinbase) - .build()) - .build(); - return miningConfiguration; - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockMinerTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockMinerTest.java deleted file mode 100644 index d6898428ce6..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockMinerTest.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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.consensus.clique.blockcreation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.FRONTIER; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.config.CliqueConfigOptions; -import org.hyperledger.besu.config.ImmutableCliqueConfigOptions; -import org.hyperledger.besu.config.JsonCliqueConfigOptions; -import org.hyperledger.besu.consensus.clique.CliqueContext; -import org.hyperledger.besu.consensus.common.ForkSpec; -import org.hyperledger.besu.consensus.common.ForksSchedule; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.crypto.KeyPair; -import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.blockcreation.BlockCreationTiming; -import org.hyperledger.besu.ethereum.blockcreation.BlockCreator; -import org.hyperledger.besu.ethereum.blockcreation.DefaultBlockScheduler; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; -import org.hyperledger.besu.ethereum.chain.MinedBlockObserver; -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.BlockImporter; -import org.hyperledger.besu.ethereum.core.Transaction; -import org.hyperledger.besu.ethereum.core.TransactionTestFixture; -import org.hyperledger.besu.ethereum.mainnet.BlockImportResult; -import org.hyperledger.besu.ethereum.mainnet.DefaultProtocolSchedule; -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.util.Subscribers; - -import java.math.BigInteger; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -import com.google.common.collect.Lists; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class CliqueBlockMinerTest { - - private ForksSchedule forksSchedule; - - @BeforeEach - public void setup() { - var options = ImmutableCliqueConfigOptions.builder().from(JsonCliqueConfigOptions.DEFAULT); - options.createEmptyBlocks(false); - forksSchedule = new ForksSchedule<>(List.of(new ForkSpec<>(0, options.build()))); - } - - @Test - void doesNotMineBlockIfNoTransactionsWhenEmptyBlocksNotAllowed() throws InterruptedException { - final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - - final Block blockToCreate = - new Block( - headerBuilder.buildHeader(), new BlockBody(Lists.newArrayList(), Lists.newArrayList())); - - final ValidatorProvider validatorProvider = mock(ValidatorProvider.class); - when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(List.of(Address.ZERO)); - - final CliqueContext cliqueContext = new CliqueContext(validatorProvider, null, null); - final ProtocolContext protocolContext = - new ProtocolContext.Builder().withConsensusContext(cliqueContext).build(); - - final CliqueBlockCreator blockCreator = mock(CliqueBlockCreator.class); - final Function blockCreatorSupplier = - (parentHeader) -> blockCreator; - when(blockCreator.createBlock(anyLong(), any())) - .thenReturn( - new BlockCreator.BlockCreationResult( - blockToCreate, - new TransactionSelectionResults(), - new BlockCreationTiming(), - Optional.empty())); - - final BlockImporter blockImporter = mock(BlockImporter.class); - final ProtocolSpec protocolSpec = mock(ProtocolSpec.class); - when(protocolSpec.getHardforkId()).thenReturn(FRONTIER); - final ProtocolSchedule protocolSchedule = singleSpecSchedule(protocolSpec); - - when(protocolSpec.getBlockImporter()).thenReturn(blockImporter); - when(blockImporter.importBlock(any(), any(), any(), any(), any())) - .thenReturn(new BlockImportResult(true)); - - final MinedBlockObserver observer = mock(MinedBlockObserver.class); - final DefaultBlockScheduler scheduler = mock(DefaultBlockScheduler.class); - when(scheduler.waitUntilNextBlockCanBeMined(any())).thenReturn(5L); - final CliqueBlockMiner miner = - new CliqueBlockMiner( - blockCreatorSupplier, - protocolSchedule, - protocolContext, - subscribersContaining(observer), - scheduler, - headerBuilder.buildHeader(), - Address.ZERO, - forksSchedule); // parent header is arbitrary for the test. - - final boolean result = miner.mineBlock(); - assertThat(result).isFalse(); - verify(blockImporter, never()) - .importBlock( - protocolContext, - blockToCreate, - HeaderValidationMode.FULL, - HeaderValidationMode.FULL, - Optional.empty()); - verify(observer, never()).blockMined(blockToCreate); - } - - @Test - void minesBlockIfHasTransactionsWhenEmptyBlocksNotAllowed() throws InterruptedException { - final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - - final TransactionTestFixture transactionTestFixture = new TransactionTestFixture(); - final KeyPair keyPair = SignatureAlgorithmFactory.getInstance().generateKeyPair(); - final Transaction transaction = transactionTestFixture.createTransaction(keyPair); - - final Block blockToCreate = - new Block( - headerBuilder.buildHeader(), new BlockBody(List.of(transaction), Lists.newArrayList())); - - final ValidatorProvider validatorProvider = mock(ValidatorProvider.class); - when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(List.of(Address.ZERO)); - - final CliqueContext cliqueContext = new CliqueContext(validatorProvider, null, null); - final ProtocolContext protocolContext = - new ProtocolContext.Builder().withConsensusContext(cliqueContext).build(); - - final CliqueBlockCreator blockCreator = mock(CliqueBlockCreator.class); - final Function blockCreatorSupplier = - (parentHeader) -> blockCreator; - when(blockCreator.createBlock(anyLong(), any())) - .thenReturn( - new BlockCreator.BlockCreationResult( - blockToCreate, - new TransactionSelectionResults(), - new BlockCreationTiming(), - Optional.empty())); - - final BlockImporter blockImporter = mock(BlockImporter.class); - final ProtocolSpec protocolSpec = mock(ProtocolSpec.class); - when(protocolSpec.getHardforkId()).thenReturn(FRONTIER); - final ProtocolSchedule protocolSchedule = singleSpecSchedule(protocolSpec); - - when(protocolSpec.getBlockImporter()).thenReturn(blockImporter); - when(blockImporter.importBlock(any(), any(), any(), any(), any())) - .thenReturn(new BlockImportResult(true)); - - final MinedBlockObserver observer = mock(MinedBlockObserver.class); - final DefaultBlockScheduler scheduler = mock(DefaultBlockScheduler.class); - when(scheduler.waitUntilNextBlockCanBeMined(any())).thenReturn(5L); - final CliqueBlockMiner miner = - new CliqueBlockMiner( - blockCreatorSupplier, - protocolSchedule, - protocolContext, - subscribersContaining(observer), - scheduler, - headerBuilder.buildHeader(), - Address.ZERO, - forksSchedule); // parent header is arbitrary for the test. - - final boolean result = miner.mineBlock(); - assertThat(result).isTrue(); - verify(blockImporter) - .importBlock( - protocolContext, - blockToCreate, - HeaderValidationMode.FULL, - HeaderValidationMode.FULL, - Optional.empty()); - verify(observer).blockMined(blockToCreate); - } - - private static Subscribers subscribersContaining( - final MinedBlockObserver... observers) { - final Subscribers result = Subscribers.create(); - for (final MinedBlockObserver obs : observers) { - result.subscribe(obs); - } - return result; - } - - private ProtocolSchedule singleSpecSchedule(final ProtocolSpec protocolSpec) { - final DefaultProtocolSchedule protocolSchedule = - new DefaultProtocolSchedule(Optional.of(BigInteger.valueOf(1234))); - protocolSchedule.putBlockNumberMilestone(0, protocolSpec); - return protocolSchedule; - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockSchedulerTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockSchedulerTest.java deleted file mode 100644 index d87281a76e7..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockSchedulerTest.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.config.CliqueConfigOptions; -import org.hyperledger.besu.config.ImmutableCliqueConfigOptions; -import org.hyperledger.besu.config.JsonCliqueConfigOptions; -import org.hyperledger.besu.consensus.common.ForkSpec; -import org.hyperledger.besu.consensus.common.ForksSchedule; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.crypto.KeyPair; -import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.blockcreation.DefaultBlockScheduler.BlockCreationTimeResult; -import org.hyperledger.besu.ethereum.core.AddressHelpers; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; -import org.hyperledger.besu.ethereum.core.Util; - -import java.time.Clock; -import java.util.List; - -import com.google.common.collect.Lists; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class CliqueBlockSchedulerTest { - private final KeyPair proposerKeyPair = SignatureAlgorithmFactory.getInstance().generateKeyPair(); - private Address localAddr; - - private final List
validatorList = Lists.newArrayList(); - private ValidatorProvider validatorProvider; - private BlockHeaderTestFixture blockHeaderBuilder; - private ForksSchedule forksSchedule; - - @BeforeEach - public void setup() { - localAddr = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); - - validatorList.add(localAddr); - validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddr, 1)); - - validatorProvider = mock(ValidatorProvider.class); - when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(validatorList); - - blockHeaderBuilder = new BlockHeaderTestFixture(); - - var initialTransition = - ImmutableCliqueConfigOptions.builder().from(JsonCliqueConfigOptions.DEFAULT); - initialTransition.blockPeriodSeconds(5); - forksSchedule = new ForksSchedule<>(List.of(new ForkSpec<>(0, initialTransition.build()))); - } - - @Test - public void inturnValidatorWaitsExactlyBlockInterval() { - final Clock clock = mock(Clock.class); - final long currentSecondsSinceEpoch = 10L; - final int secondsBetweenBlocks = 5; - when(clock.millis()).thenReturn(currentSecondsSinceEpoch * 1000); - final CliqueBlockScheduler scheduler = - new CliqueBlockScheduler(clock, validatorProvider, localAddr, forksSchedule); - - // There are 2 validators, therefore block 2 will put localAddr as the in-turn voter, therefore - // parent block should be number 1. - final BlockHeader parentHeader = - blockHeaderBuilder.number(1).timestamp(currentSecondsSinceEpoch).buildHeader(); - - final BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader); - - assertThat(result.timestampForHeader()) - .isEqualTo(currentSecondsSinceEpoch + secondsBetweenBlocks); - assertThat(result.millisecondsUntilValid()).isEqualTo(secondsBetweenBlocks * 1000); - } - - @Test - public void validatorWithTransitionForBlockTimeWaitsBlockInterval() { - final Clock clock = mock(Clock.class); - final long currentSecondsSinceEpoch = 10L; - when(clock.millis()).thenReturn(currentSecondsSinceEpoch * 1000); - - final var initialTransition = - ImmutableCliqueConfigOptions.builder().from(JsonCliqueConfigOptions.DEFAULT); - initialTransition.blockPeriodSeconds(5); - final var decreaseBlockTimeTransition = - ImmutableCliqueConfigOptions.builder().from(JsonCliqueConfigOptions.DEFAULT); - decreaseBlockTimeTransition.blockPeriodSeconds(1); - forksSchedule = - new ForksSchedule<>( - List.of( - new ForkSpec<>(0, initialTransition.build()), - new ForkSpec<>(4, decreaseBlockTimeTransition.build()))); - - final CliqueBlockScheduler scheduler = - new CliqueBlockScheduler(clock, validatorProvider, localAddr, forksSchedule); - - // getNextTimestamp for last block before transition - // There are 2 validators, therefore block 3 will put localAddr as the out-of-turn voter, - // therefore - // parent block should be number 2. - BlockHeader parentHeader = - blockHeaderBuilder.number(2).timestamp(currentSecondsSinceEpoch).buildHeader(); - BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader); - assertThat(result.timestampForHeader()).isEqualTo(currentSecondsSinceEpoch + 5); - assertThat(result.millisecondsUntilValid()).isGreaterThan(5 * 1000); - - // getNextTimestamp for transition block - // There are 2 validators, therefore block 4 will put localAddr as the in-turn voter, therefore - // parent block should be number 3. - parentHeader = blockHeaderBuilder.number(3).timestamp(currentSecondsSinceEpoch).buildHeader(); - result = scheduler.getNextTimestamp(parentHeader); - assertThat(result.timestampForHeader()).isEqualTo(currentSecondsSinceEpoch + 1); - assertThat(result.millisecondsUntilValid()).isEqualTo(1000); - - // getNextTimestamp for block after transition - // There are 2 validators, therefore block 5 will put localAddr as the out-of-turn voter, - // therefore - // parent block should be number 4. - parentHeader = blockHeaderBuilder.number(4).timestamp(currentSecondsSinceEpoch).buildHeader(); - result = scheduler.getNextTimestamp(parentHeader); - assertThat(result.timestampForHeader()).isEqualTo(currentSecondsSinceEpoch + 1); - assertThat(result.millisecondsUntilValid()).isGreaterThan(1000); - } - - @Test - public void outOfTurnValidatorWaitsLongerThanBlockInterval() { - final Clock clock = mock(Clock.class); - final long currentSecondsSinceEpoch = 10L; - when(clock.millis()).thenReturn(currentSecondsSinceEpoch * 1000); - final CliqueBlockScheduler scheduler = - new CliqueBlockScheduler(clock, validatorProvider, localAddr, forksSchedule); - - // There are 2 validators, therefore block 3 will put localAddr as the out-turn voter, therefore - // parent block should be number 2. - final BlockHeader parentHeader = - blockHeaderBuilder.number(2).timestamp(currentSecondsSinceEpoch).buildHeader(); - - final BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader); - - long secondsBetweenBlocks = 5L; - assertThat(result.timestampForHeader()) - .isEqualTo(currentSecondsSinceEpoch + secondsBetweenBlocks); - assertThat(result.millisecondsUntilValid()).isGreaterThan(secondsBetweenBlocks * 1000); - } - - @Test - public void inTurnValidatorCreatesBlockNowIFParentTimestampSufficientlyBehindNow() { - final Clock clock = mock(Clock.class); - final long currentSecondsSinceEpoch = 10L; - final long secondsBetweenBlocks = 5L; - when(clock.millis()).thenReturn(currentSecondsSinceEpoch * 1000); - final CliqueBlockScheduler scheduler = - new CliqueBlockScheduler(clock, validatorProvider, localAddr, forksSchedule); - - // There are 2 validators, therefore block 2 will put localAddr as the in-turn voter, therefore - // parent block should be number 1. - final BlockHeader parentHeader = - blockHeaderBuilder - .number(1) - .timestamp(currentSecondsSinceEpoch - secondsBetweenBlocks) - .buildHeader(); - - final BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader); - - assertThat(result.timestampForHeader()).isEqualTo(currentSecondsSinceEpoch); - assertThat(result.millisecondsUntilValid()).isEqualTo(0); - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutorTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutorTest.java deleted file mode 100644 index 953421b3653..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutorTest.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryBlockchain; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.config.GenesisConfig; -import org.hyperledger.besu.config.GenesisConfigOptions; -import org.hyperledger.besu.consensus.clique.CliqueBlockHeaderFunctions; -import org.hyperledger.besu.consensus.clique.CliqueBlockInterface; -import org.hyperledger.besu.consensus.clique.CliqueContext; -import org.hyperledger.besu.consensus.clique.CliqueExtraData; -import org.hyperledger.besu.consensus.clique.CliqueHelpers; -import org.hyperledger.besu.consensus.clique.CliqueProtocolSchedule; -import org.hyperledger.besu.consensus.common.EpochManager; -import org.hyperledger.besu.consensus.common.ForksSchedule; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.cryptoservices.NodeKey; -import org.hyperledger.besu.cryptoservices.NodeKeyUtils; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Wei; -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.chain.BadBlockManager; -import org.hyperledger.besu.ethereum.chain.MutableBlockchain; -import org.hyperledger.besu.ethereum.core.AddressHelpers; -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.ImmutableMiningConfiguration; -import org.hyperledger.besu.ethereum.core.ImmutableMiningConfiguration.MutableInitValues; -import org.hyperledger.besu.ethereum.core.MiningConfiguration; -import org.hyperledger.besu.ethereum.core.Util; -import org.hyperledger.besu.ethereum.eth.manager.EthContext; -import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; -import org.hyperledger.besu.ethereum.eth.transactions.BlobCache; -import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionBroadcaster; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; -import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter; -import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.evm.internal.EvmConfiguration; -import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; -import org.hyperledger.besu.plugin.services.MetricsSystem; -import org.hyperledger.besu.testutil.DeterministicEthScheduler; -import org.hyperledger.besu.testutil.TestClock; - -import java.time.ZoneId; -import java.util.List; -import java.util.Optional; -import java.util.Random; - -import com.google.common.collect.Lists; -import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class CliqueMinerExecutorTest { - - private static final int EPOCH_LENGTH = 10; - private static final GenesisConfigOptions GENESIS_CONFIG_OPTIONS = - GenesisConfig.fromConfig("{}").getConfigOptions(); - private final NodeKey proposerNodeKey = NodeKeyUtils.generate(); - private final Random random = new Random(21341234L); - private Address localAddress; - private final List
validatorList = Lists.newArrayList(); - private ProtocolContext cliqueProtocolContext; - private ProtocolSchedule cliqueProtocolSchedule; - private EthContext cliqueEthContext; - private BlockHeaderTestFixture blockHeaderBuilder; - private final MetricsSystem metricsSystem = new NoOpMetricsSystem(); - private final CliqueBlockInterface blockInterface = new CliqueBlockInterface(); - private final EthScheduler ethScheduler = new DeterministicEthScheduler(); - - @BeforeEach - public void setup() { - localAddress = Util.publicKeyToAddress(proposerNodeKey.getPublicKey()); - validatorList.add(localAddress); - validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddress, 1)); - validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddress, 2)); - validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddress, 3)); - - final ValidatorProvider validatorProvider = mock(ValidatorProvider.class); - when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(validatorList); - - final CliqueContext cliqueContext = new CliqueContext(validatorProvider, null, blockInterface); - CliqueHelpers.setCliqueContext(cliqueContext); - - final Block genesis = - new Block( - new BlockHeaderTestFixture().buildHeader(), - new BlockBody(Lists.newArrayList(), Lists.newArrayList())); - final MutableBlockchain blockchain = createInMemoryBlockchain(genesis); - - cliqueProtocolContext = - new ProtocolContext.Builder() - .withBlockchain(blockchain) - .withConsensusContext(cliqueContext) - .build(); - cliqueProtocolSchedule = - CliqueProtocolSchedule.create( - GENESIS_CONFIG_OPTIONS, - new ForksSchedule<>(List.of()), - proposerNodeKey, - false, - EvmConfiguration.DEFAULT, - MiningConfiguration.MINING_DISABLED, - new BadBlockManager(), - false, - BalConfiguration.DEFAULT, - new NoOpMetricsSystem()); - cliqueEthContext = mock(EthContext.class, RETURNS_DEEP_STUBS); - blockHeaderBuilder = new BlockHeaderTestFixture(); - } - - @Test - public void extraDataCreatedOnEpochBlocksContainsValidators() { - final Bytes vanityData = generateRandomVanityData(); - - final MiningConfiguration miningConfiguration = createMiningConfiguration(vanityData); - - final CliqueMinerExecutor executor = - new CliqueMinerExecutor( - cliqueProtocolContext, - cliqueProtocolSchedule, - createTransactionPool(), - proposerNodeKey, - miningConfiguration, - mock(CliqueBlockScheduler.class), - new EpochManager(EPOCH_LENGTH), - null, - ethScheduler); - - // NOTE: Passing in the *parent* block, so must be 1 less than EPOCH - final BlockHeader header = blockHeaderBuilder.number(EPOCH_LENGTH - 1).buildHeader(); - - final Bytes extraDataBytes = executor.calculateExtraData(header); - - final CliqueExtraData cliqueExtraData = - CliqueExtraData.decode( - blockHeaderBuilder - .number(EPOCH_LENGTH) - .extraData(extraDataBytes) - .blockHeaderFunctions(new CliqueBlockHeaderFunctions()) - .buildHeader()); - - assertThat(cliqueExtraData.getVanityData()).isEqualTo(vanityData); - assertThat(cliqueExtraData.getValidators()) - .containsExactly(validatorList.toArray(new Address[0])); - } - - @Test - public void extraDataForNonEpochBlocksDoesNotContainValidators() { - final Bytes vanityData = generateRandomVanityData(); - - final MiningConfiguration miningConfiguration = createMiningConfiguration(vanityData); - - final CliqueMinerExecutor executor = - new CliqueMinerExecutor( - cliqueProtocolContext, - cliqueProtocolSchedule, - createTransactionPool(), - proposerNodeKey, - miningConfiguration, - mock(CliqueBlockScheduler.class), - new EpochManager(EPOCH_LENGTH), - null, - ethScheduler); - - // Parent block was epoch, so the next block should contain no validators. - final BlockHeader header = blockHeaderBuilder.number(EPOCH_LENGTH).buildHeader(); - - final Bytes extraDataBytes = executor.calculateExtraData(header); - - final CliqueExtraData cliqueExtraData = - CliqueExtraData.decode( - blockHeaderBuilder - .number(EPOCH_LENGTH) - .extraData(extraDataBytes) - .blockHeaderFunctions(new CliqueBlockHeaderFunctions()) - .buildHeader()); - - assertThat(cliqueExtraData.getVanityData()).isEqualTo(vanityData); - assertThat(cliqueExtraData.getValidators()).isEqualTo(Lists.newArrayList()); - } - - @Test - public void shouldUseLatestVanityData() { - final Bytes initialVanityData = generateRandomVanityData(); - final Bytes modifiedVanityData = generateRandomVanityData(); - - final MiningConfiguration miningConfiguration = createMiningConfiguration(initialVanityData); - - final CliqueMinerExecutor executor = - new CliqueMinerExecutor( - cliqueProtocolContext, - cliqueProtocolSchedule, - createTransactionPool(), - proposerNodeKey, - miningConfiguration, - mock(CliqueBlockScheduler.class), - new EpochManager(EPOCH_LENGTH), - null, - ethScheduler); - - miningConfiguration.setExtraData(modifiedVanityData); - final Bytes extraDataBytes = executor.calculateExtraData(blockHeaderBuilder.buildHeader()); - - final CliqueExtraData cliqueExtraData = - CliqueExtraData.decode( - blockHeaderBuilder - .number(EPOCH_LENGTH) - .extraData(extraDataBytes) - .blockHeaderFunctions(new CliqueBlockHeaderFunctions()) - .buildHeader()); - assertThat(cliqueExtraData.getVanityData()).isEqualTo(modifiedVanityData); - } - - private TransactionPool createTransactionPool() { - final var conf = ImmutableTransactionPoolConfiguration.builder().txPoolMaxSize(1).build(); - - when(cliqueEthContext.getEthPeers().subscribeConnect(any())).thenReturn(1L); - - final TransactionPool transactionPool = - new TransactionPool( - () -> - new GasPricePendingTransactionsSorter( - conf, - TestClock.system(ZoneId.systemDefault()), - metricsSystem, - CliqueMinerExecutorTest::mockBlockHeader), - cliqueProtocolSchedule, - cliqueProtocolContext, - mock(TransactionBroadcaster.class), - cliqueEthContext, - new TransactionPoolMetrics(metricsSystem), - conf, - new BlobCache()); - - transactionPool.setEnabled(); - return transactionPool; - } - - private static BlockHeader mockBlockHeader() { - final BlockHeader blockHeader = mock(BlockHeader.class); - when(blockHeader.getBaseFee()).thenReturn(Optional.empty()); - return blockHeader; - } - - private Bytes generateRandomVanityData() { - final byte[] vanityData = new byte[32]; - random.nextBytes(vanityData); - return Bytes.wrap(vanityData); - } - - private static MiningConfiguration createMiningConfiguration(final Bytes vanityData) { - return ImmutableMiningConfiguration.builder() - .mutableInitValues( - MutableInitValues.builder() - .extraData(vanityData) - .minTransactionGasPrice(Wei.ZERO) - .coinbase(AddressHelpers.ofValue(1)) - .build()) - .build(); - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java deleted file mode 100644 index dcd869d588c..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryBlockchain; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.consensus.clique.CliqueBlockInterface; -import org.hyperledger.besu.consensus.clique.CliqueContext; -import org.hyperledger.besu.consensus.clique.CliqueHelpers; -import org.hyperledger.besu.consensus.clique.CliqueMiningTracker; -import org.hyperledger.besu.consensus.clique.TestHelpers; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -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.Hash; -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.chain.MutableBlockchain; -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.Util; -import org.hyperledger.besu.ethereum.eth.sync.state.SyncState; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import org.assertj.core.util.Lists; -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.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -@SuppressWarnings("DirectInvocationOnMock") -public class CliqueMiningCoordinatorTest { - - private static final SignatureAlgorithm SIGNATURE_ALGORITHM = - SignatureAlgorithmFactory.getInstance(); - - private final KeyPair proposerKeys = SIGNATURE_ALGORITHM.generateKeyPair(); - private final KeyPair validatorKeys = SIGNATURE_ALGORITHM.generateKeyPair(); - private final Address proposerAddress = Util.publicKeyToAddress(proposerKeys.getPublicKey()); - private final Address validatorAddress = Util.publicKeyToAddress(validatorKeys.getPublicKey()); - - private final List
validators = Lists.newArrayList(validatorAddress, proposerAddress); - - private final BlockHeaderTestFixture headerTestFixture = new BlockHeaderTestFixture(); - - private CliqueMiningTracker miningTracker; - private final CliqueBlockInterface blockInterface = new CliqueBlockInterface(); - - @Mock private MutableBlockchain blockChain; - @Mock private ProtocolContext protocolContext; - @Mock private CliqueMinerExecutor minerExecutor; - @Mock private CliqueBlockMiner blockMiner; - @Mock private SyncState syncState; - @Mock private ValidatorProvider validatorProvider; - @Mock private BlockHeader blockHeader; - - @BeforeEach - public void setup() { - headerTestFixture.number(1); - Block genesisBlock = createEmptyBlock(0, Hash.ZERO, proposerKeys); // not normally signed but ok - blockChain = createInMemoryBlockchain(genesisBlock); - - when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner)); - when(syncState.isInSync()).thenReturn(true); - - miningTracker = new CliqueMiningTracker(proposerAddress, protocolContext); - } - - @Test - public void outOfTurnBlockImportedDoesNotInterruptInTurnMiningOperation() { - setupCliqueContextAndBlockchain(); - - // As the head of the blockChain is 0 (which effectively doesn't have a signer, all validators - // are able to propose. - - when(blockMiner.getParentHeader()).thenReturn(blockHeader); - - // Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_1 - // should be created by proposer. - final CliqueMiningCoordinator coordinator = - new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); - - coordinator.enable(); - coordinator.start(); - - verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any()); - - reset(minerExecutor); - - final Block importedBlock = createEmptyBlock(1, blockChain.getChainHeadHash(), validatorKeys); - - blockChain.appendBlock(importedBlock, Collections.emptyList()); - - // The minerExecutor should not be invoked as the mining operation was conducted by an in-turn - // validator, and the created block came from an out-turn validator. - verify(minerExecutor, never()).startAsyncMining(any(), any(), any()); - } - - @Test - public void outOfTurnBlockImportedAtHigherLevelInterruptsMiningOperation() { - // As the head of the blockChain is 1 (which effectively doesn't have a signer, all validators - // are able to propose. - when(blockMiner.getParentHeader()).thenReturn(blockChain.getChainHeadHeader()); - - // Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_1 - // should be created by proposer. - final CliqueMiningCoordinator coordinator = - new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); - - coordinator.enable(); - coordinator.start(); - - verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any()); - - reset(minerExecutor); - when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner)); - - final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys); - - blockChain.appendBlock(importedBlock, Collections.emptyList()); - - // The minerExecutor should not be invoked as the mining operation was conducted by an in-turn - // validator, and the created block came from an out-turn validator. - ArgumentCaptor varArgs = ArgumentCaptor.forClass(BlockHeader.class); - verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture()); - assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader()); - } - - @Test - public void outOfTurnBlockImportedInterruptsOutOfTurnMiningOperation() { - setupCliqueContextAndBlockchain(); - - blockChain.appendBlock( - createEmptyBlock(1, blockChain.getChainHeadHash(), validatorKeys), Collections.emptyList()); - - when(blockMiner.getParentHeader()).thenReturn(blockChain.getChainHeadHeader()); - - // Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_2 - // should be created by 'validator', thus Proposer is out-of-turn. - final CliqueMiningCoordinator coordinator = - new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); - - coordinator.enable(); - coordinator.start(); - - verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any()); - - reset(minerExecutor); - when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner)); - - final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys); - - blockChain.appendBlock(importedBlock, Collections.emptyList()); - - // The minerExecutor should not be invoked as the mining operation was conducted by an in-turn - // validator, and the created block came from an out-turn validator. - ArgumentCaptor varArgs = ArgumentCaptor.forClass(BlockHeader.class); - verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture()); - assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader()); - } - - @Test - public void outOfTurnBlockImportedInterruptsNonRunningMiner() { - setupCliqueContextAndBlockchain(); - - blockChain.appendBlock( - createEmptyBlock(1, blockChain.getChainHeadHash(), proposerKeys), Collections.emptyList()); - - when(blockMiner.getParentHeader()).thenReturn(blockChain.getChainHeadHeader()); - - // Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_2 - // should be created by 'validator', thus Proposer is out-of-turn. - final CliqueMiningCoordinator coordinator = - new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); - - coordinator.enable(); - coordinator.start(); - - verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any()); - - reset(minerExecutor); - when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner)); - - final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys); - - blockChain.appendBlock(importedBlock, Collections.emptyList()); - - // The minerExecutor should not be invoked as the mining operation was conducted by an in-turn - // validator, and the created block came from an out-turn validator. - ArgumentCaptor varArgs = ArgumentCaptor.forClass(BlockHeader.class); - verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture()); - assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader()); - } - - @Test - public void locallyGeneratedBlockInvalidatesMiningEvenIfInTurn() { - // Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_1 - // should be created by Proposer, and thus will be in-turn. - final CliqueMiningCoordinator coordinator = - new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); - - coordinator.enable(); - coordinator.start(); - - verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any()); - - reset(minerExecutor); - when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner)); - - final Block importedBlock = createEmptyBlock(1, blockChain.getChainHeadHash(), proposerKeys); - blockChain.appendBlock(importedBlock, Collections.emptyList()); - - // The minerExecutor should not be invoked as the mining operation was conducted by an in-turn - // validator, and the created block came from an out-turn validator. - ArgumentCaptor varArgs = ArgumentCaptor.forClass(BlockHeader.class); - verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture()); - assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader()); - } - - private Block createEmptyBlock( - final long blockNumber, final Hash parentHash, final KeyPair signer) { - headerTestFixture.number(blockNumber).parentHash(parentHash); - final BlockHeader header = - TestHelpers.createCliqueSignedBlockHeader(headerTestFixture, signer, validators); - return new Block(header, new BlockBody(Collections.emptyList(), Collections.emptyList())); - } - - private void setupCliqueContextAndBlockchain() { - when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(validators); - - final CliqueContext cliqueContext = new CliqueContext(validatorProvider, null, blockInterface); - CliqueHelpers.setCliqueContext(cliqueContext); - when(protocolContext.getConsensusContext(CliqueContext.class)).thenReturn(cliqueContext); - - when(protocolContext.getBlockchain()).thenReturn(blockChain); - } -} diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueProposerSelectorTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueProposerSelectorTest.java deleted file mode 100644 index 18c2b8bca18..00000000000 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueProposerSelectorTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.consensus.clique.blockcreation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.core.AddressHelpers; -import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; - -import java.util.Arrays; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class CliqueProposerSelectorTest { - - private final List
validatorList = - Arrays.asList( - AddressHelpers.ofValue(1), - AddressHelpers.ofValue(2), - AddressHelpers.ofValue(3), - AddressHelpers.ofValue(4)); - private ValidatorProvider validatorProvider; - - @BeforeEach - public void setup() { - validatorProvider = mock(ValidatorProvider.class); - when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(validatorList); - } - - @Test - public void proposerForABlockIsBasedOnModBlockNumber() { - final BlockHeaderTestFixture headerBuilderFixture = new BlockHeaderTestFixture(); - - for (int prevBlockNumber = 0; prevBlockNumber < 10; prevBlockNumber++) { - headerBuilderFixture.number(prevBlockNumber); - final CliqueProposerSelector selector = new CliqueProposerSelector(validatorProvider); - final Address nextProposer = - selector.selectProposerForNextBlock(headerBuilderFixture.buildHeader()); - assertThat(nextProposer) - .isEqualTo(validatorList.get((prevBlockNumber + 1) % validatorList.size())); - } - } -} diff --git a/ethereum/ethstats/src/main/java/org/hyperledger/besu/ethstats/EthStatsService.java b/ethereum/ethstats/src/main/java/org/hyperledger/besu/ethstats/EthStatsService.java index 0ccc3efaf39..06e00b17b4a 100644 --- a/ethereum/ethstats/src/main/java/org/hyperledger/besu/ethstats/EthStatsService.java +++ b/ethereum/ethstats/src/main/java/org/hyperledger/besu/ethstats/EthStatsService.java @@ -30,7 +30,6 @@ import static org.hyperledger.besu.ethstats.request.EthStatsRequest.Type.STATS; import org.hyperledger.besu.config.GenesisConfigOptions; -import org.hyperledger.besu.consensus.clique.blockcreation.CliqueMiningCoordinator; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlockResult; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlockResultFactory; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; @@ -415,12 +414,7 @@ private void sendPendingTransactionReport() { /** Sends information about the node (is mining, is syncing, etc.) */ private void sendNodeStatsReport() { - final boolean isMiningEnabled; - if (miningCoordinator instanceof CliqueMiningCoordinator) { - isMiningEnabled = ((CliqueMiningCoordinator) miningCoordinator).isSigner(); - } else { - isMiningEnabled = miningCoordinator.isMining(); - } + final boolean isMiningEnabled = miningCoordinator.isMining(); final boolean isSyncing = syncState.isInSync(); final long gasPrice = suggestGasPrice(blockchainQueries.getBlockchain().getChainHeadBlock()); // safe to cast to int since it isn't realistic to have more than max int peers From 2649a6bbaf3a9d25a97d9bc2b881b6c8124a31c0 Mon Sep 17 00:00:00 2001 From: Cyrus Date: Mon, 16 Mar 2026 16:35:07 +0530 Subject: [PATCH 38/77] fix: use block's own hash for balance lookup in eth_estimateGas (#10042) calculateGasLimitUpperBound() was using blockHeader.getParentHash() to look up the sender's account balance. For historical and "latest" blocks this queries the state one block behind the state actually used by the transaction simulator, which can produce wrong gas estimates or spurious TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE errors when the sender's balance changed in the target block. Use the block's own hash (BlockHeader.getHash()) for concrete headers, keeping getParentHash() only for synthetic pending headers whose block does not yet exist. Signed-off-by: Shridhar Panigrahi Co-authored-by: Fabio Di Fabio Signed-off-by: Cyrus --- .../internal/methods/AbstractEstimateGas.java | 9 +++++++-- .../internal/methods/EthEstimateGasTest.java | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractEstimateGas.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractEstimateGas.java index 993e5165470..a17926ae2b7 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractEstimateGas.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractEstimateGas.java @@ -16,6 +16,7 @@ import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.CallParameterUtil.validateAndGetCallParams; +import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.StateOverrideMap; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcErrorConverter; @@ -241,8 +242,12 @@ private long calculateGasLimitUpperBound( final var sender = callParameters.getSender().get(); final var maxGasPrice = calculateTxMaxGasPrice(callParameters); if (!maxGasPrice.equals(Wei.ZERO)) { - final var maybeBalance = - getBlockchainQueries().accountBalance(sender, blockHeader.getParentHash()); + // For a concrete BlockHeader (historical or "latest"), look up the balance at that + // block's own state. For a synthetic ProcessableBlockHeader (pending), the block + // does not exist yet, so the correct state is the parent's (chain-head) state. + final Hash balanceBlockHash = + (blockHeader instanceof BlockHeader bh) ? bh.getHash() : blockHeader.getParentHash(); + final var maybeBalance = getBlockchainQueries().accountBalance(sender, balanceBlockHash); if (maybeBalance.isEmpty() || maybeBalance.get().equals(Wei.ZERO)) { return 0; } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 7fbb79d0159..01155fe5298 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.StateOverride; import org.hyperledger.besu.datatypes.StateOverrideMap; import org.hyperledger.besu.datatypes.Wei; @@ -53,6 +54,7 @@ import java.util.OptionalLong; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -485,6 +487,24 @@ public void latestBlockTagEstimateOnLatestBlock() { assertThat(method.response(request)).usingRecursiveComparison().isEqualTo(expectedResponse); } + @Test + public void shouldUseBlockHashNotParentHashForBalanceLookup() { + // Set up distinct hashes so we can verify the correct one is used + final Hash latestBlockHash = Hash.wrap(Bytes32.fromHexString("0x" + "aa".repeat(32))); + final Hash latestParentHash = Hash.wrap(Bytes32.fromHexString("0x" + "bb".repeat(32))); + when(latestBlockHeader.getHash()).thenReturn(latestBlockHash); + when(latestBlockHeader.getParentHash()).thenReturn(latestParentHash); + + final JsonRpcRequestContext request = + ethEstimateGasRequest(eip1559TransactionCallParameter(), "latest"); + mockTransientProcessorResultGasEstimate(MIN_TX_GAS_COST, true, false, latestBlockHeader); + + method.response(request); + + // Balance must be looked up at the block's own hash, not its parent + verify(blockchainQueries).accountBalance(Address.fromHexString("0x0"), latestBlockHash); + } + @Test public void shouldUseBlockNumberParamWhenPresent() { final JsonRpcRequestContext request = From 9da5ccd2ae871d23c47d118b06a56cab5370cd54 Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Mon, 16 Mar 2026 18:03:02 +0100 Subject: [PATCH 39/77] Amsterdam: EIP-8037: State Creation Gas Cost Increase (#9815) * EIP-8037: multidimensional gas metering - EVM core Implement state gas tracking in the EVM layer: - StateGasCostCalculator and Eip8037StateGasCostCalculator for cost-per-state-byte - AmsterdamGasCalculator with split regular/state gas costs - MessageFrame state gas reservoir, spill, and collision tracking - State gas charging in SSTORE, CREATE, CALL, and SELFDESTRUCT operations Signed-off-by: daniellehrner * EIP-8037: protocol-level 2D gas accounting integration Wire state gas into transaction processing, block building, and validation: - MainnetTransactionProcessor: intrinsic state gas, reservoir init, spill handling - BlockGasAccountingStrategy.AMSTERDAM: 2D gas metering (max of regular, state) - BlockGasUsedValidator.AMSTERDAM: pre-refund 2D validation - OsakaTargetingGasLimitCalculator: Amsterdam constructor with uncapped tx gas limit - Block creation: 2D headroom checks for transaction selection - TransactionProcessingResult: carry state gas used Signed-off-by: daniellehrner * EIP-8037: update execution spec tests for bal-devnet-3 Update reference test fixtures to bal@v5.2.0 for EIP-8037 compatibility. Signed-off-by: daniellehrner * EIP-8037: extract TransactionGasAccounting and add test coverage Extract gas accounting logic from MainnetTransactionProcessor into a testable TransactionGasAccounting class with builder pattern. Add tests for SSTORE state gas, block gas accounting strategy, state gas spill, and regular gas limit enforcement. Signed-off-by: daniellehrner * trigger DCO re-check Signed-off-by: daniellehrner * Add -Pcases to jmh (#9982) e.g. ./gradlew --no-daemon :ethereum:core:jmh -Pincludes=Mod -Pexcludes=Mul,Add,SMod -Pcases=MOD_256_128,MOD_256_192 Signed-off-by: Simon Dudley * Preserve caller-provided gas pricing in eth_simulateV1 results (#9972) * preserve caller-provided gas prices in TransactionSimulator When isAllowExceedingBalance is true but the caller explicitly provided non-zero gas pricing parameters, preserve them and the block header's baseFee so effective gas price is computed correctly during execution. This ensures gas fees are actually charged so that stateRoot and block hash are correct in eth_simulateV1 results. When gas params are absent or zero (typical eth_call, or explicitly zero maxFeePerGas), behavior is unchanged - all fields stay zero and baseFee is zeroed to avoid validation failures. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 * trigger DCO re-check Signed-off-by: daniellehrner * spotless Signed-off-by: daniellehrner * update to BAL v5.3.0 spec tests Signed-off-by: daniellehrner * remove referenceTestDevnet test compilation from the referenceTest gradle task Signed-off-by: daniellehrner * addressed comments Signed-off-by: daniellehrner * renamed BlockGasAccountingStrategy.calculateBlockGas to calculateTransactionRegularGas Signed-off-by: daniellehrner * 1. use BlockGasAccountingStrategy.hasBlockCapacity() in AbstractBlockProcessor.java, AbstractBlockProcessorTest.java 2. move handleStateGasSpill into its own method 3. Added CREATE state gas underflow guard 4. Improved failCodeDepositWithoutRollback documentation Signed-off-by: daniellehrner --------- Signed-off-by: daniellehrner Signed-off-by: Simon Dudley Signed-off-by: Sally MacFarlane Co-authored-by: Simon Dudley Co-authored-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .../EIP7708TransferLogAcceptanceTest.java | 4 +- .../blockcreation/AbstractBlockCreator.java | 5 +- .../txselection/BlockTransactionSelector.java | 4 +- .../TransactionSelectionResults.java | 36 +- .../BlockSizeTransactionSelector.java | 92 +++-- .../txselection/selectors/GasState.java | 41 +++ .../AbstractBlockTransactionSelectorTest.java | 17 +- .../BlockSizeTransactionSelectorTest.java | 241 ++++++++++--- .../mainnet/AbstractBlockProcessor.java | 49 ++- .../mainnet/BlockGasAccountingStrategy.java | 81 ++++- .../mainnet/BlockGasUsedValidator.java | 8 +- .../mainnet/MainnetProtocolSpecs.java | 25 +- .../mainnet/MainnetTransactionProcessor.java | 144 +++++++- .../OsakaTargetingGasLimitCalculator.java | 22 +- .../mainnet/TransactionGasAccounting.java | 129 +++++++ .../TransactionProcessingResult.java | 32 +- ...AbstractBlockProcessorIntegrationTest.java | 8 +- .../mainnet/AbstractBlockProcessorTest.java | 28 ++ .../BlockGasAccountingStrategyTest.java | 101 +++++- .../mainnet/Eip8037RegularGasLimitTest.java | 212 ++++++++++++ .../MainnetTransactionProcessorTest.java | 2 + .../mainnet/TransactionGasAccountingTest.java | 169 ++++++++++ ethereum/referencetests/build.gradle | 55 +-- .../referencetests/AccountChangesJson.java | 8 +- .../besu/evm/frame/MessageFrame.java | 161 ++++++++- .../hyperledger/besu/evm/frame/TxValues.java | 66 +++- .../gascalculator/AmsterdamGasCalculator.java | 183 +++++++++- .../Eip8037StateGasCostCalculator.java | 264 +++++++++++++++ .../besu/evm/gascalculator/GasCalculator.java | 9 + .../gascalculator/StateGasCostCalculator.java | 317 ++++++++++++++++++ .../evm/operation/AbstractCallOperation.java | 7 + .../operation/AbstractCreateOperation.java | 15 + .../besu/evm/operation/SStoreOperation.java | 12 + .../evm/operation/SelfDestructOperation.java | 7 + .../processor/AbstractMessageProcessor.java | 52 ++- .../processor/ContractCreationProcessor.java | 83 +++++ .../Eip8037StateGasCostCalculatorTest.java | 144 ++++++++ .../AbstractCreateOperationTest.java | 75 +++++ .../evm/operation/SStoreOperationTest.java | 240 +++++++++++++ gradle/verification-metadata.xml | 8 +- 40 files changed, 2926 insertions(+), 230 deletions(-) create mode 100644 ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/GasState.java create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/Eip8037RegularGasLimitTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccountingTest.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/gascalculator/Eip8037StateGasCostCalculator.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/gascalculator/StateGasCostCalculator.java create mode 100644 evm/src/test/java/org/hyperledger/besu/evm/gascalculator/Eip8037StateGasCostCalculatorTest.java diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/ethereum/EIP7708TransferLogAcceptanceTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/ethereum/EIP7708TransferLogAcceptanceTest.java index 31bc090be8e..e9afe602ba4 100644 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/ethereum/EIP7708TransferLogAcceptanceTest.java +++ b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/ethereum/EIP7708TransferLogAcceptanceTest.java @@ -218,7 +218,7 @@ public void shouldEmitMultipleTransferLogsForContractCallWithValue() throws IOEx .nonce(0) .maxPriorityFeePerGas(Wei.of(1_000_000_000)) .maxFeePerGas(Wei.fromHexString("0x02540BE400")) - .gasLimit(100_000) + .gasLimit(300_000) .to(forwarderContract) .value(transferAmount) .payload(callData) @@ -295,7 +295,7 @@ public void shouldEmitTransferLogForSelfDestructToDifferentAddress() throws IOEx .nonce(0) .maxPriorityFeePerGas(Wei.of(1_000_000_000)) .maxFeePerGas(Wei.fromHexString("0x02540BE400")) - .gasLimit(100_000) + .gasLimit(300_000) .to(selfDestructContract) .value(Wei.ZERO) .payload(callData) diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java index e5f3b7b7b97..571bf0faa6c 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java @@ -326,7 +326,10 @@ public BlockCreationResult createBlock( BodyValidation.transactionsRoot(transactionResults.getSelectedTransactions())) .receiptsRoot(BodyValidation.receiptsRoot(transactionResults.getReceipts())) .logsBloom(BodyValidation.logsBloom(transactionResults.getReceipts())) - .gasUsed(transactionResults.getCumulativeGasUsed()) + .gasUsed( + Math.max( + transactionResults.getCumulativeRegularGasUsed(), + transactionResults.getCumulativeStateGasUsed())) .extraData(extraDataCalculator.get(parentHeader)) .withdrawalsRoot( withdrawalsCanBeProcessed diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java index 5206002c023..f3f6855ee35 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java @@ -683,7 +683,7 @@ private TransactionSelectionResult handleTransactionSelected( blockSelectionContext .protocolSpec() .getBlockGasAccountingStrategy() - .calculateBlockGas(transaction, processingResult); + .calculateTransactionRegularGas(transaction, processingResult); // Receipt gas: Standard post-refund calculation (gasLimit - gasRemaining) // This is used for receipt cumulativeGasUsed field @@ -882,7 +882,7 @@ void runOnCommit() { .ifPresent(blockAccessListBuilder::apply)); transactionSelectionResults.updateSelected( - transaction, receipt, blockGasUsed, receiptGasUsed); + transaction, receipt, blockGasUsed, receiptGasUsed, processingResult.getStateGasUsed()); notifySelected(evaluationContext, processingResult); LOG.atTrace() diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java index 1bb98b3235b..7ef3dc4bf75 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java @@ -48,29 +48,33 @@ public class TransactionSelectionResults { new ConcurrentHashMap<>(); // EIP-7778: Track two separate cumulative gas values - // cumulativeGasUsed: For block gas limit enforcement (uses protocol-specific strategy) + // cumulativeRegularGasUsed: For block gas limit enforcement (uses protocol-specific strategy) // cumulativeReceiptGasUsed: For receipt cumulativeGasUsed field (always post-refund) - private long cumulativeGasUsed = 0; + private long cumulativeRegularGasUsed = 0; private long cumulativeReceiptGasUsed = 0; + // EIP-8037: Track cumulative state gas used for multidimensional gas metering + private long cumulativeStateGasUsed = 0; void updateSelected( final Transaction transaction, final TransactionReceipt receipt, final long blockGasUsed, - final long receiptGasUsed) { + final long receiptGasUsed, + final long stateGasUsed) { selectedTransactions.add(transaction); transactionsByType .computeIfAbsent(transaction.getType(), type -> new ArrayList<>()) .add(transaction); receipts.add(receipt); - cumulativeGasUsed += blockGasUsed; + cumulativeRegularGasUsed += blockGasUsed; cumulativeReceiptGasUsed += receiptGasUsed; + cumulativeStateGasUsed += stateGasUsed; LOG.atTrace() .setMessage( "New selected transaction {}, total transactions {}, cumulative block gas {}, cumulative receipt gas {}") .addArgument(transaction::toTraceLog) .addArgument(selectedTransactions::size) - .addArgument(cumulativeGasUsed) + .addArgument(cumulativeRegularGasUsed) .addArgument(cumulativeReceiptGasUsed) .log(); } @@ -92,14 +96,18 @@ public List getReceipts() { return receipts; } - public long getCumulativeGasUsed() { - return cumulativeGasUsed; + public long getCumulativeRegularGasUsed() { + return cumulativeRegularGasUsed; } public long getCumulativeReceiptGasUsed() { return cumulativeReceiptGasUsed; } + public long getCumulativeStateGasUsed() { + return cumulativeStateGasUsed; + } + public Map getNotSelectedTransactions() { return Map.copyOf(notSelectedTransactions); } @@ -137,8 +145,9 @@ public boolean equals(final Object o) { return false; } TransactionSelectionResults that = (TransactionSelectionResults) o; - return cumulativeGasUsed == that.cumulativeGasUsed + return cumulativeRegularGasUsed == that.cumulativeRegularGasUsed && cumulativeReceiptGasUsed == that.cumulativeReceiptGasUsed + && cumulativeStateGasUsed == that.cumulativeStateGasUsed && selectedTransactions.equals(that.selectedTransactions) && notSelectedTransactions.equals(that.notSelectedTransactions) && receipts.equals(that.receipts); @@ -150,15 +159,18 @@ public int hashCode() { selectedTransactions, notSelectedTransactions, receipts, - cumulativeGasUsed, - cumulativeReceiptGasUsed); + cumulativeRegularGasUsed, + cumulativeReceiptGasUsed, + cumulativeStateGasUsed); } public String toTraceLog() { - return "cumulativeGasUsed=" - + cumulativeGasUsed + return "cumulativeRegularGasUsed=" + + cumulativeRegularGasUsed + ", cumulativeReceiptGasUsed=" + cumulativeReceiptGasUsed + + ", cumulativeStateGasUsed=" + + cumulativeStateGasUsed + ", selectedTransactions=" + selectedTransactions.stream() .map(Transaction::getHash) diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java index 5a51ff4e684..08f00c21003 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java @@ -17,6 +17,7 @@ import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.mainnet.BlockGasAccountingStrategy; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; @@ -28,16 +29,22 @@ * This class extends AbstractTransactionSelector and provides a specific implementation for * evaluating transactions based on block size. It checks if a transaction is too large for the * block and determines the selection result accordingly. + * + *

For EIP-8037 multidimensional gas, this selector tracks both regular and state gas dimensions. + * A transaction fits if its gasLimit does not exceed the sum of remaining capacity in both + * dimensions. Post-processing verifies the actual gas split. */ -public class BlockSizeTransactionSelector extends AbstractStatefulTransactionSelector { +public class BlockSizeTransactionSelector extends AbstractStatefulTransactionSelector { private static final Logger LOG = LoggerFactory.getLogger(BlockSizeTransactionSelector.class); private final long blockGasLimit; + private final BlockGasAccountingStrategy gasAccountingStrategy; public BlockSizeTransactionSelector( final BlockSelectionContext context, final SelectorsStateManager selectorsStateManager) { - super(context, selectorsStateManager, 0L, SelectorsStateManager.StateDuplicator::duplicateLong); + super(context, selectorsStateManager, GasState.ZERO, GasState::duplicate); this.blockGasLimit = context.pendingBlockHeader().getGasLimit(); + this.gasAccountingStrategy = context.protocolSpec().getBlockGasAccountingStrategy(); } /** @@ -51,17 +58,17 @@ public BlockSizeTransactionSelector( public TransactionSelectionResult evaluateTransactionPreProcessing( final TransactionEvaluationContext evaluationContext) { - final long cumulativeGasUsed = getWorkingState(); + final GasState state = getWorkingState(); - if (transactionTooLargeForBlock(evaluationContext.getTransaction(), cumulativeGasUsed)) { + if (transactionTooLargeForBlock(evaluationContext.getTransaction(), state)) { LOG.atTrace() .setMessage("Transaction {} too large to select for block creation") .addArgument(evaluationContext.getPendingTransaction()::toTraceLog) .log(); - if (blockOccupancyAboveThreshold(cumulativeGasUsed)) { + if (blockOccupancyAboveThreshold(state)) { LOG.trace("Block occupancy above threshold, completing operation"); return TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD; - } else if (blockFull(cumulativeGasUsed)) { + } else if (blockFull(state)) { LOG.trace("Block full, completing operation"); return TransactionSelectionResult.BLOCK_FULL; } else { @@ -75,46 +82,67 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, final TransactionProcessingResult processingResult) { - // EIP-7778: Use the protocol-specific gas accounting strategy - // Pre-Amsterdam: gasLimit - gasRemaining (post-refund) - // Amsterdam+: estimateGasUsedByTransaction (pre-refund, prevents block gas limit circumvention) - final long gasUsedByTransaction = - context - .protocolSpec() - .getBlockGasAccountingStrategy() - .calculateBlockGas(evaluationContext.getTransaction(), processingResult); - setWorkingState(getWorkingState() + gasUsedByTransaction); - + final long txRegularGasUsed = + gasAccountingStrategy.calculateTransactionRegularGas( + evaluationContext.getTransaction(), processingResult); + final long stateGasUsed = processingResult.getStateGasUsed(); + + final GasState state = getWorkingState(); + final GasState newState = + new GasState(state.regularGas() + txRegularGasUsed, state.stateGas() + stateGasUsed); + setWorkingState(newState); + + final long gasMetered = + gasAccountingStrategy.effectiveGasUsed(newState.regularGas(), newState.stateGas()); + if (gasMetered > blockGasLimit) { + LOG.atTrace() + .setMessage( + "Transaction {} exceeds block gas limit post-processing:" + + " regularGas={}, stateGas={}, gasMetered={}, blockGasLimit={}") + .addArgument(evaluationContext.getPendingTransaction()::toTraceLog) + .addArgument(newState.regularGas()) + .addArgument(newState.stateGas()) + .addArgument(gasMetered) + .addArgument(blockGasLimit) + .log(); + return TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_GAS; + } return TransactionSelectionResult.SELECTED; } /** - * Checks if the transaction is too large for the block. + * Checks if the transaction is too large for the block using the gas accounting strategy. For 1D + * gas, this checks regular gas only. For 2D gas (EIP-8037), this considers the sum of remaining + * capacity in both dimensions. * - * @param transaction The transaction to be checked. block. - * @param cumulativeGasUsed The cumulative gas used by previous txs. + *

This is a permissive heuristic: it may allow a transaction through pre-processing that later + * exceeds the limit in post-processing, where the actual regular/state split is known. + * + * @param transaction The transaction to be checked. + * @param state The current gas state with regular and state gas. * @return True if the transaction is too large for the block, false otherwise. */ - private boolean transactionTooLargeForBlock( - final Transaction transaction, final long cumulativeGasUsed) { - - return transaction.getGasLimit() > blockGasLimit - cumulativeGasUsed; + private boolean transactionTooLargeForBlock(final Transaction transaction, final GasState state) { + return !gasAccountingStrategy.hasBlockCapacity( + transaction.getGasLimit(), state.regularGas(), state.stateGas(), blockGasLimit); } /** * Checks if the block occupancy is above the threshold. * - * @param cumulativeGasUsed The cumulative gas used by previous txs. + * @param state The current gas state. * @return True if the block occupancy is above the threshold, false otherwise. */ - private boolean blockOccupancyAboveThreshold(final long cumulativeGasUsed) { - final long gasRemaining = blockGasLimit - cumulativeGasUsed; - final double occupancyRatio = (double) cumulativeGasUsed / (double) blockGasLimit; + private boolean blockOccupancyAboveThreshold(final GasState state) { + final long gasUsed = + gasAccountingStrategy.effectiveGasUsed(state.regularGas(), state.stateGas()); + final long gasRemaining = blockGasLimit - gasUsed; + final double occupancyRatio = (double) gasUsed / (double) blockGasLimit; LOG.trace( "Min block occupancy ratio {}, gas used {}, available {}, remaining {}, used/available {}", context.miningConfiguration().getMinBlockOccupancyRatio(), - cumulativeGasUsed, + gasUsed, blockGasLimit, gasRemaining, occupancyRatio); @@ -125,11 +153,13 @@ private boolean blockOccupancyAboveThreshold(final long cumulativeGasUsed) { /** * Checks if the block is full. * - * @param cumulativeGasUsed The cumulative gas used by previous txs. + * @param state The current gas state. * @return True if the block is full, false otherwise. */ - private boolean blockFull(final long cumulativeGasUsed) { - final long gasRemaining = blockGasLimit - cumulativeGasUsed; + private boolean blockFull(final GasState state) { + final long gasUsed = + gasAccountingStrategy.effectiveGasUsed(state.regularGas(), state.stateGas()); + final long gasRemaining = blockGasLimit - gasUsed; if (gasRemaining < context.gasCalculator().getMinimumTransactionCost()) { LOG.trace( diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/GasState.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/GasState.java new file mode 100644 index 00000000000..050da604bbe --- /dev/null +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/GasState.java @@ -0,0 +1,41 @@ +/* + * 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.blockcreation.txselection.selectors; + +/** + * Tracks cumulative regular gas and state gas used during block building. Used by {@link + * BlockSizeTransactionSelector} for multidimensional gas metering (EIP-8037). + * + *

For pre-EIP-8037, stateGas is always 0. + * + * @param regularGas cumulative regular (non-state) gas used + * @param stateGas cumulative state gas used + */ +record GasState(long regularGas, long stateGas) { + + static final GasState ZERO = new GasState(0, 0); + + /** + * Duplicator function for {@link + * org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager}. Since records are + * immutable value types, returning the same instance is safe — no caller can mutate it. + * + * @param state the state to duplicate + * @return the same instance + */ + static GasState duplicate(final GasState state) { + return state; + } +} diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java index ddacd2ca8f0..d43b4706b84 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java @@ -260,7 +260,7 @@ public void emptyPendingTransactionsResultsInEmptyVettingResult() { assertThat(results.getSelectedTransactions()).isEmpty(); assertThat(results.getNotSelectedTransactions()).isEmpty(); assertThat(results.getReceipts()).isEmpty(); - assertThat(results.getCumulativeGasUsed()).isEqualTo(0); + assertThat(results.getCumulativeRegularGasUsed()).isEqualTo(0); } @Test @@ -286,7 +286,7 @@ public void validPendingTransactionIsIncludedInTheBlock() { assertThat(results.getSelectedTransactions()).containsExactly(transaction); assertThat(results.getNotSelectedTransactions()).isEmpty(); assertThat(results.getReceipts().size()).isEqualTo(1); - assertThat(results.getCumulativeGasUsed()).isEqualTo(99995L); + assertThat(results.getCumulativeRegularGasUsed()).isEqualTo(99995L); } @Test @@ -314,7 +314,7 @@ public void validPendingTransactionIsNotIncludedIfSelectionCancelled() { assertThat(results.getNotSelectedTransactions()) .containsOnly(entry(transaction, TransactionSelectionResult.SELECTION_CANCELLED)); assertThat(results.getReceipts().size()).isEqualTo(0); - assertThat(results.getCumulativeGasUsed()).isEqualTo(0L); + assertThat(results.getCumulativeRegularGasUsed()).isEqualTo(0L); } @Test @@ -355,7 +355,7 @@ public void invalidTransactionsAreSkippedButBlockStillFills() { assertThat(results.getSelectedTransactions().size()).isEqualTo(4); assertThat(results.getSelectedTransactions().contains(invalidTx)).isFalse(); assertThat(results.getReceipts().size()).isEqualTo(4); - assertThat(results.getCumulativeGasUsed()).isEqualTo(400_000); + assertThat(results.getCumulativeRegularGasUsed()).isEqualTo(400_000); } @Test @@ -391,7 +391,7 @@ public void subsetOfPendingTransactionsIncludedWhenBlockGasLimitHit() { transactionsToInject.get(3), TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD)); assertThat(results.getReceipts().size()).isEqualTo(3); - assertThat(results.getCumulativeGasUsed()).isEqualTo(300_000); + assertThat(results.getCumulativeRegularGasUsed()).isEqualTo(300_000); // Ensure receipts have the correct cumulative gas assertThat(results.getReceipts().get(0).getCumulativeGasUsed()).isEqualTo(100_000); @@ -568,7 +568,7 @@ public void transactionSelectionStopsWhenBlockIsFull() { entry( transactionsToInject.get(4), TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD)); - assertThat(results.getCumulativeGasUsed()).isEqualTo(blockHeader.getGasLimit()); + assertThat(results.getCumulativeRegularGasUsed()).isEqualTo(blockHeader.getGasLimit()); } @Test @@ -624,7 +624,8 @@ public void transactionSelectionStopsWhenRemainingGasIsNotEnoughForAnyMoreTransa transactionsToInject.get(1), TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_GAS), entry(transactionsToInject.get(3), TransactionSelectionResult.BLOCK_FULL)); - assertThat(blockHeader.getGasLimit() - results.getCumulativeGasUsed()).isLessThan(minTxGasCost); + assertThat(blockHeader.getGasLimit() - results.getCumulativeRegularGasUsed()) + .isLessThan(minTxGasCost); } @Test @@ -1367,7 +1368,7 @@ private void internalBlockSelectionTimeoutSimulation( .isTrue(); assertThat(results.getReceipts().size()).isEqualTo(2); - assertThat(results.getCumulativeGasUsed()).isEqualTo(200_000); + assertThat(results.getCumulativeRegularGasUsed()).isEqualTo(200_000); // Ensure receipts have the correct cumulative gas assertThat(results.getReceipts().get(0).getCumulativeGasUsed()).isEqualTo(100_000); diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java index ed4e2bc3a95..38495810239 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java @@ -93,7 +93,8 @@ void singleTransactionBelowBlockGasLimitIsSelected() { selectorsStateManager.blockSelectionStarted(); evaluateAndAssertSelected(txEvaluationContext, remainingGas(0)); - assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT); + assertThat(selector.getWorkingState().regularGas()).isEqualTo(TRANSFER_GAS_LIMIT); + assertThat(selector.getWorkingState().stateGas()).isEqualTo(0); } @Test @@ -106,7 +107,7 @@ void singleTransactionAboveBlockGasLimitIsNotSelected() { selectorsStateManager.blockSelectionStarted(); evaluateAndAssertNotSelected(txEvaluationContext, TX_TOO_LARGE_FOR_REMAINING_GAS); - assertThat(selector.getWorkingState()).isEqualTo(0); + assertThat(selector.getWorkingState().regularGas()).isEqualTo(0); } @Test @@ -120,7 +121,8 @@ void correctlyCumulatesOnlyTheEffectiveGasUsedAfterProcessing() { selectorsStateManager.blockSelectionStarted(); evaluateAndAssertSelected(txEvaluationContext, remainingGas(remainingGas)); - assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * 2 - remainingGas); + assertThat(selector.getWorkingState().regularGas()) + .isEqualTo(TRANSFER_GAS_LIMIT * 2 - remainingGas); } @Test @@ -145,7 +147,7 @@ void moreTransactionsBelowBlockGasLimitAreSelected() { evaluateAndAssertSelected(txEvaluationContext, remainingGas(0)); }); - assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); + assertThat(selector.getWorkingState().regularGas()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); } @Test @@ -170,7 +172,7 @@ void moreTransactionsThanBlockCanFitOnlySomeAreSelected() { evaluateAndAssertSelected(txEvaluationContext, remainingGas(0)); }); - assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); + assertThat(selector.getWorkingState().regularGas()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); // last tx is too big for the remaining gas final long tooBigGasLimit = BLOCK_GAS_LIMIT - (TRANSFER_GAS_LIMIT * txCount) + 1; @@ -185,7 +187,7 @@ void moreTransactionsThanBlockCanFitOnlySomeAreSelected() { NEVER_CANCELLED); evaluateAndAssertNotSelected(bigTxEvaluationContext, TX_TOO_LARGE_FOR_REMAINING_GAS); - assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); + assertThat(selector.getWorkingState().regularGas()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); } @Test @@ -203,7 +205,7 @@ void identifyWhenBlockOccupancyIsAboveThreshold() { blockSelectionContext.pendingBlockHeader(), tx1, null, null, null, NEVER_CANCELLED); evaluateAndAssertSelected(txEvaluationContext1, remainingGas(0)); - assertThat(selector.getWorkingState()).isEqualTo(justAboveOccupancyRatioGasLimit); + assertThat(selector.getWorkingState().regularGas()).isEqualTo(justAboveOccupancyRatioGasLimit); final var tx2 = createPendingTransaction(justAboveOccupancyRatioGasLimit); @@ -212,7 +214,7 @@ void identifyWhenBlockOccupancyIsAboveThreshold() { blockSelectionContext.pendingBlockHeader(), tx2, null, null, null, NEVER_CANCELLED); evaluateAndAssertNotSelected(txEvaluationContext2, BLOCK_OCCUPANCY_ABOVE_THRESHOLD); - assertThat(selector.getWorkingState()).isEqualTo(justAboveOccupancyRatioGasLimit); + assertThat(selector.getWorkingState().regularGas()).isEqualTo(justAboveOccupancyRatioGasLimit); } @Test @@ -235,7 +237,7 @@ void identifyWhenBlockIsFull() { blockSelectionContext.pendingBlockHeader(), tx1, null, null, null, NEVER_CANCELLED); evaluateAndAssertSelected(txEvaluationContext1, remainingGas(0)); - assertThat(selector.getWorkingState()).isEqualTo(fillBlockGasLimit); + assertThat(selector.getWorkingState().regularGas()).isEqualTo(fillBlockGasLimit); final var tx2 = createPendingTransaction(TRANSFER_GAS_LIMIT); @@ -244,49 +246,7 @@ void identifyWhenBlockIsFull() { blockSelectionContext.pendingBlockHeader(), tx2, null, null, null, NEVER_CANCELLED); evaluateAndAssertNotSelected(txEvaluationContext2, BLOCK_FULL); - assertThat(selector.getWorkingState()).isEqualTo(fillBlockGasLimit); - } - - private void evaluateAndAssertSelected( - final TransactionEvaluationContext txEvaluationContext, - final TransactionProcessingResult transactionProcessingResult) { - assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)).isEqualTo(SELECTED); - assertThat( - selector.evaluateTransactionPostProcessing( - txEvaluationContext, transactionProcessingResult)) - .isEqualTo(SELECTED); - } - - private void evaluateAndAssertNotSelected( - final TransactionEvaluationContext txEvaluationContext, - final TransactionSelectionResult preProcessedResult) { - assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)) - .isEqualTo(preProcessedResult); - } - - private PendingTransaction createPendingTransaction(final long gasLimit) { - return PendingTransaction.newPendingTransaction( - createTransaction(TransactionType.EIP1559, gasLimit), false, false, MAX_SCORE); - } - - private Transaction createTransaction(final TransactionType type, final long gasLimit) { - - var tx = - new TransactionTestFixture() - .to(Optional.of(Address.fromHexString("0x634316eA0EE79c701c6F67C53A4C54cBAfd2316d"))) - .nonce(0) - .gasLimit(gasLimit) - .type(type) - .maxFeePerGas(Optional.of(Wei.of(1000))) - .maxPriorityFeePerGas(Optional.of(Wei.of(100))); - - return tx.createTransaction(KEYS); - } - - private TransactionProcessingResult remainingGas(final long remainingGas) { - final var txProcessingResult = mock(TransactionProcessingResult.class); - when(txProcessingResult.getGasRemaining()).thenReturn(remainingGas); - return txProcessingResult; + assertThat(selector.getWorkingState().regularGas()).isEqualTo(fillBlockGasLimit); } /** @@ -298,7 +258,7 @@ private TransactionProcessingResult remainingGas(final long remainingGas) { void eip7778StrategyUsesPreRefundGasForBlockAccounting() { // Reconfigure with EIP-7778 strategy when(blockSelectionContext.protocolSpec().getBlockGasAccountingStrategy()) - .thenReturn(BlockGasAccountingStrategy.EIP7778); + .thenReturn(BlockGasAccountingStrategy.AMSTERDAM); selector = new BlockSizeTransactionSelector(blockSelectionContext, selectorsStateManager); // Create a transaction with gas limit 50,000 @@ -312,6 +272,7 @@ void eip7778StrategyUsesPreRefundGasForBlockAccounting() { final var txProcessingResult = mock(TransactionProcessingResult.class); when(txProcessingResult.getEstimateGasUsedByTransaction()).thenReturn(preRefundGasUsed); + when(txProcessingResult.getStateGasUsed()).thenReturn(0L); final var txEvaluationContext = new TransactionEvaluationContext( @@ -320,7 +281,7 @@ void eip7778StrategyUsesPreRefundGasForBlockAccounting() { evaluateAndAssertSelected(txEvaluationContext, txProcessingResult); // EIP-7778: Should use pre-refund gas (40,000), NOT post-refund (30,000) - assertThat(selector.getWorkingState()) + assertThat(selector.getWorkingState().regularGas()) .as("EIP-7778 should use pre-refund gas for block accounting") .isEqualTo(preRefundGasUsed); } @@ -344,6 +305,7 @@ void frontierStrategyUsesPostRefundGasForBlockAccounting() { final var txProcessingResult = mock(TransactionProcessingResult.class); when(txProcessingResult.getGasRemaining()).thenReturn(postRefundGasRemaining); + when(txProcessingResult.getStateGasUsed()).thenReturn(0L); final var txEvaluationContext = new TransactionEvaluationContext( @@ -352,8 +314,177 @@ void frontierStrategyUsesPostRefundGasForBlockAccounting() { evaluateAndAssertSelected(txEvaluationContext, txProcessingResult); // FRONTIER: Should use post-refund gas (30,000) - assertThat(selector.getWorkingState()) + assertThat(selector.getWorkingState().regularGas()) .as("FRONTIER should use post-refund gas for block accounting") .isEqualTo(expectedPostRefundUsed); } + + /** + * EIP-8037 2D gas: A transaction that would fail 1D pre-processing passes 2D pre-processing + * because the remaining capacity spans both dimensions. + * + *

Scenario: block limit=30M, regular=25M used, state=5M used. A tx with gasLimit=10M would + * fail 1D (10M > 5M remaining regular) but passes 2D (headroom = 5M + 25M = 30M >= 10M). + */ + @Test + void eip8037TwoDimensionalPreProcessingAcceptsTxThatWouldFail1D() { + when(blockSelectionContext.pendingBlockHeader().getGasLimit()).thenReturn(30_000_000L); + when(blockSelectionContext.protocolSpec().getBlockGasAccountingStrategy()) + .thenReturn(BlockGasAccountingStrategy.AMSTERDAM); + selector = new BlockSizeTransactionSelector(blockSelectionContext, selectorsStateManager); + selectorsStateManager.blockSelectionStarted(); + + // First tx: uses 25M regular, 5M state + final var tx1 = createPendingTransaction(30_000_000L); + final var result1 = mock(TransactionProcessingResult.class); + when(result1.getEstimateGasUsedByTransaction()).thenReturn(30_000_000L); + when(result1.getStateGasUsed()).thenReturn(5_000_000L); + // calculateTransactionRegularGas returns 30M - 5M = 25M regular + + final var ctx1 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx1, null, null, null, NEVER_CANCELLED); + assertThat(selector.evaluateTransactionPreProcessing(ctx1)).isEqualTo(SELECTED); + assertThat(selector.evaluateTransactionPostProcessing(ctx1, result1)).isEqualTo(SELECTED); + + // State: regular=25M, state=5M + assertThat(selector.getWorkingState().regularGas()).isEqualTo(25_000_000L); + assertThat(selector.getWorkingState().stateGas()).isEqualTo(5_000_000L); + + // Second tx with gasLimit=10M: 1D would reject (10M > 30M-25M=5M remaining regular) + // But 2D headroom = max(0,30M-25M) + max(0,30M-5M) = 5M + 25M = 30M >= 10M -> passes + final var tx2 = createPendingTransaction(10_000_000L); + final var ctx2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx2, null, null, null, NEVER_CANCELLED); + assertThat(selector.evaluateTransactionPreProcessing(ctx2)).isEqualTo(SELECTED); + } + + /** + * EIP-8037 2D gas: Post-processing rejects (TX_TOO_LARGE_FOR_REMAINING_GAS) when the actual gas + * split causes gas_metered = max(regular, state) to exceed the block gas limit. + */ + @Test + void eip8037PostProcessingRejectsWhenGasMeteredExceedsLimit() { + when(blockSelectionContext.pendingBlockHeader().getGasLimit()).thenReturn(30_000_000L); + when(blockSelectionContext.protocolSpec().getBlockGasAccountingStrategy()) + .thenReturn(BlockGasAccountingStrategy.AMSTERDAM); + selector = new BlockSizeTransactionSelector(blockSelectionContext, selectorsStateManager); + selectorsStateManager.blockSelectionStarted(); + + // First tx: uses 20M regular, 20M state + final var tx1 = createPendingTransaction(40_000_000L); + final var result1 = mock(TransactionProcessingResult.class); + when(result1.getEstimateGasUsedByTransaction()).thenReturn(40_000_000L); + when(result1.getStateGasUsed()).thenReturn(20_000_000L); + + final var ctx1 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx1, null, null, null, NEVER_CANCELLED); + assertThat(selector.evaluateTransactionPreProcessing(ctx1)).isEqualTo(SELECTED); + assertThat(selector.evaluateTransactionPostProcessing(ctx1, result1)).isEqualTo(SELECTED); + + // State: regular=20M, state=20M, gasMetered=max(20M,20M)=20M <= 30M ok + assertThat(selector.getWorkingState().regularGas()).isEqualTo(20_000_000L); + assertThat(selector.getWorkingState().stateGas()).isEqualTo(20_000_000L); + + // Second tx: would push state gas over limit + // Uses 1M regular, 15M state -> cumulative state = 35M, gasMetered=max(21M,35M)=35M > 30M + final var tx2 = createPendingTransaction(16_000_000L); + final var result2 = mock(TransactionProcessingResult.class); + when(result2.getEstimateGasUsedByTransaction()).thenReturn(16_000_000L); + when(result2.getStateGasUsed()).thenReturn(15_000_000L); + + final var ctx2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx2, null, null, null, NEVER_CANCELLED); + assertThat(selector.evaluateTransactionPreProcessing(ctx2)).isEqualTo(SELECTED); + assertThat(selector.evaluateTransactionPostProcessing(ctx2, result2)) + .isEqualTo(TX_TOO_LARGE_FOR_REMAINING_GAS); + } + + /** + * EIP-8037 2D gas: effectiveGasUsed drives occupancy/blockFull checks using max(regular,state). + * Both dimensions must be nearly full for the 2D headroom check to fail and trigger blockFull. + */ + @Test + void eip8037EffectiveGasUsedDrivesBlockFullCheck() { + when(blockSelectionContext.pendingBlockHeader().getGasLimit()).thenReturn(1_000_000L); + when(blockSelectionContext.protocolSpec().getBlockGasAccountingStrategy()) + .thenReturn(BlockGasAccountingStrategy.AMSTERDAM); + when(blockSelectionContext.gasCalculator().getMinimumTransactionCost()) + .thenReturn(TRANSFER_GAS_LIMIT); + selector = new BlockSizeTransactionSelector(blockSelectionContext, selectorsStateManager); + selectorsStateManager.blockSelectionStarted(); + miningConfiguration.setMinBlockOccupancyRatio(1.0); + + // Fill block with both dimensions nearly full: regular=990K, state=990K + // gasMetered = max(990K, 990K) = 990K; remaining = 10K < 21K min cost + // 2D headroom = max(0,1M-990K) + max(0,1M-990K) = 10K + 10K = 20K + final var tx1 = createPendingTransaction(1_980_000L); + final var result1 = mock(TransactionProcessingResult.class); + when(result1.getEstimateGasUsedByTransaction()).thenReturn(1_980_000L); + when(result1.getStateGasUsed()).thenReturn(990_000L); + // regular gas = 1_980_000 - 990_000 = 990_000 + + final var ctx1 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx1, null, null, null, NEVER_CANCELLED); + assertThat(selector.evaluateTransactionPreProcessing(ctx1)).isEqualTo(SELECTED); + assertThat(selector.evaluateTransactionPostProcessing(ctx1, result1)).isEqualTo(SELECTED); + + assertThat(selector.getWorkingState().regularGas()).isEqualTo(990_000L); + assertThat(selector.getWorkingState().stateGas()).isEqualTo(990_000L); + + // tx2 gasLimit=21K > 2D headroom of 20K → transactionTooLargeForBlock=true + // effectiveGasUsed=max(990K,990K)=990K, remaining=10K < 21K → blockFull + final var tx2 = createPendingTransaction(TRANSFER_GAS_LIMIT); + final var ctx2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx2, null, null, null, NEVER_CANCELLED); + assertThat(selector.evaluateTransactionPreProcessing(ctx2)).isEqualTo(BLOCK_FULL); + } + + private void evaluateAndAssertSelected( + final TransactionEvaluationContext txEvaluationContext, + final TransactionProcessingResult transactionProcessingResult) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)).isEqualTo(SELECTED); + assertThat( + selector.evaluateTransactionPostProcessing( + txEvaluationContext, transactionProcessingResult)) + .isEqualTo(SELECTED); + } + + private void evaluateAndAssertNotSelected( + final TransactionEvaluationContext txEvaluationContext, + final TransactionSelectionResult preProcessedResult) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)) + .isEqualTo(preProcessedResult); + } + + private PendingTransaction createPendingTransaction(final long gasLimit) { + return PendingTransaction.newPendingTransaction( + createTransaction(TransactionType.EIP1559, gasLimit), false, false, MAX_SCORE); + } + + private Transaction createTransaction(final TransactionType type, final long gasLimit) { + + var tx = + new TransactionTestFixture() + .to(Optional.of(Address.fromHexString("0x634316eA0EE79c701c6F67C53A4C54cBAfd2316d"))) + .nonce(0) + .gasLimit(gasLimit) + .type(type) + .maxFeePerGas(Optional.of(Wei.of(1000))) + .maxPriorityFeePerGas(Optional.of(Wei.of(100))); + + return tx.createTransaction(KEYS); + } + + private TransactionProcessingResult remainingGas(final long remainingGas) { + final var txProcessingResult = mock(TransactionProcessingResult.class); + when(txProcessingResult.getGasRemaining()).thenReturn(remainingGas); + when(txProcessingResult.getStateGasUsed()).thenReturn(0L); + return txProcessingResult; + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java index 75710614159..2801936e11c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java @@ -211,12 +211,13 @@ public BlockProcessingResult processBlock( final PreprocessingFunction preprocessingBlockFunction) { final List receipts = new ArrayList<>(); // EIP-7778: Track two separate cumulative gas values - // cumulativeBlockGasUsed: For block gas limit enforcement (uses protocol-specific strategy) + // cumulativeRegularGasUsed: For block gas limit enforcement (uses protocol-specific strategy) // - Pre-Amsterdam: gasLimit - gasRemaining (post-refund) // - Amsterdam+: pre-refund gas (prevents block gas limit circumvention via refunds) // cumulativeReceiptGasUsed: For receipt cumulativeGasUsed field (always post-refund) - long cumulativeBlockGasUsed = 0; + long cumulativeRegularGasUsed = 0; long cumulativeReceiptGasUsed = 0; + long cumulativeStateGasUsed = 0; long currentBlobGasUsed = 0; var blockHeader = block.getHeader(); @@ -298,7 +299,15 @@ public BlockProcessingResult processBlock( if (!(transactionUpdater instanceof StackedUpdater)) { transactionUpdater = blockUpdater; } - if (!hasAvailableBlockBudget(blockHeader, transaction, cumulativeBlockGasUsed)) { + // EIP-8037: 2D-aware budget check — delegates to BlockGasAccountingStrategy so that + // block import uses the same headroom logic as block building + // (BlockSizeTransactionSelector). + if (!hasAvailableBlockBudget( + blockHeader, + transaction, + cumulativeRegularGasUsed, + cumulativeStateGasUsed, + protocolSpec.getBlockGasAccountingStrategy())) { return new BlockProcessingResult(Optional.empty(), "provided gas insufficient"); } @@ -341,14 +350,16 @@ public BlockProcessingResult processBlock( // EIP-7778: Update both cumulative gas values // Block gas uses protocol-specific strategy (pre-refund for Amsterdam+) - cumulativeBlockGasUsed += + cumulativeRegularGasUsed += protocolSpec .getBlockGasAccountingStrategy() - .calculateBlockGas(transaction, transactionProcessingResult); + .calculateTransactionRegularGas(transaction, transactionProcessingResult); // Receipt gas always uses standard post-refund calculation cumulativeReceiptGasUsed += BlockGasAccountingStrategy.calculateReceiptGas( transaction, transactionProcessingResult); + // EIP-8037: Accumulate state gas used + cumulativeStateGasUsed += transactionProcessingResult.getStateGasUsed(); final var optionalVersionedHashes = transaction.getVersionedHashes(); if (optionalVersionedHashes.isPresent()) { final var versionedHashes = optionalVersionedHashes.get(); @@ -538,14 +549,13 @@ public BlockProcessingResult processBlock( return new BlockProcessingResult(Optional.empty(), e); } + // EIP-8037: gas_metered = max(cumulative_regular, cumulative_state) + final long gasMetered = Math.max(cumulativeRegularGasUsed, cumulativeStateGasUsed); + return new BlockProcessingResult( Optional.of( new BlockProcessingOutputs( - worldState, - receipts, - maybeRequests, - maybeBlockAccessList, - cumulativeBlockGasUsed)), + worldState, receipts, maybeRequests, maybeBlockAccessList, gasMetered)), parallelizedTxFound ? Optional.of(nbParallelTx) : Optional.empty()); } finally { stateRootCommitter.cancel(); @@ -578,14 +588,23 @@ protected TransactionProcessingResult getTransactionProcessingResult( @SuppressWarnings( "java:S2629") // INFO level logging rarely disabled in this project per maintainer feedback protected boolean hasAvailableBlockBudget( - final BlockHeader blockHeader, final Transaction transaction, final long currentGasUsed) { - final long remainingGasBudget = blockHeader.getGasLimit() - currentGasUsed; - if (Long.compareUnsigned(transaction.getGasLimit(), remainingGasBudget) > 0) { + final BlockHeader blockHeader, + final Transaction transaction, + final long cumulativeRegularGasUsed, + final long cumulativeStateGasUsed, + final BlockGasAccountingStrategy strategy) { + if (!strategy.hasBlockCapacity( + transaction.getGasLimit(), + cumulativeRegularGasUsed, + cumulativeStateGasUsed, + blockHeader.getGasLimit())) { LOG.info( "Block processing error: transaction gas limit {} exceeds available block budget" - + " remaining {}. Block {} Transaction {}", + + " (regular={}, state={}, limit={}). Block {} Transaction {}", transaction.getGasLimit(), - remainingGasBudget, + cumulativeRegularGasUsed, + cumulativeStateGasUsed, + blockHeader.getGasLimit(), blockHeader.getHash().getBytes().toHexString(), transaction.getHash().getBytes().toHexString()); return false; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockGasAccountingStrategy.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockGasAccountingStrategy.java index 273d0be4554..6668717e046 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockGasAccountingStrategy.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockGasAccountingStrategy.java @@ -21,23 +21,53 @@ * Strategy interface for calculating gas to add to a block's cumulative gas used. This allows * different hard forks to use different gas accounting methods. * - *

Prior to EIP-7778: Block gas is calculated POST-refund (gasLimit - gasRemaining), which + *

Prior to Amsterdam: Block gas is calculated POST-refund (gasLimit - gasRemaining), which * includes the benefit of gas refunds from SSTORE operations. * - *

EIP-7778 (Amsterdam+): Block gas is calculated PRE-refund (estimateGasUsedByTransaction), - * preventing block gas limit circumvention through refund credits. + *

Amsterdam (EIP-7778 + EIP-8037): Block gas is calculated PRE-refund and split into regular and + * state dimensions, preventing block gas limit circumvention through refund credits. */ -@FunctionalInterface public interface BlockGasAccountingStrategy { /** - * Calculate the gas to add to the block's cumulative gas used for a transaction. + * Calculate a transaction's regular gas contribution to the block's cumulative gas used. * * @param transaction the transaction being processed * @param result the transaction processing result - * @return the gas amount to add to the block's cumulative gas used + * @return the regular gas used by the transaction for block accounting */ - long calculateBlockGas(Transaction transaction, TransactionProcessingResult result); + long calculateTransactionRegularGas(Transaction transaction, TransactionProcessingResult result); + + /** + * Check whether the block has capacity for a transaction with the given gas limit. For 1D gas + * (pre-EIP-8037), this checks regular gas only. For 2D gas (EIP-8037), this considers the sum of + * remaining capacity in both regular and state dimensions. + * + * @param txGasLimit the gas limit of the candidate transaction + * @param cumulativeRegularGas cumulative regular gas used so far + * @param cumulativeStateGas cumulative state gas used so far + * @param blockGasLimit the block gas limit + * @return true if the block has capacity for this transaction + */ + default boolean hasBlockCapacity( + final long txGasLimit, + final long cumulativeRegularGas, + final long cumulativeStateGas, + final long blockGasLimit) { + return txGasLimit <= blockGasLimit - cumulativeRegularGas; + } + + /** + * Calculate the effective gas used for occupancy and fullness checks. For 1D gas, this is just + * the regular gas. For 2D gas (EIP-8037), this is max(regular, state). + * + * @param cumulativeRegularGas cumulative regular gas used + * @param cumulativeStateGas cumulative state gas used + * @return the effective gas used + */ + default long effectiveGasUsed(final long cumulativeRegularGas, final long cumulativeStateGas) { + return cumulativeRegularGas; + } /** * Frontier through BPO5: Uses post-refund gas (gasLimit - gasRemaining). This is the traditional @@ -46,10 +76,41 @@ public interface BlockGasAccountingStrategy { BlockGasAccountingStrategy FRONTIER = (tx, result) -> tx.getGasLimit() - result.getGasRemaining(); /** - * EIP-7778 (Amsterdam+): Uses pre-refund gas (estimateGasUsedByTransaction). This prevents block - * gas limit circumvention by not crediting refunds back to the block's gas budget. + * Amsterdam (EIP-7778 + EIP-8037): Uses pre-refund gas split into regular and state dimensions. + * + *

EIP-7778: Block gas is calculated pre-refund (estimateGasUsedByTransaction), preventing + * block gas limit circumvention through refund credits. + * + *

EIP-8037: Gas is split into regular and state portions. Regular gas = + * estimateGasUsedByTransaction - stateGasUsed. Block gas_metered = max(cumulative_regular, + * cumulative_state). */ - BlockGasAccountingStrategy EIP7778 = (tx, result) -> result.getEstimateGasUsedByTransaction(); + BlockGasAccountingStrategy AMSTERDAM = + new BlockGasAccountingStrategy() { + @Override + public long calculateTransactionRegularGas( + final Transaction transaction, final TransactionProcessingResult result) { + return result.getEstimateGasUsedByTransaction() - result.getStateGasUsed(); + } + + @Override + public boolean hasBlockCapacity( + final long txGasLimit, + final long cumulativeRegularGas, + final long cumulativeStateGas, + final long blockGasLimit) { + final long headroom = + Math.max(0, blockGasLimit - cumulativeRegularGas) + + Math.max(0, blockGasLimit - cumulativeStateGas); + return txGasLimit <= headroom; + } + + @Override + public long effectiveGasUsed( + final long cumulativeRegularGas, final long cumulativeStateGas) { + return Math.max(cumulativeRegularGas, cumulativeStateGas); + } + }; /** * Calculates the gas to be used in transaction receipts. This is always the standard post-refund diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockGasUsedValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockGasUsedValidator.java index 3ee1b172555..477a1ed485c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockGasUsedValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockGasUsedValidator.java @@ -56,15 +56,15 @@ boolean validate( }; /** - * EIP-7778 (Amsterdam+): Validates header.gasUsed against cumulativeBlockGasUsed when available. - * Header.gasUsed is pre-refund gas, while receipt.cumulativeGasUsed is post-refund gas, so they - * differ when refunds apply. + * Amsterdam (EIP-7778 + EIP-8037): Validates header.gasUsed against cumulativeBlockGasUsed when + * available. Header.gasUsed is pre-refund gas, while receipt.cumulativeGasUsed is post-refund + * gas, so they differ when refunds apply. * *

During full block processing, cumulativeBlockGasUsed is provided and must match * header.gasUsed. During light validation (sync), cumulativeBlockGasUsed is empty and validation * is skipped - the receiptsRoot already validates receipt integrity. */ - BlockGasUsedValidator EIP7778 = + BlockGasUsedValidator AMSTERDAM = (header, receipts, blockGas) -> { // If we have block gas from processing, validate against it // Otherwise (light validation during sync), skip - receiptsRoot validates receipts diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java index fd2ef40a5c5..0545264c05e 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java @@ -1225,10 +1225,27 @@ static ProtocolSpecBuilder amsterdamDefinition( .blockAccessListFactory(new BlockAccessListFactory()) .blockAccessListValidatorBuilder(MainnetBlockAccessListValidator::create) .stateRootCommitterFactory(new StateRootCommitterFactoryBal(balConfiguration)) - - // EIP-7778: Block gas accounting without refunds (prevents block gas limit circumvention) - .blockGasAccountingStrategy(BlockGasAccountingStrategy.EIP7778) - .blockGasUsedValidator(BlockGasUsedValidator.EIP7778) + // EIP-8037: Disable validation-time TX_MAX_GAS_LIMIT cap (enforced at runtime on regular + // gas) + .gasLimitCalculatorBuilder( + (feeMarket, gasCalculator, blobSchedule) -> { + final long londonForkBlock = genesisConfigOptions.getLondonBlockNumber().orElse(0L); + return new OsakaTargetingGasLimitCalculator( + londonForkBlock, + (BaseFeeMarket) feeMarket, + gasCalculator, + blobSchedule.getMax(), + blobSchedule.getTarget(), + miningConfiguration.getMaxBlobsPerTransaction(), + miningConfiguration.getMaxBlobsPerBlock(), + Long.MAX_VALUE); + }) + // EIP-8037: Amsterdam gas calculator with state gas cost support + .gasCalculator(AmsterdamGasCalculator::new) + // Amsterdam (EIP-7778 + EIP-8037): Pre-refund 2D gas accounting + .blockGasAccountingStrategy(BlockGasAccountingStrategy.AMSTERDAM) + // Amsterdam: Validator uses pre-refund gas_metered = max(regular, state) from processing + .blockGasUsedValidator(BlockGasUsedValidator.AMSTERDAM) .hardforkId(AMSTERDAM); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java index da342988a64..666ca545831 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java @@ -268,6 +268,7 @@ public TransactionProcessingResult processTransaction( } 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"); @@ -279,9 +280,9 @@ public TransactionProcessingResult processTransaction( .get() .process(delegationUpdater, transaction, accessLocationTracker); eip2930WarmAddressList.addAll(codeDelegationResult.accessedDelegatorAddresses()); + alreadyExistingDelegators = codeDelegationResult.alreadyExistingDelegators(); codeDelegationRefund = - gasCalculator.calculateDelegateCodeGasRefund( - (codeDelegationResult.alreadyExistingDelegators())); + gasCalculator.calculateDelegateCodeGasRefund(alreadyExistingDelegators); delegationUpdater.commit(); } @@ -306,16 +307,41 @@ public TransactionProcessingResult processTransaction( gasCalculator.accessListGasCost(eip2930AccessListEntries.size(), accessListStorageCount); final long codeDelegationGas = gasCalculator.delegateCodeGasCost(transaction.codeDelegationListSize()); - final long intrinsicGas = + final long intrinsicRegularGas = gasCalculator.transactionIntrinsicGasCost( transaction, clampedAdd(accessListGas, codeDelegationGas)); - final long gasAvailable = transaction.getGasLimit() - intrinsicGas; + // 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(), - intrinsicGas); + intrinsicRegularGas); final WorldUpdater worldUpdater = worldState.updater(); @@ -382,13 +408,78 @@ public TransactionProcessingResult processTransaction( .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 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: On exceptional halt of the initial frame, zero the reservoir so all gas is + // consumed. Child frame reverts restore the reservoir via undo, but the initial frame's + // halt means all gas is forfeit. For REVERT, the reservoir was already restored by + // rollback and should be returned to the sender. + if (initialFrame.getExceptionalHaltReason().isPresent()) { + initialFrame.setStateGasReservoir(0L); + } + + // 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). + // EIP-8037: 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 (initialFrame.getState() == MessageFrame.State.COMPLETED_SUCCESS) { + if (txSucceeded) { worldUpdater.commit(); } else { if (initialFrame.getExceptionalHaltReason().isPresent()) { @@ -397,6 +488,12 @@ public TransactionProcessingResult processTransaction( 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? @@ -409,8 +506,11 @@ public TransactionProcessingResult processTransaction( // 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 = - gasCalculator.calculateGasRefund(transaction, initialFrame, codeDelegationRefund); + regularGasLimitExceeded + ? 0L + : gasCalculator.calculateGasRefund(transaction, initialFrame, codeDelegationRefund); final Wei refundedWei = transactionGasPrice.multiply(refundedGas); final Wei balancePriorToRefund = sender.getBalance(); sender.incrementBalance(refundedWei); @@ -424,14 +524,27 @@ public TransactionProcessingResult processTransaction( // 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 - final long executionGas = transaction.getGasLimit() - initialFrame.getRemainingGas(); + // EIP-8037: Gas accounting with multidimensional gas support final long floorCost = gasCalculator.transactionFloorCost( transaction.getPayload(), transaction.getPayloadZeroBytes()); - final long gasUsedByTransaction = Math.max(executionGas, floorCost); - - // update the coinbase - final long usedGas = transaction.getGasLimit() - refundedGas; + final TransactionGasAccounting.GasResult gasResult = + TransactionGasAccounting.builder() + .txGasLimit(transaction.getGasLimit()) + .remainingGas(initialFrame.getRemainingGas()) + .stateGasReservoir(initialFrame.getStateGasReservoir()) + .stateGasUsed(initialFrame.getStateGasUsed()) + .initialFrameStateGasSpill(initialFrameStateGasSpill) + .stateGasSpillBurned(initialFrame.getStateGasSpillBurned()) + .regularGasCollisionBurned(initialFrame.getRegularGasCollisionBurned()) + .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(); @@ -448,6 +561,7 @@ public TransactionProcessingResult processTransaction( gasUsedByTransaction, refundedGas, usedGas, + effectiveStateGas, ValidationResult.invalid( TransactionInvalidReason.TRANSACTION_PRICE_TOO_LOW, "transaction price must be greater than base fee"), @@ -482,7 +596,7 @@ public TransactionProcessingResult processTransaction( operationTracer.traceEndTransaction( worldState.updater(), transaction, - initialFrame.getState() == MessageFrame.State.COMPLETED_SUCCESS, + txSucceeded, initialFrame.getOutputData(), initialFrame.getLogs(), gasUsedByTransaction, @@ -498,12 +612,13 @@ public TransactionProcessingResult processTransaction( final Optional partialBlockAccessView = accessLocationTracker.map(tracker -> tracker.createPartialBlockAccessView(worldState)); - if (initialFrame.getState() == MessageFrame.State.COMPLETED_SUCCESS) { + if (txSucceeded) { return TransactionProcessingResult.successful( initialFrame.getLogs(), gasUsedByTransaction, refundedGas, usedGas, + effectiveStateGas, initialFrame.getOutputData(), partialBlockAccessView, validationResult); @@ -524,6 +639,7 @@ public TransactionProcessingResult processTransaction( gasUsedByTransaction, refundedGas, usedGas, + effectiveStateGas, validationResult, initialFrame.getRevertReason(), initialFrame.getExceptionalHaltReason(), diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculator.java index a80b44e1b0c..7600651a082 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/OsakaTargetingGasLimitCalculator.java @@ -49,6 +49,26 @@ public OsakaTargetingGasLimitCalculator( final int targetBlobsPerBlock, final OptionalInt maxBlobsPerTransaction, final OptionalInt userMaxBlobsPerBlock) { + this( + londonForkBlock, + feeMarket, + gasCalculator, + maxBlobsPerBlock, + targetBlobsPerBlock, + maxBlobsPerTransaction, + userMaxBlobsPerBlock, + DEFAULT_TRANSACTION_GAS_LIMIT_CAP_OSAKA); + } + + public OsakaTargetingGasLimitCalculator( + final long londonForkBlock, + final BaseFeeMarket feeMarket, + final GasCalculator gasCalculator, + final int maxBlobsPerBlock, + final int targetBlobsPerBlock, + final OptionalInt maxBlobsPerTransaction, + final OptionalInt userMaxBlobsPerBlock, + final long transactionGasLimitCap) { super(londonForkBlock, feeMarket, gasCalculator, maxBlobsPerBlock, targetBlobsPerBlock); final long blobGasPerBlob = gasCalculator.getBlobGasPerBlob(); int effectiveMaxBlobsPerTx = maxBlobsPerTransaction.orElse(DEFAULT_MAX_BLOBS_PER_TRANSACTION); @@ -60,7 +80,7 @@ public OsakaTargetingGasLimitCalculator( maxBlobsPerBlock); effectiveMaxBlobsPerTx = maxBlobsPerBlock; } - this.transactionGasLimitCap = DEFAULT_TRANSACTION_GAS_LIMIT_CAP_OSAKA; + this.transactionGasLimitCap = transactionGasLimitCap; this.transactionBlobGasLimitCap = blobGasPerBlob * effectiveMaxBlobsPerTx; if (userMaxBlobsPerBlock.isPresent()) { final int effectiveMax = Math.min(userMaxBlobsPerBlock.getAsInt(), maxBlobsPerBlock); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java new file mode 100644 index 00000000000..c99be511830 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java @@ -0,0 +1,129 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.mainnet; + +import org.immutables.value.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Encapsulates the gas accounting logic for transaction processing, including EIP-8037 + * multidimensional gas. + * + *

This extracts the complex gas computation from {@link MainnetTransactionProcessor} into a + * testable, stateless helper. Uses a generated builder (via Immutables) to prevent parameter + * ordering mistakes — the many long fields are easily confused without named setters. + * + *

Usage: {@code TransactionGasAccounting.builder().txGasLimit(...).remainingGas(...)... + * .build().calculate()} + */ +@Value.Immutable +public abstract class TransactionGasAccounting { + + private static final Logger LOG = LoggerFactory.getLogger(TransactionGasAccounting.class); + + /** Result of the gas accounting calculation. */ + public record GasResult(long effectiveStateGas, long gasUsedByTransaction, long usedGas) {} + + /** The transaction gas limit. */ + public abstract long txGasLimit(); + + /** Gas remaining in the initial frame after execution. */ + public abstract long remainingGas(); + + /** Leftover state gas reservoir in the initial frame. */ + public abstract long stateGasReservoir(); + + /** State gas consumed by the initial frame. */ + public abstract long stateGasUsed(); + + /** State gas spilled from the initial frame's own revert/halt. */ + public abstract long initialFrameStateGasSpill(); + + /** Total state gas spilled into gasRemaining from reverted frames. */ + public abstract long stateGasSpillBurned(); + + /** Gas burned by CREATE children that halted before executing code (address collision). */ + public abstract long regularGasCollisionBurned(); + + /** Gas refunded to the sender. */ + public abstract long refundedGas(); + + /** Transaction floor cost (EIP-7623), 0 for pre-Prague. */ + public abstract long floorCost(); + + /** Whether the regular gas limit was exceeded (EIP-8037). */ + public abstract boolean regularGasLimitExceeded(); + + /** Creates a new builder. */ + public static ImmutableTransactionGasAccounting.Builder builder() { + return ImmutableTransactionGasAccounting.builder(); + } + + /** + * Calculate gas accounting for a completed transaction. + * + *

Two paths: + * + *

    + *
  • regularGasLimitExceeded=true: All gas consumed. effectiveStateGas = stateGasUsed + + * initialFrameStateGasSpill. + *
  • regularGasLimitExceeded=false: Computes executionGas, stateGas, regularGas with + * double-counting avoidance. Floor cost applies to regularGas only. + *
+ * + * @return the gas result containing effectiveStateGas, gasUsedByTransaction, and usedGas + */ + public GasResult calculate() { + if (regularGasLimitExceeded()) { + final long effectiveStateGas = stateGasUsed() + initialFrameStateGasSpill(); + return new GasResult(effectiveStateGas, txGasLimit(), txGasLimit()); + } + + // EIP-8037: Include leftover reservoir in remaining gas for execution gas calculation + final long executionGas = txGasLimit() - remainingGas() - stateGasReservoir(); + // EIP-8037: Floor applies to regular gas only, not total gas. + // Pre-Amsterdam: stateGasUsed=0, spillBurned=0, collisionBurned=0 — identical. + // stateGasSpillBurned: state gas that spilled into gasRemaining from reverted frames. + // For child frame reverts: not metered as regular or state gas (invisible to block). + // For initial frame revert/halt: counts as state gas for block accounting, because the + // transaction's state gas consumption is final regardless of execution outcome. + // regularGasCollisionBurned: gas burned by CREATE children that halted before executing + // code (address collision); excluded from block regular gas but still charged as fees. + final long stateGas = stateGasUsed() + initialFrameStateGasSpill(); + // initialFrameStateGasSpill is already included in spillBurned AND stateGas, + // so subtract it from spillBurned to avoid double-counting. + final long regularGas = + executionGas + - stateGas + - (stateGasSpillBurned() - initialFrameStateGasSpill()) + - regularGasCollisionBurned(); + if (regularGas < 0) { + // This should not happen under normal circumstances. A negative regularGas indicates a + // bug in gas accounting — log at error level to ensure visibility. + LOG.error( + "Negative regularGas={} (executionGas={}, stateGas={}, spillBurned={}, initialSpill={}, collisionBurned={})", + regularGas, + executionGas, + stateGas, + stateGasSpillBurned(), + initialFrameStateGasSpill(), + regularGasCollisionBurned()); + } + final long gasUsedByTransaction = Math.max(regularGas, floorCost()) + stateGas; + final long usedGas = txGasLimit() - refundedGas(); + return new GasResult(stateGas, gasUsedByTransaction, usedGas); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/processing/TransactionProcessingResult.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/processing/TransactionProcessingResult.java index 61dcc403203..acbd6fad536 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/processing/TransactionProcessingResult.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/processing/TransactionProcessingResult.java @@ -50,6 +50,8 @@ public enum Status { private final long gasSpent; + private final long stateGasUsed; + private final List logs; private final Bytes output; @@ -71,6 +73,7 @@ public static TransactionProcessingResult invalid( -1, -1, -1, + 0, Bytes.EMPTY, validationResult, Optional.empty(), @@ -82,6 +85,7 @@ public static TransactionProcessingResult failed( final long gasUsedByTransaction, final long gasRemaining, final long gasSpent, + final long stateGasUsed, final ValidationResult validationResult, final Optional revertReason, final Optional exceptionalHaltReason, @@ -92,6 +96,7 @@ public static TransactionProcessingResult failed( gasUsedByTransaction, gasRemaining, gasSpent, + stateGasUsed, Bytes.EMPTY, validationResult, revertReason, @@ -114,6 +119,7 @@ public static TransactionProcessingResult failed( gasUsedByTransaction, gasRemaining, gasUsedByTransaction, + 0, validationResult, revertReason, exceptionalHaltReason, @@ -125,6 +131,7 @@ public static TransactionProcessingResult successful( final long gasUsedByTransaction, final long gasRemaining, final long gasSpent, + final long stateGasUsed, final Bytes output, final Optional partialBlockAccessView, final ValidationResult validationResult) { @@ -134,6 +141,7 @@ public static TransactionProcessingResult successful( gasUsedByTransaction, gasRemaining, gasSpent, + stateGasUsed, output, validationResult, Optional.empty(), @@ -156,6 +164,7 @@ public static TransactionProcessingResult successful( gasUsedByTransaction, gasRemaining, gasUsedByTransaction, + 0, output, partialBlockAccessView, validationResult); @@ -180,6 +189,7 @@ public TransactionProcessingResult( estimateGasUsedByTransaction, gasRemaining, estimateGasUsedByTransaction, + 0, output, validationResult, revertReason, @@ -193,6 +203,7 @@ public TransactionProcessingResult( final long estimateGasUsedByTransaction, final long gasRemaining, final long gasSpent, + final long stateGasUsed, final Bytes output, final ValidationResult validationResult, final Optional revertReason, @@ -202,6 +213,7 @@ public TransactionProcessingResult( this.estimateGasUsedByTransaction = estimateGasUsedByTransaction; this.gasRemaining = gasRemaining; this.gasSpent = gasSpent; + this.stateGasUsed = stateGasUsed; this.output = output; this.validationResult = validationResult; this.revertReason = revertReason; @@ -229,6 +241,7 @@ public TransactionProcessingResult( estimateGasUsedByTransaction, gasRemaining, estimateGasUsedByTransaction, + 0, output, validationResult, revertReason, @@ -236,13 +249,14 @@ public TransactionProcessingResult( partialBlockAccessView); } - /** Constructor with gasSpent (for Amsterdam+ forks with EIP-7778). */ + /** Constructor with gasSpent and stateGasUsed (for Amsterdam+ forks with EIP-7778/EIP-8037). */ public TransactionProcessingResult( final Status status, final List logs, final long estimateGasUsedByTransaction, final long gasRemaining, final long gasSpent, + final long stateGasUsed, final Bytes output, final ValidationResult validationResult, final Optional revertReason, @@ -253,6 +267,7 @@ public TransactionProcessingResult( this.estimateGasUsedByTransaction = estimateGasUsedByTransaction; this.gasRemaining = gasRemaining; this.gasSpent = gasSpent; + this.stateGasUsed = stateGasUsed; this.output = output; this.validationResult = validationResult; this.revertReason = revertReason; @@ -325,6 +340,19 @@ public long getGasSpent() { return gasSpent; } + /** + * Returns the state gas used by the transaction (EIP-8037). + * + *

This represents the gas consumed by state-creation operations (CREATE, SSTORE 0→nonzero, + * CALL to new accounts, code deposits, EIP-7702 delegations). State gas is tracked separately + * from regular gas for multidimensional gas metering. + * + * @return the state gas used + */ + public long getStateGasUsed() { + return stateGasUsed; + } + /** * Returns the status of the transaction after being processed. * @@ -438,6 +466,8 @@ public String toString() { + gasRemaining + ", gasSpent=" + gasSpent + + ", stateGasUsed=" + + stateGasUsed + ", logs=" + logs + ", output=" diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java index cb71d18eed5..ae0ac9b7d46 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java @@ -818,7 +818,7 @@ void processAccountReadThenUpdateTx(final BlockProcessor blockProcessor) { Block blockWithTransactions = createBlockWithTransactions( - "0x2150c4988c5064c272bb9b9e1119627d41ca1d7b2f93b7624b8c79f84d327f8d", + "0x3912cef7496ffde5dc6822b2d268c582faeadcb261c66c80a333aca6f5da9de6", Wei.of(5), transactionTransfer, getcontractBalanceTransaction, @@ -886,7 +886,7 @@ void processAccountUpdateThenReadTx(final BlockProcessor blockProcessor) { Block blockWithTransactions = createBlockWithTransactions( - "0x32e68243a5de9ec1ab65f355c0d1f1ddfeb0770a5853966224653007c02ac75f", + "0xb5b3d7b39b66b3e829b038d6ba9b38a3597cedd87df504828336dedb5f3e799f", Wei.of(5), transactionTransfer, sendEthFromContractTransaction, @@ -953,7 +953,7 @@ void processAccountReadThenUpdateTxWithTwoAccounts(final BlockProcessor blockPro Block blockWithTransactions = createBlockWithTransactions( - "0x61943cc0897cfed1c9b53f59fef91f0902944db9bc30320a7eda4f64612895f6", + "0xf81c87537a0a166a48ede069bf50358163ae697d1005cbaa7e6083f22a60a2fb", Wei.of(5), transactionTransfer, getcontractBalanceTransaction, @@ -1022,7 +1022,7 @@ void processAccountUpdateThenReadTxWithTwoAccounts(final BlockProcessor blockPro Block blockWithTransactions = createBlockWithTransactions( - "0x61943cc0897cfed1c9b53f59fef91f0902944db9bc30320a7eda4f64612895f6", + "0xf81c87537a0a166a48ede069bf50358163ae697d1005cbaa7e6083f22a60a2fb", Wei.of(5), transactionTransfer, sendEthFromContractTransaction, diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorTest.java index fdd8523eb13..e58f866b702 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorTest.java @@ -16,9 +16,11 @@ import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,6 +36,7 @@ import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.Withdrawal; import org.hyperledger.besu.ethereum.mainnet.blockhash.FrontierPreExecutionProcessor; import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterFactoryDefault; @@ -121,6 +124,31 @@ void withNoProcessorAndWithdrawals_WithdrawalsAreNotProcessed() { verify(withdrawalsProcessor, never()).processWithdrawals(any(), any(), any(), any()); } + @Test + void hasAvailableBlockBudget_delegates2DCheckToStrategy() { + // EIP-8037: hasAvailableBlockBudget must delegate to + // BlockGasAccountingStrategy.hasBlockCapacity + // so that block import uses the same 2D headroom logic as block building. + final long blockGasLimit = 100_000L; + final BlockHeader header = new BlockHeaderTestFixture().gasLimit(blockGasLimit).buildHeader(); + final Transaction tx = mock(Transaction.class); + when(tx.getGasLimit()).thenReturn(50_000L); + when(tx.getHash()).thenReturn(Hash.fromHexStringLenient("0x1234")); + + // Regular=60k, State=40k. 1D headroom=40k < txGasLimit=50k → would fail. + // 2D headroom = (100k-60k) + (100k-40k) = 100k >= 50k → should pass with AMSTERDAM. + assertThat( + blockProcessor.hasAvailableBlockBudget( + header, tx, 60_000L, 40_000L, BlockGasAccountingStrategy.AMSTERDAM)) + .isTrue(); + + // Same scenario with FRONTIER (1D check): should fail + assertThat( + blockProcessor.hasAvailableBlockBudget( + header, tx, 60_000L, 40_000L, BlockGasAccountingStrategy.FRONTIER)) + .isFalse(); + } + private static class TestBlockProcessor extends AbstractBlockProcessor { protected TestBlockProcessor( diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/BlockGasAccountingStrategyTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/BlockGasAccountingStrategyTest.java index 061e30ac9eb..e1318dcd973 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/BlockGasAccountingStrategyTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/BlockGasAccountingStrategyTest.java @@ -26,11 +26,11 @@ /** * Tests for {@link BlockGasAccountingStrategy}. * - *

EIP-7778 changes how gas is accounted for at the block level: + *

Amsterdam (EIP-7778 + EIP-8037) changes how gas is accounted for at the block level: * *

    - *
  • Pre-EIP-7778 (FRONTIER): Block gas = gasLimit - gasRemaining (post-refund) - *
  • EIP-7778 (Amsterdam+): Block gas = estimateGasUsedByTransaction (pre-refund) + *
  • Pre-Amsterdam (FRONTIER): Block gas = gasLimit - gasRemaining (post-refund) + *
  • Amsterdam: Block gas = pre-refund gas, split into regular and state dimensions *
*/ public class BlockGasAccountingStrategyTest { @@ -51,23 +51,26 @@ public void frontierStrategy_usesPostRefundGas() { when(result.getEstimateGasUsedByTransaction()).thenReturn(PRE_REFUND_GAS); // Frontier strategy: gasLimit - gasRemaining = 100,000 - 30,000 = 70,000 - final long blockGas = BlockGasAccountingStrategy.FRONTIER.calculateBlockGas(tx, result); + final long blockGas = + BlockGasAccountingStrategy.FRONTIER.calculateTransactionRegularGas(tx, result); assertThat(blockGas).isEqualTo(PRE_REFUND_GAS); } @Test - public void eip7778Strategy_usesPreRefundGas() { - // Setup: Transaction with gas limit 100k, processed with pre-refund gas of 70k + public void amsterdamStrategy_usesPreRefundGas() { + // Setup: Transaction with gas limit 100k, processed with pre-refund gas of 70k, no state gas final Transaction tx = mock(Transaction.class); when(tx.getGasLimit()).thenReturn(GAS_LIMIT); final TransactionProcessingResult result = mock(TransactionProcessingResult.class); when(result.getGasRemaining()).thenReturn(GAS_REMAINING); when(result.getEstimateGasUsedByTransaction()).thenReturn(PRE_REFUND_GAS); + when(result.getStateGasUsed()).thenReturn(0L); - // EIP-7778 strategy: uses estimateGasUsedByTransaction directly = 70,000 - final long blockGas = BlockGasAccountingStrategy.EIP7778.calculateBlockGas(tx, result); + // Amsterdam strategy: estimateGasUsedByTransaction - stateGasUsed = 70,000 - 0 = 70,000 + final long blockGas = + BlockGasAccountingStrategy.AMSTERDAM.calculateTransactionRegularGas(tx, result); assertThat(blockGas).isEqualTo(PRE_REFUND_GAS); } @@ -88,16 +91,19 @@ public void strategiesDifferWhenRefundsApply() { final TransactionProcessingResult result = mock(TransactionProcessingResult.class); when(result.getGasRemaining()).thenReturn(gasRemainingAfterRefund); when(result.getEstimateGasUsedByTransaction()).thenReturn(preRefundGasUsed); + when(result.getStateGasUsed()).thenReturn(0L); // Frontier: 100,000 - 40,000 = 60,000 (benefits from refund) - final long frontierGas = BlockGasAccountingStrategy.FRONTIER.calculateBlockGas(tx, result); - // EIP-7778: 70,000 (no refund benefit for block accounting) - final long eip7778Gas = BlockGasAccountingStrategy.EIP7778.calculateBlockGas(tx, result); + final long frontierGas = + BlockGasAccountingStrategy.FRONTIER.calculateTransactionRegularGas(tx, result); + // Amsterdam: 70,000 (no refund benefit for block accounting) + final long amsterdamGas = + BlockGasAccountingStrategy.AMSTERDAM.calculateTransactionRegularGas(tx, result); assertThat(frontierGas).isEqualTo(60_000L); - assertThat(eip7778Gas).isEqualTo(70_000L); - // EIP-7778 accounts for more gas, preventing block gas limit circumvention - assertThat(eip7778Gas).isGreaterThan(frontierGas); + assertThat(amsterdamGas).isEqualTo(70_000L); + // Amsterdam accounts for more gas, preventing block gas limit circumvention + assertThat(amsterdamGas).isGreaterThan(frontierGas); } @Test @@ -112,11 +118,72 @@ public void strategiesEqualWhenNoRefunds() { final TransactionProcessingResult result = mock(TransactionProcessingResult.class); when(result.getGasRemaining()).thenReturn(gasRemaining); when(result.getEstimateGasUsedByTransaction()).thenReturn(gasUsed); + when(result.getStateGasUsed()).thenReturn(0L); - final long frontierGas = BlockGasAccountingStrategy.FRONTIER.calculateBlockGas(tx, result); - final long eip7778Gas = BlockGasAccountingStrategy.EIP7778.calculateBlockGas(tx, result); + final long frontierGas = + BlockGasAccountingStrategy.FRONTIER.calculateTransactionRegularGas(tx, result); + final long amsterdamGas = + BlockGasAccountingStrategy.AMSTERDAM.calculateTransactionRegularGas(tx, result); assertThat(frontierGas).isEqualTo(gasUsed); - assertThat(eip7778Gas).isEqualTo(gasUsed); + assertThat(amsterdamGas).isEqualTo(gasUsed); + } + + @Test + public void amsterdamStrategy_subtractsStateGasFromBlockGas() { + final Transaction tx = mock(Transaction.class); + when(tx.getGasLimit()).thenReturn(GAS_LIMIT); + + final TransactionProcessingResult result = mock(TransactionProcessingResult.class); + when(result.getGasRemaining()).thenReturn(GAS_REMAINING); + // estimateGasUsedByTransaction = 70,000 (pre-refund), stateGas = 10,000 + when(result.getEstimateGasUsedByTransaction()).thenReturn(PRE_REFUND_GAS); + when(result.getStateGasUsed()).thenReturn(10_000L); + + // Amsterdam block gas = estimateGas - stateGas = 70,000 - 10,000 = 60,000 + final long blockGas = + BlockGasAccountingStrategy.AMSTERDAM.calculateTransactionRegularGas(tx, result); + assertThat(blockGas).isEqualTo(60_000L); + } + + @Test + public void amsterdamStrategy_effectiveGasUsedIsMaxOfDimensions() { + // max(regular=50k, state=80k) = 80k + assertThat(BlockGasAccountingStrategy.AMSTERDAM.effectiveGasUsed(50_000L, 80_000L)) + .isEqualTo(80_000L); + // max(regular=80k, state=50k) = 80k + assertThat(BlockGasAccountingStrategy.AMSTERDAM.effectiveGasUsed(80_000L, 50_000L)) + .isEqualTo(80_000L); + // Frontier always returns regular gas only + assertThat(BlockGasAccountingStrategy.FRONTIER.effectiveGasUsed(50_000L, 80_000L)) + .isEqualTo(50_000L); + } + + @Test + public void amsterdamStrategy_hasBlockCapacityWith2DHeadroom() { + final long blockGasLimit = 100_000L; + // Regular used: 60k, State used: 40k + // Headroom = max(0, 100k-60k) + max(0, 100k-40k) = 40k + 60k = 100k + assertThat( + BlockGasAccountingStrategy.AMSTERDAM.hasBlockCapacity( + 100_000L, 60_000L, 40_000L, blockGasLimit)) + .isTrue(); + // txGasLimit=101k > headroom=100k + assertThat( + BlockGasAccountingStrategy.AMSTERDAM.hasBlockCapacity( + 100_001L, 60_000L, 40_000L, blockGasLimit)) + .isFalse(); + + // Frontier only checks regular dimension + // Regular headroom = 100k - 60k = 40k, txGasLimit=50k > 40k + assertThat( + BlockGasAccountingStrategy.FRONTIER.hasBlockCapacity( + 50_000L, 60_000L, 40_000L, blockGasLimit)) + .isFalse(); + // But Amsterdam has 2D headroom: 40k + 60k = 100k >= 50k + assertThat( + BlockGasAccountingStrategy.AMSTERDAM.hasBlockCapacity( + 50_000L, 60_000L, 40_000L, blockGasLimit)) + .isTrue(); } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/Eip8037RegularGasLimitTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/Eip8037RegularGasLimitTest.java new file mode 100644 index 00000000000..f851e271edb --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/Eip8037RegularGasLimitTest.java @@ -0,0 +1,212 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Address; +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.feemarket.FeeMarket; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.blockhash.BlockHashLookup; +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.AmsterdamGasCalculator; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.processor.ContractCreationProcessor; +import org.hyperledger.besu.evm.processor.MessageCallProcessor; +import org.hyperledger.besu.evm.worldstate.CodeDelegationService; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.math.BigInteger; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class Eip8037RegularGasLimitTest { + + private static final int MAX_STACK_SIZE = 1024; + + private final GasCalculator gasCalculator = new AmsterdamGasCalculator(); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private TransactionValidatorFactory transactionValidatorFactory; + + @Mock private ContractCreationProcessor contractCreationProcessor; + @Mock private MessageCallProcessor messageCallProcessor; + + @Mock private WorldUpdater worldState; + @Mock private ProcessableBlockHeader blockHeader; + @Mock private Transaction transaction; + @Mock private BlockHashLookup blockHashLookup; + + @Mock private MutableAccount senderAccount; + + private MainnetTransactionProcessor createProcessor() { + return MainnetTransactionProcessor.builder() + .gasCalculator(gasCalculator) + .transactionValidatorFactory(transactionValidatorFactory) + .contractCreationProcessor(contractCreationProcessor) + .messageCallProcessor(messageCallProcessor) + .clearEmptyAccounts(false) + .warmCoinbase(false) + .maxStackSize(MAX_STACK_SIZE) + .feeMarket(FeeMarket.legacy()) + .coinbaseFeePriceCalculator(CoinbaseFeePriceCalculator.frontier()) + .codeDelegationProcessor( + new CodeDelegationProcessor( + Optional.of(BigInteger.ONE), BigInteger.TEN, new CodeDelegationService())) + .build(); + } + + private void setupCommonMocks(final long gasLimit) { + final Address senderAddress = + Address.fromHexString("0x5555555555555555555555555555555555555555"); + final Address toAddress = Address.fromHexString("0x2222222222222222222222222222222222222222"); + + when(transaction.getType()).thenReturn(TransactionType.EIP1559); + when(transaction.getPayload()).thenReturn(Bytes.EMPTY); + when(transaction.getSender()).thenReturn(senderAddress); + when(transaction.getValue()).thenReturn(Wei.ZERO); + when(transaction.getTo()).thenReturn(Optional.of(toAddress)); + when(transaction.getGasLimit()).thenReturn(gasLimit); + + when(transactionValidatorFactory.get().validate(any(), any(), any(), any())) + .thenReturn(ValidationResult.valid()); + when(transactionValidatorFactory.get().validateForSender(any(), any(), any())) + .thenReturn(ValidationResult.valid()); + when(worldState.getOrCreateSenderAccount(any())).thenReturn(senderAccount); + when(worldState.updater()).thenReturn(worldState); + } + + @Test + void regularGasExceedingTxMaxGasLimitRevertsTransaction() { + setupCommonMocks(20_000_000L); + + doAnswer( + invocation -> { + final MessageFrame frame = invocation.getArgument(0); + // Simulate EXCEPTIONAL_HALT (e.g. ran out of gas mid-execution). + // Setting the halt reason causes MTP to zero the state gas reservoir, + // so totalConsumed = txGasLimit - 0 - 0 = 20M (instead of 20M - reservoir). + frame.setExceptionalHaltReason(Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); + frame.setGasRemaining(0); + // stateGas = 2M; regularConsumed = 20M - 2M = 18M > TX_MAX_GAS_LIMIT (16,777,216) + frame.incrementStateGasUsed(2_000_000L); + frame.getMessageFrameStack().pop(); + return null; + }) + .when(messageCallProcessor) + .process(any(), any()); + + final TransactionProcessingResult result = + createProcessor() + .processTransaction( + worldState, + blockHeader, + transaction, + Address.fromHexString("0x4242424242424242424242424242424242424242"), + blockHashLookup, + ImmutableTransactionValidationParams.builder().build(), + Wei.ZERO); + + assertThat(result.isSuccessful()).isFalse(); + assertThat(result.getEstimateGasUsedByTransaction()).isEqualTo(20_000_000L); + assertThat(result.getValidationResult().getInvalidReason()) + .isEqualTo(TransactionInvalidReason.EXECUTION_HALTED); + } + + @Test + void regularGasWithinTxMaxGasLimitSucceeds() { + setupCommonMocks(20_000_000L); + + doAnswer( + invocation -> { + final MessageFrame frame = invocation.getArgument(0); + frame.setState(MessageFrame.State.COMPLETED_SUCCESS); + // totalConsumed = 20M - 5M = 15M + // regularConsumed = 15M - 0 = 15M < TX_MAX_GAS_LIMIT (16,777,216) -> passes + frame.setGasRemaining(5_000_000L); + frame.getMessageFrameStack().pop(); + return null; + }) + .when(messageCallProcessor) + .process(any(), any()); + + final TransactionProcessingResult result = + createProcessor() + .processTransaction( + worldState, + blockHeader, + transaction, + Address.fromHexString("0x4242424242424242424242424242424242424242"), + blockHashLookup, + ImmutableTransactionValidationParams.builder().build(), + Wei.ZERO); + + assertThat(result.isSuccessful()).isTrue(); + } + + @Test + void stateGasCanPushTotalBeyondTxMaxGasLimitWithoutRevert() { + // Total gas > TX_MAX_GAS_LIMIT but regular gas portion is within limit + setupCommonMocks(20_000_000L); + + doAnswer( + invocation -> { + final MessageFrame frame = invocation.getArgument(0); + frame.setState(MessageFrame.State.COMPLETED_SUCCESS); + // totalConsumed = 20M - 1M = 19M (exceeds TX_MAX_GAS_LIMIT) + // stateGas = 5M + // regularConsumed = 19M - 5M = 14M < TX_MAX_GAS_LIMIT -> passes + frame.setGasRemaining(1_000_000L); + frame.incrementStateGasUsed(5_000_000L); + frame.getMessageFrameStack().pop(); + return null; + }) + .when(messageCallProcessor) + .process(any(), any()); + + final TransactionProcessingResult result = + createProcessor() + .processTransaction( + worldState, + blockHeader, + transaction, + Address.fromHexString("0x4242424242424242424242424242424242424242"), + blockHashLookup, + ImmutableTransactionValidationParams.builder().build(), + Wei.ZERO); + + // Total gas exceeds TX_MAX_GAS_LIMIT but only regular gas is checked + assertThat(result.isSuccessful()).isTrue(); + assertThat(result.getStateGasUsed()).isEqualTo(5_000_000L); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessorTest.java index d7f773587d0..472bda4d092 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessorTest.java @@ -107,6 +107,7 @@ void shouldWarmCoinbaseIfRequested() { when(transaction.getType()).thenReturn(TransactionType.EIP1559); when(transaction.getPayload()).thenReturn(Bytes.EMPTY); + when(transaction.getGasLimit()).thenReturn(100_000L); when(transaction.getSender()).thenReturn(senderAddress); when(transaction.getValue()).thenReturn(Wei.ZERO); when(transactionValidatorFactory.get().validate(any(), any(), any(), any())) @@ -167,6 +168,7 @@ void shouldTraceEndTxOnFailingTransaction(final Exception exception) { when(transaction.getType()).thenReturn(TransactionType.EIP1559); when(transaction.getTo()).thenReturn(toAddress); when(transaction.getPayload()).thenReturn(Bytes.EMPTY); + when(transaction.getGasLimit()).thenReturn(100_000L); when(transaction.getSender()).thenReturn(senderAddress); when(transaction.getValue()).thenReturn(Wei.ZERO); when(transactionValidatorFactory.get().validate(any(), any(), any(), any())) diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccountingTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccountingTest.java new file mode 100644 index 00000000000..ff021908f53 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccountingTest.java @@ -0,0 +1,169 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.mainnet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +/** Tests for {@link TransactionGasAccounting}. */ +public class TransactionGasAccountingTest { + + /** Returns a builder with all fields set to 0/false — a valid baseline. */ + private static ImmutableTransactionGasAccounting.Builder baseBuilder() { + return TransactionGasAccounting.builder() + .txGasLimit(0L) + .remainingGas(0L) + .stateGasReservoir(0L) + .stateGasUsed(0L) + .initialFrameStateGasSpill(0L) + .stateGasSpillBurned(0L) + .regularGasCollisionBurned(0L) + .refundedGas(0L) + .floorCost(0L) + .regularGasLimitExceeded(false); + } + + @Test + public void normalPath_regularGasComputedCorrectly() { + // Simple execution: 100k gas limit, 30k remaining, no reservoir, no state gas + final var result = + baseBuilder() + .txGasLimit(100_000L) + .remainingGas(30_000L) + .refundedGas(5_000L) + .build() + .calculate(); + + // executionGas = 100k - 30k - 0 = 70k + // stateGas = 0, regularGas = 70k - 0 - 0 - 0 = 70k + // gasUsedByTransaction = max(70k, 0) + 0 = 70k + // usedGas = 100k - 5k = 95k + assertThat(result.effectiveStateGas()).isEqualTo(0L); + assertThat(result.gasUsedByTransaction()).isEqualTo(70_000L); + assertThat(result.usedGas()).isEqualTo(95_000L); + } + + @Test + public void normalPath_withStateGas() { + // Execution with state gas: 100k limit, 20k remaining, 10k reservoir, 10k state gas used + final var result = + baseBuilder() + .txGasLimit(100_000L) + .remainingGas(20_000L) + .stateGasReservoir(10_000L) + .stateGasUsed(10_000L) + .refundedGas(5_000L) + .build() + .calculate(); + + // executionGas = 100k - 20k - 10k = 70k + // stateGas = 10k + 0 = 10k, regularGas = 70k - 10k - 0 - 0 = 60k + // gasUsedByTransaction = max(60k, 0) + 10k = 70k + // usedGas = 100k - 5k = 95k + assertThat(result.effectiveStateGas()).isEqualTo(10_000L); + assertThat(result.gasUsedByTransaction()).isEqualTo(70_000L); + assertThat(result.usedGas()).isEqualTo(95_000L); + } + + @Test + public void floorCostOverridesRegularGas() { + // Floor cost higher than actual regular gas + final var result = + baseBuilder() + .txGasLimit(100_000L) + .remainingGas(60_000L) + .floorCost(50_000L) + .build() + .calculate(); + + // executionGas = 100k - 60k - 0 = 40k + // regularGas = 40k, floorCost = 50k -> max(40k, 50k) = 50k + // gasUsedByTransaction = 50k + 0 = 50k + assertThat(result.gasUsedByTransaction()).isEqualTo(50_000L); + assertThat(result.usedGas()).isEqualTo(100_000L); + } + + @Test + public void regularGasLimitExceeded_allGasConsumed() { + final var result = + baseBuilder() + .txGasLimit(100_000L) + .remainingGas(20_000L) + .stateGasReservoir(5_000L) + .stateGasUsed(30_000L) + .initialFrameStateGasSpill(2_000L) + .stateGasSpillBurned(5_000L) + .regularGasLimitExceeded(true) + .build() + .calculate(); + + // All gas consumed when regular gas limit exceeded + assertThat(result.effectiveStateGas()).isEqualTo(32_000L); // 30k + 2k spill + assertThat(result.gasUsedByTransaction()).isEqualTo(100_000L); + assertThat(result.usedGas()).isEqualTo(100_000L); + } + + @Test + public void stateGasSpill_doubleCountingAvoided() { + // initialFrameStateGasSpill=3000 is included in both stateGas AND spillBurned. + // The calculation must subtract it from spillBurned to avoid double-counting. + final var result = + baseBuilder() + .txGasLimit(100_000L) + .remainingGas(10_000L) + .stateGasUsed(20_000L) + .initialFrameStateGasSpill(3_000L) + .stateGasSpillBurned(8_000L) + .build() + .calculate(); + + // executionGas = 100k - 10k - 0 = 90k + // stateGas = 20k + 3k = 23k + // spillBurned correction = 8k - 3k = 5k (avoid double-counting initialFrameStateGasSpill) + // regularGas = 90k - 23k - 5k - 0 = 62k + // gasUsedByTransaction = max(62k, 0) + 23k = 85k + assertThat(result.effectiveStateGas()).isEqualTo(23_000L); + assertThat(result.gasUsedByTransaction()).isEqualTo(85_000L); + assertThat(result.usedGas()).isEqualTo(100_000L); + } + + @Test + public void zeroStateGas_preAmsterdamEquivalent() { + // Pre-Amsterdam: stateGasUsed=0, spillBurned=0, collisionBurned=0 + // Should behave identically to pre-8037 gas accounting + final var result = + baseBuilder() + .txGasLimit(100_000L) + .remainingGas(40_000L) + .refundedGas(10_000L) + .build() + .calculate(); + + // executionGas = 100k - 40k = 60k + // regularGas = 60k, gasUsedByTransaction = 60k, usedGas = 100k - 10k = 90k + assertThat(result.effectiveStateGas()).isEqualTo(0L); + assertThat(result.gasUsedByTransaction()).isEqualTo(60_000L); + assertThat(result.usedGas()).isEqualTo(90_000L); + } + + @Test + public void build_failsWhenFieldMissing() { + assertThatThrownBy(() -> TransactionGasAccounting.builder().txGasLimit(100_000L).build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("remainingGas"); + } +} diff --git a/ethereum/referencetests/build.gradle b/ethereum/referencetests/build.gradle index e1ff91883eb..655f4ba774f 100644 --- a/ethereum/referencetests/build.gradle +++ b/ethereum/referencetests/build.gradle @@ -217,7 +217,6 @@ sourceSets { eipBlockchainReferenceTests, eipStateReferenceTests, executionSpecTests, - executionSpecDevnetTests, generalstateReferenceTests, generalstateRegressionReferenceTests, customBlockchainReferenceTests @@ -227,20 +226,37 @@ sourceSets { 'src/reference-test/external-resources', 'src/reference-test/templates', 'build/execution-spec-tests/', - 'build/execution-spec-devnet-tests/', 'src/reference-test/resources/custom' } } + referenceTestDevnet { + java { + compileClasspath += main.output + referenceTest.output + runtimeClasspath += main.output + referenceTest.output + srcDirs executionSpecDevnetTests + } + resources { + srcDirs 'build/execution-spec-devnet-tests/' + } + } } -// Both stable and devnet execution-spec-tests tarballs contain fixtures/.meta/index.json +// stable execution-spec-tests tarball contains fixtures/.meta/index.json tasks.named('processReferenceTestResources') { - dependsOn extractStableFixtures, extractDevnetFixtures + dependsOn extractStableFixtures + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +tasks.named('processReferenceTestDevnetResources') { + dependsOn extractDevnetFixtures duplicatesStrategy = DuplicatesStrategy.EXCLUDE } configurations { referenceTestAnnotationProcessor.extendsFrom testAnnotationProcessor + referenceTestDevnetAnnotationProcessor.extendsFrom testAnnotationProcessor + referenceTestDevnetImplementation.extendsFrom referenceTestImplementation + referenceTestDevnetRuntimeOnly.extendsFrom referenceTestRuntimeOnly // we need this because referenceTestImplementation defaults to 'canBeResolved=false'. tarConfig.extendsFrom referenceTestImplementation tarConfig { @@ -298,7 +314,7 @@ dependencies { ) devnetTarConfig( group: 'ethereum', name: 'execution-spec-tests', - version: 'bal@v5.1.0', classifier: 'fixtures_bal', ext: 'tar.gz' + version: 'bal@v5.3.0', classifier: 'fixtures_bal', ext: 'tar.gz' ) referenceTestImplementation 'com.fasterxml.jackson.core:jackson-databind' referenceTestImplementation 'com.google.guava:guava' @@ -322,19 +338,13 @@ tasks.register('referenceTests', Test) { description = 'Runs ETH reference tests.' testClassesDirs = sourceSets.referenceTest.output.classesDirs classpath = sourceSets.referenceTest.runtimeClasspath - filter { - excludeTestsMatching "org.hyperledger.besu.ethereum.vm.executionspecdevnet.*" - } } tasks.register('referenceTestsDevnet', Test) { useJUnitPlatform() description = 'Runs execution-spec-tests pre-release (devnet) reference tests.' - testClassesDirs = sourceSets.referenceTest.output.classesDirs - classpath = sourceSets.referenceTest.runtimeClasspath - filter { - includeTestsMatching "org.hyperledger.besu.ethereum.vm.executionspecdevnet.*" - } + testClassesDirs = sourceSets.referenceTestDevnet.output.classesDirs + classpath = sourceSets.referenceTestDevnet.runtimeClasspath } tasks.register('validateReferenceTestSubmodule') { @@ -466,18 +476,23 @@ def generateTestFilesGroupedByDirectory( } // Extract a group key from path segments for hardfork/EIP-based test grouping. -// parts = path segments after groupBaseDir, e.g. ["prague", "eip7702_set_code_tx", "test.json"] +// Supports both old layout (hardfork/eip/file.json) and new per-fork layout (for_fork/hardfork/eip/file.json). def extractGroupKey(String[] parts) { def groupKey - if (parts.length >= 3 && parts[0] == "static") { + // Strip leading for_{fork}/ directory if present (new per-fork fixture layout) + def effectiveParts = parts + if (parts.length >= 2 && parts[0].startsWith("for_")) { + effectiveParts = parts[1..parts.length - 1] as String[] + } + if (effectiveParts.length >= 3 && effectiveParts[0] == "static") { // static dirs have deeper nesting: static/state_tests/stCreate2/file.json → static_stCreate2 - groupKey = "static_" + parts[parts.length - 2] - } else if (parts.length >= 3) { + groupKey = "static_" + effectiveParts[effectiveParts.length - 2] + } else if (effectiveParts.length >= 3) { // Normal: hardfork/eip/file.json → hardfork_eip - groupKey = parts[0] + "_" + parts[1] - } else if (parts.length == 2) { + groupKey = effectiveParts[0] + "_" + effectiveParts[1] + } else if (effectiveParts.length == 2) { // File directly under hardfork dir: hardfork/file.json → hardfork - groupKey = parts[0] + groupKey = effectiveParts[0] } else { groupKey = "unknown" } diff --git a/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/AccountChangesJson.java b/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/AccountChangesJson.java index e51fe584f48..4c3fd8f42b0 100644 --- a/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/AccountChangesJson.java +++ b/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/AccountChangesJson.java @@ -162,20 +162,20 @@ public NonceChange toNonceChange() { @JsonIgnoreProperties(ignoreUnknown = true) public static class CodeChangeJson { private final String blockAccessIndex; - private final String postCode; + private final String newCode; @JsonCreator public CodeChangeJson( @JsonProperty("blockAccessIndex") final String blockAccessIndex, - @JsonProperty("postCode") final String postCode) { + @JsonProperty("newCode") final String newCode) { this.blockAccessIndex = blockAccessIndex; - this.postCode = postCode; + this.newCode = newCode; } public CodeChange toCodeChange() { return new CodeChange( decodeIndex(blockAccessIndex), - postCode != null ? Bytes.fromHexString(postCode) : Bytes.EMPTY); + newCode != null ? Bytes.fromHexString(newCode) : Bytes.EMPTY); } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java index 8af519097dd..29f2c597a22 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java @@ -17,9 +17,7 @@ import static com.google.common.base.Preconditions.checkState; import static java.util.Collections.emptySet; -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.Log; import org.hyperledger.besu.datatypes.VersionedHash; @@ -33,7 +31,6 @@ import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.worldstate.WorldUpdater; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; @@ -44,7 +41,6 @@ import java.util.Set; import java.util.function.Consumer; -import com.google.common.collect.HashBasedTable; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Table; @@ -241,7 +237,7 @@ public enum Type { private Optional eip7928AccessList = Optional.empty(); /** The mark of the undoable collections at the creation of this message frame */ - private final long undoMark; + private long undoMark; /** * Builder builder. @@ -808,6 +804,141 @@ public long getGasRefund() { return txValues.gasRefunds().get(); } + /** + * Increment the state gas used (EIP-8037). This is NOT undone on revert since consumed gas is + * consumed regardless of execution outcome. + * + * @param amount The amount of state gas to add + */ + public void incrementStateGasUsed(final long amount) { + txValues.stateGasUsed().set(txValues.stateGasUsed().get() + amount); + } + + /** + * Return the accumulated state gas used (EIP-8037). + * + * @return accumulated state gas used + */ + public long getStateGasUsed() { + return txValues.stateGasUsed().get(); + } + + /** + * Gets the state gas reservoir (EIP-8037). + * + * @return the state gas reservoir amount + */ + public long getStateGasReservoir() { + return txValues.stateGasReservoir().get(); + } + + /** + * Sets the state gas reservoir (EIP-8037). + * + * @param amount the amount to set the reservoir to + */ + public void setStateGasReservoir(final long amount) { + txValues.stateGasReservoir().set(amount); + } + + /** + * Increments the state gas reservoir (EIP-8037). Used for state gas refunds. + * + * @param amount the amount to add to the reservoir + */ + public void incrementStateGasReservoir(final long amount) { + txValues.stateGasReservoir().set(txValues.stateGasReservoir().get() + amount); + } + + /** + * Consumes state gas: draws from the reservoir first, then from gasRemaining. Also increments + * stateGasUsed. Returns false if insufficient total gas (reservoir + gasRemaining). + * + * @param amount the amount of state gas to consume + * @return true if the gas was successfully consumed, false if insufficient gas + */ + public boolean consumeStateGas(final long amount) { + final long reservoir = txValues.stateGasReservoir().get(); + if (reservoir >= amount) { + txValues.stateGasReservoir().set(reservoir - amount); + } else { + // Overflow goes to gasRemaining + final long overflow = amount - reservoir; + if (gasRemaining < overflow) { + return false; + } + txValues.stateGasReservoir().set(0L); + gasRemaining -= overflow; + } + txValues.stateGasUsed().set(txValues.stateGasUsed().get() + amount); + return true; + } + + /** + * Consumes state gas, draining all available gas even when the full amount cannot be covered. + * Always increments stateGasUsed by the full amount regardless of gas availability. Used when a + * transaction-level (depth-0) contract creation fails after state gas has been partially + * committed: we must record the charge for block accounting even though execution fails. + * + * @param amount the amount of state gas to consume + * @return true if sufficient gas was available, false if gas was insufficient (but drained + * anyway) + */ + public boolean consumeStateGasForced(final long amount) { + final long reservoir = txValues.stateGasReservoir().get(); + if (reservoir >= amount) { + txValues.stateGasReservoir().set(reservoir - amount); + txValues.stateGasUsed().set(txValues.stateGasUsed().get() + amount); + return true; + } else { + final long overflow = amount - reservoir; + txValues.stateGasReservoir().set(0L); + final boolean sufficient = gasRemaining >= overflow; + gasRemaining = Math.max(0L, gasRemaining - overflow); + txValues.stateGasUsed().set(txValues.stateGasUsed().get() + amount); + return sufficient; + } + } + + /** + * Accumulates state gas that spilled into gasRemaining in a reverted child frame (EIP-8037). This + * counter is NOT undone on revert — it tracks permanently burned spill gas for block accounting. + * + * @param amount the spill amount to accumulate + */ + public void accumulateStateGasSpillBurned(final long amount) { + txValues.stateGasSpillBurned()[0] += amount; + } + + /** + * Returns the total state gas spill burned by reverted child frames (EIP-8037). + * + * @return accumulated spill burned + */ + public long getStateGasSpillBurned() { + return txValues.stateGasSpillBurned()[0]; + } + + /** + * Accumulates regular gas burned by a CREATE child frame that halted before executing code + * (address collision). NOT undone on revert — excluded from block gas accounting but still + * charged for fee purposes. + * + * @param amount the collision gas amount to accumulate + */ + public void accumulateRegularGasCollisionBurned(final long amount) { + txValues.regularGasCollisionBurned()[0] += amount; + } + + /** + * Returns accumulated regular gas burned by pre-execution CREATE collision halts. + * + * @return accumulated collision gas burned + */ + public long getRegularGasCollisionBurned() { + return txValues.regularGasCollisionBurned()[0]; + } + /** * Add recipient to the self-destruct set if not already present. * @@ -1248,6 +1379,16 @@ public void rollback() { txValues.undoChanges(undoMark); } + /** + * Advances the undo mark to the current point, so that subsequent rollback() calls will not undo + * changes made before this point. Used by the transaction processor to protect intrinsic state + * gas charges (EIP-8037 auth delegation and contract creation) from being rolled back when the + * initial frame's execution reverts. + */ + public void advanceUndoMark() { + this.undoMark = txValues.transientStorage().mark(); + } + /** * Accessor for versionedHashes, if present. * @@ -1634,22 +1775,16 @@ public MessageFrame build() { HashSet
warmedUpAddresses = new HashSet<>(); warmedUpAddresses.add(contract); newTxValues = - new TxValues( + TxValues.forTransaction( blockHashLookup, maxStackSize, UndoSet.of(warmedUpAddresses), - UndoTable.of(HashBasedTable.create()), originator, gasPrice, blobGasPrice, blockValues, - new ArrayDeque<>(), miningBeneficiary, - versionedHashes, - UndoTable.of(HashBasedTable.create()), - UndoSet.of(new HashSet<>()), - UndoSet.of(new HashSet<>()), - new UndoScalar<>(0L)); + versionedHashes); updater = worldUpdater; newStatic = isStatic; } else { diff --git a/evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java b/evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java index 58110080459..5fbe6b878fd 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java @@ -22,10 +22,13 @@ import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.evm.blockhash.BlockHashLookup; +import java.util.ArrayDeque; import java.util.Deque; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import com.google.common.collect.HashBasedTable; import org.apache.tuweni.bytes.Bytes32; /** @@ -48,6 +51,14 @@ * @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 regularGasCollisionBurned EIP-8037 accumulated regular gas burned by CREATE child frames + * that halted before executing any code (address collision); NOT undone on revert. Excluded + * from block regular gas accounting but still counts toward fee deduction. */ public record TxValues( BlockHashLookup blockHashLookup, @@ -64,7 +75,58 @@ public record TxValues( UndoTable transientStorage, UndoSet
creates, UndoSet
selfDestructs, - UndoScalar gasRefunds) { + UndoScalar gasRefunds, + UndoScalar stateGasUsed, + UndoScalar stateGasReservoir, + long[] stateGasSpillBurned, + long[] regularGasCollisionBurned) { + + /** + * 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
warmedUpAddresses, + final Address originator, + final Wei gasPrice, + final Wei blobGasPrice, + final BlockValues blockValues, + final Address miningBeneficiary, + final Optional> versionedHashes) { + return new TxValues( + blockHashLookup, + maxStackSize, + warmedUpAddresses, + UndoTable.of(HashBasedTable.create()), + originator, + gasPrice, + blobGasPrice, + blockValues, + new ArrayDeque<>(), + miningBeneficiary, + versionedHashes, + UndoTable.of(HashBasedTable.create()), + UndoSet.of(new HashSet<>()), + UndoSet.of(new HashSet<>()), + 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. @@ -78,5 +140,7 @@ public void undoChanges(final long mark) { creates.undo(mark); selfDestructs.undo(mark); gasRefunds.undo(mark); + stateGasUsed.undo(mark); + stateGasReservoir.undo(mark); } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculator.java index 9e0e1009234..94c3c231c68 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculator.java @@ -14,8 +14,23 @@ */ package org.hyperledger.besu.evm.gascalculator; +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 java.util.function.Supplier; + +import org.apache.tuweni.units.bigints.UInt256; + /** - * Gas Calculator for Amsterdam + * Gas Calculator for Amsterdam hard fork. + * + *

Introduces EIP-8037 multidimensional gas metering with state gas costs that depend on the + * block gas limit. All state-creation costs that were previously charged as regular gas are split: + * the state portion is charged as state gas (drawn from the reservoir), while the regular portion + * is reduced. * *

    *
  • EIP-7928: gas cost per item for block access list size limit @@ -23,14 +38,34 @@ */ public class AmsterdamGasCalculator extends OsakaGasCalculator { + // EIP-8037: New regular gas constants for Amsterdam + private static final long TX_CREATE_COST = 9_000L; + + // EIP-8037: SSTORE_SET regular gas drops from 20,000 to 2,900 (state portion charged separately) + private static final long SSTORE_SET_GAS = SSTORE_RESET_GAS; + + // EIP-8037: 0→X→0 refund is 2,800 per spec (GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - + // GAS_WARM_ACCESS) + private static final long SSTORE_SET_GAS_LESS_SLOAD_GAS = SSTORE_SET_GAS - WARM_STORAGE_READ_COST; + + // SSTORE_CLEARS_SCHEDULE unchanged from EIP-3529 (London): 4,800 + private static final long SSTORE_CLEARS_SCHEDULE = SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_COST; + private static final long NEGATIVE_SSTORE_CLEARS_SCHEDULE = -SSTORE_CLEARS_SCHEDULE; + /** * EIP-7928: gas cost per item for block access list size limit (bal_items <= block_gas_limit / * ITEM_COST). */ private static final long BLOCK_ACCESS_LIST_ITEM_COST = 2000L; + /** The EIP-8037 state gas cost calculator. */ + private final Eip8037StateGasCostCalculator stateGasCostCalc = + new Eip8037StateGasCostCalculator(); + /** Instantiates a new Amsterdam Gas Calculator. */ - public AmsterdamGasCalculator() {} + public AmsterdamGasCalculator() { + super(); + } /** * Instantiates a new Amsterdam Gas Calculator @@ -51,8 +86,152 @@ public AmsterdamGasCalculator(final int maxPrecompile) { super(maxPrecompile); } + @Override + public StateGasCostCalculator stateGasCostCalculator() { + return stateGasCostCalc; + } + @Override public long getBlockAccessListItemCost() { return BLOCK_ACCESS_LIST_ITEM_COST; } + + // --- EIP-8037 Gas Cost Overrides --- + + @Override + public long txCreateCost() { + return TX_CREATE_COST; + } + + @Override + protected long txCreateExtraGasCost() { + return TX_CREATE_COST; + } + + @Override + public long codeDepositGasCost(final int codeSize) { + // 6 * ceil(codeSize / 32) — hash cost only; state portion (cpsb * codeSize) charged separately + return stateGasCostCalc.codeDepositHashGas(codeSize); + } + + @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) { + // Same as SpuriousDragon but do NOT add newAccountGasCost(). + // State gas for new accounts (112 * cpsb) is charged via chargeCallNewAccountStateGas. + return staticCallCost; + } + + @Override + public long calculateStorageCost( + final UInt256 newValue, + final Supplier currentValue, + final Supplier originalValue) { + // Same Berlin truth table, but SSTORE_SET_GAS is now 2,900 (state portion charged separately) + final UInt256 localCurrentValue = currentValue.get(); + if (localCurrentValue.equals(newValue)) { + return WARM_STORAGE_READ_COST; + } else { + final UInt256 localOriginalValue = originalValue.get(); + if (localOriginalValue.equals(localCurrentValue)) { + return localOriginalValue.isZero() ? SSTORE_SET_GAS : SSTORE_RESET_GAS; + } else { + return WARM_STORAGE_READ_COST; + } + } + } + + @Override + public long selfDestructOperationGasCost(final Account recipient, final Wei inheritance) { + // Always static cost (5,000). State gas (112 * cpsb) for new accounts charged separately. + return selfDestructOperationStaticGasCost(); + } + + @Override + public long delegateCodeGasCost(final int delegateCodeListLength) { + // 7,500 per delegation (regular portion only, state gas charged separately) + return stateGasCostCalc.authBaseRegularGas() * delegateCodeListLength; + } + + @Override + public long calculateDelegateCodeGasRefund(final long alreadyExistingAccounts) { + // No refund needed — regular cost is lower, state gas uses its own refund path + return 0L; + } + + @Override + public long calculateGasRefund( + final Transaction transaction, + final MessageFrame initialFrame, + final long codeDelegationRefund) { + + final long gasLimit = transaction.getGasLimit(); + // EIP-8037: Include leftover reservoir in remaining gas so unspent state gas is returned. + final long totalRemaining = + initialFrame.getRemainingGas() + initialFrame.getStateGasReservoir(); + final long totalConsumed = gasLimit - totalRemaining; + + final long selfDestructRefund = + getSelfDestructRefundAmount() * initialFrame.getSelfDestructs().size(); + final long executionRefund = + initialFrame.getGasRefund() + selfDestructRefund + codeDelegationRefund; + // 1/5 cap on total consumed gas (regular + state) + final long maxRefundAllowance = totalConsumed / getMaxRefundQuotient(); + final long refundAllowance = Math.min(executionRefund, maxRefundAllowance); + + final long gasUsed = totalConsumed - refundAllowance; + final long floorCost = + transactionFloorCost(transaction.getPayload(), transaction.getPayloadZeroBytes()); + return gasLimit - Math.max(gasUsed, floorCost); + } + + @Override + public long calculateStorageRefundAmount( + final UInt256 newValue, + final Supplier currentValue, + final Supplier originalValue) { + // Same Berlin truth table structure, but with updated constants + final UInt256 localCurrentValue = currentValue.get(); + if (localCurrentValue.equals(newValue)) { + return 0L; + } else { + final UInt256 localOriginalValue = originalValue.get(); + if (localOriginalValue.equals(localCurrentValue)) { + if (localOriginalValue.isZero()) { + return 0L; + } else if (newValue.isZero()) { + return SSTORE_CLEARS_SCHEDULE; + } else { + return 0L; + } + } else { + long refund = 0L; + if (!localOriginalValue.isZero()) { + if (localCurrentValue.isZero()) { + refund = NEGATIVE_SSTORE_CLEARS_SCHEDULE; + } else if (newValue.isZero()) { + refund = SSTORE_CLEARS_SCHEDULE; + } + } + + if (localOriginalValue.equals(newValue)) { + refund = + refund + + (localOriginalValue.isZero() + ? SSTORE_SET_GAS_LESS_SLOAD_GAS + : SSTORE_RESET_GAS_LESS_SLOAD_GAS); + } + return refund; + } + } + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/Eip8037StateGasCostCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/Eip8037StateGasCostCalculator.java new file mode 100644 index 00000000000..5946226cc22 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/Eip8037StateGasCostCalculator.java @@ -0,0 +1,264 @@ +/* + * 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.evm.gascalculator; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.frame.MessageFrame; + +import java.util.function.Supplier; + +import org.apache.tuweni.units.bigints.UInt256; + +/** + * EIP-8037 state gas cost calculator implementation. + * + *

    Computes state gas costs based on a dynamic cost_per_state_byte (cpsb) derived from the block + * gas limit: + * + *

    + *   cpsb = ceil(((gas_limit / 2) * 7200 * 365) / TARGET_STATE_GROWTH_PER_YEAR)
    + * 
    + * + *

    Where TARGET_STATE_GROWTH_PER_YEAR = 100 * 1024^3 bytes (100 GiB). + */ +public class Eip8037StateGasCostCalculator implements StateGasCostCalculator { + + /** The target state growth per year in bytes (100 GiB). */ + static final long TARGET_STATE_GROWTH_PER_YEAR = 100L * 1024L * 1024L * 1024L; + + /** Seconds per year used in cpsb calculation (7200 slots/day * 365 days). */ + static final long SLOTS_PER_YEAR = 7200L * 365L; + + /** + * Number of state bytes per new account (20-byte address + 8-byte nonce + 32-byte balance + + * 32-byte code hash + 20-byte storage root = 112 bytes). + */ + static final int STATE_BYTES_PER_ACCOUNT = 112; + + /** Number of state bytes per storage slot (32 bytes for key + value). */ + static final int STATE_BYTES_PER_STORAGE_SLOT = 32; + + /** Number of state bytes per auth delegation (23 bytes). */ + static final int STATE_BYTES_PER_AUTH = 23; + + /** + * Regular gas for storage set (GAS_STORAGE_UPDATE - GAS_COLD_SLOAD = 5000 - 2100 = 2900). The + * state portion (32 * cpsb) is charged separately. + */ + static final long STORAGE_SET_REGULAR_GAS = 2_900L; + + /** + * Regular gas for EIP-7702 auth base (calldata + ecrecover + cold access + warm write ≈ 7500). + * The state portion (23 * cpsb) is charged separately. + */ + static final long AUTH_BASE_REGULAR_GAS = 7_500L; + + /** Offset added before quantization, subtracted after. */ + static final int CPSB_OFFSET = 9578; + + /** Number of significant bits retained in quantized cpsb. */ + static final int CPSB_SIGNIFICANT_BITS = 5; + + /** Keccak256 word gas cost for code deposit hashing. */ + static final long KECCAK256_WORD_GAS_COST = 6L; + + /** The mainnet transaction gas limit cap from EIP-7825, enforced at runtime on regular gas. */ + static final long TX_MAX_GAS_LIMIT = 16_777_216L; + + /** Instantiates a new EIP-8037 state gas cost calculator. */ + public Eip8037StateGasCostCalculator() {} + + /** + * Hardcoded cost per state byte for devnet-3. This value (1174) corresponds to a 100M block gas + * limit. The test framework cannot currently handle dynamic gas prices, so cpsb is treated as a + * fork constant. For devnet-4, the full EIP-8037 dynamic calculation will be restored. + */ + static final long DEVNET_COST_PER_STATE_BYTE = 1174L; + + @Override + public long costPerStateByte(final long blockGasLimit) { + // TODO(devnet-4): Restore dynamic cpsb calculation based on block gas limit: + // return costPerStateByteFromGasLimit(blockGasLimit); + return DEVNET_COST_PER_STATE_BYTE; + } + + /** + * Dynamic cost per state byte calculation from the full EIP-8037 specification. Derives cpsb from + * the block gas limit by targeting TARGET_STATE_GROWTH_PER_YEAR at 50% average gas utilization, + * then quantizes to retain CPSB_SIGNIFICANT_BITS significant bits. + * + *

    Currently unused (hardcoded for devnet-3). Will be re-enabled for devnet-4. + * + * @param blockGasLimit the block gas limit + * @return the quantized cost per state byte + */ + static long costPerStateByteFromGasLimit(final long blockGasLimit) { + // cpsb = ceil(((gas_limit / 2) * SLOTS_PER_YEAR) / TARGET_STATE_GROWTH_PER_YEAR) + final long numerator = (blockGasLimit / 2) * SLOTS_PER_YEAR; + final long raw = (numerator + TARGET_STATE_GROWTH_PER_YEAR - 1) / TARGET_STATE_GROWTH_PER_YEAR; + if (raw == 0) { + return 0L; + } + // Quantize: retain only CPSB_SIGNIFICANT_BITS significant bits of (raw + CPSB_OFFSET) + final long shifted = raw + CPSB_OFFSET; + final int bitLen = Long.SIZE - Long.numberOfLeadingZeros(shifted); + final int shift = Math.max(bitLen - CPSB_SIGNIFICANT_BITS, 0); + return Math.max(((shifted >> shift) << shift) - CPSB_OFFSET, 1L); + } + + @Override + public long createStateGas(final long blockGasLimit) { + return STATE_BYTES_PER_ACCOUNT * costPerStateByte(blockGasLimit); + } + + @Override + public long codeDepositStateGas(final int codeSize, final long blockGasLimit) { + return costPerStateByte(blockGasLimit) * codeSize; + } + + @Override + public long codeDepositHashGas(final int codeSize) { + // 6 * ceil(codeSize / 32) + return KECCAK256_WORD_GAS_COST * ((codeSize + 31) / 32); + } + + @Override + public long newAccountStateGas(final long blockGasLimit) { + return createStateGas(blockGasLimit); + } + + @Override + public long storageSetStateGas(final long blockGasLimit) { + return STATE_BYTES_PER_STORAGE_SLOT * costPerStateByte(blockGasLimit); + } + + @Override + public long storageSetRegularGas() { + return STORAGE_SET_REGULAR_GAS; + } + + @Override + public long authBaseStateGas(final long blockGasLimit) { + return STATE_BYTES_PER_AUTH * costPerStateByte(blockGasLimit); + } + + @Override + public long authBaseRegularGas() { + return AUTH_BASE_REGULAR_GAS; + } + + @Override + public long emptyAccountDelegationStateGas(final long blockGasLimit) { + return createStateGas(blockGasLimit); + } + + @Override + public long transactionRegularGasLimit() { + return TX_MAX_GAS_LIMIT; + } + + @Override + public boolean isActive() { + return true; + } + + // ---- Charge method overrides ---- + + @Override + public boolean chargeStorageSetStateGas( + final MessageFrame frame, + final UInt256 newValue, + final Supplier currentValue, + final Supplier originalValue) { + final UInt256 currentVal = currentValue.get(); + final UInt256 originalVal = originalValue.get(); + // State gas applies only for the SSTORE_SET case: original is zero and we're setting nonzero + if (originalVal.isZero() && currentVal.isZero() && !newValue.isZero()) { + final long blockGasLimit = frame.getBlockValues().getGasLimit(); + return frame.consumeStateGas(storageSetStateGas(blockGasLimit)); + } + return true; + } + + @Override + public boolean chargeCreateStateGas(final MessageFrame frame) { + final long blockGasLimit = frame.getBlockValues().getGasLimit(); + return frame.consumeStateGas(createStateGas(blockGasLimit)); + } + + @Override + public boolean chargeCodeDepositStateGas(final MessageFrame frame, final int codeSize) { + final long blockGasLimit = frame.getBlockValues().getGasLimit(); + return frame.consumeStateGas(codeDepositStateGas(codeSize, blockGasLimit)); + } + + @Override + public boolean chargeCallNewAccountStateGas( + final MessageFrame frame, final Address recipientAddress, final Wei transferValue) { + if (!transferValue.isZero()) { + final Account recipient = frame.getWorldUpdater().get(recipientAddress); + if (recipient == null || recipient.isEmpty()) { + final long blockGasLimit = frame.getBlockValues().getGasLimit(); + return frame.consumeStateGas(newAccountStateGas(blockGasLimit)); + } + } + return true; + } + + @Override + public boolean chargeSelfDestructNewAccountStateGas( + final MessageFrame frame, final Account beneficiary, final Wei originatorBalance) { + if ((beneficiary == null || beneficiary.isEmpty()) && !originatorBalance.isZero()) { + final long blockGasLimit = frame.getBlockValues().getGasLimit(); + return frame.consumeStateGas(newAccountStateGas(blockGasLimit)); + } + return true; + } + + @Override + public boolean chargeCodeDelegationStateGas( + final MessageFrame frame, final long totalDelegations, final long alreadyExistingDelegators) { + final long blockGasLimit = frame.getBlockValues().getGasLimit(); + // Each authorization incurs auth base state gas (23 * cpsb) + if (!frame.consumeStateGas(authBaseStateGas(blockGasLimit) * totalDelegations)) { + return false; + } + // New empty accounts incur additional state gas (112 * cpsb) + final long newEmptyAccounts = totalDelegations - alreadyExistingDelegators; + if (newEmptyAccounts > 0) { + return frame.consumeStateGas( + emptyAccountDelegationStateGas(blockGasLimit) * newEmptyAccounts); + } + return true; + } + + @Override + public void refundStorageSetStateGas( + final MessageFrame frame, + final UInt256 newValue, + final Supplier currentValue, + final Supplier originalValue) { + // Only refund for 0→X→0: original is zero, current is nonzero, new is zero + if (newValue.isZero() && !currentValue.get().isZero() && originalValue.get().isZero()) { + final long blockGasLimit = frame.getBlockValues().getGasLimit(); + final long refundAmount = storageSetStateGas(blockGasLimit); + // State gas refund goes into the regular refund counter, subject to the 1/5 cap. + // stateGasUsed is NOT decremented — it tracks gross state gas consumed. + frame.incrementGasRefund(refundAmount); + } + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java index 7cf1aed769e..97e8ebecd41 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java @@ -672,4 +672,13 @@ default long calculateCodeDelegationResolutionGas( final MessageFrame frame, final Account targetAccount) { return 0L; } + + /** + * Returns the state gas cost calculator for EIP-8037 multidimensional gas metering. + * + * @return the state gas cost calculator (NONE by default) + */ + default StateGasCostCalculator stateGasCostCalculator() { + return StateGasCostCalculator.NONE; + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/StateGasCostCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/StateGasCostCalculator.java new file mode 100644 index 00000000000..a676f068caa --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/StateGasCostCalculator.java @@ -0,0 +1,317 @@ +/* + * 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.evm.gascalculator; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.frame.MessageFrame; + +import java.util.function.Supplier; + +import org.apache.tuweni.units.bigints.UInt256; + +/** + * Strategy interface for EIP-8037 state creation gas cost calculations. + * + *

    EIP-8037 introduces multidimensional gas metering, splitting gas into regular gas and state + * gas. State-creation operations (CREATE, SSTORE 0->nonzero, CALL to new accounts, code deposits, + * EIP-7702 delegations) get their costs split into a regular gas portion and a state gas portion, + * where the state gas depends on a dynamic cost_per_state_byte (cpsb) derived from the block gas + * limit. + * + *

    Operations call the {@code charge*} methods to deduct state gas. The default (NONE) + * implementation is a no-op; the EIP-8037 implementation performs the actual deduction. + */ +public interface StateGasCostCalculator { + + /** + * Returns the cost per state byte for the given block gas limit. + * + * @param blockGasLimit the block gas limit + * @return the cost per state byte + */ + long costPerStateByte(long blockGasLimit); + + /** + * Returns the state gas for a CREATE operation (112 * cpsb). + * + * @param blockGasLimit the block gas limit + * @return the state gas for CREATE + */ + long createStateGas(long blockGasLimit); + + /** + * Returns the state gas for code deposit (cpsb * codeSize). + * + * @param codeSize the size of the code in bytes + * @param blockGasLimit the block gas limit + * @return the state gas for code deposit + */ + long codeDepositStateGas(int codeSize, long blockGasLimit); + + /** + * Returns the regular gas for code deposit hashing (6 * ceil(codeSize/32)). + * + * @param codeSize the size of the code in bytes + * @return the regular gas for code deposit hashing + */ + long codeDepositHashGas(int codeSize); + + /** + * Returns the state gas for creating a new account (112 * cpsb). + * + * @param blockGasLimit the block gas limit + * @return the state gas for new account creation + */ + long newAccountStateGas(long blockGasLimit); + + /** + * Returns the state gas for storage set 0->nonzero (32 * cpsb). + * + * @param blockGasLimit the block gas limit + * @return the state gas for storage set + */ + long storageSetStateGas(long blockGasLimit); + + /** + * Returns the regular gas for storage set (replacing the 20000 SSTORE_SET_GAS). + * + * @return the regular gas for storage set + */ + long storageSetRegularGas(); + + /** + * Returns the state gas for EIP-7702 auth base (23 * cpsb). + * + * @param blockGasLimit the block gas limit + * @return the state gas for auth base + */ + long authBaseStateGas(long blockGasLimit); + + /** + * Returns the regular gas for EIP-7702 auth base. + * + * @return the regular gas for auth base + */ + long authBaseRegularGas(); + + /** + * Returns the state gas for empty account delegation (112 * cpsb). + * + * @param blockGasLimit the block gas limit + * @return the state gas for empty account delegation + */ + long emptyAccountDelegationStateGas(long blockGasLimit); + + /** + * Returns the maximum regular gas allowed per transaction (TX_MAX_GAS_LIMIT from EIP-7825). + * EIP-8037 changes this from a validation condition to a runtime revert condition on regular gas + * only. Returns {@code Long.MAX_VALUE} when state gas metering is not active. + * + * @return the maximum regular gas per transaction + */ + long transactionRegularGasLimit(); + + /** + * Returns whether multidimensional gas metering (EIP-8037) is active. + * + * @return true when state gas metering is active + */ + default boolean isActive() { + return false; + } + + // ---- Charge methods (strategy pattern for state gas deduction) ---- + + /** + * Charges state gas for SSTORE 0→nonzero (storage set). Only charges when the original value is + * zero, current value is zero, and the new value is nonzero. + * + * @param frame the message frame + * @param newValue the new storage value being written + * @param currentValue supplier for the current storage value + * @param originalValue supplier for the original storage value + * @return true if gas was successfully charged, false if insufficient gas + */ + default boolean chargeStorageSetStateGas( + final MessageFrame frame, + final UInt256 newValue, + final Supplier currentValue, + final Supplier originalValue) { + return true; + } + + /** + * Charges state gas for CREATE/CREATE2 operations (new account: 112 * cpsb). + * + * @param frame the message frame + * @return true if gas was successfully charged, false if insufficient gas + */ + default boolean chargeCreateStateGas(final MessageFrame frame) { + return true; + } + + /** + * Charges state gas for code deposit (cpsb * codeSize). + * + * @param frame the message frame + * @param codeSize the size of the deployed code in bytes + * @return true if gas was successfully charged, false if insufficient gas + */ + default boolean chargeCodeDepositStateGas(final MessageFrame frame, final int codeSize) { + return true; + } + + /** + * Charges state gas for CALL-family operations that create a new account. Only charges when the + * transfer value is nonzero and the recipient does not exist or is empty. + * + * @param frame the message frame + * @param recipientAddress the recipient address + * @param transferValue the value being transferred + * @return true if gas was successfully charged, false if insufficient gas + */ + default boolean chargeCallNewAccountStateGas( + final MessageFrame frame, final Address recipientAddress, final Wei transferValue) { + return true; + } + + /** + * Charges state gas for SELFDESTRUCT that sends to a new account. Only charges when the + * beneficiary does not exist or is empty and the originator has nonzero balance. + * + * @param frame the message frame + * @param beneficiary the beneficiary account (may be null) + * @param originatorBalance the originator's balance + * @return true if gas was successfully charged, false if insufficient gas + */ + default boolean chargeSelfDestructNewAccountStateGas( + final MessageFrame frame, final Account beneficiary, final Wei originatorBalance) { + return true; + } + + /** + * Charges state gas for EIP-7702 code delegation intrinsic costs. + * + * @param frame the message frame + * @param totalDelegations total number of code delegations + * @param alreadyExistingDelegators number of delegators that already existed + * @return true if gas was successfully charged, false if insufficient gas + */ + default boolean chargeCodeDelegationStateGas( + final MessageFrame frame, final long totalDelegations, final long alreadyExistingDelegators) { + return true; + } + + /** + * Refunds state gas for SSTORE when reverting a storage set (0→X→0). Only refunds when the new + * value is zero, the current value is nonzero, and the original value is zero. + * + * @param frame the message frame + * @param newValue the new storage value being written + * @param currentValue supplier for the current storage value + * @param originalValue supplier for the original storage value + */ + default void refundStorageSetStateGas( + final MessageFrame frame, + final UInt256 newValue, + final Supplier currentValue, + final Supplier originalValue) {} + + /** + * Computes the intrinsic state gas for a transaction. This is the worst-case state gas charged + * upfront (assuming all delegation targets are new accounts). Existing-account refunds are + * applied later during processing. + * + * @param blockGasLimit the block gas limit + * @param isContractCreation whether the transaction creates a contract + * @param codeDelegationCount number of EIP-7702 code delegations + * @return the intrinsic state gas + */ + default long transactionIntrinsicStateGas( + final long blockGasLimit, final boolean isContractCreation, final long codeDelegationCount) { + long stateGas = 0; + if (isContractCreation) { + stateGas += createStateGas(blockGasLimit); + } + if (codeDelegationCount > 0) { + // Worst case: all delegators are new accounts → (112 + 23) * cpsb each + stateGas += + (emptyAccountDelegationStateGas(blockGasLimit) + authBaseStateGas(blockGasLimit)) + * codeDelegationCount; + } + return stateGas; + } + + /** A no-op implementation that returns 0 for all state gas costs and performs no charging. */ + StateGasCostCalculator NONE = + new StateGasCostCalculator() { + @Override + public long costPerStateByte(final long blockGasLimit) { + return 0L; + } + + @Override + public long createStateGas(final long blockGasLimit) { + return 0L; + } + + @Override + public long codeDepositStateGas(final int codeSize, final long blockGasLimit) { + return 0L; + } + + @Override + public long codeDepositHashGas(final int codeSize) { + return 0L; + } + + @Override + public long newAccountStateGas(final long blockGasLimit) { + return 0L; + } + + @Override + public long storageSetStateGas(final long blockGasLimit) { + return 0L; + } + + @Override + public long storageSetRegularGas() { + return 0L; + } + + @Override + public long authBaseStateGas(final long blockGasLimit) { + return 0L; + } + + @Override + public long authBaseRegularGas() { + return 0L; + } + + @Override + public long emptyAccountDelegationStateGas(final long blockGasLimit) { + return 0L; + } + + @Override + public long transactionRegularGasLimit() { + return Long.MAX_VALUE; + } + }; +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCallOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCallOperation.java index 6130f48c117..57ba8e03916 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCallOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCallOperation.java @@ -237,6 +237,13 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { } frame.decrementRemainingGas(cost); + // EIP-8037: Charge state gas for new account creation in CALL + if (!gasCalculator() + .stateGasCostCalculator() + .chargeCallNewAccountStateGas(frame, recipientAddress, transferValue)) { + return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); + } + frame.clearReturnData(); final Account account = getAccount(frame.getRecipientAddress(), frame); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java index 7ed27d2fe5c..8e7791c9e47 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java @@ -89,6 +89,20 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { frame.clearReturnData(); + // EIP-8037: Charge state gas for CREATE operation (new account creation: 112 * cpsb) + // Charged before balance/depth/initcode-size checks — state gas is consumed even on failure. + // For oversized initcode, the spill mechanism tracks the burned state gas via spillBurned. + if (!gasCalculator().stateGasCostCalculator().chargeCreateStateGas(frame)) { + return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); + } + + // Re-check after state gas charge: when the reservoir is empty, consumeStateGas spills + // overflow into gasRemaining. The subsequent decrementRemainingGas(cost) would underflow + // if gasRemaining was reduced below cost by the spill. + if (frame.getRemainingGas() < cost) { + return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); + } + Code code = codeSupplier.get(); if (code != null && code.getSize() > evm.getMaxInitcodeSize()) { @@ -112,6 +126,7 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { account.incrementNonce(); frame.decrementRemainingGas(cost); + spawnChildMessage(frame, code); frame.incrementRemainingGas(cost); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/SStoreOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/SStoreOperation.java index 9d0a7e2aa54..92a1de7b975 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/SStoreOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/SStoreOperation.java @@ -101,6 +101,18 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { gasCalculator() .calculateStorageRefundAmount(newValue, currentValueSupplier, originalValueSupplier)); + // EIP-8037: Refund state gas for 0→X→0 (storage set then clear) + gasCalculator() + .stateGasCostCalculator() + .refundStorageSetStateGas(frame, newValue, currentValueSupplier, originalValueSupplier); + + // EIP-8037: Charge state gas for storage set (0 -> nonzero) + if (!gasCalculator() + .stateGasCostCalculator() + .chargeStorageSetStateGas(frame, newValue, currentValueSupplier, originalValueSupplier)) { + return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); + } + account.setStorageValue(key, newValue); frame.storageWasUpdated(key, newValue); frame.getEip7928AccessList().ifPresent(t -> t.addSlotAccessForAccount(address, key)); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/SelfDestructOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/SelfDestructOperation.java index ed7ea612330..c7dd3d46ccc 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/SelfDestructOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/SelfDestructOperation.java @@ -103,6 +103,13 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); } + // EIP-8037: Charge state gas for new account creation in SELFDESTRUCT + if (!gasCalculator() + .stateGasCostCalculator() + .chargeSelfDestructNewAccountStateGas(frame, beneficiaryNullable, originatorBalance)) { + return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); + } + // We passed preliminary checks, get mutable accounts. final MutableAccount beneficiaryAccount = getOrCreateAccount(beneficiaryAddress, frame); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java b/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java index eb9d974ae78..5e6677c6a34 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java @@ -135,12 +135,53 @@ private void clearAccumulatedStateBesidesGasAndOutput(final MessageFrame frame) } /** - * Gets called when the message frame encounters an exceptional halt. + * EIP-8037: Handles state gas spill on revert/halt. When state changes are rolled back, the state + * gas that was consumed is restored. Any "spill" (state gas that had overflowed from the + * reservoir into gasRemaining) is routed back: for child frames it returns to the reservoir for + * parent re-use; for the initial frame it is tracked in stateGasSpillBurned for transaction-level + * gas accounting. * * @param frame The message frame */ - private void exceptionalHalt(final MessageFrame frame) { + private void handleStateGasSpill(final MessageFrame frame) { + final long stateGasUsedBefore = frame.getStateGasUsed(); + final long reservoirBefore = frame.getStateGasReservoir(); + clearAccumulatedStateBesidesGasAndOutput(frame); + + final long stateGasRestored = stateGasUsedBefore - frame.getStateGasUsed(); + final long reservoirRestored = frame.getStateGasReservoir() - reservoirBefore; + final long spill = Math.max(0L, stateGasRestored - reservoirRestored); + if (spill > 0) { + if (frame.getMessageFrameStack().size() > 1) { + // Child frame: return spill to reservoir for parent to re-use + frame.incrementStateGasReservoir(spill); + } else { + // Initial frame: track spill for transaction-level gas accounting + frame.accumulateStateGasSpillBurned(spill); + } + } + } + + /** + * Gets called when the message frame encounters an exceptional halt. + * + * @param frame The message frame + * @param preExecutionHalt true if the halt occurred before any code was executed (e.g. address + * collision in CONTRACT_CREATION detected in start()) + */ + private void exceptionalHalt(final MessageFrame frame, final boolean preExecutionHalt) { + handleStateGasSpill(frame); + + // EIP-8037: Gas burned by a CREATE child that halted before executing any code (address + // collision) is excluded from block regular gas accounting. It still counts toward fees. + if (preExecutionHalt && frame.getType() == MessageFrame.Type.CONTRACT_CREATION) { + final long collisionGas = frame.getRemainingGas(); + if (collisionGas > 0) { + frame.accumulateRegularGasCollisionBurned(collisionGas); + } + } + frame.clearGasRemaining(); frame.clearOutputData(); frame.setState(MessageFrame.State.COMPLETED_FAILED); @@ -152,7 +193,7 @@ private void exceptionalHalt(final MessageFrame frame) { * @param frame The message frame */ protected void revert(final MessageFrame frame) { - clearAccumulatedStateBesidesGasAndOutput(frame); + handleStateGasSpill(frame); frame.setState(MessageFrame.State.COMPLETED_FAILED); } @@ -207,7 +248,8 @@ public void process(final MessageFrame frame, final OperationTracer operationTra } } - if (frame.getState() == MessageFrame.State.CODE_EXECUTING) { + final boolean wasCodeExecuting = (frame.getState() == MessageFrame.State.CODE_EXECUTING); + if (wasCodeExecuting) { codeExecute(frame, operationTracer); if (frame.getState() == MessageFrame.State.CODE_SUSPENDED) { @@ -220,7 +262,7 @@ public void process(final MessageFrame frame, final OperationTracer operationTra } if (frame.getState() == MessageFrame.State.EXCEPTIONAL_HALT) { - exceptionalHalt(frame); + exceptionalHalt(frame, !wasCodeExecuting); } if (frame.getState() == MessageFrame.State.REVERT) { diff --git a/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java b/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java index 4eb9b65571e..d264c91e9d6 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java @@ -196,6 +196,32 @@ public void codeSuccess(final MessageFrame frame, final OperationTracer operatio if (invalidReason.isEmpty()) { frame.decrementRemainingGas(depositFee); + // EIP-8037: Charge state gas for code deposit (cpsb * codeSize) + if (!evm.getGasCalculator() + .stateGasCostCalculator() + .chargeCodeDepositStateGas(frame, contractCode.size())) { + LOG.trace("Contract creation error: insufficient state gas for code deposit"); + // EIP-8037: For depth-0 (initial tx) frames, use forced charge + no-rollback failure so + // that stateGasUsed is preserved for block gas accounting (e.g. short_one_gas test). + if (frame.getDepth() == 0) { + final long stateGasAmount = + evm.getGasCalculator() + .stateGasCostCalculator() + .codeDepositStateGas(contractCode.size(), frame.getBlockValues().getGasLimit()); + if (stateGasAmount > 0) { + frame.consumeStateGasForced(stateGasAmount); + } + failCodeDepositWithoutRollback( + frame, operationTracer, Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); + } else { + frame.setExceptionalHaltReason(Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); + frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); + operationTracer.traceAccountCreationResult( + frame, Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); + } + return; + } + // Finalize contract creation, setting the contract code. final MutableAccount contract = frame.getWorldUpdater().getOrCreate(frame.getContractAddress()); @@ -210,6 +236,26 @@ public void codeSuccess(final MessageFrame frame, final OperationTracer operatio operationTracer.traceAccountCreationResult(frame, Optional.empty()); } } else { + // EIP-8037: For depth-0 (initial tx) frames with code that fails validation (e.g. + // CODE_TOO_LARGE), charge the code deposit state gas before failing so the charge is + // preserved in stateGasUsed for block gas accounting (e.g. over_max test). + // Use consumeStateGas (not forced) — it returns false without modifying on failure, so + // we can safely fall through to EXCEPTIONAL_HALT if gas is insufficient. + if (frame.getDepth() == 0) { + final long stateGasAmount = + evm.getGasCalculator() + .stateGasCostCalculator() + .codeDepositStateGas(contractCode.size(), frame.getBlockValues().getGasLimit()); + if (stateGasAmount > 0 && frame.consumeStateGas(stateGasAmount)) { + // Sufficient state gas: charge succeeded, fail without rollback so stateGasUsed + // is preserved for block accounting. + final Optional haltReason = invalidReason.get(); + failCodeDepositWithoutRollback(frame, operationTracer, haltReason); + return; + } + // Insufficient state gas or no state gas (non-EIP-8037): fall through to + // EXCEPTIONAL_HALT. + } final Optional exceptionalHaltReason = invalidReason.get(); frame.setExceptionalHaltReason(exceptionalHaltReason); frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); @@ -217,4 +263,41 @@ public void codeSuccess(final MessageFrame frame, final OperationTracer operatio } } } + + /** + * Fails a depth-0 code deposit without triggering the normal EXCEPTIONAL_HALT rollback path. This + * preserves stateGasUsed for EIP-8037 block gas accounting. The world state is still reverted and + * all gas is cleared. + * + * @param frame the message frame + * @param operationTracer the operation tracer + * @param haltReason the exceptional halt reason to report + */ + private void failCodeDepositWithoutRollback( + final MessageFrame frame, + final OperationTracer operationTracer, + final Optional haltReason) { + LOG.trace( + "Contract creation failed (no rollback): {} for address {}", + haltReason, + frame.getContractAddress()); + // Revert world state changes without calling frame.rollback() (which would undo stateGasUsed). + // revert() undoes the world state mutations from this frame's execution. + // commit() propagates the reverted (clean) state to the parent updater. + // frame.rollback() is deliberately avoided: it would undo stateGasUsed tracking via the + // UndoScalar mechanism, which must be preserved for EIP-8037 block gas accounting. + frame.getWorldUpdater().revert(); + frame.getWorldUpdater().commit(); + frame.clearLogs(); + frame.clearGasRefund(); + frame.clearGasRemaining(); + frame.clearOutputData(); + // Do NOT call frame.setExceptionalHaltReason() here. + // MainnetTransactionProcessor (processTransaction ~line 454) zeros the state gas reservoir when + // exceptionalHaltReason is present. For depth-0 code deposit failures, the reservoir must be + // preserved to avoid inflating block gas accounting. COMPLETED_FAILED state is sufficient to + // signal failure. If MTP's reservoir-zeroing logic changes, this assumption must be revisited. + frame.setState(MessageFrame.State.COMPLETED_FAILED); + operationTracer.traceAccountCreationResult(frame, haltReason); + } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/Eip8037StateGasCostCalculatorTest.java b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/Eip8037StateGasCostCalculatorTest.java new file mode 100644 index 00000000000..c439c3b9d90 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/Eip8037StateGasCostCalculatorTest.java @@ -0,0 +1,144 @@ +/* + * 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.evm.gascalculator; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class Eip8037StateGasCostCalculatorTest { + + private static final long DEVNET_CPSB = 1174L; + + private final Eip8037StateGasCostCalculator calculator = new Eip8037StateGasCostCalculator(); + + // --- Devnet-3: hardcoded cpsb tests --- + + @Test + void costPerStateByteReturnsHardcodedValue() { + // Devnet-3: cpsb is hardcoded to 1174 regardless of block gas limit + assertThat(calculator.costPerStateByte(36_000_000L)).isEqualTo(DEVNET_CPSB); + assertThat(calculator.costPerStateByte(60_000_000L)).isEqualTo(DEVNET_CPSB); + assertThat(calculator.costPerStateByte(100_000_000L)).isEqualTo(DEVNET_CPSB); + assertThat(calculator.costPerStateByte(0L)).isEqualTo(DEVNET_CPSB); + } + + // --- Dynamic cpsb calculation tests (for devnet-4 re-enablement) --- + + @Test + void dynamicCostPerStateByteAt36MGasLimit() { + // raw cpsb = ceil(440.607...) = 441, quantized with offset 9578 → 150 + assertThat(Eip8037StateGasCostCalculator.costPerStateByteFromGasLimit(36_000_000L)) + .isEqualTo(150L); + } + + @Test + void dynamicCostPerStateByteAt60MGasLimit() { + assertThat(Eip8037StateGasCostCalculator.costPerStateByteFromGasLimit(60_000_000L)) + .isEqualTo(662L); + } + + @Test + void dynamicCostPerStateByteAt100MGasLimit() { + assertThat(Eip8037StateGasCostCalculator.costPerStateByteFromGasLimit(100_000_000L)) + .isEqualTo(1174L); + } + + @Test + void dynamicCostPerStateByteAtZeroGasLimit() { + assertThat(Eip8037StateGasCostCalculator.costPerStateByteFromGasLimit(0L)).isEqualTo(0L); + } + + @Test + void dynamicCostPerStateByteAtMinimalGasLimits() { + // gasLimit=1: (1/2)=0 in integer division, numerator=0, raw=0, return 0 + assertThat(Eip8037StateGasCostCalculator.costPerStateByteFromGasLimit(1L)).isEqualTo(0L); + // gasLimit=2: raw=1, shifted=9579, bitLen=14, shift=9, + // ((9579>>9)<<9) = 18*512 = 9216, 9216-9578 = -362, max(-362, 1) = 1 + assertThat(Eip8037StateGasCostCalculator.costPerStateByteFromGasLimit(2L)).isEqualTo(1L); + } + + // --- Derived cost tests (use hardcoded cpsb) --- + + @Test + void createStateGas() { + // 112 bytes per account * cpsb(1174) = 131_488 + assertThat(calculator.createStateGas(100_000_000L)).isEqualTo(112L * DEVNET_CPSB); + } + + @Test + void storageSetStateGas() { + // 32 bytes per storage slot * cpsb(1174) = 37_568 + assertThat(calculator.storageSetStateGas(100_000_000L)).isEqualTo(32L * DEVNET_CPSB); + } + + @Test + void codeDepositStateGas() { + // cpsb * codeSize + assertThat(calculator.codeDepositStateGas(100, 100_000_000L)).isEqualTo(DEVNET_CPSB * 100L); + assertThat(calculator.codeDepositStateGas(0, 100_000_000L)).isEqualTo(0L); + } + + @Test + void codeDepositHashGas() { + // 6 * ceil(codeSize / 32) + assertThat(calculator.codeDepositHashGas(0)).isEqualTo(0L); + assertThat(calculator.codeDepositHashGas(1)).isEqualTo(6L); + assertThat(calculator.codeDepositHashGas(32)).isEqualTo(6L); + assertThat(calculator.codeDepositHashGas(33)).isEqualTo(12L); + assertThat(calculator.codeDepositHashGas(100)).isEqualTo(24L); + } + + @Test + void newAccountStateGasMatchesCreate() { + assertThat(calculator.newAccountStateGas(100_000_000L)) + .isEqualTo(calculator.createStateGas(100_000_000L)); + } + + @Test + void authBaseStateGas() { + // 23 bytes per auth * cpsb(1174) = 27_002 + assertThat(calculator.authBaseStateGas(100_000_000L)).isEqualTo(23L * DEVNET_CPSB); + } + + @Test + void emptyAccountDelegationStateGasMatchesCreate() { + assertThat(calculator.emptyAccountDelegationStateGas(100_000_000L)) + .isEqualTo(calculator.createStateGas(100_000_000L)); + } + + @Test + void constantRegularGasCosts() { + assertThat(calculator.storageSetRegularGas()).isEqualTo(2_900L); + assertThat(calculator.authBaseRegularGas()).isEqualTo(7_500L); + assertThat(calculator.transactionRegularGasLimit()).isEqualTo(16_777_216L); + } + + @Test + void noneImplementationReturnsZeroForAllCosts() { + final StateGasCostCalculator none = StateGasCostCalculator.NONE; + assertThat(none.costPerStateByte(100_000_000L)).isEqualTo(0L); + assertThat(none.createStateGas(100_000_000L)).isEqualTo(0L); + assertThat(none.storageSetStateGas(100_000_000L)).isEqualTo(0L); + assertThat(none.codeDepositStateGas(100, 100_000_000L)).isEqualTo(0L); + assertThat(none.codeDepositHashGas(100)).isEqualTo(0L); + assertThat(none.newAccountStateGas(100_000_000L)).isEqualTo(0L); + assertThat(none.authBaseStateGas(100_000_000L)).isEqualTo(0L); + assertThat(none.emptyAccountDelegationStateGas(100_000_000L)).isEqualTo(0L); + assertThat(none.storageSetRegularGas()).isEqualTo(0L); + assertThat(none.authBaseRegularGas()).isEqualTo(0L); + assertThat(none.transactionRegularGasLimit()).isEqualTo(Long.MAX_VALUE); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/AbstractCreateOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/AbstractCreateOperationTest.java index eff84c517a7..b32cde42a12 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/AbstractCreateOperationTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/AbstractCreateOperationTest.java @@ -33,10 +33,13 @@ import org.hyperledger.besu.evm.frame.BlockValues; import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.AmsterdamGasCalculator; import org.hyperledger.besu.evm.gascalculator.ConstantinopleGasCalculator; +import org.hyperledger.besu.evm.gascalculator.Eip8037StateGasCostCalculator; import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.internal.EvmConfiguration; import org.hyperledger.besu.evm.processor.ContractCreationProcessor; +import org.hyperledger.besu.evm.testutils.FakeBlockValues; import org.hyperledger.besu.evm.tracing.OperationTracer; import org.hyperledger.besu.evm.worldstate.WorldUpdater; @@ -203,4 +206,76 @@ void onFailure() { assertThat(operation.failureHaltReason) .contains(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS); } + + @Test + void createHaltsWhenStateGasSpillReducesGasRemainingBelowCost() { + // EIP-8037: When the state gas reservoir is empty, consumeStateGas spills overflow into + // gasRemaining. If this reduces gasRemaining below the operation cost, the operation must + // halt with INSUFFICIENT_GAS rather than underflowing at decrementRemainingGas. + final long blockGasLimit = 36_000_000L; + final GasCalculator amsterdamCalc = new AmsterdamGasCalculator(); + final FakeCreateOperation amsterdamOp = new FakeCreateOperation(amsterdamCalc); + + // State gas for CREATE at 36M = 112 * 150 = 16,800 + final long stateGas = new Eip8037StateGasCostCalculator().createStateGas(blockGasLimit); + + final UInt256 memoryOffset = UInt256.fromHexString("0xFF"); + final MessageFrame frame = + MessageFrame.builder() + .type(MessageFrame.Type.CONTRACT_CREATION) + .contract(Address.ZERO) + .inputData(Bytes.EMPTY) + .sender(Address.fromHexString(SENDER)) + .value(Wei.ZERO) + .apparentValue(Wei.ZERO) + .code(new Code(SIMPLE_CREATE)) + .completer(__ -> {}) + .address(Address.fromHexString(SENDER)) + .blockHashLookup((__, ___) -> Hash.ZERO) + .blockValues( + new FakeBlockValues(1337) { + @Override + public long getGasLimit() { + return blockGasLimit; + } + }) + .gasPrice(Wei.ZERO) + .miningBeneficiary(Address.ZERO) + .originator(Address.ZERO) + .initialGas(100_000L) + .worldUpdater(worldUpdater) + .build(); + + // Push CREATE args: value=0, offset=0xFF, size=5 (SIMPLE_CREATE) + frame.pushStackItem(Bytes.ofUnsignedLong(SIMPLE_CREATE.size())); + frame.pushStackItem(memoryOffset); + frame.pushStackItem(Bytes.EMPTY); // value = 0 + frame.expandMemory(0, 500); + frame.writeMemory(memoryOffset.trimLeadingZeros().toInt(), SIMPLE_CREATE.size(), SIMPLE_CREATE); + + when(account.getNonce()).thenReturn(55L); + when(account.getBalance()).thenReturn(Wei.ZERO); + when(worldUpdater.getAccount(any())).thenReturn(account); + when(worldUpdater.get(any())).thenReturn(account); + when(worldUpdater.getSenderAccount(any())).thenReturn(account); + when(worldUpdater.getOrCreate(any())).thenReturn(newAccount); + when(newAccount.getCode()).thenReturn(Bytes.EMPTY); + when(newAccount.isStorageEmpty()).thenReturn(true); + when(worldUpdater.updater()).thenReturn(worldUpdater); + + // Compute the operation cost so we can set initialGas to trigger the underflow scenario + final EVM evm = MainnetEVMs.amsterdam(EvmConfiguration.DEFAULT); + final long cost = amsterdamOp.cost(frame, () -> new Code(SIMPLE_CREATE)); + + // Set gasRemaining to: cost + (stateGas - 1). This ensures the initial check (gas >= cost) + // passes, but after state gas spills into gasRemaining (reservoir=0), gasRemaining < cost. + final long initialGas = cost + stateGas - 1; + frame.setGasRemaining(initialGas); + // Ensure reservoir is empty so state gas must spill into gasRemaining + frame.setStateGasReservoir(0L); + + final Operation.OperationResult result = amsterdamOp.execute(frame, evm); + + assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.INSUFFICIENT_GAS); + } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/SStoreOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/SStoreOperationTest.java index 6f3c7fd21f6..cced7ff3d7b 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/SStoreOperationTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/SStoreOperationTest.java @@ -22,7 +22,9 @@ import org.hyperledger.besu.evm.frame.BlockValues; import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.AmsterdamGasCalculator; import org.hyperledger.besu.evm.gascalculator.ConstantinopleGasCalculator; +import org.hyperledger.besu.evm.gascalculator.Eip8037StateGasCostCalculator; import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.operation.Operation.OperationResult; import org.hyperledger.besu.evm.testutils.FakeBlockValues; @@ -89,6 +91,244 @@ void storeOperation( assertThat(result.getHaltReason()).isEqualTo(expectedHalt); } + @Test + void sstoreZeroToNonzeroTracksStateGasWithAmsterdam() { + final GasCalculator amsterdamCalc = new AmsterdamGasCalculator(); + final SStoreOperation operation = + new SStoreOperation(amsterdamCalc, SStoreOperation.EIP_1706_MINIMUM); + + final long blockGasLimit = 36_000_000L; + final Address address = Address.fromHexString("0x18675309"); + final ToyWorld toyWorld = new ToyWorld(); + final WorldUpdater worldStateUpdater = toyWorld.updater(); + + final MessageFrame frame = + new TestMessageFrameBuilder() + .address(address) + .worldUpdater(worldStateUpdater) + .blockValues( + new FakeBlockValues(1337) { + @Override + public long getGasLimit() { + return blockGasLimit; + } + }) + .initialGas(100_000L) + .build(); + worldStateUpdater.getOrCreate(address).setBalance(Wei.of(1)); + worldStateUpdater.commit(); + + // key=1, newValue=42 (0 -> nonzero triggers state gas) + frame.pushStackItem(UInt256.valueOf(42)); + frame.pushStackItem(UInt256.ONE); + + final OperationResult result = operation.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + + // State gas: 32 * cpsb(36M) = 32 * 150 = 4_800 + final long expectedStateGas = + 32L * new Eip8037StateGasCostCalculator().costPerStateByte(blockGasLimit); + assertThat(frame.getStateGasUsed()).isEqualTo(expectedStateGas); + } + + @Test + void sstoreNonzeroToNonzeroDoesNotTrackStateGas() { + final GasCalculator amsterdamCalc = new AmsterdamGasCalculator(); + final SStoreOperation operation = + new SStoreOperation(amsterdamCalc, SStoreOperation.EIP_1706_MINIMUM); + + final long blockGasLimit = 36_000_000L; + final Address address = Address.fromHexString("0x18675309"); + final ToyWorld toyWorld = new ToyWorld(); + + // Set up base state with nonzero storage + final WorldUpdater baseUpdater = toyWorld.updater(); + final var baseAccount = baseUpdater.getOrCreate(address); + baseAccount.setBalance(Wei.of(1)); + baseAccount.setStorageValue(UInt256.ONE, UInt256.valueOf(99)); + baseUpdater.commit(); + + // Fresh updater for transaction context + final WorldUpdater txUpdater = toyWorld.updater(); + final MessageFrame frame = + new TestMessageFrameBuilder() + .address(address) + .worldUpdater(txUpdater) + .blockValues( + new FakeBlockValues(1337) { + @Override + public long getGasLimit() { + return blockGasLimit; + } + }) + .initialGas(100_000L) + .build(); + + // key=1, newValue=42 (nonzero -> nonzero, no state gas) + frame.pushStackItem(UInt256.valueOf(42)); + frame.pushStackItem(UInt256.ONE); + + final OperationResult result = operation.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + + // No state gas for nonzero -> nonzero + assertThat(frame.getStateGasUsed()).isEqualTo(0L); + } + + @Test + void sstoreZeroToNonzeroToZeroRefundsStateGas() { + final GasCalculator amsterdamCalc = new AmsterdamGasCalculator(); + final SStoreOperation operation = + new SStoreOperation(amsterdamCalc, SStoreOperation.EIP_1706_MINIMUM); + + final long blockGasLimit = 36_000_000L; + final Address address = Address.fromHexString("0x18675309"); + final ToyWorld toyWorld = new ToyWorld(); + + // Set up base state with balance + final WorldUpdater baseUpdater = toyWorld.updater(); + baseUpdater.getOrCreate(address).setBalance(Wei.of(1)); + baseUpdater.commit(); + + // Fresh updater for transaction context (original values tracked from base) + final WorldUpdater txUpdater = toyWorld.updater(); + final MessageFrame frame = + new TestMessageFrameBuilder() + .address(address) + .worldUpdater(txUpdater) + .blockValues( + new FakeBlockValues(1337) { + @Override + public long getGasLimit() { + return blockGasLimit; + } + }) + .initialGas(100_000L) + .build(); + frame.setStateGasReservoir(100_000L); + + // First SSTORE: key=1, value=42 (0 -> nonzero, triggers state gas) + frame.pushStackItem(UInt256.valueOf(42)); + frame.pushStackItem(UInt256.ONE); + final OperationResult result1 = operation.execute(frame, null); + assertThat(result1.getHaltReason()).isNull(); + + final long expectedStateGas = + 32L * new Eip8037StateGasCostCalculator().costPerStateByte(blockGasLimit); + assertThat(frame.getStateGasUsed()).isEqualTo(expectedStateGas); // 37,568 + + // Second SSTORE: key=1, value=0 (nonzero -> 0, original=0 triggers state gas refund) + frame.pushStackItem(UInt256.ZERO); + frame.pushStackItem(UInt256.ONE); + final OperationResult result2 = operation.execute(frame, null); + assertThat(result2.getHaltReason()).isNull(); + + // State gas refund (37,568) + regular SSTORE refund for 0->X->0 (2,800) + assertThat(frame.getGasRefund()).isEqualTo(expectedStateGas + 2_800L); + // stateGasUsed tracks gross consumption, not decremented by refunds + assertThat(frame.getStateGasUsed()).isEqualTo(expectedStateGas); + } + + @Test + void sstoreNonzeroToZeroOriginalNonzeroNoStateGasRefund() { + final GasCalculator amsterdamCalc = new AmsterdamGasCalculator(); + final SStoreOperation operation = + new SStoreOperation(amsterdamCalc, SStoreOperation.EIP_1706_MINIMUM); + + final long blockGasLimit = 36_000_000L; + final Address address = Address.fromHexString("0x18675309"); + final ToyWorld toyWorld = new ToyWorld(); + + // Set up base state with nonzero storage + final WorldUpdater baseUpdater = toyWorld.updater(); + final var baseAccount = baseUpdater.getOrCreate(address); + baseAccount.setBalance(Wei.of(1)); + baseAccount.setStorageValue(UInt256.ONE, UInt256.valueOf(99)); + baseUpdater.commit(); + + // Fresh updater for transaction context + final WorldUpdater txUpdater = toyWorld.updater(); + final MessageFrame frame = + new TestMessageFrameBuilder() + .address(address) + .worldUpdater(txUpdater) + .blockValues( + new FakeBlockValues(1337) { + @Override + public long getGasLimit() { + return blockGasLimit; + } + }) + .initialGas(100_000L) + .build(); + + // SSTORE key=1, value=0 (nonzero -> 0, original nonzero — no state gas) + frame.pushStackItem(UInt256.ZERO); + frame.pushStackItem(UInt256.ONE); + final OperationResult result = operation.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + + // No state gas for clearing when original was nonzero + assertThat(frame.getStateGasUsed()).isEqualTo(0L); + // Only the regular SSTORE_CLEARS_SCHEDULE refund (4,800) + assertThat(frame.getGasRefund()).isEqualTo(4_800L); + } + + @Test + void sstoreStateGasSpillsFromReservoirToGasRemaining() { + final GasCalculator amsterdamCalc = new AmsterdamGasCalculator(); + final SStoreOperation operation = + new SStoreOperation(amsterdamCalc, SStoreOperation.EIP_1706_MINIMUM); + + final long blockGasLimit = 36_000_000L; + final Address address = Address.fromHexString("0x18675309"); + final ToyWorld toyWorld = new ToyWorld(); + + // Set up base state with balance + final WorldUpdater baseUpdater = toyWorld.updater(); + baseUpdater.getOrCreate(address).setBalance(Wei.of(1)); + baseUpdater.commit(); + + // Fresh updater for transaction context + final WorldUpdater txUpdater = toyWorld.updater(); + final MessageFrame frame = + new TestMessageFrameBuilder() + .address(address) + .worldUpdater(txUpdater) + .blockValues( + new FakeBlockValues(1337) { + @Override + public long getGasLimit() { + return blockGasLimit; + } + }) + .initialGas(100_000L) + .build(); + + // Set reservoir to less than what the SSTORE will need + frame.setStateGasReservoir(10_000L); + final long gasBeforeSstore = frame.getRemainingGas(); + + // SSTORE 0 -> nonzero: needs 37,568 state gas but only 10k in reservoir + frame.pushStackItem(UInt256.valueOf(42)); + frame.pushStackItem(UInt256.ONE); + final OperationResult result = operation.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + + final long expectedStateGas = + 32L * new Eip8037StateGasCostCalculator().costPerStateByte(blockGasLimit); + final long expectedSpill = expectedStateGas - 10_000L; // 27,568 + + // Reservoir fully drained + assertThat(frame.getStateGasReservoir()).isEqualTo(0L); + // Total state gas consumed + assertThat(frame.getStateGasUsed()).isEqualTo(expectedStateGas); + // gasRemaining decreased by the spill amount only (regular SSTORE cost is deducted by the + // EVM after execute returns, not by the operation itself) + final long expectedRemainingGas = gasBeforeSstore - expectedSpill; + assertThat(frame.getRemainingGas()).isEqualTo(expectedRemainingGas); + } + @Test void dryRunDetector() { assertThat(true) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9315d954fed..e3c72034b1e 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1721,14 +1721,14 @@ - - - + + + - + From ea02a8a11dae1ed8db71d108d3adf3d71165f1e3 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 17 Mar 2026 05:31:59 +1000 Subject: [PATCH 40/77] snap server - log enabled/disabled in config overview (#10039) * log snap server enabled/disabled in config overview * starting snapserver msg at info Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane --- .../besu/cli/ConfigurationOverviewBuilder.java | 9 +++++---- .../besu/ethereum/eth/manager/snap/SnapServer.java | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java b/app/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java index 9d6d7f1a4d3..7d6352ec3bf 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java +++ b/app/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java @@ -77,6 +77,7 @@ public class ConfigurationOverviewBuilder { private Long targetGasLimit; private Integer maxBlobsPerTransaction; private Integer maxBlobsPerBlock; + private static final String SNAP_SYNC_MODE = "SNAP"; /** * Create a new ConfigurationOverviewBuilder. @@ -477,6 +478,10 @@ public String build() { if (syncMode != null) { lines.add("Sync mode: " + syncMode); + if (syncMode.equalsIgnoreCase(SNAP_SYNC_MODE)) { + final String snapServerStatus = isSnapServerEnabled ? "enabled" : "disabled"; + lines.add(" SNAP Sync server " + snapServerStatus); + } } if (syncMinPeers != null) { @@ -548,10 +553,6 @@ public String build() { lines.add(chainPruningString.toString()); } - if (isSnapServerEnabled) { - lines.add("Snap Sync server enabled"); - } - if (isHighSpec) { lines.add("Experimental high spec configuration enabled"); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServer.java index f3d4c83c642..a94d253ab4a 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServer.java @@ -148,7 +148,7 @@ public synchronized SnapServer start() { if (worldStateKeyValueStorage.getDataStorageFormat().isBonsaiFormat() && (worldStateStorageCoordinator.isMatchingFlatMode(FlatDbMode.FULL) || worldStateStorageCoordinator.isMatchingFlatMode(FlatDbMode.ARCHIVE))) { - LOGGER.debug("Starting SnapServer with Bonsai full flat db"); + LOGGER.info("Starting SnapServer with Bonsai full flat db"); var bonsaiArchive = protocolContext .map(ProtocolContext::getWorldStateArchive) From 6af2fa265baf4788867931ac1bcd196d390026c3 Mon Sep 17 00:00:00 2001 From: Cyrus Date: Tue, 17 Mar 2026 06:01:36 +0530 Subject: [PATCH 41/77] fix: avoid TOCTOU race in eth_getBlockByNumber latestResult (#10051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: avoid TOCTOU race in eth_getBlockByNumber latestResult getSyncStatus() was being called twice in latestResult() — once to check isEmpty() and again to call .get(). If sync completed between those two calls (clearSyncTarget() fires on the sync thread), the second call returned Optional.empty() and .get() threw NoSuchElementException, crashing the RPC handler. Fixed by capturing the result in a local variable so both the check and the read operate on the same snapshot. Added a test that reproduces the race: getSyncStatus() returns a present Optional on the first call and an empty one on the second, confirming the handler no longer throws. Signed-off-by: Shridhar Panigrahi --------- Signed-off-by: Shridhar Panigrahi Co-authored-by: Sally MacFarlane Signed-off-by: Cyrus --- .../internal/methods/EthGetBlockByNumber.java | 7 +++-- .../methods/EthGetBlockByNumberTest.java | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBlockByNumber.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBlockByNumber.java index 761870ddd12..0ec5472ebc0 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBlockByNumber.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBlockByNumber.java @@ -26,7 +26,9 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.Synchronizer; +import org.hyperledger.besu.plugin.data.SyncStatus; +import java.util.Optional; import java.util.function.Supplier; import com.google.common.base.Suppliers; @@ -96,10 +98,11 @@ protected Object latestResult(final JsonRpcRequestContext request) { .get() .getWorldStateArchive() .isWorldStateAvailable(stateRoot, block)) { - if (this.synchronizer.getSyncStatus().isEmpty()) { // we are already in sync + final Optional maybeSyncStatus = this.synchronizer.getSyncStatus(); + if (maybeSyncStatus.isEmpty()) { // we are already in sync return resultByBlockNumber(request, headBlockNumber); } else { // out of sync, return highest pulled block - long headishBlock = this.synchronizer.getSyncStatus().get().getCurrentBlock(); + long headishBlock = maybeSyncStatus.get().getCurrentBlock(); return resultByBlockNumber(request, headishBlock); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBlockByNumberTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBlockByNumberTest.java index 619d0a82f4b..5243b8ec861 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBlockByNumberTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBlockByNumberTest.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryBlockchain; import static org.mockito.Mockito.spy; @@ -35,6 +36,7 @@ import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.DefaultSyncStatus; import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.core.Synchronizer; import org.hyperledger.besu.ethereum.core.TransactionReceipt; @@ -43,6 +45,8 @@ import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -196,6 +200,32 @@ private void assertSuccessPos(final String tag, final long height) { assertSuccess(tag, height); } + @Test + public void latestDoesNotThrowWhenSyncStatusClearedBetweenChecks() { + // Simulates the race where sync completes between the isEmpty() check and the .get() call. + // Before the fix, getSyncStatus() was called twice — once for the isEmpty() guard and once to + // read getCurrentBlock(). If clearSyncTarget() fired on the sync thread between those two + // calls, the second call returned Optional.empty() and .get() threw NoSuchElementException, + // crashing the RPC handler for eth_getBlockByNumber("latest"). + final long currentBlock = BLOCKCHAIN_LENGTH - 2; + final AtomicInteger callCount = new AtomicInteger(0); + when(synchronizer.getSyncStatus()) + .thenAnswer( + invocation -> { + // First call: looks like sync is still in progress. + // Second call (if it were made): sync has just completed, no status. + if (callCount.incrementAndGet() == 1) { + return Optional.of( + new DefaultSyncStatus( + 0, currentBlock, currentBlock + 10, Optional.empty(), Optional.empty())); + } + return Optional.empty(); + }); + + assertThatCode(() -> method.response(requestWithParams("latest", "false"))) + .doesNotThrowAnyException(); + } + private JsonRpcRequestContext requestWithParams(final Object... params) { return new JsonRpcRequestContext(new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params)); } From 373d3f93820a0cfabdbd20eb21dfed955a6d34c1 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 17 Mar 2026 11:15:15 +1000 Subject: [PATCH 42/77] reduce number of scenarios tested (#10055) Signed-off-by: Sally MacFarlane --- ...neCommonAncestorTaskParameterizedTest.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest.java index 72c3c0f4d39..0947a8fe7e1 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest.java @@ -48,8 +48,10 @@ import org.hyperledger.besu.plugin.services.MetricsSystem; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -102,9 +104,24 @@ public static Stream parameters() { final int[] requestSizes = {5, 12, chainHeight, chainHeight * 2}; final Stream.Builder builder = Stream.builder(); for (final int requestSize : requestSizes) { - for (int i = 0; i <= chainHeight; i++) { - builder.add(Arguments.of(requestSize, i)); + // Test boundary-sensitive heights rather than every height 0..chainHeight. + // The algorithm batches headers in groups of requestSize, so only heights at + // batch boundaries (and their neighbours) exercise different code paths. + final Set heights = new LinkedHashSet<>(); + heights.add(0); + heights.add(1); + heights.add(chainHeight / 2); + heights.add(chainHeight - 1); + heights.add(chainHeight); + for (int boundary = requestSize; boundary < chainHeight; boundary += requestSize) { + heights.add(boundary - 1); + heights.add(boundary); + heights.add(boundary + 1); } + heights.stream() + .filter(h -> h >= 0 && h <= chainHeight) + .sorted() + .forEach(h -> builder.add(Arguments.of(requestSize, h))); } return builder.build(); } From ecaa3e5ac517e8c31490c816e3508162392b8258 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 17 Mar 2026 12:38:07 +1000 Subject: [PATCH 43/77] PeerTaskValidation accept NO_RESULTS_RETURNED (#10056) Per the ETH spec, a peer may return an empty response to GetBlockHeaders if it does not have the requested block. This is not a protocol violation. Treating it as a useless response causes the peer to be disconnected and denylisted after 5 retries, which is particularly harmful when only one server peer is available (e.g. during snap sync pivot selection). A server that is slightly behind the current safe block will always return empty for pivot header requests. The client should wait for the next safe block announcement rather than disconnecting its only peer. Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .../eth/manager/peertask/PeerTaskValidationResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java index 5693f506b77..8edec234ef5 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java @@ -19,7 +19,7 @@ import java.util.Optional; public enum PeerTaskValidationResponse { - NO_RESULTS_RETURNED(null, true), + NO_RESULTS_RETURNED(null, false), TOO_MANY_RESULTS_RETURNED(null, true), RESULTS_DO_NOT_MATCH_QUERY(null, true), NON_SEQUENTIAL_HEADERS_RETURNED( From b1d5679c1c0870533d87d526896afe2b9407e553 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 17 Mar 2026 15:24:09 +1000 Subject: [PATCH 44/77] reduce the number of iterations (#10057) Signed-off-by: Sally MacFarlane --- .../org/hyperledger/besu/chainexport/Era1BlockExporterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/org/hyperledger/besu/chainexport/Era1BlockExporterTest.java b/app/src/test/java/org/hyperledger/besu/chainexport/Era1BlockExporterTest.java index 1a1d846abfc..e48d35c1803 100644 --- a/app/src/test/java/org/hyperledger/besu/chainexport/Era1BlockExporterTest.java +++ b/app/src/test/java/org/hyperledger/besu/chainexport/Era1BlockExporterTest.java @@ -80,7 +80,7 @@ public void beforeTest() { public void testExport() throws IOException { Mockito.when(era1AccumulatorFactory.getEra1Accumulator()).thenReturn(era1Accumulator); List blockVerifications = new ArrayList<>(); - for (int i = 0; i < 8192; i++) { + for (int i = 0; i < 10; i++) { final int blockNumber = i; Block block = Mockito.mock(Block.class); Hash blockHash = Hash.wrap(Bytes32.random()); From 79bc2a35db1547db7ce99de1434ea3d26f67ff50 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 17 Mar 2026 15:54:32 +1000 Subject: [PATCH 45/77] BySpecTests: run setup once instead of for each spec file (#10059) * run setup once instead of for each spec file * volatile boolean Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane --- .../AbstractJsonRpcHttpBySpecTest.java | 44 ++++++++++++++++--- .../DebugGethTraceJsonRpcHttpBySpecTest.java | 4 +- .../bonsai/DebugJsonRpcHttpBySpecTest.java | 4 +- .../DebugTraceJsonRpcHttpBySpecTest.java | 4 +- .../EthByzantiumJsonRpcHttpBySpecTest.java | 4 +- .../jsonrpc/bonsai/EthConfigBySpecTest.java | 4 +- .../bonsai/EthJsonRpcHttpBySpecTest.java | 4 +- .../bonsai/EthSimulateV1BySpecTest.java | 4 +- ...estingBuildBlockJsonRpcHttpBySpecTest.java | 4 +- .../bonsai/TraceJsonRpcHttpBySpecTest.java | 4 +- .../forest/DebugJsonRpcHttpBySpecTest.java | 4 +- .../DebugTraceJsonRpcHttpBySpecTest.java | 4 +- .../EthByzantiumJsonRpcHttpBySpecTest.java | 4 +- .../forest/EthJsonRpcHttpBySpecTest.java | 4 +- .../forest/TraceJsonRpcHttpBySpecTest.java | 4 +- 15 files changed, 52 insertions(+), 48 deletions(-) diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpBySpecTest.java index 79737c98ca5..db64222dde9 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpBySpecTest.java @@ -45,13 +45,19 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public abstract class AbstractJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpServiceTest { private static final boolean UPDATE_SPECS = Boolean.getBoolean("besu.test.update.specs"); private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final ObjectMapper prettyObjectMapper = + new ObjectMapper().configure(INDENT_OUTPUT, true); private static final Pattern GAS_MATCH_FOR_STATE_DIFF = Pattern.compile("\"balance\":(?!\"=\").*?},"); private static final Pattern GAS_MATCH_FOR_VM_TRACE = @@ -62,6 +68,34 @@ public abstract class AbstractJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpS Pattern.compile("\"error\"\\s*:\\s*\"([^\"]+)\""); private URL specURL; + private volatile boolean initialized = false; + + /** + * Subclasses implement this to perform one-time blockchain + service setup. Called the first time + * {@link #setup()} runs; skipped on subsequent parameterized test invocations. + */ + protected abstract void doSetup() throws Exception; + + @Override + @BeforeEach + public void setup() throws Exception { + if (!initialized) { + initialized = true; + doSetup(); + } else if (filterManager != null) { + filterManager.stop(); + } + } + + @AfterAll + public void teardownAll() { + super.shutdownServer(); + } + + @Override + public void shutdownServer() { + // Suppressed: teardown happens once in @AfterAll, not after every parameterized test case + } @ParameterizedTest(name = "{index}: {0}") @MethodSource("specs") @@ -325,18 +359,16 @@ private void checkResponse( expectedResult = normalizeErrorMessages(expectedResult); actualResult = normalizeErrorMessages(actualResult); - final ObjectMapper mapper = new ObjectMapper(); - mapper.configure(INDENT_OUTPUT, true); assertThat( - mapper + prettyObjectMapper .writerWithDefaultPrettyPrinter() .withoutAttribute("creationMethod") - .writeValueAsString(mapper.readTree(actualResult))) + .writeValueAsString(prettyObjectMapper.readTree(actualResult))) .isEqualTo( - mapper + prettyObjectMapper .writerWithDefaultPrettyPrinter() .withoutAttribute("creationMethod") - .writeValueAsString(mapper.readTree(expectedResult))); + .writeValueAsString(prettyObjectMapper.readTree(expectedResult))); } // Check error diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugGethTraceJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugGethTraceJsonRpcHttpBySpecTest.java index 22c8b94b471..20d6975518a 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugGethTraceJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugGethTraceJsonRpcHttpBySpecTest.java @@ -20,13 +20,11 @@ import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class DebugGethTraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBonsaiBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugJsonRpcHttpBySpecTest.java index e1a13f1a802..8c38f0975be 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugJsonRpcHttpBySpecTest.java @@ -18,14 +18,12 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class DebugJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBonsaiBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugTraceJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugTraceJsonRpcHttpBySpecTest.java index 3d066d14e9e..82cdcde5a59 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugTraceJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/DebugTraceJsonRpcHttpBySpecTest.java @@ -20,14 +20,12 @@ import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class DebugTraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBonsaiBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthByzantiumJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthByzantiumJsonRpcHttpBySpecTest.java index f1b6b695a86..508a724d87c 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthByzantiumJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthByzantiumJsonRpcHttpBySpecTest.java @@ -20,14 +20,12 @@ import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class EthByzantiumJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBonsaiBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthConfigBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthConfigBySpecTest.java index 6566cb4092a..b034159f642 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthConfigBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthConfigBySpecTest.java @@ -20,14 +20,12 @@ import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class EthConfigBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBonsaiBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthJsonRpcHttpBySpecTest.java index 1bb5f801ebc..9e16861fdbc 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthJsonRpcHttpBySpecTest.java @@ -18,14 +18,12 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class EthJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBonsaiBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthSimulateV1BySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthSimulateV1BySpecTest.java index b32e23bfd21..7cc7899c194 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthSimulateV1BySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/EthSimulateV1BySpecTest.java @@ -24,7 +24,6 @@ import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** @@ -34,8 +33,7 @@ public class EthSimulateV1BySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBonsaiBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TestingBuildBlockJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TestingBuildBlockJsonRpcHttpBySpecTest.java index b53d368bce3..fc323a18c38 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TestingBuildBlockJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TestingBuildBlockJsonRpcHttpBySpecTest.java @@ -39,7 +39,6 @@ import java.util.Optional; import org.apache.tuweni.bytes.Bytes32; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class TestingBuildBlockJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @@ -51,8 +50,7 @@ public class TestingBuildBlockJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpB Address.fromHexString("0x627306090abaB3A6e1400e9345bC60c78a8BEf57"); @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { blockchainSetupUtil = getBlockchainSetupUtil(DataStorageFormat.BONSAI); blockchainSetupUtil.importAllBlocks(); startService(); diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TraceJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TraceJsonRpcHttpBySpecTest.java index 51d573ddae7..487fc734de8 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TraceJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TraceJsonRpcHttpBySpecTest.java @@ -20,14 +20,12 @@ import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class TraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBonsaiBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/DebugJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/DebugJsonRpcHttpBySpecTest.java index bc8d4178ed1..fb99d320a23 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/DebugJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/DebugJsonRpcHttpBySpecTest.java @@ -18,14 +18,12 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class DebugJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/DebugTraceJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/DebugTraceJsonRpcHttpBySpecTest.java index d90873fbd2f..058a7d5f654 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/DebugTraceJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/DebugTraceJsonRpcHttpBySpecTest.java @@ -20,14 +20,12 @@ import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class DebugTraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/EthByzantiumJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/EthByzantiumJsonRpcHttpBySpecTest.java index c7c2d131a10..9ce37ea7d47 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/EthByzantiumJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/EthByzantiumJsonRpcHttpBySpecTest.java @@ -20,14 +20,12 @@ import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class EthByzantiumJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/EthJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/EthJsonRpcHttpBySpecTest.java index 041c0c711e1..c64d19b6c5d 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/EthJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/EthJsonRpcHttpBySpecTest.java @@ -18,14 +18,12 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class EthJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBlockchain(); startService(); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/TraceJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/TraceJsonRpcHttpBySpecTest.java index 078d4fdbe1b..9388db1ee21 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/TraceJsonRpcHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/forest/TraceJsonRpcHttpBySpecTest.java @@ -20,7 +20,6 @@ import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -28,8 +27,7 @@ public class TraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override - @BeforeEach - public void setup() throws Exception { + protected void doSetup() throws Exception { setupBlockchain(); startService(); } From 01aa894d966cba0b90b8ffea7b9350e548f5b1da Mon Sep 17 00:00:00 2001 From: Kanchan Kaur <87459628+kkaur01@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:10:15 +1000 Subject: [PATCH 46/77] =?UTF-8?q?Fix=20eth=5FsimulateV1=20to=20accept=20bo?= =?UTF-8?q?th=20input=20and=20data=20fields=20with=20differen=E2=80=A6=20(?= =?UTF-8?q?#9996)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix eth_simulateV1 to accept both input and data fields with different values * Added details of the fix to changelog Signed-off-by: kkaur01 --------- Signed-off-by: kkaur01 Signed-off-by: Kanchan Kaur <87459628+kkaur01@users.noreply.github.com> --- CHANGELOG.md | 2 + .../JsonBlockStateCallParameter.java | 5 +- .../SimulateCallParameterDeserializer.java | 56 +++++++++++++++++++ .../internal/methods/EthSimulateV1Test.java | 37 ++++++++++++ ...idWithDifferentInputAndDataAttributes.json | 2 +- 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SimulateCallParameterDeserializer.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d81f8ea1f..d12ae7cb019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ ### Bug fixes - BFT forks that change block period on time-based forks don't take effect [9681](https://github.com/hyperledger/besu/issues/9681) - Fix QBFT `RLPException` when decoding proposals from pre-26.1.0 nodes that do not include the `blockAccessList` field [#9977](https://github.com/hyperledger/besu/pull/9977) +- Fix eth_simulateV1 discrepancy [9960] (https://github.com/besu-eth/besu/issues/9960) eth_simulateV1 now accepts calls where both input and data +are provided with different values, using input as per the execution-apis spec instead of returning an error. - Wait for peers before starting chain download. Prevents an OutOfMemory (OOM) error when the node has zero peers [#9979](https://github.com/hyperledger/besu/pull/9979) ### Additions and Improvements diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java index 96453fd3e31..3fc928796b0 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java @@ -23,12 +23,15 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @JsonIgnoreProperties(ignoreUnknown = true) public class JsonBlockStateCallParameter extends BlockStateCall { @JsonCreator public JsonBlockStateCallParameter( - @JsonProperty("calls") final List calls, + @JsonProperty("calls") + @JsonDeserialize(contentUsing = SimulateCallParameterDeserializer.class) + final List calls, @JsonProperty("blockOverrides") final BlockOverridesParameter blockOverrides, @JsonProperty("stateOverrides") final StateOverrideMap stateOverrides) { super(calls, blockOverrides, stateOverrides); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SimulateCallParameterDeserializer.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SimulateCallParameterDeserializer.java new file mode 100644 index 00000000000..5dcb08b8afe --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SimulateCallParameterDeserializer.java @@ -0,0 +1,56 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters; + +import org.hyperledger.besu.ethereum.transaction.CallParameter; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Custom deserializer for {@link CallParameter} within eth_simulateV1 calls. + * + *

    When both {@code input} and {@code data} are provided with different values, {@code input} + * takes precedence and {@code data} is ignored. This matches the behaviour of other EL clients + * (Geth, Nethermind, Reth, Erigon) for eth_simulateV1, where only {@code input} is defined in the + * execution-apis spec. + * + *

    For eth_call and other methods, the standard {@link CallParameter} validation still applies + * and an error is returned when the two fields conflict. + */ +public class SimulateCallParameterDeserializer extends StdDeserializer { + + public SimulateCallParameterDeserializer() { + super(CallParameter.class); + } + + @Override + public CallParameter deserialize(final JsonParser p, final DeserializationContext ctxt) + throws IOException { + final ObjectNode node = p.readValueAsTree(); + final JsonNode inputNode = node.get("input"); + final JsonNode dataNode = node.get("data"); + if (inputNode != null && dataNode != null && !inputNode.equals(dataNode)) { + // input takes precedence; remove data so the standard CallParameter check does not fire + node.remove("data"); + } + return p.getCodec().treeToValue(node, CallParameter.class); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSimulateV1Test.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSimulateV1Test.java index 3f79044022a..c5bf2b0725e 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSimulateV1Test.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSimulateV1Test.java @@ -33,6 +33,7 @@ import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.transaction.BlockSimulationParameter; import org.hyperledger.besu.ethereum.transaction.BlockSimulator; import org.hyperledger.besu.ethereum.transaction.TransactionSimulator; import org.hyperledger.besu.ethereum.transaction.exceptions.BlockStateCallError; @@ -40,11 +41,14 @@ import org.hyperledger.besu.evm.precompile.PrecompileContractRegistry; import java.util.List; +import java.util.Map; import java.util.Set; +import org.apache.tuweni.bytes.Bytes; 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.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -135,6 +139,39 @@ public void shouldReturnOriginalErrorCodeWhenUpfrontCostExceedsBalanceWithoutVal .isEqualTo(BlockStateCallError.UPFRONT_COST_EXCEEDS_BALANCE.getCode()); } + @Test + public void shouldNotReturnInvalidParamsWhenInputAndDataHaveDifferentValues() { + setupMethodWithMockSimulator(); + setupBlockchainForLatest(); + when(blockSimulator.process(any(BlockHeader.class), any())).thenReturn(List.of()); + + // Reproduces issue #9960: both input and data provided with different values. + // Other EL clients (Geth, Nethermind, Reth, Erigon) accept this and use input. + final Map callObj = + Map.of( + "from", "0xc000000000000000000000000000000000000000", + "to", "0xd000000000000000000000000000000000000000", + "input", "0xDEADBEEF", + "data", "0xCAFEBABE"); + final Map simulateParam = + Map.of("blockStateCalls", List.of(Map.of("calls", List.of(callObj))), "validation", false); + + final JsonRpcRequestContext request = + new JsonRpcRequestContext( + new JsonRpcRequest("2.0", "eth_simulateV1", new Object[] {simulateParam, "latest"})); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isNotInstanceOf(JsonRpcErrorResponse.class); + + final ArgumentCaptor captor = + ArgumentCaptor.forClass(BlockSimulationParameter.class); + verify(blockSimulator).process(any(BlockHeader.class), captor.capture()); + final Bytes payload = + captor.getValue().getBlockStateCalls().get(0).getCalls().get(0).getPayload().orElseThrow(); + assertThat(payload).isEqualTo(Bytes.fromHexString("0xDEADBEEF")); + } + @Test public void shouldReturnInvalidParamsWhenParameterParsingFails() { setupMethodWithMockSimulator(); diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_invalidWithDifferentInputAndDataAttributes.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_invalidWithDifferentInputAndDataAttributes.json index e304f0cbbd6..39cf7291af8 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_invalidWithDifferentInputAndDataAttributes.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_invalidWithDifferentInputAndDataAttributes.json @@ -22,4 +22,4 @@ } }, "statusCode": 200 -} \ No newline at end of file +} From caa1bd20b3d9ab1d859d511d81cd45abde2d9cb0 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 17 Mar 2026 18:36:52 +1000 Subject: [PATCH 47/77] logging improvements for breach of protocol for rlp (#10038) Signed-off-by: Sally MacFarlane Co-authored-by: Stefan Pingel <16143240+pinges@users.noreply.github.com> --- .../besu/ethereum/eth/manager/EthProtocolManager.java | 6 ++++-- .../besu/ethereum/eth/manager/RequestManager.java | 3 ++- .../besu/ethereum/eth/manager/snap/SnapProtocolManager.java | 6 +++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java index 86143925ca2..c9cc17317c9 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java @@ -315,10 +315,12 @@ public void processMessage(final Capability capability, final Message message) { } } catch (final RLPException e) { LOG.atDebug() - .setMessage("Received malformed message {} (BREACH_OF_PROTOCOL), disconnecting: {}, {}") + .setMessage( + "Received malformed message code={} data={} (BREACH_OF_PROTOCOL), disconnecting: {}") + .addArgument(code) .addArgument(messageData::getData) .addArgument(ethPeer::toString) - .addArgument(e::toString) + .setCause(e) .log(); ethPeer.disconnect( diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/RequestManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/RequestManager.java index 828addc57f2..20d7c31dfdd 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/RequestManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/RequestManager.java @@ -80,7 +80,8 @@ public void dispatchResponse(final EthMessage ethMessage) { () -> peer.recordUselessResponse("Request ID incorrect")); } catch (final RLPException e) { LOG.debug( - "Received malformed message {} (BREACH_OF_PROTOCOL), disconnecting: {}", + "Received malformed message code={} data={} (BREACH_OF_PROTOCOL), disconnecting: {}", + ethMessage.getData().getCode(), ethMessage.getData(), peer, e); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManager.java index cd805983460..0b42c35a578 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManager.java @@ -127,7 +127,11 @@ public void processMessage(final Capability cap, final Message message) { .map(responseData -> responseData.wrapMessageData(requestIdAndEthMessage.getKey())); } catch (final RLPException e) { LOG.debug( - "Received malformed message {} , disconnecting: {}", messageData.getData(), ethPeer, e); + "Received malformed message code={} data={} (BREACH_OF_PROTOCOL), disconnecting: {}", + messageData.getCode(), + messageData.getData(), + ethPeer, + e); ethPeer.disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); } maybeResponseData.ifPresent( From 93ff415381b2622a7cdcaec59c3462dbdf892bad Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Tue, 17 Mar 2026 11:47:10 +0100 Subject: [PATCH 48/77] Plugin API: pass pending block header when creating selectors (#10034) Signed-off-by: Fabio Di Fabio --- CHANGELOG.md | 3 +++ .../AbstractTestTransactionSelectorPlugin.java | 1 + .../acceptance/plugins/TestBundlePlugin.java | 1 + .../TransactionSelectionServiceImpl.java | 5 ++++- .../blockcreation/AbstractBlockCreator.java | 2 +- .../AbstractBlockTransactionSelectorTest.java | 17 +++++++++++------ .../besu/ethereum/core/MiningConfiguration.java | 1 + plugin-api/build.gradle | 2 +- .../services/TransactionSelectionService.java | 3 ++- .../PluginTransactionSelectorFactory.java | 17 +++++++++++++++++ 10 files changed, 42 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d12ae7cb019..a68b742a8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ - Holesky network is deprecated [#9437](https://github.com/hyperledger/besu/pull/9437) - Sunsetting features - for more context on the reasoning behind the deprecation of these features, including alternative options, read [this blog post](https://www.lfdecentralizedtrust.org/blog/sunsetting-tessera-and-simplifying-hyperledger-besu) - Proof of Work consensus (PoW) +- Plugin API + - `PluginTransactionSelectorFactory.create(final SelectorsStateManager selectorsStateManager)` is deprecated for removal ### Bug fixes - BFT forks that change block period on time-based forks don't take effect [9681](https://github.com/hyperledger/besu/issues/9681) @@ -39,6 +41,7 @@ are provided with different values, using input as per the execution-apis spec i - Implement `txpool_status` RPC method [#10002](https://github.com/hyperledger/besu/pull/10002) - Support [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975): eth/70 - partial block receipt lists - Limit pooled tx requests by size and remove pre-eth/68 transaction announcement support [#9990](https://github.com/besu-eth/besu/pull/9990) +- Plugin API: pass pending block header when creating selectors [#10034](https://github.com/besu-eth/besu/pull/10034) ## 26.2.0 diff --git a/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/AbstractTestTransactionSelectorPlugin.java b/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/AbstractTestTransactionSelectorPlugin.java index ea451b858a6..9722c49ee78 100644 --- a/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/AbstractTestTransactionSelectorPlugin.java +++ b/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/AbstractTestTransactionSelectorPlugin.java @@ -131,6 +131,7 @@ public void selectPendingTransactions( @Override public PluginTransactionSelector create( + final ProcessableBlockHeader pendingBlockHeader, final SelectorsStateManager selectorsStateManager) { return new PluginTransactionSelector() { diff --git a/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestBundlePlugin.java b/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestBundlePlugin.java index 4bf96175248..e2b8ff8290c 100644 --- a/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestBundlePlugin.java +++ b/acceptance-tests/detached-test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestBundlePlugin.java @@ -130,6 +130,7 @@ public void selectPendingTransactions( @Override public PluginTransactionSelector create( + final ProcessableBlockHeader pendingBlockHeader, final SelectorsStateManager selectorsStateManager) { return new PluginTransactionSelector() { diff --git a/app/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java b/app/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java index 240cc444f89..c67e2b8e0f0 100644 --- a/app/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java +++ b/app/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java @@ -53,12 +53,15 @@ public TransactionSelectionServiceImpl() {} @Override public PluginTransactionSelector createPluginTransactionSelector( + final ProcessableBlockHeader processableBlockHeader, final SelectorsStateManager selectorsStateManager) { if (factories == null) { return PluginTransactionSelector.ACCEPT_ALL; } return new AggregatedPluginTransactionSelector( - factories.stream().map(factory -> factory.create(selectorsStateManager)).toList()); + factories.stream() + .map(factory -> factory.create(processableBlockHeader, selectorsStateManager)) + .toList()); } @Override diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java index 571bf0faa6c..0a042f5bcfd 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java @@ -222,7 +222,7 @@ public BlockCreationResult createBlock( final var pluginTransactionSelector = miningConfiguration .getTransactionSelectionService() - .createPluginTransactionSelector(selectorsStateManager); + .createPluginTransactionSelector(processableBlockHeader, selectorsStateManager); final var operationTracer = pluginTransactionSelector.getOperationTracer(); operationTracer.traceStartBlock( disposableWorldState, processableBlockHeader, miningBeneficiary); diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java index d43b4706b84..8ee1e819a8b 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java @@ -706,6 +706,7 @@ public TransactionSelectionResult evaluateTransactionPostProcessing( new PluginTransactionSelectorFactory() { @Override public PluginTransactionSelector create( + final org.hyperledger.besu.plugin.data.ProcessableBlockHeader pendingBlockHeader, final SelectorsStateManager selectorsStateManager) { return pluginTransactionSelector; } @@ -717,6 +718,7 @@ public PluginTransactionSelector create( new PluginTransactionSelectorFactory() { @Override public PluginTransactionSelector create( + final org.hyperledger.besu.plugin.data.ProcessableBlockHeader pendingBlockHeader, final SelectorsStateManager selectorsStateManager) { return colletorPluginTransactionSelector; } @@ -797,6 +799,7 @@ public TransactionSelectionResult evaluateTransactionPostProcessing( new PluginTransactionSelectorFactory() { @Override public PluginTransactionSelector create( + final org.hyperledger.besu.plugin.data.ProcessableBlockHeader pendingBlockHeader, final SelectorsStateManager selectorsStateManager) { return pluginTransactionSelector; } @@ -808,6 +811,7 @@ public PluginTransactionSelector create( new PluginTransactionSelectorFactory() { @Override public PluginTransactionSelector create( + final org.hyperledger.besu.plugin.data.ProcessableBlockHeader pendingBlockHeader, final SelectorsStateManager selectorsStateManager) { return colletorPluginTransactionSelector; } @@ -865,7 +869,7 @@ record Mocks( when(transactionSelector.evaluateTransactionPreProcessing(any())).thenReturn(SELECTED); when(transactionSelector.evaluateTransactionPostProcessing(any(), any())) .thenReturn(SELECTED); - when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector); + when(transactionSelectorFactory.create(any(), any())).thenReturn(transactionSelector); return new Mocks(transactionSelectorFactory, transactionSelector); }; @@ -1163,7 +1167,7 @@ public void txEvaluationContextIsCancelledReturnsTrueOnTimeout() { final PluginTransactionSelectorFactory transactionSelectorFactory = mock(PluginTransactionSelectorFactory.class); - when(transactionSelectorFactory.create(any())) + when(transactionSelectorFactory.create(any(), any())) .thenReturn( new PluginTransactionSelector() { @Override @@ -1224,7 +1228,7 @@ public void txEvaluationContextIsCancelledReturnsTrueOnCancellation() { final AtomicReference selector = new AtomicReference<>(); final PluginTransactionSelectorFactory transactionSelectorFactory = mock(PluginTransactionSelectorFactory.class); - when(transactionSelectorFactory.create(any())) + when(transactionSelectorFactory.create(any(), any())) .thenReturn( new PluginTransactionSelector() { @Override @@ -1331,7 +1335,7 @@ private void internalBlockSelectionTimeoutSimulation( final PluginTransactionSelectorFactory transactionSelectorFactory = mock(PluginTransactionSelectorFactory.class); - when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector); + when(transactionSelectorFactory.create(any(), any())).thenReturn(transactionSelector); transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -1495,7 +1499,7 @@ private void internalBlockSelectionTimeoutSimulationInvalidTxs( final PluginTransactionSelectorFactory transactionSelectorFactory = mock(PluginTransactionSelectorFactory.class); - when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector); + when(transactionSelectorFactory.create(any(), any())).thenReturn(transactionSelector); transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -1595,7 +1599,8 @@ protected BlockTransactionSelector createBlockSelector( miningBeneficiary, blobGasPrice, protocolSpec, - transactionSelectionService.createPluginTransactionSelector(selectorsStateManager), + transactionSelectionService.createPluginTransactionSelector( + blockHeader, selectorsStateManager), ethScheduler, selectorsStateManager, Optional.empty()); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java index d4f2646b07e..546775728a7 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java @@ -187,6 +187,7 @@ public TransactionSelectionService getTransactionSelectionService() { return new TransactionSelectionService() { @Override public PluginTransactionSelector createPluginTransactionSelector( + final ProcessableBlockHeader pendingBlockHeader, final SelectorsStateManager selectorsStateManager) { return PluginTransactionSelector.ACCEPT_ALL; } diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index ce21a880080..550bd6731e8 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -71,7 +71,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = '0EGHYlomDjcBMwQmKy9qhqGaLhcIxkWUthAGdYSTxNc=' + knownHash = '6hDXxhwrNO1YWmdkQKoG89kPWcS8j3CBjPeoePbjms0=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java index 35181372f0d..b8da13ead6d 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java @@ -31,11 +31,12 @@ public interface TransactionSelectionService extends BesuService { /** * Create a transaction selector plugin * + * @param pendingBlockHeader the header of the block being created * @param selectorsStateManager the selectors state manager * @return the transaction selector plugin */ PluginTransactionSelector createPluginTransactionSelector( - SelectorsStateManager selectorsStateManager); + ProcessableBlockHeader pendingBlockHeader, SelectorsStateManager selectorsStateManager); /** * Called during the block creation to allow plugins to propose their own pending transactions for diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java index 4bcdb9f1473..cb8106706aa 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java @@ -32,11 +32,28 @@ public interface PluginTransactionSelectorFactory { * * @param selectorsStateManager the selectors state manager * @return the transaction selector + * @deprecated use {@link PluginTransactionSelectorFactory#create(ProcessableBlockHeader, + * SelectorsStateManager)} instead */ + @Deprecated(forRemoval = true) default PluginTransactionSelector create(final SelectorsStateManager selectorsStateManager) { return PluginTransactionSelector.ACCEPT_ALL; } + /** + * Create a plugin transaction selector, that can be used during block creation to apply custom + * filters to proposed pending transactions + * + * @param pendingBlockHeader the header of the block being created + * @param selectorsStateManager the selectors state manager + * @return the transaction selector + */ + default PluginTransactionSelector create( + final ProcessableBlockHeader pendingBlockHeader, + final SelectorsStateManager selectorsStateManager) { + return create(selectorsStateManager); + } + /** * Called during the block creation to allow plugins to propose their own pending transactions for * block inclusion From 7124710442378560e1e315262ea144eab153bdef Mon Sep 17 00:00:00 2001 From: ahamlat Date: Tue, 17 Mar 2026 12:49:11 +0100 Subject: [PATCH 49/77] Use cache locality to improve Shift opcodes (#9878) * Use CPU cache locality and avoid on-CPU cache misses Signed-off-by: Ameziane H. Signed-off-by: ahamlat --- CHANGELOG.md | 4 + .../evm/operation/SarOperationOptimized.java | 65 ++++++++++------ .../evm/operation/Shift256Operations.java | 44 +++++++++++ .../evm/operation/ShlOperationOptimized.java | 78 ++++++++++++++----- .../evm/operation/ShrOperationOptimized.java | 59 ++++++++++---- 5 files changed, 190 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a68b742a8c1..466628ede96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,11 @@ are provided with different values, using input as per the execution-apis spec i - Implement `txpool_status` RPC method [#10002](https://github.com/hyperledger/besu/pull/10002) - Support [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975): eth/70 - partial block receipt lists - Limit pooled tx requests by size and remove pre-eth/68 transaction announcement support [#9990](https://github.com/besu-eth/besu/pull/9990) +<<<<<<< optimize/register-based-shift +- Use cache locality to improve Shift opcodes [#9878](https://github.com/besu-eth/besu/pull/9878) +======= - Plugin API: pass pending block header when creating selectors [#10034](https://github.com/besu-eth/besu/pull/10034) +>>>>>>> main ## 26.2.0 diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/SarOperationOptimized.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/SarOperationOptimized.java index adbea0cb47a..70815987f84 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/SarOperationOptimized.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/SarOperationOptimized.java @@ -16,7 +16,10 @@ import static org.hyperledger.besu.evm.operation.Shift256Operations.ALL_ONES; import static org.hyperledger.besu.evm.operation.Shift256Operations.ALL_ONES_BYTES; +import static org.hyperledger.besu.evm.operation.Shift256Operations.getLong; import static org.hyperledger.besu.evm.operation.Shift256Operations.isShiftOverflow; +import static org.hyperledger.besu.evm.operation.Shift256Operations.putLong; +import static org.hyperledger.besu.evm.operation.Shift256Operations.shiftRight; import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.frame.MessageFrame; @@ -94,30 +97,46 @@ public static OperationResult staticOperation(final MessageFrame frame) { */ private static Bytes sar256(final byte[] in, final int shift, final boolean negative) { if (shift == 0) return Bytes.wrap(in); - - final int shiftBytes = shift >>> 3; // /8 - final int shiftBits = shift & 7; // %8 - final int fill = negative ? 0xFF : 0x00; - - final byte[] out = new byte[32]; - - // Pre-fill sign-extended bytes (indices below shiftBytes are fully sign-extended) - if (negative && shiftBytes > 0) { - Arrays.fill(out, 0, shiftBytes, (byte) 0xFF); + long w0 = getLong(in, 0); + long w1 = getLong(in, 8); + long w2 = getLong(in, 16); + long w3 = getLong(in, 24); + final long fill = negative ? -1L : 0L; + // Number of whole 64-bit words to shift (shift / 64). + final int wordShift = shift >>> 6; + // Remaining intra-word bit shift (shift % 64). + final int bitShift = shift & 63; + switch (wordShift) { + case 0: + w3 = shiftRight(w3, w2, bitShift); + w2 = shiftRight(w2, w1, bitShift); + w1 = shiftRight(w1, w0, bitShift); + w0 = shiftRight(w0, fill, bitShift); + break; + case 1: + w3 = shiftRight(w2, w1, bitShift); + w2 = shiftRight(w1, w0, bitShift); + w1 = shiftRight(w0, fill, bitShift); + w0 = fill; + break; + case 2: + w3 = shiftRight(w1, w0, bitShift); + w2 = shiftRight(w0, fill, bitShift); + w1 = fill; + w0 = fill; + break; + case 3: + w3 = shiftRight(w0, fill, bitShift); + w2 = fill; + w1 = fill; + w0 = fill; + break; } - - // Only iterate bytes that receive shifted data from the input - for (int i = 31; i >= shiftBytes; i--) { - final int srcIndex = i - shiftBytes; - final int curr = in[srcIndex] & 0xFF; - if (shiftBits == 0) { - out[i] = (byte) curr; - } else { - final int prev = (srcIndex - 1 >= 0) ? (in[srcIndex - 1] & 0xFF) : fill; - out[i] = (byte) ((curr >>> shiftBits) | (prev << (8 - shiftBits))); - } - } - + final byte[] out = new byte[32]; + putLong(out, 0, w0); + putLong(out, 8, w1); + putLong(out, 16, w2); + putLong(out, 24, w3); return Bytes.wrap(out); } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/Shift256Operations.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/Shift256Operations.java index edfdab6be03..8e4075bb3b7 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/Shift256Operations.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/Shift256Operations.java @@ -14,6 +14,9 @@ */ package org.hyperledger.besu.evm.operation; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; import java.util.Arrays; import org.apache.tuweni.bytes.Bytes; @@ -24,6 +27,9 @@ */ public final class Shift256Operations { + private static final VarHandle LONG_HANDLE = + MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.BIG_ENDIAN); + /** An array of 31 0 bytes */ private static final byte[] ZERO_31 = new byte[31]; @@ -50,4 +56,42 @@ public static boolean isShiftOverflow(final byte[] shiftBytes) { if (len <= 0) return false; return !Arrays.equals(shiftBytes, 0, len, ZERO_31, 0, len); } + + /** + * Reads a big-endian long from a byte array at the given offset. + * + * @param arr the byte array (must have at least offset + 8 bytes) + * @param offset the byte offset to read from + * @return the long value + */ + static long getLong(final byte[] arr, final int offset) { + return (long) LONG_HANDLE.get(arr, offset); + } + + /** + * Writes a big-endian long to a byte array at the given offset. + * + * @param arr the byte array (must have at least offset + 8 bytes) + * @param offset the byte offset to write to + * @param value the long value to write + */ + static void putLong(final byte[] arr, final int offset, final long value) { + LONG_HANDLE.set(arr, offset, value); + } + + /** + * Shifts a 64-bit word right and carries in bits from the previous more-significant word. + * + *

    The {@code bitShift == 0} fast path avoids Java long-shift masking, where a shift by 64 is + * treated as a shift by 0. + * + * @param value the current word + * @param prevValue the previous more-significant word + * @param bitShift the intra-word shift amount in the range {@code [0..63]} + * @return the shifted word + */ + static long shiftRight(final long value, final long prevValue, final int bitShift) { + if (bitShift == 0) return value; + return (value >>> bitShift) | (prevValue << (64 - bitShift)); + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/ShlOperationOptimized.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/ShlOperationOptimized.java index a691636e060..0b9804ae6ee 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/ShlOperationOptimized.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/ShlOperationOptimized.java @@ -14,7 +14,9 @@ */ package org.hyperledger.besu.evm.operation; +import static org.hyperledger.besu.evm.operation.Shift256Operations.getLong; import static org.hyperledger.besu.evm.operation.Shift256Operations.isShiftOverflow; +import static org.hyperledger.besu.evm.operation.Shift256Operations.putLong; import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.frame.MessageFrame; @@ -89,29 +91,63 @@ public static OperationResult staticOperation(final MessageFrame frame) { * @return the shifted 256-bit value */ private static Bytes shl256(final byte[] in, final int shift) { - if (shift == 0) { - return Bytes.wrap(in); + if (shift == 0) return Bytes.wrap(in); + long w0 = getLong(in, 0); + long w1 = getLong(in, 8); + long w2 = getLong(in, 16); + long w3 = getLong(in, 24); + + // Number of whole 64-bit words to shift (shift / 64). + final int wordShift = shift >>> 6; + // Remaining intra-word bit shift (shift % 64). + final int bitShift = shift & 63; + switch (wordShift) { + case 0: + w0 = shiftLeft(w0, w1, bitShift); + w1 = shiftLeft(w1, w2, bitShift); + w2 = shiftLeft(w2, w3, bitShift); + w3 = shiftLeft(w3, 0, bitShift); + break; + case 1: + w0 = shiftLeft(w1, w2, bitShift); + w1 = shiftLeft(w2, w3, bitShift); + w2 = shiftLeft(w3, 0, bitShift); + w3 = 0; + break; + case 2: + w0 = shiftLeft(w2, w3, bitShift); + w1 = shiftLeft(w3, 0, bitShift); + w2 = 0; + w3 = 0; + break; + case 3: + w0 = shiftLeft(w3, 0, bitShift); + w1 = 0; + w2 = 0; + w3 = 0; + break; } - - final int shiftBytes = shift >>> 3; // /8 - final int shiftBits = shift & 7; // %8 - final byte[] out = new byte[32]; - - // Shift left: bytes move to lower indices (towards index 0) - // Bytes at index >= (32 - shiftBytes) are guaranteed zero (already from new byte[32]) - final int limit = 32 - shiftBytes; - for (int i = 0; i < limit; i++) { - final int srcIndex = i + shiftBytes; - final int curr = in[srcIndex] & 0xFF; - if (shiftBits == 0) { - out[i] = (byte) curr; - } else { - final int next = (srcIndex + 1 < 32) ? (in[srcIndex + 1] & 0xFF) : 0; - out[i] = (byte) ((curr << shiftBits) | (next >>> (8 - shiftBits))); - } - } - + putLong(out, 0, w0); + putLong(out, 8, w1); + putLong(out, 16, w2); + putLong(out, 24, w3); return Bytes.wrap(out); } + + /** + * Shifts a 64-bit word left and carries in bits from the next less-significant word. + * + *

    The {@code bitShift == 0} fast path avoids Java long-shift masking, where a shift by 64 is + * treated as a shift by 0. + * + * @param value the current word + * @param nextValue the next less-significant word + * @param bitShift the intra-word shift amount in the range {@code [0..63]} + * @return the shifted word + */ + private static long shiftLeft(final long value, final long nextValue, final int bitShift) { + if (bitShift == 0) return value; + return (value << bitShift) | (nextValue >>> (64 - bitShift)); + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/ShrOperationOptimized.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/ShrOperationOptimized.java index 1eeb1b1ca5c..e32bd22b875 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/ShrOperationOptimized.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/ShrOperationOptimized.java @@ -14,7 +14,10 @@ */ package org.hyperledger.besu.evm.operation; +import static org.hyperledger.besu.evm.operation.Shift256Operations.getLong; import static org.hyperledger.besu.evm.operation.Shift256Operations.isShiftOverflow; +import static org.hyperledger.besu.evm.operation.Shift256Operations.putLong; +import static org.hyperledger.besu.evm.operation.Shift256Operations.shiftRight; import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.frame.MessageFrame; @@ -93,24 +96,48 @@ private static Bytes shr256(final byte[] in, final int shift) { return Bytes.wrap(in); } - final int shiftBytes = shift >>> 3; // /8 - final int shiftBits = shift & 7; // %8 - - final byte[] out = new byte[32]; - - // Shift right: bytes move to higher indices (towards index 31) - // Bytes below shiftBytes are guaranteed zero (already from new byte[32]) - for (int i = 31; i >= shiftBytes; i--) { - final int srcIndex = i - shiftBytes; - final int curr = in[srcIndex] & 0xFF; - if (shiftBits == 0) { - out[i] = (byte) curr; - } else { - final int prev = (srcIndex - 1 >= 0) ? (in[srcIndex - 1] & 0xFF) : 0; - out[i] = (byte) ((curr >>> shiftBits) | (prev << (8 - shiftBits))); - } + long w0 = getLong(in, 0); + long w1 = getLong(in, 8); + long w2 = getLong(in, 16); + long w3 = getLong(in, 24); + + // Number of whole 64-bit words to shift (shift / 64). + final int wordShift = shift >>> 6; + // Remaining intra-word bit shift (shift % 64). + final int bitShift = shift & 63; + + switch (wordShift) { + case 0: + w3 = shiftRight(w3, w2, bitShift); + w2 = shiftRight(w2, w1, bitShift); + w1 = shiftRight(w1, w0, bitShift); + w0 = shiftRight(w0, 0, bitShift); + break; + case 1: + w3 = shiftRight(w2, w1, bitShift); + w2 = shiftRight(w1, w0, bitShift); + w1 = shiftRight(w0, 0, bitShift); + w0 = 0; + break; + case 2: + w3 = shiftRight(w1, w0, bitShift); + w2 = shiftRight(w0, 0, bitShift); + w1 = 0; + w0 = 0; + break; + case 3: + w3 = shiftRight(w0, 0, bitShift); + w2 = 0; + w1 = 0; + w0 = 0; + break; } + final byte[] out = new byte[32]; + putLong(out, 0, w0); + putLong(out, 8, w1); + putLong(out, 16, w2); + putLong(out, 24, w3); return Bytes.wrap(out); } } From 9bf59b531f97d4310672fea8e613b2a85785d99a Mon Sep 17 00:00:00 2001 From: Karim Taam Date: Tue, 17 Mar 2026 18:29:18 +0400 Subject: [PATCH 50/77] refactor stateroot commiter (#9879) Signed-off-by: Karim Taam --- .../cli/options/BalConfigurationOptions.java | 7 - .../methods/EthGetTransactionReceiptTest.java | 6 +- .../TestingBuildBlockIntegrationTest.java | 3 +- .../besu/ethereum/core/MutableWorldState.java | 13 +- .../mainnet/AbstractBlockProcessor.java | 8 +- .../ethereum/mainnet/BalConfiguration.java | 6 - .../mainnet/BlockProcessingMetrics.java | 42 +--- .../mainnet/MainnetProtocolSpecs.java | 4 +- .../ethereum/mainnet/ProtocolSpecBuilder.java | 4 +- .../block/access/list/BlockAccessList.java | 8 + .../BalStateRootCommitterFactory.java | 222 ++++++++++++++++++ ... => DefaultStateRootCommitterFactory.java} | 6 +- .../StateRootCommitter.java | 57 ++++- .../StateRootCommitterFactory.java | 2 +- .../StateRootCommitterFactoryBal.java | 58 ----- .../StateRootCommitterImplBal.java | 200 ---------------- .../StateRootCommitterImplSync.java | 33 --- .../worldview/ForestMutableWorldState.java | 19 +- .../worldview/BalStateRootCalculator.java | 123 ++++++++++ ...lockAccessListStateRootHashCalculator.java | 143 ----------- .../bonsai/worldview/BonsaiWorldState.java | 47 ++-- .../common/worldview/PathBasedWorldState.java | 71 +++--- .../PathBasedWorldStateUpdateAccumulator.java | 147 ++++++------ .../BlockImportExceptionHandlingTest.java | 4 +- .../besu/ethereum/core/SyncBlockBodyTest.java | 4 +- ...AbstractBlockProcessorIntegrationTest.java | 5 +- .../mainnet/AbstractBlockProcessorTest.java | 4 +- .../mainnet/MainnetBlockProcessorTest.java | 4 +- ... => BalStateRootCommitterFactoryTest.java} | 86 ++----- ...t.java => BalStateRootCalculatorTest.java} | 4 +- 30 files changed, 585 insertions(+), 755 deletions(-) create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/BalStateRootCommitterFactory.java rename ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/{StateRootCommitterFactoryDefault.java => DefaultStateRootCommitterFactory.java} (86%) delete mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactoryBal.java delete mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplBal.java delete mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplSync.java create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BalStateRootCalculator.java delete mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BlockAccessListStateRootHashCalculator.java rename ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/{StateRootCommitterImplBalTest.java => BalStateRootCommitterFactoryTest.java} (90%) rename ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/{BlockAccessListStateRootHashCalculatorTest.java => BalStateRootCalculatorTest.java} (98%) diff --git a/app/src/main/java/org/hyperledger/besu/cli/options/BalConfigurationOptions.java b/app/src/main/java/org/hyperledger/besu/cli/options/BalConfigurationOptions.java index 979ebde4c7e..b6cd1ef0605 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/options/BalConfigurationOptions.java +++ b/app/src/main/java/org/hyperledger/besu/cli/options/BalConfigurationOptions.java @@ -26,12 +26,6 @@ public class BalConfigurationOptions { /** Default constructor. */ public BalConfigurationOptions() {} - @CommandLine.Option( - names = {"--Xbal-optimization-enabled"}, - hidden = true, - description = "Allows disabling BAL-based optimizations.") - boolean balOptimizationEnabled = true; - @CommandLine.Option( names = {"--Xbal-perfect-parallelization-enabled"}, hidden = true, @@ -79,7 +73,6 @@ public BalConfigurationOptions() {} */ public BalConfiguration toDomainObject() { return ImmutableBalConfiguration.builder() - .isBalOptimisationEnabled(balOptimizationEnabled) .isPerfectParallelizationEnabled(balPerfectParallelizationEnabled) .shouldLogBalsOnMismatch(balLogBalsOnMismatch) .isBalLenientOnStateRootMismatch(balLenientOnStateRootMismatch) diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java index 25ab9cca84b..516379730a9 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java @@ -53,7 +53,7 @@ import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.blockhash.FrontierPreExecutionProcessor; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterFactoryDefault; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.DefaultStateRootCommitterFactory; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.evm.gascalculator.CancunGasCalculator; @@ -178,7 +178,7 @@ public String description() { true, Optional.empty(), Optional.empty(), - new StateRootCommitterFactoryDefault(), + new DefaultStateRootCommitterFactory(), BlockGasAccountingStrategy.FRONTIER, BlockGasUsedValidator.FRONTIER); private final ProtocolSpec statusTransactionTypeSpec = @@ -214,7 +214,7 @@ public String description() { true, Optional.empty(), Optional.empty(), - new StateRootCommitterFactoryDefault(), + new DefaultStateRootCommitterFactory(), BlockGasAccountingStrategy.FRONTIER, BlockGasUsedValidator.FRONTIER); diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java index 01c0b7c46ba..a53972c03fe 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java @@ -391,7 +391,8 @@ private TestContext createTestContext(final boolean withBAL) { new BadBlockManager(), false, ImmutableBalConfiguration.builder() - .isBalOptimisationEnabled(withBAL) + .isBalStateRootTrusted(withBAL) + .isPerfectParallelizationEnabled(withBAL) .build(), new NoOpMetricsSystem()) .createProtocolSchedule()) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MutableWorldState.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MutableWorldState.java index 0da6c7e4c19..6961c6542b5 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MutableWorldState.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MutableWorldState.java @@ -14,11 +14,7 @@ */ package org.hyperledger.besu.ethereum.core; -import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitter; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterImplSync; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig; -import org.hyperledger.besu.ethereum.worldstate.WorldStateKeyValueStorage; import org.hyperledger.besu.evm.worldstate.MutableWorldView; import org.hyperledger.besu.evm.worldstate.WorldState; import org.hyperledger.besu.plugin.data.BlockHeader; @@ -37,14 +33,7 @@ public interface MutableWorldState extends WorldState, MutableWorldView { void persist(BlockHeader blockHeader, StateRootCommitter committer); default void persist(final BlockHeader blockHeader) { - persist(blockHeader, new StateRootCommitterImplSync()); - } - - default Hash calculateOrReadRootHash( - final WorldStateKeyValueStorage.Updater stateUpdater, - final BlockHeader blockHeader, - final WorldStateConfig cfg) { - throw new UnsupportedOperationException("calculateOrReadRootHash is not supported"); + persist(blockHeader, StateRootCommitter.SYNCHRONOUS); } default MutableWorldState freezeStorage() { diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java index 2801936e11c..e9f93121f01 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java @@ -239,10 +239,10 @@ public BlockProcessingResult processBlock( blockTracer.traceStartBlock(worldState, blockHeader, miningBeneficiary); final StateRootCommitter stateRootCommitter = - blockProcessingMetrics.wrapStateRootCommitter( - protocolSpec - .getStateRootCommitterFactory() - .forBlock(protocolContext, blockHeader, blockAccessList)); + protocolSpec + .getStateRootCommitterFactory() + .forBlock(protocolContext, blockHeader, blockAccessList) + .timed(blockProcessingMetrics.stateRootCalculationTimer()); final Optional blockAccessListBuilder = protocolSpec diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BalConfiguration.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BalConfiguration.java index a6d8800bd80..fb5cfc0f536 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BalConfiguration.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BalConfiguration.java @@ -24,12 +24,6 @@ public interface BalConfiguration { BalConfiguration DEFAULT = ImmutableBalConfiguration.builder().build(); - /** Returns whether BAL-based optimisations should be disabled entirely. */ - @Value.Default - default boolean isBalOptimisationEnabled() { - return true; - } - /** Returns whether the BAL-computed state root should be trusted without verification. */ @Value.Default default boolean isBalStateRootTrusted() { diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockProcessingMetrics.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockProcessingMetrics.java index d99f764ff6c..65116446b37 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockProcessingMetrics.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockProcessingMetrics.java @@ -14,15 +14,9 @@ */ package org.hyperledger.besu.ethereum.mainnet; -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.ethereum.core.MutableWorldState; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.AccountChanges; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitter; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig; -import org.hyperledger.besu.ethereum.worldstate.WorldStateKeyValueStorage; import org.hyperledger.besu.metrics.BesuMetricCategory; -import org.hyperledger.besu.plugin.data.BlockHeader; import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.metrics.Counter; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; @@ -76,8 +70,8 @@ public BlockProcessingMetrics(final MetricsSystem metricsSystem) { "Time taken by state root calculation"); } - public StateRootCommitter wrapStateRootCommitter(final StateRootCommitter wrapped) { - return new TimedStateRootCommitter(wrapped, stateRootCalculationTimer); + public OperationTimer stateRootCalculationTimer() { + return stateRootCalculationTimer; } public void recordBlockAccessListMetrics(final BlockAccessList bal) { @@ -114,36 +108,6 @@ private long countUpdatedStorageSlots(final BlockAccessList bal) { } private boolean hasAnyChange(final AccountChanges accountChanges) { - return !accountChanges.balanceChanges().isEmpty() - || !accountChanges.nonceChanges().isEmpty() - || !accountChanges.codeChanges().isEmpty() - || !accountChanges.storageChanges().isEmpty(); - } - - private static class TimedStateRootCommitter implements StateRootCommitter { - private final StateRootCommitter wrapped; - private final OperationTimer operationTimer; - - private TimedStateRootCommitter( - final StateRootCommitter wrapped, final OperationTimer operationTimer) { - this.wrapped = wrapped; - this.operationTimer = operationTimer; - } - - @Override - public Hash computeRootAndCommit( - final MutableWorldState worldState, - final WorldStateKeyValueStorage.Updater stateUpdater, - final BlockHeader blockHeader, - final WorldStateConfig cfg) { - try (var timing = operationTimer.startTimer()) { - return wrapped.computeRootAndCommit(worldState, stateUpdater, blockHeader, cfg); - } - } - - @Override - public void cancel() { - wrapped.cancel(); - } + return accountChanges.hasAnyChange(); } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java index 0545264c05e..0eadce9189d 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java @@ -77,7 +77,7 @@ import org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator; import org.hyperledger.besu.ethereum.mainnet.requests.RequestContractAddresses; import org.hyperledger.besu.ethereum.mainnet.requests.RequestProcessorCoordinator; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterFactoryBal; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.BalStateRootCommitterFactory; import org.hyperledger.besu.ethereum.mainnet.transactionpool.OsakaTransactionPoolPreProcessor; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.evm.MainnetEVMs; @@ -1224,7 +1224,7 @@ static ProtocolSpecBuilder amsterdamDefinition( .build()) .blockAccessListFactory(new BlockAccessListFactory()) .blockAccessListValidatorBuilder(MainnetBlockAccessListValidator::create) - .stateRootCommitterFactory(new StateRootCommitterFactoryBal(balConfiguration)) + .stateRootCommitterFactory(new BalStateRootCommitterFactory(balConfiguration)) // EIP-8037: Disable validation-time TX_MAX_GAS_LIMIT cap (enforced at runtime on regular // gas) .gasLimitCalculatorBuilder( diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpecBuilder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpecBuilder.java index ebfa2ec0bc5..908f63b0c06 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpecBuilder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpecBuilder.java @@ -30,8 +30,8 @@ import org.hyperledger.besu.ethereum.mainnet.requests.ProhibitedRequestValidator; import org.hyperledger.besu.ethereum.mainnet.requests.RequestProcessorCoordinator; import org.hyperledger.besu.ethereum.mainnet.requests.RequestsValidator; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.DefaultStateRootCommitterFactory; import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterFactory; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterFactoryDefault; import org.hyperledger.besu.ethereum.mainnet.transactionpool.TransactionPoolPreProcessor; import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.gascalculator.GasCalculator; @@ -98,7 +98,7 @@ public class ProtocolSpecBuilder { private TransactionPoolPreProcessor transactionPoolPreProcessor; private BlockAccessListFactory blockAccessListFactory; private StateRootCommitterFactory stateRootCommitterFactory = - new StateRootCommitterFactoryDefault(); + new DefaultStateRootCommitterFactory(); private BalConfiguration balConfiguration = BalConfiguration.DEFAULT; private BlockGasAccountingStrategy blockGasAccountingStrategy = BlockGasAccountingStrategy.FRONTIER; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessList.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessList.java index 4de3dab39d2..5adc80fb5fe 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessList.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessList.java @@ -117,6 +117,14 @@ public record AccountChanges( List balanceChanges, List nonceChanges, List codeChanges) { + + public boolean hasAnyChange() { + return !balanceChanges.isEmpty() + || !nonceChanges.isEmpty() + || !codeChanges.isEmpty() + || !storageChanges.isEmpty(); + } + @Override public String toString() { return "AccountChanges{" diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/BalStateRootCommitterFactory.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/BalStateRootCommitterFactory.java new file mode 100644 index 00000000000..394064fb57f --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/BalStateRootCommitterFactory.java @@ -0,0 +1,222 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.mainnet.staterootcommitter; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.trie.forest.ForestWorldStateArchive; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BalStateRootCalculator; +import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.PathBasedWorldStateProvider; +import org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedLayeredWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.PathBasedWorldState; +import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.accumulator.PathBasedWorldStateUpdateAccumulator; +import org.hyperledger.besu.ethereum.worldstate.WorldStateKeyValueStorage; +import org.hyperledger.besu.plugin.data.BlockHeader; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public final class BalStateRootCommitterFactory implements StateRootCommitterFactory { + + private static final Logger LOG = LoggerFactory.getLogger(BalStateRootCommitterFactory.class); + + private final BalConfiguration balConfiguration; + + public BalStateRootCommitterFactory(final BalConfiguration balConfiguration) { + this.balConfiguration = balConfiguration; + } + + @Override + public StateRootCommitter forBlock( + final ProtocolContext protocolContext, + final BlockHeader blockHeader, + final Optional maybeBal) { + + if (maybeBal.isEmpty() + || protocolContext.getWorldStateArchive() instanceof ForestWorldStateArchive + || isTrieDisabled(protocolContext)) { + return StateRootCommitter.SYNCHRONOUS; + } + + final CompletableFuture balFuture = + BalStateRootCalculator.computeAsync(protocolContext, blockHeader, maybeBal.get()); + final Duration timeout = balConfiguration.getBalStateRootTimeout(); + + if (balConfiguration.isBalStateRootTrusted()) { + return new TrustedBalCommitter(balFuture, timeout); + } + return new VerifyingBalCommitter( + balFuture, timeout, balConfiguration.isBalLenientOnStateRootMismatch()); + } + + // The BAL-computed root is the authoritative source. The standard trie + // computation (supplier) is never invoked. + private static final class TrustedBalCommitter implements StateRootCommitter { + + private final CompletableFuture balFuture; + private final Duration timeout; + + TrustedBalCommitter( + final CompletableFuture balFuture, final Duration timeout) { + this.balFuture = balFuture; + this.timeout = timeout; + } + + @Override + public Hash computeRoot( + final Supplier stateRootSupplier, + final MutableWorldState worldState, + final WorldStateKeyValueStorage.Updater stateUpdater, + final BlockHeader blockHeader) { + + final BalRootComputation bal = + awaitBal(balFuture, timeout, /* lenient= */ false) + .orElseThrow(); // unreachable in strict mode + + if (!blockHeader.getStateRoot().equals(bal.root())) { + throw new IllegalStateException("BAL-computed root does not match block header state root"); + } + + importBalStateChanges( + (PathBasedWorldState) worldState, + (PathBasedWorldStateKeyValueStorage.Updater) stateUpdater, + bal); + return bal.root(); + } + + @Override + public void cancel() { + balFuture.cancel(true); + } + } + + // The standard trie computation is the source of truth. The BAL result + // is awaited only to cross-check. + private static final class VerifyingBalCommitter implements StateRootCommitter { + + private final CompletableFuture balFuture; + private final Duration timeout; + private final boolean lenient; + + VerifyingBalCommitter( + final CompletableFuture balFuture, + final Duration timeout, + final boolean lenient) { + this.balFuture = balFuture; + this.timeout = timeout; + this.lenient = lenient; + } + + @Override + public Hash computeRoot( + final Supplier stateRootSupplier, + final MutableWorldState worldState, + final WorldStateKeyValueStorage.Updater stateUpdater, + final BlockHeader blockHeader) { + + final Hash syncRoot = stateRootSupplier.get(); + final Optional maybeBal = awaitBal(balFuture, timeout, lenient); + + if (maybeBal.isEmpty()) { + LOG.warn("BAL root unavailable (lenient mode); proceeding with computed root {}", syncRoot); + return syncRoot; + } + + final Hash balRoot = maybeBal.get().root(); + if (!syncRoot.equals(balRoot)) { + final String msg = + String.format("BAL root mismatch: computed %s vs BAL %s", syncRoot, balRoot); + if (lenient) { + LOG.error(msg); + } else { + throw new IllegalStateException(msg); + } + } + return syncRoot; + } + + @Override + public void cancel() { + balFuture.cancel(true); + } + } + + private static void importBalStateChanges( + final PathBasedWorldState worldState, + final PathBasedWorldStateKeyValueStorage.Updater stateUpdater, + final BalRootComputation bal) { + + final PathBasedWorldStateUpdateAccumulator balAccumulator = bal.accumulator(); + final PathBasedWorldStateUpdateAccumulator blockAccumulator = worldState.updater(); + blockAccumulator.importStateChangesFromSource(balAccumulator); + + if (!worldState.isStorageFrozen()) { + final PathBasedLayeredWorldStateKeyValueStorage balStorage = + (PathBasedLayeredWorldStateKeyValueStorage) balAccumulator.getWorldStateStorage(); + balStorage.mergeTo(stateUpdater.getWorldStateTransaction()); + } + } + + /** + * Awaits the BAL computation result with the configured timeout. In lenient mode, failures are + * logged and an empty Optional is returned. In strict mode, failures throw. + */ + private static Optional awaitBal( + final CompletableFuture future, + final Duration timeout, + final boolean lenient) { + try { + if (timeout.isNegative()) { + return Optional.of(future.join()); + } + return Optional.of(future.get(timeout.toNanos(), TimeUnit.NANOSECONDS)); + } catch (final TimeoutException e) { + future.cancel(true); + return handleFailure(lenient, "Timed out waiting " + timeout + " for BAL state root", e); + } catch (final ExecutionException e) { + return handleFailure(lenient, "Failed to compute BAL state root", e.getCause()); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + return handleFailure(lenient, "Interrupted while waiting for BAL state root", e); + } + } + + private static Optional handleFailure( + final boolean lenient, final String message, final Throwable cause) { + if (lenient) { + LOG.warn("{} (lenient mode); proceeding.", message, cause); + return Optional.empty(); + } + throw new IllegalStateException(message, cause); + } + + private static boolean isTrieDisabled(final ProtocolContext protocolContext) { + return protocolContext.getWorldStateArchive() instanceof PathBasedWorldStateProvider provider + && provider.getWorldStateSharedSpec().isTrieDisabled(); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactoryDefault.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/DefaultStateRootCommitterFactory.java similarity index 86% rename from ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactoryDefault.java rename to ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/DefaultStateRootCommitterFactory.java index 0cfe5b9bcb5..e89b1da8dd9 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactoryDefault.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/DefaultStateRootCommitterFactory.java @@ -15,17 +15,17 @@ package org.hyperledger.besu.ethereum.mainnet.staterootcommitter; import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.plugin.data.BlockHeader; import java.util.Optional; -public final class StateRootCommitterFactoryDefault implements StateRootCommitterFactory { +public final class DefaultStateRootCommitterFactory implements StateRootCommitterFactory { @Override public StateRootCommitter forBlock( final ProtocolContext protocolContext, final BlockHeader blockHeader, final Optional maybeBal) { - return new StateRootCommitterImplSync(); + return StateRootCommitter.SYNCHRONOUS; } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitter.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitter.java index b7b1c903bf9..5286b087e4d 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitter.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitter.java @@ -16,16 +16,65 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.MutableWorldState; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig; import org.hyperledger.besu.ethereum.worldstate.WorldStateKeyValueStorage; import org.hyperledger.besu.plugin.data.BlockHeader; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer; +import java.util.function.Supplier; + +/** + * Strategy for computing the state root hash during block persistence. + * + *

    The caller provides a {@code stateRootSupplier} that encapsulates the standard (synchronous) + * trie computation. Implementations may: + * + *

      + *
    • Simply invoke the supplier (sync mode) + *
    • Ignore it and return a pre-computed root (BAL trusted mode) + *
    • Invoke it, then cross-check against an independently computed root (BAL verification mode) + *
    + */ public interface StateRootCommitter { - Hash computeRootAndCommit( + + /** Computes the state root synchronously via the standard trie path. */ + StateRootCommitter SYNCHRONOUS = + (supplier, worldState, stateUpdater, blockHeader) -> supplier.get(); + + /** + * Compute (or retrieve) the state root and apply any additional side-effects. + * + * @param stateRootSupplier lazily computes the state root via the standard trie path + * @param worldState the world state being persisted (used by BAL to import state changes) + * @param stateUpdater the storage updater (used by BAL to merge trie nodes) + * @param blockHeader the block being persisted + * @return the authoritative state root hash + */ + Hash computeRoot( + Supplier stateRootSupplier, MutableWorldState worldState, WorldStateKeyValueStorage.Updater stateUpdater, - BlockHeader blockHeader, - WorldStateConfig worldStateConfig); + BlockHeader blockHeader); default void cancel() {} + + default StateRootCommitter timed(final OperationTimer timer) { + final StateRootCommitter delegate = this; + return new StateRootCommitter() { + @Override + public Hash computeRoot( + final Supplier stateRootSupplier, + final MutableWorldState worldState, + final WorldStateKeyValueStorage.Updater stateUpdater, + final BlockHeader blockHeader) { + try (var ignored = timer.startTimer()) { + return delegate.computeRoot(stateRootSupplier, worldState, stateUpdater, blockHeader); + } + } + + @Override + public void cancel() { + delegate.cancel(); + } + }; + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactory.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactory.java index 40c6118d1ce..3393c82ca55 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactory.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactory.java @@ -15,8 +15,8 @@ package org.hyperledger.besu.ethereum.mainnet.staterootcommitter; import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.plugin.data.BlockHeader; import java.util.Optional; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactoryBal.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactoryBal.java deleted file mode 100644 index 568e08cc233..00000000000 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterFactoryBal.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.staterootcommitter; - -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; -import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; -import org.hyperledger.besu.ethereum.trie.forest.ForestWorldStateArchive; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BlockAccessListStateRootHashCalculator; - -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -public final class StateRootCommitterFactoryBal implements StateRootCommitterFactory { - - private final BalConfiguration balConfiguration; - - public StateRootCommitterFactoryBal(final BalConfiguration balConfiguration) { - this.balConfiguration = balConfiguration; - } - - @Override - public StateRootCommitter forBlock( - final ProtocolContext protocolContext, - final BlockHeader blockHeader, - final Optional maybeBal) { - if (!balConfiguration.isBalOptimisationEnabled()) { - return new StateRootCommitterImplSync(); - } - - if (maybeBal.isEmpty()) { - return new StateRootCommitterImplSync(); - } - - // This is temporary workaround to not launch state root pre-computation in Forest mode - if (protocolContext.getWorldStateArchive() instanceof ForestWorldStateArchive) { - return new StateRootCommitterImplSync(); - } - - final CompletableFuture balRootFuture = - BlockAccessListStateRootHashCalculator.computeAsync( - protocolContext, blockHeader, maybeBal.get()); - return new StateRootCommitterImplBal(balRootFuture, balConfiguration); - } -} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplBal.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplBal.java deleted file mode 100644 index da9b99fe604..00000000000 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplBal.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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.staterootcommitter; - -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.ethereum.core.MutableWorldState; -import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; -import org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedLayeredWorldStateKeyValueStorage; -import org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedWorldStateKeyValueStorage; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.PathBasedWorldState; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.accumulator.PathBasedWorldStateUpdateAccumulator; -import org.hyperledger.besu.ethereum.worldstate.WorldStateKeyValueStorage; -import org.hyperledger.besu.plugin.data.BlockHeader; - -import java.time.Duration; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@SuppressWarnings({"rawtypes", "unchecked"}) -final class StateRootCommitterImplBal implements StateRootCommitter { - - private static final Logger LOG = LoggerFactory.getLogger(StateRootCommitterImplBal.class); - - private final CompletableFuture balRootFuture; - private final BalConfiguration balConfiguration; - private final StateRootCommitterImplSync syncCommitter = new StateRootCommitterImplSync(); - - StateRootCommitterImplBal( - final CompletableFuture balRootFuture, - final BalConfiguration balConfiguration) { - this.balRootFuture = balRootFuture; - this.balConfiguration = balConfiguration; - } - - @Override - public Hash computeRootAndCommit( - final MutableWorldState worldState, - final WorldStateKeyValueStorage.Updater stateUpdater, - final BlockHeader blockHeader, - final WorldStateConfig cfg) { - - final Duration balRootTimeout = balConfiguration.getBalStateRootTimeout(); - - if (balConfiguration.isBalStateRootTrusted()) { - return computeWithTrustedBalRoot( - (PathBasedWorldState) worldState, - (PathBasedWorldStateKeyValueStorage.Updater) stateUpdater, - blockHeader, - balRootTimeout); - } - - return computeWithBalVerification(worldState, stateUpdater, blockHeader, cfg, balRootTimeout); - } - - @Override - public void cancel() { - balRootFuture.cancel(true); - } - - private Hash computeWithTrustedBalRoot( - final PathBasedWorldState worldState, - final PathBasedWorldStateKeyValueStorage.Updater stateUpdater, - final BlockHeader blockHeader, - final Duration balRootTimeout) { - - final BalRootComputation balComputation = waitForBalRootStrict(balRootTimeout); - final Hash balStateRoot = balComputation.root(); - - if (!blockHeader.getStateRoot().equals(balStateRoot)) { - throw new IllegalStateException("BAL-computed root does not match block header state root"); - } - importBalStateChanges(worldState, stateUpdater, balComputation); - - return balStateRoot; - } - - private Hash computeWithBalVerification( - final MutableWorldState worldState, - final WorldStateKeyValueStorage.Updater stateUpdater, - final BlockHeader blockHeader, - final WorldStateConfig cfg, - final Duration balRootTimeout) { - - final Hash computedRoot = - syncCommitter.computeRootAndCommit(worldState, stateUpdater, blockHeader, cfg); - - if (balConfiguration.isBalLenientOnStateRootMismatch()) { - return handleBalLenientMode(computedRoot, balRootTimeout); - } else { - return handleBalStrictMode(computedRoot, balRootTimeout); - } - } - - private Hash handleBalLenientMode(final Hash computedRoot, final Duration balRootTimeout) { - final Optional maybeBalRoot = waitForBalRootLenient(balRootTimeout); - - if (maybeBalRoot.isEmpty()) { - LOG.warn( - "BAL root unavailable (lenient mode); proceeding with computed state root {}", - computedRoot); - return computedRoot; - } - - final Hash balRoot = maybeBalRoot.get().root(); - if (!computedRoot.equals(balRoot)) { - LOG.error("BAL root mismatch: computed {} vs BAL {}", computedRoot, balRoot); - } - - return computedRoot; - } - - private Hash handleBalStrictMode(final Hash computedRoot, final Duration balRootTimeout) { - final Hash balRoot = waitForBalRootStrict(balRootTimeout).root(); - - if (!computedRoot.equals(balRoot)) { - final String errorMessage = - String.format("BAL root mismatch: computed %s vs BAL %s", computedRoot, balRoot); - throw new IllegalStateException(errorMessage); - } - - return computedRoot; - } - - private void importBalStateChanges( - final PathBasedWorldState worldState, - final PathBasedWorldStateKeyValueStorage.Updater stateUpdater, - final BalRootComputation balComputation) { - - final PathBasedWorldStateUpdateAccumulator balAccumulator = balComputation.accumulator(); - final PathBasedWorldStateUpdateAccumulator blockAccumulator = worldState.updater(); - - blockAccumulator.importStateChangesFromSource(balAccumulator); - - final PathBasedLayeredWorldStateKeyValueStorage balStateUpdater = - (PathBasedLayeredWorldStateKeyValueStorage) balAccumulator.getWorldStateStorage(); - if (!worldState.isStorageFrozen()) { - balStateUpdater.mergeTo(stateUpdater.getWorldStateTransaction()); - } - } - - private BalRootComputation waitForBalRootStrict(final Duration balRootTimeout) { - try { - return getWithConfiguredTimeout(balRootTimeout); - } catch (final TimeoutException e) { - balRootFuture.cancel(true); - throw new IllegalStateException( - String.format("Timed out waiting %s for BAL state root", balRootTimeout), e); - } catch (final ExecutionException e) { - throw new IllegalStateException("Failed to compute BAL state root", e.getCause()); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while waiting for BAL state root", e); - } - } - - private Optional waitForBalRootLenient(final Duration balRootTimeout) { - try { - return Optional.of(getWithConfiguredTimeout(balRootTimeout)); - } catch (final TimeoutException e) { - balRootFuture.cancel(true); - LOG.warn( - "Timed out waiting {} for BAL state root (lenient mode); proceeding.", balRootTimeout); - return Optional.empty(); - } catch (final ExecutionException e) { - LOG.warn("Failed to compute BAL state root (lenient mode); proceeding.", e.getCause()); - return Optional.empty(); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - LOG.warn("Interrupted while waiting for BAL state root (lenient mode); proceeding."); - return Optional.empty(); - } - } - - private BalRootComputation getWithConfiguredTimeout(final Duration balRootTimeout) - throws InterruptedException, ExecutionException, TimeoutException { - if (balRootTimeout.isNegative()) { - return balRootFuture.join(); - } - return balRootFuture.get(balRootTimeout.toNanos(), TimeUnit.NANOSECONDS); - } -} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplSync.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplSync.java deleted file mode 100644 index 0454ca23046..00000000000 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplSync.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.staterootcommitter; - -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.ethereum.core.MutableWorldState; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig; -import org.hyperledger.besu.ethereum.worldstate.WorldStateKeyValueStorage; -import org.hyperledger.besu.plugin.data.BlockHeader; - -public final class StateRootCommitterImplSync implements StateRootCommitter { - - @Override - public Hash computeRootAndCommit( - final MutableWorldState worldState, - final WorldStateKeyValueStorage.Updater stateUpdater, - final BlockHeader blockHeader, - final WorldStateConfig cfg) { - return worldState.calculateOrReadRootHash(stateUpdater, blockHeader, cfg); - } -} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/forest/worldview/ForestMutableWorldState.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/forest/worldview/ForestMutableWorldState.java index bf0ddf8463d..ef67117249b 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/forest/worldview/ForestMutableWorldState.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/forest/worldview/ForestMutableWorldState.java @@ -25,7 +25,6 @@ import org.hyperledger.besu.ethereum.trie.MerkleTrie; import org.hyperledger.besu.ethereum.trie.common.PmtStateTrieAccountValue; import org.hyperledger.besu.ethereum.trie.forest.storage.ForestWorldStateKeyValueStorage; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig; import org.hyperledger.besu.ethereum.trie.patricia.StoredMerklePatriciaTrie; import org.hyperledger.besu.ethereum.worldstate.WorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.worldstate.WorldStatePreimageStorage; @@ -179,42 +178,28 @@ public final boolean equals(final Object other) { public void persist(final BlockHeader blockHeader, final StateRootCommitter committer) { final ForestWorldStateKeyValueStorage.Updater stateUpdater = worldStateKeyValueStorage.updater(); - committer.computeRootAndCommit( - this, stateUpdater, blockHeader, WorldStateConfig.createStatefulConfigWithTrie()); + committer.computeRoot(() -> applyAndComputeRoot(stateUpdater), this, stateUpdater, blockHeader); } - @Override - public Hash calculateOrReadRootHash( - final WorldStateKeyValueStorage.Updater stateUpdater, - final BlockHeader blockHeader, - final WorldStateConfig cfg) { - - final ForestWorldStateKeyValueStorage.Updater forestUpdater = - (ForestWorldStateKeyValueStorage.Updater) stateUpdater; - // Store updated code + private Hash applyAndComputeRoot(final ForestWorldStateKeyValueStorage.Updater forestUpdater) { for (final Bytes code : updatedAccountCode.values()) { forestUpdater.putCode(code); } - // Commit account storage tries for (final MerkleTrie updatedStorage : updatedStorageTries.values()) { updatedStorage.commit( (location, hash, value) -> forestUpdater.putAccountStorageTrieNode(hash, value)); } - // Commit account updates accountStateTrie.commit( (location, hash, value) -> forestUpdater.putAccountStateTrieNode(hash, value)); - // Persist preimages final WorldStatePreimageStorage.Updater preimageUpdater = preimageStorage.updater(); newStorageKeyPreimages.forEach(preimageUpdater::putStorageTrieKeyPreimage); newAccountKeyPreimages.forEach(preimageUpdater::putAccountTrieKeyPreimage); - // Clear pending changes that we just flushed updatedStorageTries.clear(); updatedAccountCode.clear(); newStorageKeyPreimages.clear(); - // Push changes to underlying storage preimageUpdater.commit(); forestUpdater.commit(); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BalStateRootCalculator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BalStateRootCalculator.java new file mode 100644 index 00000000000..ddca24f990a --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BalStateRootCalculator.java @@ -0,0 +1,123 @@ +/* + * 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 + */ +package org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview; + +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.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.AccountChanges; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.SlotChanges; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.BalRootComputation; +import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams; +import org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.PathBasedWorldState; +import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.accumulator.PathBasedWorldStateUpdateAccumulator; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.plugin.data.BlockHeader; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.apache.tuweni.units.bigints.UInt256; + +@SuppressWarnings("rawtypes") +public class BalStateRootCalculator { + + private BalStateRootCalculator() {} + + public static CompletableFuture computeAsync( + final ProtocolContext protocolContext, + final BlockHeader blockHeader, + final BlockAccessList bal) { + return CompletableFuture.supplyAsync( + () -> { + try (BonsaiWorldState ws = openParentWorldState(protocolContext, blockHeader)) { + applyBalChanges(ws.getAccumulator(), bal); + return computeRoot(ws); + } + }); + } + + private static BonsaiWorldState openParentWorldState( + final ProtocolContext protocolContext, final BlockHeader blockHeader) { + final Hash parentHash = blockHeader.getParentHash(); + final BlockHeader parentHeader = + protocolContext + .getBlockchain() + .getBlockHeader(parentHash) + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Parent %s of block %s not found", + parentHash, blockHeader.getBlockHash()))); + final BonsaiWorldState ws = + (BonsaiWorldState) + protocolContext + .getWorldStateArchive() + .getWorldState( + WorldStateQueryParams.withBlockHeaderAndNoUpdateNodeHead(parentHeader)) + .orElseThrow(); + ws.disableCacheMerkleTrieLoader(); + return ws; + } + + private static void applyBalChanges( + final PathBasedWorldStateUpdateAccumulator accumulator, final BlockAccessList bal) { + for (final AccountChanges changes : bal.accountChanges()) { + if (!changes.hasAnyChange()) { + continue; + } + final Address address = changes.address(); + final MutableAccount account = accumulator.getOrCreate(address); + + lastOf(changes.balanceChanges()) + .ifPresent(c -> account.setBalance(Wei.wrap(c.postBalance()))); + lastOf(changes.nonceChanges()).ifPresent(c -> account.setNonce(c.newNonce())); + lastOf(changes.codeChanges()).ifPresent(c -> account.setCode(c.newCode())); + + for (final SlotChanges slot : changes.storageChanges()) { + lastOf(slot.changes()) + .ifPresent( + change -> + slot.slot() + .getSlotKey() + .ifPresent( + key -> { + final UInt256 value = change.newValue(); + account.setStorageValue(key, value == null ? UInt256.ZERO : value); + })); + } + } + accumulator.clearAccountsThatAreEmpty(); + accumulator.commit(); + } + + private static BalRootComputation computeRoot(final PathBasedWorldState worldState) { + final PathBasedWorldStateUpdateAccumulator accumulator = worldState.getAccumulator(); + final PathBasedWorldStateKeyValueStorage.Updater updater = + worldState.getWorldStateStorage().updater(); + final Hash root = worldState.calculateRootHash(Optional.of(updater), accumulator); + updater.commit(); + return new BalRootComputation(root, accumulator); + } + + private static Optional lastOf(final List list) { + return list.isEmpty() ? Optional.empty() : Optional.of(list.getLast()); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BlockAccessListStateRootHashCalculator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BlockAccessListStateRootHashCalculator.java deleted file mode 100644 index d1aea6992d1..00000000000 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BlockAccessListStateRootHashCalculator.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview; - -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.mainnet.block.access.list.BlockAccessList; -import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.AccountChanges; -import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BalanceChange; -import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.CodeChange; -import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.NonceChange; -import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.SlotChanges; -import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.StorageChange; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.BalRootComputation; -import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams; -import org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedWorldStateKeyValueStorage; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.PathBasedWorldState; -import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.accumulator.PathBasedWorldStateUpdateAccumulator; -import org.hyperledger.besu.evm.account.MutableAccount; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -import org.apache.tuweni.units.bigints.UInt256; - -@SuppressWarnings("rawtypes") -public class BlockAccessListStateRootHashCalculator { - - private BlockAccessListStateRootHashCalculator() {} - - private static BonsaiWorldState prepareWorldState( - final ProtocolContext protocolContext, final BlockHeader blockHeader) { - final Hash parentHash = blockHeader.getParentHash(); - final Optional maybeParentHeader = - protocolContext.getBlockchain().getBlockHeader(parentHash); - if (maybeParentHeader.isEmpty()) { - throw new IllegalStateException( - String.format("Parent %s of block %s not found", parentHash, blockHeader.getHash())); - } - final BonsaiWorldState ws = - (BonsaiWorldState) - protocolContext - .getWorldStateArchive() - .getWorldState( - WorldStateQueryParams.withBlockHeaderAndNoUpdateNodeHead( - maybeParentHeader.get())) - .orElseThrow(); - ws.disableCacheMerkleTrieLoader(); - return ws; - } - - private static BalRootComputation accumulateAccessListAndComputeRoot( - final PathBasedWorldState worldState, final BlockAccessList blockAccessList) { - - final PathBasedWorldStateUpdateAccumulator accumulator = worldState.getAccumulator(); - PathBasedWorldStateKeyValueStorage.Updater worldStateKeyValueStorageUpdater = - worldState.getWorldStateStorage().updater(); - - for (AccountChanges accountChanges : blockAccessList.accountChanges()) { - final Address address = accountChanges.address(); - - final List balanceChanges = accountChanges.balanceChanges(); - final List nonceChanges = accountChanges.nonceChanges(); - final List codeChanges = accountChanges.codeChanges(); - final List storageChanges = accountChanges.storageChanges(); - - final boolean anyChange = - !balanceChanges.isEmpty() - || !nonceChanges.isEmpty() - || !codeChanges.isEmpty() - || !storageChanges.isEmpty(); - - if (!anyChange) { - continue; - } - - final MutableAccount account = accumulator.getOrCreate(address); - - if (!balanceChanges.isEmpty()) { - final BalanceChange change = balanceChanges.get(balanceChanges.size() - 1); - account.setBalance(Wei.wrap(change.postBalance())); - } - - if (!nonceChanges.isEmpty()) { - final NonceChange change = nonceChanges.get(nonceChanges.size() - 1); - account.setNonce(change.newNonce()); - } - - if (!codeChanges.isEmpty()) { - final CodeChange change = codeChanges.get(codeChanges.size() - 1); - account.setCode(change.newCode()); - } - - for (SlotChanges slotChanges : storageChanges) { - final List changes = slotChanges.changes(); - if (!changes.isEmpty()) { - final StorageChange change = changes.get(changes.size() - 1); - final Optional maybeKey = slotChanges.slot().getSlotKey(); - if (maybeKey.isPresent()) { - final UInt256 key = maybeKey.get(); - final UInt256 value = change.newValue(); - account.setStorageValue(key, value == null ? UInt256.ZERO : value); - } - } - } - } - - accumulator.clearAccountsThatAreEmpty(); - accumulator.commit(); - final Hash root = - worldState.calculateRootHash(Optional.of(worldStateKeyValueStorageUpdater), accumulator); - worldStateKeyValueStorageUpdater.commit(); - return new BalRootComputation(root, accumulator); - } - - public static CompletableFuture computeAsync( - final ProtocolContext protocolContext, - final BlockHeader blockHeader, - final BlockAccessList bal) { - return CompletableFuture.supplyAsync( - () -> { - try (BonsaiWorldState ws = prepareWorldState(protocolContext, blockHeader)) { - return accumulateAccessListAndComputeRoot(ws, bal); - } - }); - } -} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldState.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldState.java index 978f1e2dc40..c73a0c11468 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldState.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldState.java @@ -120,34 +120,51 @@ public BonsaiWorldStateKeyValueStorage getWorldStateStorage() { public Hash calculateRootHash( final Optional maybeStateUpdater, final PathBasedWorldStateUpdateAccumulator worldStateUpdater) { - return internalCalculateRootHash( + return applyUpdatesAndComputeRoot( maybeStateUpdater.map(BonsaiWorldStateKeyValueStorage.Updater.class::cast), (BonsaiWorldStateUpdateAccumulator) worldStateUpdater); } - private Hash internalCalculateRootHash( + /** + * Applies all pending state changes and computes the new state root hash. + * + *

    The update proceeds in four ordered phases: + * + *

      + *
    1. Clear storage for self-destructed accounts + *
    2. Update per-account storage tries (must precede account updates to produce storage roots) + *
    3. Update contract code + *
    4. Update account trie and compute the new root hash + *
    + */ + private Hash applyUpdatesAndComputeRoot( final Optional maybeStateUpdater, final BonsaiWorldStateUpdateAccumulator worldStateUpdater) { + // Phase 1 — clear storage for accounts that were self-destructed clearStorage(maybeStateUpdater, worldStateUpdater); - // This must be done before updating the accounts so - // that we can get the storage state hash + // Phase 2 — update every account's storage trie (must happen before account updates + // because the storage root is embedded in the account node) + final boolean canParallelize = maybeStateUpdater.isEmpty(); Stream>>> storageStream = worldStateUpdater.getStorageToUpdate().entrySet().stream(); - if (maybeStateUpdater.isEmpty()) { - storageStream = - storageStream - .parallel(); // if we are not updating the state updater we can use parallel stream + if (canParallelize) { + storageStream = storageStream.parallel(); } storageStream.forEach( - addressMapEntry -> - updateAccountStorageState(maybeStateUpdater, worldStateUpdater, addressMapEntry)); + entry -> updateAccountStorageState(maybeStateUpdater, worldStateUpdater, entry)); - // Third update the code. This has the side effect of ensuring a code hash is calculated. + // Phase 3 — persist contract code changes (also computes code hashes) updateCode(maybeStateUpdater, worldStateUpdater); - // next walk the account trie + // Phase 4 — update the account trie, commit, and return the new root hash + return commitAccountTrieAndComputeRoot(maybeStateUpdater, worldStateUpdater); + } + + private Hash commitAccountTrieAndComputeRoot( + final Optional maybeStateUpdater, + final BonsaiWorldStateUpdateAccumulator worldStateUpdater) { final MerkleTrie accountTrie = createTrie( (location, hash) -> @@ -155,15 +172,11 @@ private Hash internalCalculateRootHash( getWorldStateStorage(), location, hash), Bytes32.wrap(worldStateRootHash.getBytes())); - // for manicured tries and composting, collect branches here (not implemented) updateTheAccounts(maybeStateUpdater, worldStateUpdater, accountTrie); - // TODO write to a cache and then generate a layer update from that and the - // DB tx updates. Right now it is just DB updates. maybeStateUpdater.ifPresent( bonsaiUpdater -> accountTrie.commit(bonsaiUpdater::putAccountStateTrieNode)); - final Bytes32 rootHash = accountTrie.getRootHash(); - return Hash.wrap(rootHash); + return Hash.wrap(accountTrie.getRootHash()); } private void updateTheAccounts( diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/PathBasedWorldState.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/PathBasedWorldState.java index cdaf0095303..7504f3f868c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/PathBasedWorldState.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/PathBasedWorldState.java @@ -164,53 +164,17 @@ public PathBasedWorldStateUpdateAccumulator getAccumulator() { return accumulator; } - protected Hash unsafeRootHashUpdate( - final BlockHeader blockHeader, - final PathBasedWorldStateKeyValueStorage.Updater stateUpdater) { - // calling calculateRootHash in order to update the state - calculateRootHash(isStorageFrozen ? Optional.empty() : Optional.of(stateUpdater), accumulator); - return blockHeader.getStateRoot(); - } - @Override public MutableWorldState disableTrie() { this.worldStateConfig.setTrieDisabled(true); return this; } - @Override - public Hash calculateOrReadRootHash( - final WorldStateKeyValueStorage.Updater stateUpdater, - final BlockHeader blockHeader, - final WorldStateConfig cfg) { - final PathBasedWorldStateKeyValueStorage.Updater pathBasedUpdater = - (PathBasedWorldStateKeyValueStorage.Updater) stateUpdater; - if (blockHeader == null || !cfg.isTrieDisabled()) { - // TODO - rename calculateRootHash() to be clearer that it updates state, it doesn't just - // calculate a hash - return calculateRootHash( - isStorageFrozen ? Optional.empty() : Optional.of(pathBasedUpdater), accumulator); - } else { - // if the trie is disabled, we cannot calculate the state root, so we directly use the root - // of the block. It's important to understand that in all networks, - // the state root must be validated independently and the block should not be trusted - // implicitly. This mode - // can be used in cases where Besu would just be a follower of another trusted client. - LOG.atDebug() - .setMessage("Unsafe state root verification for block header {}") - .addArgument(blockHeader) - .log(); - return unsafeRootHashUpdate(blockHeader, pathBasedUpdater); - } - } - @Override public void persist(final BlockHeader blockHeader, final StateRootCommitter committer) { - - final Optional maybeBlockHeader = Optional.ofNullable(blockHeader); LOG.atDebug() .setMessage("Persist world state for block {}") - .addArgument(maybeBlockHeader) + .addArgument(() -> Optional.ofNullable(blockHeader)) .log(); boolean success = false; @@ -222,11 +186,9 @@ public void persist(final BlockHeader blockHeader, final StateRootCommitter comm try { final Hash calculatedRootHash = - committer.computeRootAndCommit(this, stateUpdater, blockHeader, worldStateConfig); + committer.computeRoot( + buildStateRootSupplier(stateUpdater, blockHeader), this, stateUpdater, blockHeader); - // if we are persisted with a block header, and the prior state is the parent - // then persist the TrieLog for that transition. - // If specified but not a direct descendant simply store the new block hash. if (blockHeader != null) { verifyWorldStateRoot(calculatedRootHash, blockHeader); saveTrieLog = @@ -235,7 +197,6 @@ public void persist(final BlockHeader blockHeader, final StateRootCommitter comm }; cacheWorldState = () -> cachedWorldStorageManager.addCachedLayer(blockHeader, calculatedRootHash, this); - stateUpdater .getWorldStateTransaction() .put( @@ -274,7 +235,6 @@ public void persist(final BlockHeader blockHeader, final StateRootCommitter comm // optionally save the committed worldstate state in the cache cacheWorldState.run(); } - accumulator.reset(); } else { stateUpdater.rollback(); @@ -283,6 +243,31 @@ public void persist(final BlockHeader blockHeader, final StateRootCommitter comm } } + /** + * Builds a lazy supplier that, when invoked, applies all accumulated state changes to the trie + * and returns the resulting state root hash. This supplier is passed to the {@link + * StateRootCommitter}, which may invoke it (sync / BAL-verification) or skip it entirely + * (BAL-trusted mode). + */ + private java.util.function.Supplier buildStateRootSupplier( + final PathBasedWorldStateKeyValueStorage.Updater stateUpdater, + final BlockHeader blockHeader) { + final Optional updaterForTrie = + isStorageFrozen ? Optional.empty() : Optional.of(stateUpdater); + + return () -> { + if (blockHeader != null && worldStateConfig.isTrieDisabled()) { + LOG.atDebug() + .setMessage("Unsafe state root verification for block header {}") + .addArgument(blockHeader) + .log(); + calculateRootHash(updaterForTrie, accumulator); + return blockHeader.getStateRoot(); + } + return calculateRootHash(updaterForTrie, accumulator); + }; + } + protected void verifyWorldStateRoot(final Hash calculatedStateRoot, final BlockHeader header) { if (!worldStateConfig.isTrieDisabled() && !calculatedStateRoot.equals(header.getStateRoot())) { throw new StateRootMismatchException(header.getStateRoot(), calculatedStateRoot); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/accumulator/PathBasedWorldStateUpdateAccumulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/accumulator/PathBasedWorldStateUpdateAccumulator.java index 494a22fc170..6ff962e6f01 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/accumulator/PathBasedWorldStateUpdateAccumulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/accumulator/PathBasedWorldStateUpdateAccumulator.java @@ -108,117 +108,108 @@ public void cloneFromUpdater(final PathBasedWorldStateUpdateAccumulator * the modification from the source will be taken. This approach ensures that the source's state * changes are prioritized and overrides any conflicting changes in the current state. * - * @param source The source accumulator + * @param source The source accumulator whose changes take priority */ public void importStateChangesFromSource( final PathBasedWorldStateUpdateAccumulator source) { - source - .getAccountsToUpdate() - .forEach( - (address, pathBasedValue) -> { - ACCOUNT copyPrior = - pathBasedValue.getPrior() != null - ? copyAccount(pathBasedValue.getPrior(), this, false) - : null; - ACCOUNT copyUpdated = - pathBasedValue.getUpdated() != null - ? copyAccount(pathBasedValue.getUpdated(), this, true) - : null; - accountsToUpdate.put( - address, - new PathBasedValue<>(copyPrior, copyUpdated, pathBasedValue.isLastStepCleared())); - }); - source - .getCodeToUpdate() - .forEach( - (address, pathBasedValue) -> { - codeToUpdate.put( - address, - new PathBasedValue<>( - pathBasedValue.getPrior(), - pathBasedValue.getUpdated(), - pathBasedValue.isLastStepCleared())); - }); - source - .getStorageToUpdate() - .forEach( - (address, slots) -> { - StorageConsumingMap> storageConsumingMap = - storageToUpdate.computeIfAbsent( - address, - k -> - new StorageConsumingMap<>( - address, new ConcurrentHashMap<>(), storagePreloader)); - slots.forEach( - (storageSlotKey, uInt256PathBasedValue) -> { - storageConsumingMap.put( - storageSlotKey, - new PathBasedValue<>( - uInt256PathBasedValue.getPrior(), - uInt256PathBasedValue.getUpdated(), - uInt256PathBasedValue.isLastStepCleared())); - }); - }); + importFrom(source, ImportMode.UPSERT); storageToClear.addAll(source.storageToClear); - storageKeyHashLookup.putAll(source.storageKeyHashLookup); - - this.isAccumulatorStateChanged = true; } /** - * Imports unchanged state data from an external source into the current state. This method - * focuses on integrating state data from the specified source that has been read but not - * modified. - * - *

    The method ensures that only new, unmodified data from the source is added to the current - * state. If a state data has already been read or modified in the current state, it will not be - * added again to avoid overwriting any existing modifications. + * Imports unchanged (prior-only) state data from an external source. Only data not already + * present in this accumulator is imported — existing entries are never overwritten. Both prior + * and updated values are set to the source's prior value (i.e. read-only snapshot). * - * @param source The source accumulator + * @param source The source accumulator to import prior state from */ public void importPriorStateFromSource( final PathBasedWorldStateUpdateAccumulator source) { + importFrom(source, ImportMode.INSERT); + } + + private enum ImportMode { + /** Insert new entries and update existing ones with values from the source. */ + UPSERT, + /** Insert new entries only, existing entries are left untouched. */ + INSERT + } + + private void importFrom( + final PathBasedWorldStateUpdateAccumulator source, final ImportMode mode) { + final boolean priorOnly = mode == ImportMode.INSERT; source .getAccountsToUpdate() .forEach( - (address, pathBasedValue) -> { - ACCOUNT copyPrior = - pathBasedValue.getPrior() != null - ? copyAccount(pathBasedValue.getPrior(), this, false) - : null; - ACCOUNT copyUpdated = - pathBasedValue.getPrior() != null - ? copyAccount(pathBasedValue.getPrior(), this, true) + (address, srcValue) -> { + final ACCOUNT copyPrior = + srcValue.getPrior() != null + ? copyAccount(srcValue.getPrior(), this, false) : null; - accountsToUpdate.putIfAbsent(address, new PathBasedValue<>(copyPrior, copyUpdated)); + final ACCOUNT copyUpdated = + priorOnly + ? (srcValue.getPrior() != null + ? copyAccount(srcValue.getPrior(), this, true) + : null) + : (srcValue.getUpdated() != null + ? copyAccount(srcValue.getUpdated(), this, true) + : null); + final PathBasedValue newValue = + priorOnly + ? new PathBasedValue<>(copyPrior, copyUpdated) + : new PathBasedValue<>(copyPrior, copyUpdated, srcValue.isLastStepCleared()); + if (priorOnly) { + accountsToUpdate.putIfAbsent(address, newValue); + } else { + accountsToUpdate.put(address, newValue); + } }); + source .getCodeToUpdate() .forEach( - (address, pathBasedValue) -> { - codeToUpdate.putIfAbsent( - address, - new PathBasedValue<>(pathBasedValue.getPrior(), pathBasedValue.getPrior())); + (address, srcValue) -> { + final Bytes prior = srcValue.getPrior(); + final Bytes updated = priorOnly ? prior : srcValue.getUpdated(); + final PathBasedValue newValue = + priorOnly + ? new PathBasedValue<>(prior, updated) + : new PathBasedValue<>(prior, updated, srcValue.isLastStepCleared()); + if (priorOnly) { + codeToUpdate.putIfAbsent(address, newValue); + } else { + codeToUpdate.put(address, newValue); + } }); + source .getStorageToUpdate() .forEach( (address, slots) -> { - StorageConsumingMap> storageConsumingMap = + final StorageConsumingMap> targetSlots = storageToUpdate.computeIfAbsent( address, k -> new StorageConsumingMap<>( address, new ConcurrentHashMap<>(), storagePreloader)); slots.forEach( - (storageSlotKey, uInt256PathBasedValue) -> { - storageConsumingMap.putIfAbsent( - storageSlotKey, - new PathBasedValue<>( - uInt256PathBasedValue.getPrior(), uInt256PathBasedValue.getPrior())); + (slotKey, srcSlot) -> { + final UInt256 slotPrior = srcSlot.getPrior(); + final UInt256 slotUpdated = priorOnly ? slotPrior : srcSlot.getUpdated(); + final PathBasedValue newSlotValue = + priorOnly + ? new PathBasedValue<>(slotPrior, slotUpdated) + : new PathBasedValue<>( + slotPrior, slotUpdated, srcSlot.isLastStepCleared()); + if (priorOnly) { + targetSlots.putIfAbsent(slotKey, newSlotValue); + } else { + targetSlots.put(slotKey, newSlotValue); + } }); }); + storageKeyHashLookup.putAll(source.storageKeyHashLookup); this.isAccumulatorStateChanged = true; } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/BlockImportExceptionHandlingTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/BlockImportExceptionHandlingTest.java index a906f7651d7..4f2697ac7b1 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/BlockImportExceptionHandlingTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/BlockImportExceptionHandlingTest.java @@ -46,7 +46,7 @@ import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.blockhash.FrontierPreExecutionProcessor; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterFactoryDefault; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.DefaultStateRootCommitterFactory; import org.hyperledger.besu.ethereum.storage.StorageProvider; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiWorldStateProvider; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.CodeCache; @@ -132,7 +132,7 @@ public void setup() { when(protocolSpec.getFeeMarket()).thenReturn(feeMarket); when(blockAccessListValidator.validate(any(), any(), anyInt())).thenReturn(true); when(protocolSpec.getStateRootCommitterFactory()) - .thenReturn(new StateRootCommitterFactoryDefault()); + .thenReturn(new DefaultStateRootCommitterFactory()); mainnetBlockValidator = MainnetBlockValidatorBuilder.frontier( blockHeaderValidator, blockBodyValidator, blockProcessor, blockAccessListValidator); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/SyncBlockBodyTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/SyncBlockBodyTest.java index b987694a7a0..bc03acbe125 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/SyncBlockBodyTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/SyncBlockBodyTest.java @@ -31,7 +31,7 @@ import org.hyperledger.besu.ethereum.mainnet.PoWHasher; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterFactoryDefault; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.DefaultStateRootCommitterFactory; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.evm.precompile.PrecompileContractRegistry; @@ -224,7 +224,7 @@ private static ProtocolSpec getProtocolSpec() { true, Optional.empty(), Optional.empty(), - new StateRootCommitterFactoryDefault(), + new DefaultStateRootCommitterFactory(), BlockGasAccountingStrategy.FRONTIER, BlockGasUsedValidator.FRONTIER); } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java index ae0ac9b7d46..9832cdc1267 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java @@ -41,7 +41,7 @@ import org.hyperledger.besu.ethereum.mainnet.parallelization.MainnetParallelBlockProcessor; import org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelTransactionPreprocessing; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiAccount; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BlockAccessListStateRootHashCalculator; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BalStateRootCalculator; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; @@ -1286,8 +1286,7 @@ private void assertBalComputesHeaderRoot(final Block block, final BlockProcessin result.getYield().orElseThrow().getBlockAccessList().orElseThrow(); final Hash computedRoot = - BlockAccessListStateRootHashCalculator.computeAsync( - protocolContext, block.getHeader(), blockAccessList) + BalStateRootCalculator.computeAsync(protocolContext, block.getHeader(), blockAccessList) .join() .root(); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorTest.java index e58f866b702..443d8eebe22 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorTest.java @@ -39,7 +39,7 @@ import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.Withdrawal; import org.hyperledger.besu.ethereum.mainnet.blockhash.FrontierPreExecutionProcessor; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterFactoryDefault; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.DefaultStateRootCommitterFactory; import org.hyperledger.besu.ethereum.referencetests.ReferenceTestBlockchain; import org.hyperledger.besu.ethereum.referencetests.ReferenceTestWorldState; @@ -75,7 +75,7 @@ void baseSetup() { .thenReturn(new FrontierPreExecutionProcessor()); lenient() .when(protocolSpec.getStateRootCommitterFactory()) - .thenReturn(new StateRootCommitterFactoryDefault()); + .thenReturn(new DefaultStateRootCommitterFactory()); blockProcessor = new TestBlockProcessor( transactionProcessor, diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockProcessorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockProcessorTest.java index 26e5af529bd..1a4c0540f66 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockProcessorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockProcessorTest.java @@ -30,7 +30,7 @@ import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; import org.hyperledger.besu.ethereum.core.MutableWorldState; import org.hyperledger.besu.ethereum.mainnet.blockhash.FrontierPreExecutionProcessor; -import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitterFactoryDefault; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.DefaultStateRootCommitterFactory; import org.hyperledger.besu.ethereum.referencetests.ReferenceTestBlockchain; import org.hyperledger.besu.ethereum.referencetests.ReferenceTestWorldState; @@ -55,7 +55,7 @@ public void setup() { when(protocolSchedule.getByBlockHeader(any())).thenReturn(protocolSpec); when(protocolSpec.getPreExecutionProcessor()).thenReturn(new FrontierPreExecutionProcessor()); when(protocolSpec.getStateRootCommitterFactory()) - .thenReturn(new StateRootCommitterFactoryDefault()); + .thenReturn(new DefaultStateRootCommitterFactory()); } @Test diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplBalTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/BalStateRootCommitterFactoryTest.java similarity index 90% rename from ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplBalTest.java rename to ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/BalStateRootCommitterFactoryTest.java index 6b38514ad99..385cbe14f8d 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/StateRootCommitterImplBalTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/staterootcommitter/BalStateRootCommitterFactoryTest.java @@ -51,7 +51,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class StateRootCommitterImplBalTest { +class BalStateRootCommitterFactoryTest { private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); @@ -106,11 +106,10 @@ void trustedMode_balAndSyncCommitterProduceSameRoot() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -160,11 +159,10 @@ void trustedMode_balRootMismatchThrowsException() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -213,11 +211,10 @@ void verificationMode_strictMode_matchingRootsSucceed() throws Exception { ImmutableBalConfiguration.builder() .isBalStateRootTrusted(false) .isBalLenientOnStateRootMismatch(false) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -268,11 +265,10 @@ void verificationMode_strictMode_mismatchThrowsException() throws Exception { ImmutableBalConfiguration.builder() .isBalStateRootTrusted(false) .isBalLenientOnStateRootMismatch(false) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -319,11 +315,10 @@ void verificationMode_lenientMode_mismatchLogsButSucceeds() throws Exception { ImmutableBalConfiguration.builder() .isBalStateRootTrusted(false) .isBalLenientOnStateRootMismatch(true) // Lenient mode - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -368,11 +363,10 @@ void cancel_cancelsBalFutureGracefully() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -391,53 +385,15 @@ void factoryReturnsSync_whenBalNotPresent() { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.empty()); - assertThat(committer).isInstanceOf(StateRootCommitterImplSync.class); - } - - @Test - void factoryReturnsSync_whenBalOptimisationDisabled() { - - final Address address = Address.fromHexString("0x0000000000000000000000000000000000000039"); - final Wei newBalance = Wei.of(1_234_567L); - - final BlockAccessList bal = - new BlockAccessList( - List.of( - new AccountChanges( - address, - List.of(), - List.of(), - List.of(new BalanceChange(0, newBalance)), - List.of(), - List.of()))); - - final BlockHeader blockHeader = - new BlockHeaderTestFixture() - .parentHash(chainHeadHeader.getHash()) - .number(chainHeadHeader.getNumber() + 1L) - .buildHeader(); - - final BalConfiguration balConfig = - ImmutableBalConfiguration.builder() - .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(false) // Disabled - .balStateRootTimeout(DEFAULT_TIMEOUT) - .build(); - - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); - final StateRootCommitter committer = - factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); - - assertThat(committer).isInstanceOf(StateRootCommitterImplSync.class); + assertThat(committer).isSameAs(StateRootCommitter.SYNCHRONOUS); } @Test @@ -495,11 +451,10 @@ void multipleAccountChanges_producesCorrectRoot() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -539,11 +494,10 @@ void emptyBalAccessList_producesCorrectRoot() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -586,11 +540,10 @@ void trustedMode_notMergeBalStateChangesForFrozenSate() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -639,11 +592,10 @@ void trustedMode_mergeBalStateChanges_balanceAndNonce() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -715,11 +667,10 @@ void trustedMode_mergeBalStateChanges_withStorage() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -783,11 +734,10 @@ void trustedMode_mergeBalStateChanges_withCode() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -866,11 +816,10 @@ void trustedMode_mergeBalStateChanges_complexScenario() throws Exception { final BalConfiguration balConfig = ImmutableBalConfiguration.builder() .isBalStateRootTrusted(true) - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); @@ -927,11 +876,10 @@ void verificationMode_doesNotImportBalStateChanges() throws Exception { ImmutableBalConfiguration.builder() .isBalStateRootTrusted(false) // Verification mode .isBalLenientOnStateRootMismatch(true) // Lenient to allow mismatch - .isBalOptimisationEnabled(true) .balStateRootTimeout(DEFAULT_TIMEOUT) .build(); - final StateRootCommitterFactory factory = new StateRootCommitterFactoryBal(balConfig); + final StateRootCommitterFactory factory = new BalStateRootCommitterFactory(balConfig); final StateRootCommitter committer = factory.forBlock(protocolContext, blockHeader, Optional.of(bal)); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BlockAccessListStateRootHashCalculatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BalStateRootCalculatorTest.java similarity index 98% rename from ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BlockAccessListStateRootHashCalculatorTest.java rename to ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BalStateRootCalculatorTest.java index 2dd6ff4a8f1..c8aa25d0042 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BlockAccessListStateRootHashCalculatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BalStateRootCalculatorTest.java @@ -50,7 +50,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class BlockAccessListStateRootHashCalculatorTest { +class BalStateRootCalculatorTest { private static final Duration FUTURE_TIMEOUT = Duration.ofSeconds(3); @@ -304,6 +304,6 @@ private CompletableFuture computeRootFromBalAsync(final Bloc .number(chainHeadHeader.getNumber() + 1L) .buildHeader(); - return BlockAccessListStateRootHashCalculator.computeAsync(protocolContext, blockHeader, bal); + return BalStateRootCalculator.computeAsync(protocolContext, blockHeader, bal); } } From fccd0ea4b00f92a3a0fe9e004c46735add0097f6 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Tue, 17 Mar 2026 22:24:03 +0100 Subject: [PATCH 51/77] Deprecate `--min-block-occupancy-ratio` for removal and make it noop (#10036) Signed-off-by: Fabio Di Fabio --- CHANGELOG.md | 4 + .../besu/cli/options/MiningOptions.java | 18 ++-- .../besu/cli/options/MiningOptionsTest.java | 5 +- .../merge/blockcreation/MergeCoordinator.java | 10 -- .../AbstractJsonRpcHttpServiceTest.java | 1 - .../BlockSizeTransactionSelector.java | 28 +----- .../AbstractBlockCreatorTest.java | 1 - .../AbstractBlockTransactionSelectorTest.java | 99 +++---------------- ...FeeMarketBlockTransactionSelectorTest.java | 15 +-- .../TestingBuildBlockIntegrationTest.java | 1 - .../BlockSizeTransactionSelectorTest.java | 32 ------ .../ethereum/core/MiningConfiguration.java | 21 ---- .../bonsai/AbstractIsolationTests.java | 1 - plugin-api/build.gradle | 2 +- .../data/TransactionSelectionResult.java | 7 -- 15 files changed, 33 insertions(+), 212 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 466628ede96..0c7f31acc9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ ### Breaking Changes - Clique consensus has been removed. Besu can no longer start or mine on pure Clique networks. Syncing networks that started as Clique and have since transitioned to PoS via `terminalTotalDifficulty` (e.g. Linea Mainnet) are still supported. [#9852](https://github.com/hyperledger/besu/pull/9852) +- Deprecated `--min-block-occupancy-ratio` for removal and make it noop. That option, that is ignored on PoS networks, is related to the deprecated PoW, and allowed to broadcast a mined block as soon as it reached a satisfying fill threshold. The option is still recognized, but it has no effect and will be completely removed in a future release. [#10036](https://github.com/besu-eth/besu/pull/10036) +- Plugin API + - Removed `TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD`, in general it could be replaced with `BLOCK_FULL` ### Upcoming Breaking Changes - RPC changes to enhance compatibility with other ELs @@ -18,6 +21,7 @@ - Holesky network is deprecated [#9437](https://github.com/hyperledger/besu/pull/9437) - Sunsetting features - for more context on the reasoning behind the deprecation of these features, including alternative options, read [this blog post](https://www.lfdecentralizedtrust.org/blog/sunsetting-tessera-and-simplifying-hyperledger-besu) - Proof of Work consensus (PoW) +- `--min-block-occupancy-ratio` is deprecated and will be removed in a future release - Plugin API - `PluginTransactionSelectorFactory.create(final SelectorsStateManager selectorsStateManager)` is deprecated for removal diff --git a/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java b/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java index b5d6d83ec1a..fa2d76037b4 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java +++ b/app/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java @@ -20,7 +20,6 @@ import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POA_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.MutableInitValues.DEFAULT_EXTRA_DATA; -import static org.hyperledger.besu.ethereum.core.MiningConfiguration.MutableInitValues.DEFAULT_MIN_BLOCK_OCCUPANCY_RATIO; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.MutableInitValues.DEFAULT_MIN_PRIORITY_FEE_PER_GAS; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.MutableInitValues.DEFAULT_MIN_TRANSACTION_GAS_PRICE; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.Unstable.DEFAULT_POS_BLOCK_CREATION_MAX_TIME; @@ -50,6 +49,10 @@ /** The Mining CLI options. */ public class MiningOptions implements CLIOptions { + + private static final String DEPRECATION_PREFIX = + "Deprecated. PoW consensus is deprecated. See CHANGELOG for alternative options. "; + @Option( names = {"--miner-extra-data"}, description = @@ -59,8 +62,13 @@ public class MiningOptions implements CLIOptions { @Option( names = {"--min-block-occupancy-ratio"}, - description = "Minimum occupancy ratio for a mined block (default: ${DEFAULT-VALUE})") - private Double minBlockOccupancyRatio = DEFAULT_MIN_BLOCK_OCCUPANCY_RATIO; + hidden = true, + description = + DEPRECATION_PREFIX + + "Minimum occupancy ratio for a mined block (default: ${DEFAULT-VALUE})") + @SuppressWarnings("UnusedVariable") + @Deprecated + private Double minBlockOccupancyRatio = null; @Option( names = {"--min-gas-price"}, @@ -261,7 +269,6 @@ static MiningOptions fromConfig(final MiningConfiguration miningConfiguration) { miningOptions.extraData = miningConfiguration.getExtraData(); miningOptions.minTransactionGasPrice = miningConfiguration.getMinTransactionGasPrice(); miningOptions.minPriorityFeePerGas = miningConfiguration.getMinPriorityFeePerGas(); - miningOptions.minBlockOccupancyRatio = miningConfiguration.getMinBlockOccupancyRatio(); miningOptions.posBlockTxsSelectionMaxTime = miningConfiguration.getPosBlockTxsSelectionMaxTime(); miningOptions.poaBlockTxsSelectionMaxTime = @@ -296,8 +303,7 @@ public MiningConfiguration toDomainObject() { MutableInitValues.builder() .extraData(extraData) .minTransactionGasPrice(minTransactionGasPrice) - .minPriorityFeePerGas(minPriorityFeePerGas) - .minBlockOccupancyRatio(minBlockOccupancyRatio); + .minPriorityFeePerGas(minPriorityFeePerGas); if (maxBlobsPerTransaction != null) { updatableInitValuesBuilder.maxBlobsPerTransaction(maxBlobsPerTransaction); diff --git a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java index c7af2fcda4c..e0340f74ee0 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java @@ -408,10 +408,7 @@ protected MiningConfiguration createDefaultDomainObject() { protected MiningConfiguration createCustomizedDomainObject() { return ImmutableMiningConfiguration.builder() .mutableInitValues( - MutableInitValues.builder() - .extraData(Bytes.fromHexString("0xabc321")) - .minBlockOccupancyRatio(0.5) - .build()) + MutableInitValues.builder().extraData(Bytes.fromHexString("0xabc321")).build()) .unstable(Unstable.builder().posBlockCreationMaxTime(1000).build()) .build(); } diff --git a/consensus/merge/src/main/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinator.java b/consensus/merge/src/main/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinator.java index 7313dcd8c22..e1470212d35 100644 --- a/consensus/merge/src/main/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinator.java +++ b/consensus/merge/src/main/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinator.java @@ -76,15 +76,6 @@ public class MergeCoordinator implements MergeMiningCoordinator, BadChainListener { private static final Logger LOG = LoggerFactory.getLogger(MergeCoordinator.class); - /** - * On PoS you do not need to compete with other nodes for block production, since you have an - * allocated slot for that, so in this case make sense to always try to fill the block, if there - * are enough pending transactions, until the remaining gas is less than the minimum needed for - * the smaller transaction. So for PoS the min-block-occupancy-ratio option is set to always try - * to fill 100% of the block. - */ - private static final double TRY_FILL_BLOCK = 1.0; - /** The Mining parameters. */ protected final MiningConfiguration miningConfiguration; @@ -175,7 +166,6 @@ public MergeCoordinator( if (miningParams.getTargetGasLimit().isEmpty()) { getDefaultGasLimit(protocolSchedule).ifPresent(miningParams::setTargetGasLimit); } - miningParams.setMinBlockOccupancyRatio(TRY_FILL_BLOCK); this.miningConfiguration = miningParams; this.mergeBlockCreatorFactory = mergeBlockCreatorFactory; diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java index 37448d40403..967b2fca2e6 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java @@ -160,7 +160,6 @@ protected MiningConfiguration createMiningConfiguration() { MutableInitValues.builder() .extraData(Bytes.EMPTY) .minTransactionGasPrice(Wei.ONE) - .minBlockOccupancyRatio(0d) .coinbase(Address.ZERO) .build()) .build(); diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java index 08f00c21003..e71f32b9ea9 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java @@ -65,10 +65,7 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( .setMessage("Transaction {} too large to select for block creation") .addArgument(evaluationContext.getPendingTransaction()::toTraceLog) .log(); - if (blockOccupancyAboveThreshold(state)) { - LOG.trace("Block occupancy above threshold, completing operation"); - return TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD; - } else if (blockFull(state)) { + if (blockFull(state)) { LOG.trace("Block full, completing operation"); return TransactionSelectionResult.BLOCK_FULL; } else { @@ -127,29 +124,6 @@ private boolean transactionTooLargeForBlock(final Transaction transaction, final transaction.getGasLimit(), state.regularGas(), state.stateGas(), blockGasLimit); } - /** - * Checks if the block occupancy is above the threshold. - * - * @param state The current gas state. - * @return True if the block occupancy is above the threshold, false otherwise. - */ - private boolean blockOccupancyAboveThreshold(final GasState state) { - final long gasUsed = - gasAccountingStrategy.effectiveGasUsed(state.regularGas(), state.stateGas()); - final long gasRemaining = blockGasLimit - gasUsed; - final double occupancyRatio = (double) gasUsed / (double) blockGasLimit; - - LOG.trace( - "Min block occupancy ratio {}, gas used {}, available {}, remaining {}, used/available {}", - context.miningConfiguration().getMinBlockOccupancyRatio(), - gasUsed, - blockGasLimit, - gasRemaining, - occupancyRatio); - - return occupancyRatio >= context.miningConfiguration().getMinBlockOccupancyRatio(); - } - /** * Checks if the block is full. * diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreatorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreatorTest.java index 7d466958b86..59a1d9fe9c3 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreatorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreatorTest.java @@ -460,7 +460,6 @@ private CreateOn createBlockCreator(final ProtocolSpecAdapters protocolSpecAdapt MutableInitValues.builder() .extraData(Bytes.fromHexString("deadbeef")) .minTransactionGasPrice(Wei.ONE) - .minBlockOccupancyRatio(0d) .coinbase(Address.ZERO) .build()) .build(); diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java index 8ee1e819a8b..7352ec4accc 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java @@ -21,7 +21,6 @@ import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER1; import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER2; import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER3; -import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER4; import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER5; import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME; import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.EXECUTION_INTERRUPTED; @@ -132,8 +131,6 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public abstract class AbstractBlockTransactionSelectorTest { - protected static final double MIN_OCCUPANCY_80_PERCENT = 0.8; - protected static final double MIN_OCCUPANCY_100_PERCENT = 1; protected static final BigInteger CHAIN_ID = BigInteger.valueOf(42L); protected final MetricsSystem metricsSystem = new NoOpMetricsSystem(); @@ -162,10 +159,7 @@ public void setup() { transactionSelectionService = new TransactionSelectionServiceImpl(); defaultTestMiningConfiguration = createMiningParameters( - transactionSelectionService, - Wei.ZERO, - MIN_OCCUPANCY_80_PERCENT, - DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME); + transactionSelectionService, Wei.ZERO, DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME); final Block genesisBlock = GenesisState.fromConfig(genesisConfig, protocolSchedule, new CodeCache()).getBlock(); @@ -386,10 +380,7 @@ public void subsetOfPendingTransactionsIncludedWhenBlockGasLimitHit() { assertThat(results.getSelectedTransactions().containsAll(transactionsToInject.subList(0, 3))) .isTrue(); assertThat(results.getNotSelectedTransactions()) - .containsOnly( - entry( - transactionsToInject.get(3), - TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD)); + .containsOnly(entry(transactionsToInject.get(3), TransactionSelectionResult.BLOCK_FULL)); assertThat(results.getReceipts().size()).isEqualTo(3); assertThat(results.getCumulativeRegularGasUsed()).isEqualTo(300_000); @@ -434,43 +425,6 @@ public void transactionTooLargeForBlockDoesNotPreventMoreBeingAddedIfBlockOccupa .containsOnly(entry(txs[1], TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_GAS)); } - @Test - public void transactionSelectionStopsWhenSufficientBlockOccupancyIsReached() { - final ProcessableBlockHeader blockHeader = createBlock(300_000); - final Address miningBeneficiary = AddressHelpers.ofValue(1); - final BlockTransactionSelector selector = - createBlockSelectorAndSetupTxPool( - defaultTestMiningConfiguration, - transactionProcessor, - blockHeader, - miningBeneficiary, - Wei.ZERO, - transactionSelectionService); - - // Add 4 transactions to the Pending Transactions 15% (ok), 79% (ok), 25% (too large), 10% - // (not included, it would fit, however previous transaction was too large and block was - // suitably populated). - // NOTE - PendingTransactions will output these in nonce order. - final Transaction[] txs = - new Transaction[] { - createTransaction(0, Wei.of(10), (long) (blockHeader.getGasLimit() * 0.15), SENDER1), - createTransaction(0, Wei.of(10), (long) (blockHeader.getGasLimit() * 0.79), SENDER2), - createTransaction(0, Wei.of(10), (long) (blockHeader.getGasLimit() * 0.25), SENDER3), - createTransaction(0, Wei.of(10), (long) (blockHeader.getGasLimit() * 0.1), SENDER4) - }; - - for (Transaction tx : txs) { - ensureTransactionIsValid(tx); - } - transactionPool.addRemoteTransactions(Arrays.stream(txs).toList()); - - final TransactionSelectionResults results = selector.buildTransactionListForBlock(); - - assertThat(results.getSelectedTransactions()).containsExactly(txs[0], txs[1]); - assertThat(results.getNotSelectedTransactions()) - .containsOnly(entry(txs[2], TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD)); - } - @Test public void ifATransactionIsNotSelectedFollowingOnesFromTheSameSenderAreSkipped() { final ProcessableBlockHeader blockHeader = createBlock(300_000); @@ -517,10 +471,7 @@ public void transactionSelectionStopsWhenBlockIsFull() { final BlockTransactionSelector selector = createBlockSelectorAndSetupTxPool( createMiningParameters( - transactionSelectionService, - Wei.ZERO, - MIN_OCCUPANCY_100_PERCENT, - DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), + transactionSelectionService, Wei.ZERO, DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -534,7 +485,7 @@ public void transactionSelectionStopsWhenBlockIsFull() { // 1) 90% of block (skipped since too large) // 2) enough gas to only leave space for a transaction with the min gas cost (selected) // 3) min gas cost (selected and 100% block gas used) - // 4) min gas cost (not selected since selection stopped after tx 3) + // 4) min gas cost (not selected since block is full) // NOTE - PendingTransactions outputs these in nonce order final long gasLimit0s1 = (long) (blockHeader.getGasLimit() * 0.9); @@ -565,9 +516,7 @@ public void transactionSelectionStopsWhenBlockIsFull() { entry( transactionsToInject.get(1), TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_GAS), - entry( - transactionsToInject.get(4), - TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD)); + entry(transactionsToInject.get(4), TransactionSelectionResult.BLOCK_FULL)); assertThat(results.getCumulativeRegularGasUsed()).isEqualTo(blockHeader.getGasLimit()); } @@ -579,10 +528,7 @@ public void transactionSelectionStopsWhenRemainingGasIsNotEnoughForAnyMoreTransa final BlockTransactionSelector selector = createBlockSelectorAndSetupTxPool( createMiningParameters( - transactionSelectionService, - Wei.ZERO, - MIN_OCCUPANCY_100_PERCENT, - DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), + transactionSelectionService, Wei.ZERO, DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -826,10 +772,7 @@ public PluginTransactionSelector create( final BlockTransactionSelector selector = createBlockSelectorAndSetupTxPool( createMiningParameters( - transactionSelectionService, - Wei.ZERO, - MIN_OCCUPANCY_80_PERCENT, - DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), + transactionSelectionService, Wei.ZERO, DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -1141,10 +1084,7 @@ public void shouldHandleTimeoutBeforeAnyTransactionIsEvaluated() { final BlockTransactionSelector selector = createBlockSelectorAndSetupTxPool( createMiningParameters( - transactionSelectionService, - Wei.ZERO, - MIN_OCCUPANCY_100_PERCENT, - PositiveNumber.fromInt(txsSelectionMaxTime)), + transactionSelectionService, Wei.ZERO, PositiveNumber.fromInt(txsSelectionMaxTime)), transactionProcessor, createBlock(301_000), AddressHelpers.ofValue(1), @@ -1199,10 +1139,7 @@ public TransactionSelectionResult evaluateTransactionPostProcessing( final BlockTransactionSelector selector = createBlockSelectorAndSetupTxPool( createMiningParameters( - transactionSelectionService, - Wei.ZERO, - MIN_OCCUPANCY_100_PERCENT, - PositiveNumber.fromInt(txsSelectionMaxTime)), + transactionSelectionService, Wei.ZERO, PositiveNumber.fromInt(txsSelectionMaxTime)), transactionProcessor, createBlock(301_000), AddressHelpers.ofValue(1), @@ -1255,10 +1192,7 @@ public TransactionSelectionResult evaluateTransactionPostProcessing( selector.set( createBlockSelectorAndSetupTxPool( createMiningParameters( - transactionSelectionService, - Wei.ZERO, - MIN_OCCUPANCY_100_PERCENT, - PositiveNumber.fromInt(1000)), + transactionSelectionService, Wei.ZERO, PositiveNumber.fromInt(1000)), transactionProcessor, createBlock(301_000), AddressHelpers.ofValue(1), @@ -1346,13 +1280,11 @@ private void internalBlockSelectionTimeoutSimulation( ? createMiningParameters( transactionSelectionService, Wei.ZERO, - MIN_OCCUPANCY_100_PERCENT, poaGenesisBlockPeriod, PositiveNumber.fromInt(75)) : createMiningParameters( transactionSelectionService, Wei.ZERO, - MIN_OCCUPANCY_100_PERCENT, PositiveNumber.fromInt(blockTxsSelectionMaxTime)), transactionProcessor, blockHeader, @@ -1510,13 +1442,11 @@ private void internalBlockSelectionTimeoutSimulationInvalidTxs( ? createMiningParameters( transactionSelectionService, Wei.ZERO, - MIN_OCCUPANCY_100_PERCENT, poaGenesisBlockPeriod, PositiveNumber.fromInt(75)) : createMiningParameters( transactionSelectionService, Wei.ZERO, - MIN_OCCUPANCY_100_PERCENT, PositiveNumber.fromInt(blockTxsSelectionMaxTime)), transactionProcessor, blockHeader, @@ -1762,14 +1692,9 @@ private BlockHeader blockHeader(final long number) { protected MiningConfiguration createMiningParameters( final TransactionSelectionService transactionSelectionService, final Wei minGasPrice, - final double minBlockOccupancyRatio, final PositiveNumber txsSelectionMaxTime) { return ImmutableMiningConfiguration.builder() - .mutableInitValues( - MutableInitValues.builder() - .minTransactionGasPrice(minGasPrice) - .minBlockOccupancyRatio(minBlockOccupancyRatio) - .build()) + .mutableInitValues(MutableInitValues.builder().minTransactionGasPrice(minGasPrice).build()) .transactionSelectionService(transactionSelectionService) .posBlockTxsSelectionMaxTime(txsSelectionMaxTime) .build(); @@ -1778,14 +1703,12 @@ protected MiningConfiguration createMiningParameters( protected MiningConfiguration createMiningParameters( final TransactionSelectionService transactionSelectionService, final Wei minGasPrice, - final double minBlockOccupancyRatio, final int genesisBlockPeriodSeconds, final PositiveNumber minBlockTimePercentage) { return ImmutableMiningConfiguration.builder() .mutableInitValues( MutableInitValues.builder() .minTransactionGasPrice(minGasPrice) - .minBlockOccupancyRatio(minBlockOccupancyRatio) .blockPeriodSeconds(genesisBlockPeriodSeconds) .build()) .transactionSelectionService(transactionSelectionService) diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/LondonFeeMarketBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/LondonFeeMarketBlockTransactionSelectorTest.java index ff291277d92..3f275fec483 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/LondonFeeMarketBlockTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/LondonFeeMarketBlockTransactionSelectorTest.java @@ -119,10 +119,7 @@ public void eip1559TransactionCurrentGasPriceLessThanMinimumIsSkippedAndKeptInTh final BlockTransactionSelector selector = createBlockSelectorAndSetupTxPool( createMiningParameters( - transactionSelectionService, - Wei.of(6), - MIN_OCCUPANCY_80_PERCENT, - DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), + transactionSelectionService, Wei.of(6), DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -151,10 +148,7 @@ public void eip1559TransactionCurrentGasPriceGreaterThanMinimumIsSelected() { final BlockTransactionSelector selector = createBlockSelectorAndSetupTxPool( createMiningParameters( - transactionSelectionService, - Wei.of(6), - MIN_OCCUPANCY_80_PERCENT, - DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), + transactionSelectionService, Wei.of(6), DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, @@ -182,10 +176,7 @@ public void eip1559PriorityTransactionCurrentGasPriceLessThanMinimumIsSelected() final BlockTransactionSelector selector = createBlockSelectorAndSetupTxPool( createMiningParameters( - transactionSelectionService, - Wei.of(6), - MIN_OCCUPANCY_80_PERCENT, - DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), + transactionSelectionService, Wei.of(6), DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME), transactionProcessor, blockHeader, miningBeneficiary, diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java index a53972c03fe..92b5b64a7a1 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java @@ -431,7 +431,6 @@ private TestContext createTestContext(final boolean withBAL) { MutableInitValues.builder() .extraData(Bytes.fromHexString("deadbeef")) .minTransactionGasPrice(Wei.ONE) - .minBlockOccupancyRatio(0d) .coinbase(Address.ZERO) .build()) .build(); diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java index 38495810239..d25585fe823 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java @@ -17,7 +17,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MAX_SCORE; import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOCK_FULL; -import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD; import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_GAS; import static org.mockito.Answers.RETURNS_DEEP_STUBS; @@ -190,33 +189,6 @@ void moreTransactionsThanBlockCanFitOnlySomeAreSelected() { assertThat(selector.getWorkingState().regularGas()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); } - @Test - void identifyWhenBlockOccupancyIsAboveThreshold() { - selectorsStateManager.blockSelectionStarted(); - - // create 2 txs with a gas limit just above the min block occupancy ratio - // so the first is accepted while the second not - final long justAboveOccupancyRatioGasLimit = - (long) (BLOCK_GAS_LIMIT * miningConfiguration.getMinBlockOccupancyRatio()) + 100; - final var tx1 = createPendingTransaction(justAboveOccupancyRatioGasLimit); - - final var txEvaluationContext1 = - new TransactionEvaluationContext( - blockSelectionContext.pendingBlockHeader(), tx1, null, null, null, NEVER_CANCELLED); - evaluateAndAssertSelected(txEvaluationContext1, remainingGas(0)); - - assertThat(selector.getWorkingState().regularGas()).isEqualTo(justAboveOccupancyRatioGasLimit); - - final var tx2 = createPendingTransaction(justAboveOccupancyRatioGasLimit); - - final var txEvaluationContext2 = - new TransactionEvaluationContext( - blockSelectionContext.pendingBlockHeader(), tx2, null, null, null, NEVER_CANCELLED); - evaluateAndAssertNotSelected(txEvaluationContext2, BLOCK_OCCUPANCY_ABOVE_THRESHOLD); - - assertThat(selector.getWorkingState().regularGas()).isEqualTo(justAboveOccupancyRatioGasLimit); - } - @Test void identifyWhenBlockIsFull() { when(blockSelectionContext.gasCalculator().getMinimumTransactionCost()) @@ -224,9 +196,6 @@ void identifyWhenBlockIsFull() { selectorsStateManager.blockSelectionStarted(); - // allow to completely fill the block - miningConfiguration.setMinBlockOccupancyRatio(1.0); - // create 2 txs, where the first fill the block leaving less gas than the min required by a // transfer final long fillBlockGasLimit = BLOCK_GAS_LIMIT - TRANSFER_GAS_LIMIT + 1; @@ -416,7 +385,6 @@ void eip8037EffectiveGasUsedDrivesBlockFullCheck() { .thenReturn(TRANSFER_GAS_LIMIT); selector = new BlockSizeTransactionSelector(blockSelectionContext, selectorsStateManager); selectorsStateManager.blockSelectionStarted(); - miningConfiguration.setMinBlockOccupancyRatio(1.0); // Fill block with both dimensions nearly full: regular=990K, state=990K // gasMetered = max(990K, 990K) = 990K; remaining = 10K < 21K min cost diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java index 546775728a7..8c2b279979c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java @@ -114,15 +114,6 @@ public MiningConfiguration setTargetGasLimit(final long targetGasLimit) { return this; } - public double getMinBlockOccupancyRatio() { - return getMutableRuntimeValues().minBlockOccupancyRatio; - } - - public MiningConfiguration setMinBlockOccupancyRatio(final double minBlockOccupancyRatio) { - getMutableRuntimeValues().minBlockOccupancyRatio = minBlockOccupancyRatio; - return this; - } - /** * Returns the maximum blobs per transaction. Note: Only applies from Osaka hardfork onwards. * Returns empty if not explicitly set by the user. @@ -246,7 +237,6 @@ public interface MutableInitValues { Wei DEFAULT_MIN_TRANSACTION_GAS_PRICE = Wei.of(1000); Wei DEFAULT_MIN_PRIORITY_FEE_PER_GAS = Wei.ZERO; - double DEFAULT_MIN_BLOCK_OCCUPANCY_RATIO = 0.8; MutableInitValues DEFAULT = ImmutableMiningConfiguration.MutableInitValues.builder().build(); @@ -270,11 +260,6 @@ default Wei getMinPriorityFeePerGas() { return DEFAULT_MIN_PRIORITY_FEE_PER_GAS; } - @Value.Default - default double getMinBlockOccupancyRatio() { - return DEFAULT_MIN_BLOCK_OCCUPANCY_RATIO; - } - /** * Returns the maximum blobs per transaction. Note: Only applies from Osaka hardfork onwards. * Empty means use the fork-specific default from the gas limit calculator. @@ -297,7 +282,6 @@ static class MutableRuntimeValues { private volatile Bytes extraData; private volatile Wei minTransactionGasPrice; private volatile Wei minPriorityFeePerGas; - private volatile double minBlockOccupancyRatio; private volatile Optional

    coinbase; private volatile OptionalLong targetGasLimit; private volatile Optional> nonceGenerator; @@ -309,7 +293,6 @@ private MutableRuntimeValues(final MutableInitValues initValues) { extraData = initValues.getExtraData(); minTransactionGasPrice = initValues.getMinTransactionGasPrice(); minPriorityFeePerGas = initValues.getMinPriorityFeePerGas(); - minBlockOccupancyRatio = initValues.getMinBlockOccupancyRatio(); coinbase = initValues.getCoinbase(); targetGasLimit = initValues.getTargetGasLimit(); nonceGenerator = initValues.nonceGenerator(); @@ -323,7 +306,6 @@ public boolean equals(final Object o) { if (o == null || getClass() != o.getClass()) return false; final MutableRuntimeValues that = (MutableRuntimeValues) o; return miningEnabled == that.miningEnabled - && Double.compare(minBlockOccupancyRatio, that.minBlockOccupancyRatio) == 0 && Objects.equals(extraData, that.extraData) && Objects.equals(minTransactionGasPrice, that.minTransactionGasPrice) && Objects.equals(coinbase, that.coinbase) @@ -341,7 +323,6 @@ public int hashCode() { extraData, minTransactionGasPrice, minPriorityFeePerGas, - minBlockOccupancyRatio, coinbase, targetGasLimit, nonceGenerator, @@ -359,8 +340,6 @@ public String toString() { + minTransactionGasPrice + ", minPriorityFeePerGas=" + minPriorityFeePerGas - + ", minBlockOccupancyRatio=" - + minBlockOccupancyRatio + ", coinbase=" + coinbase + ", targetGasLimit=" diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/AbstractIsolationTests.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/AbstractIsolationTests.java index 755fdf989fa..06bebf527c3 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/AbstractIsolationTests.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/AbstractIsolationTests.java @@ -319,7 +319,6 @@ static TestBlockCreator forHeader( .extraData(Bytes.fromHexString("deadbeef")) .targetGasLimit(30_000_000L) .minTransactionGasPrice(Wei.ONE) - .minBlockOccupancyRatio(0d) .coinbase(Address.ZERO) .build()) .build(); diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index 550bd6731e8..c2f94bbd38d 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -71,7 +71,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = '6hDXxhwrNO1YWmdkQKoG89kPWcS8j3CBjPeoePbjms0=' + knownHash = '0rrsKtgrD9Erb8oygs0p8vDgk9jNWI8TvFFuGhyTOto=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSelectionResult.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSelectionResult.java index f15bad8bb8c..07883c21d9b 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSelectionResult.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSelectionResult.java @@ -164,13 +164,6 @@ public boolean penalize() { public static final TransactionSelectionResult INVALID_TX_EVALUATION_TOO_LONG = new TransactionSelectionResult(BaseStatus.INVALID_TX_EVALUATION_TOO_LONG); - /** - * The transaction has not been selected since too large and the occupancy of the block is enough - * to stop the selection. - */ - public static final TransactionSelectionResult BLOCK_OCCUPANCY_ABOVE_THRESHOLD = - new TransactionSelectionResult(BaseStatus.BLOCK_OCCUPANCY_ABOVE_THRESHOLD); - /** * There was an unhandled exception during the evaluation of the transaction. If this occurs, it * indicates there is a bug somewhere. From fc80747ae4210cc8e24ab6257369f5ce35866922 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 18 Mar 2026 10:37:02 +1000 Subject: [PATCH 52/77] Add max used gas to eth_simulateV1 (#10066) * add max-gas-used to eth_simulateV1 results * pre-refund gas amounts for EIP-7702 delegation transactions Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane --- CHANGELOG.md | 4 +--- .../jsonrpc/internal/results/BlockStateCallResult.java | 3 ++- .../jsonrpc/internal/results/CallProcessingResult.java | 9 +++++++++ .../eth_simulateV1-code-delegation-with-authority.json | 1 + .../simulateV1/specs/eth_simulateV1-code-delegation.json | 2 ++ .../eth/simulateV1/specs/eth_simulateV1-simple.json | 2 ++ .../eth/simulateV1/specs/eth_simulateV1-trie-log.json | 2 ++ .../eth/simulateV1/specs/eth_simulateV1_blobs.json | 2 ++ 8 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c7f31acc9a..790d2be303f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,11 +45,9 @@ are provided with different values, using input as per the execution-apis spec i - Implement `txpool_status` RPC method [#10002](https://github.com/hyperledger/besu/pull/10002) - Support [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975): eth/70 - partial block receipt lists - Limit pooled tx requests by size and remove pre-eth/68 transaction announcement support [#9990](https://github.com/besu-eth/besu/pull/9990) -<<<<<<< optimize/register-based-shift - Use cache locality to improve Shift opcodes [#9878](https://github.com/besu-eth/besu/pull/9878) -======= +- Add maxUsedGas field to eth_simulateV1 results [#10066](https://github.com/besu-eth/besu/pull/10066) - Plugin API: pass pending block header when creating selectors [#10034](https://github.com/besu-eth/besu/pull/10034) ->>>>>>> main ## 26.2.0 diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockStateCallResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockStateCallResult.java index 9621e3d0539..1d70507d225 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockStateCallResult.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockStateCallResult.java @@ -147,7 +147,8 @@ private static CallProcessingResult createTransactionProcessingResult( return new CallProcessingResult( result.isSuccessful() ? 1 : 0, result.getOutput(), - simulatorResult.getGasEstimate(), + result.getGasSpent(), + result.getEstimateGasUsedByTransaction(), getError(result), new LogsResult(transactionLogs)); } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/CallProcessingResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/CallProcessingResult.java index 5341515d24b..9b68faf76b9 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/CallProcessingResult.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/CallProcessingResult.java @@ -32,6 +32,9 @@ public class CallProcessingResult { @JsonProperty("gasUsed") private final String gasUsed; + @JsonProperty("maxUsedGas") + private final String maxUsedGas; + @JsonProperty("error") private final JsonRpcError error; @@ -42,11 +45,13 @@ public CallProcessingResult( @JsonProperty("status") final int status, @JsonProperty("returnData") final Bytes returnData, @JsonProperty("gasUsed") final long gasUsed, + @JsonProperty("maxUsedGas") final long maxUsedGas, @JsonProperty("error") final JsonRpcError error, @JsonProperty("logs") final LogsResult logs) { this.status = Quantity.create(status); this.returnData = returnData.toString(); this.gasUsed = Quantity.create(gasUsed); + this.maxUsedGas = Quantity.create(maxUsedGas); this.error = error; this.logs = logs; } @@ -63,6 +68,10 @@ public String getGasUsed() { return gasUsed; } + public String getMaxUsedGas() { + return maxUsedGas; + } + @JsonInclude(JsonInclude.Include.NON_NULL) public JsonRpcError getError() { return error; diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-code-delegation-with-authority.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-code-delegation-with-authority.json index 9e4ac188dfa..571ed5811fa 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-code-delegation-with-authority.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-code-delegation-with-authority.json @@ -74,6 +74,7 @@ ] } ], + "maxUsedGas": "0x13b30", "returnData": "0x", "status": "0x1" } diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-code-delegation.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-code-delegation.json index f3cf41269d5..1a87482ca61 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-code-delegation.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-code-delegation.json @@ -68,12 +68,14 @@ { "gasUsed": "0xa637", "logs": [], + "maxUsedGas": "0xcfc4", "returnData": "0x", "status": "0x1" }, { "gasUsed": "0x5db5", "logs": [], + "maxUsedGas": "0x5db5", "returnData": "0x0000000000000000000000000000000000000000000000000000000000000000", "status": "0x1" } diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-simple.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-simple.json index e0ecdd7ab13..5b1be7eaa01 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-simple.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-simple.json @@ -42,12 +42,14 @@ { "gasUsed": "0x5208", "logs": [], + "maxUsedGas": "0x5208", "returnData": "0x", "status": "0x1" }, { "gasUsed": "0x5208", "logs": [], + "maxUsedGas": "0x5208", "returnData": "0x", "status": "0x1" } diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-trie-log.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-trie-log.json index 03fcb69ca5e..69db86cb7fc 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-trie-log.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1-trie-log.json @@ -43,12 +43,14 @@ { "gasUsed": "0x5208", "logs": [], + "maxUsedGas": "0x5208", "returnData": "0x", "status": "0x1" }, { "gasUsed": "0x5208", "logs": [], + "maxUsedGas": "0x5208", "returnData": "0x", "status": "0x1" } diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1_blobs.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1_blobs.json index 2e7f6bb705e..9439122cfb9 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1_blobs.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/simulateV1/specs/eth_simulateV1_blobs.json @@ -66,6 +66,7 @@ { "gasUsed": "0x53f6", "logs": [], + "maxUsedGas": "0x53f6", "returnData": "0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c44401400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "status": "0x1" } @@ -130,6 +131,7 @@ { "gasUsed": "0x53f6", "logs": [], + "maxUsedGas": "0x53f6", "returnData": "0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c44401400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "status": "0x1" } From 2453042883a9f65a89acd5be932c05b608596c8a Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 18 Mar 2026 11:16:45 +1000 Subject: [PATCH 53/77] fix: send slim account encoding in snap AccountRange responses (#9877) * fix: send slim account encoding in snap AccountRange responses The snap protocol spec requires accounts to be sent in slim RLP encoding on the wire, where empty storageRoot and codeHash fields are encoded as 0x80 (RLP empty bytes) rather than the full 32-byte default hashes. Besu was sending the full encoding, causing response sizes to be larger than expected and failing hive snap protocol AccountRange tests. * test: verify AccountRangeMessage uses slim encoding on wire Signed-off-by: Sally MacFarlane Co-authored-by: Claude Opus 4.6 Co-authored-by: Stefan Pingel <16143240+pinges@users.noreply.github.com> --- .../messages/snap/AccountRangeMessage.java | 3 +- .../snap/AccountRangeMessageTest.java | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessage.java index 17dbda4499f..9eb606e1df8 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessage.java @@ -19,6 +19,7 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; +import org.hyperledger.besu.ethereum.rlp.RLP; import org.hyperledger.besu.ethereum.rlp.RLPInput; import org.hyperledger.besu.ethereum.trie.common.PmtStateTrieAccountValue; @@ -71,7 +72,7 @@ public static AccountRangeMessage create( (entry, rlpOutput) -> { rlpOutput.startList(); rlpOutput.writeBytes(entry.getKey()); - rlpOutput.writeRLPBytes(entry.getValue()); + rlpOutput.writeRLPBytes(toSlimAccount(RLP.input(entry.getValue()))); rlpOutput.endList(); }); tmp.writeList(proof, (bytes, rlpOutput) -> rlpOutput.writeBytes(bytes)); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessageTest.java index de84b639f33..b1e902b3015 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessageTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessageTest.java @@ -58,6 +58,44 @@ public void roundTripTest() { assertThat(range.proofs()).isEqualTo(proofs); } + @Test + public void createUsesSlimEncodingOnWire() { + // Create an account with empty storage and code (the common case) + final PmtStateTrieAccountValue emptyAccount = + new PmtStateTrieAccountValue(1L, Wei.of(2L), Hash.EMPTY_TRIE_HASH, Hash.EMPTY); + final Map accounts = new HashMap<>(); + accounts.put(Bytes32.leftPad(Bytes.of(1)), RLP.encode(emptyAccount::writeTo)); + + final MessageData message = AccountRangeMessage.create(accounts, List.of()); + final Bytes wireBytes = message.getData(); + + // The wire bytes should NOT contain the full 32-byte EMPTY_TRIE_HASH or EMPTY code hash. + // In slim encoding, these are replaced with 0x80 (RLP empty bytes). + assertThat(wireBytes.toHexString()) + .doesNotContain(Hash.EMPTY_TRIE_HASH.toHexString().substring(2)); + assertThat(wireBytes.toHexString()).doesNotContain(Hash.EMPTY.toHexString().substring(2)); + } + + @Test + public void createPreservesNonEmptyHashesOnWire() { + // Create an account with non-empty storage root and code hash + final Hash storageRoot = Hash.wrap(Bytes32.leftPad(Bytes.fromHexString("0xdeadbeef"))); + final Hash codeHash = Hash.wrap(Bytes32.leftPad(Bytes.fromHexString("0xcafebabe"))); + final PmtStateTrieAccountValue account = + new PmtStateTrieAccountValue(5L, Wei.of(100L), storageRoot, codeHash); + final Map accounts = new HashMap<>(); + accounts.put(Bytes32.leftPad(Bytes.of(1)), RLP.encode(account::writeTo)); + + // Round-trip: create message, read it back + final MessageData message = AccountRangeMessage.create(accounts, List.of()); + final AccountRangeMessage parsed = + AccountRangeMessage.readFrom(new RawMessage(SnapV1.ACCOUNT_RANGE, message.getData())); + final AccountRangeMessage.AccountRangeData range = parsed.accountData(false); + + // Non-empty hashes should survive the round trip + assertThat(range.accounts()).isEqualTo(accounts); + } + @Test public void toSlimAccountTest() { // Initialize nonce and balance From c8d8b713dd204fea0fe02b04ee9006c5d8b44435 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 18 Mar 2026 11:41:06 +1000 Subject: [PATCH 54/77] Fix typed receipt encoding in putSyncTransactionReceipts (#10044) * Fix typed receipt encoding in putSyncTransactionReceipts Receipts received via snap sync (eth/68) arrive through ReceiptsMessage.deserializeReceiptLists() which calls readBytes(), stripping the outer RLP bytes-element wrapper and leaving raw typeCode||rlp_body bytes (first byte in 0x01-0x7f). When these were passed directly to SimpleNoCopyRlpEncoder.encodeList(), the single-byte type code (e.g. 0x02 for EIP-1559) was stored as a standalone RLP item and the rlp_body as another, splitting one receipt into two items. On read-back, TransactionReceiptDecoder.readFrom() saw only the 1-byte type code, called slice(1) producing Bytes.EMPTY, and enterList() threw RLPException -- causing the server to disconnect peers with BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED. Fix by checking if the raw bytes start with a byte < 0x80 (raw EIP-2718 type code with no RLP wrapper) and, if so, encoding them as a proper RLP bytes element via NO_COPY_RLP_ENCODER.encode(). Receipts that already carry a valid RLP element header (>= 0x80: either a string prefix 0x80-0xbf or list prefix 0xc0-0xff) are stored unchanged. Adds a test that simulates the network receive path (readBytes() stripping the outer wrapper) for EIP-1559 and ACCESS_LIST receipts. Signed-off-by: Sally MacFarlane * review comments Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Stefan Pingel <16143240+pinges@users.noreply.github.com> --- ...ueStoragePrefixedKeyBlockchainStorage.java | 20 ++++++- ...oragePrefixedKeyBlockchainStorageTest.java | 56 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorage.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorage.java index 999dd963eb1..381c6ac1709 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorage.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorage.java @@ -365,7 +365,9 @@ public void putSyncTransactionReceipts( TRANSACTION_RECEIPTS_PREFIX, blockHash.getBytes(), NO_COPY_RLP_ENCODER.encodeList( - transactionReceipts.stream().map(SyncTransactionReceipt::getRlpBytes).toList())); + transactionReceipts.stream() + .map(r -> normalizeReceiptRlpElement(r.getRlpBytes())) + .toList())); } @Override @@ -454,6 +456,22 @@ private void remove(final Bytes prefix, final Bytes key) { blockchainTransaction.remove(Bytes.concatenate(prefix, key).toArrayUnsafe()); } + /** + * Normalizes a receipt RLP element for storage. + * + *

    EIP-2718 typed receipts received from the network arrive via readBytes() which strips the + * outer RLP bytes-element wrapper, leaving raw typeCode||rlp_body (first byte in 0x01-0x7f). + * These must be re-wrapped as a single RLP bytes element so that + * TransactionReceiptDecoder.decodeTypedReceiptComponents can decode them. Receipts that are + * already valid RLP elements (first byte >= 0x80) are stored as-is. + */ + private Bytes normalizeReceiptRlpElement(final Bytes rawBytes) { + if (rawBytes.isEmpty() || Byte.toUnsignedInt(rawBytes.get(0)) >= 0x80) { + return rawBytes; + } + return NO_COPY_RLP_ENCODER.encode(rawBytes); + } + private Bytes rlpEncode(final List receipts) { return RLP.encode( o -> diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorageTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorageTest.java index 9d3d4e6bce2..d660c6df0ab 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorageTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorageTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.mock; import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.TransactionType; import org.hyperledger.besu.ethereum.chain.BlockchainStorage.Updater; import org.hyperledger.besu.ethereum.chain.VariablesStorage; import org.hyperledger.besu.ethereum.chain.VariablesStorage.Keys; @@ -37,6 +38,7 @@ import org.hyperledger.besu.ethereum.core.encoding.receipt.SyncTransactionReceiptDecoder; import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.plugin.services.storage.KeyValueStorage; import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; @@ -191,4 +193,58 @@ public void testUpdaterPutSyncTransactionReceipts() { Assertions.assertEquals(4, loadedReceipts.size()); Assertions.assertTrue(loadedReceipts.containsAll(transactionReceipts)); } + + /** + * Tests that typed (EIP-1559) receipts received from the network via snap sync are stored and + * retrieved correctly. On the network, typed receipts are transmitted as RLP bytes elements; + * ReceiptsMessage.deserializeReceiptLists() calls readBytes() which strips the RLP wrapper, + * leaving raw typeCode||rlp_body bytes. This simulates that path and verifies round-trip + * correctness through putSyncTransactionReceipts / getTransactionReceipts. + */ + @Test + public void testUpdaterPutSyncTransactionReceipts_typedReceiptsFromNetwork() { + populateBlockchainStorage(kvBlockchain, variableValues); + + final var blockchainStorage = + new KeyValueStoragePrefixedKeyBlockchainStorage( + kvBlockchain, variablesStorage, blockHeaderFunctions, false); + + BlockDataGenerator generator = new BlockDataGenerator(); + SyncTransactionReceiptDecoder syncReceiptDecoder = new SyncTransactionReceiptDecoder(); + + Hash blockHash = generator.hash(); + // Create explicitly typed (EIP-1559) receipts to exercise the typed-receipt storage path + List transactionReceipts = + List.of( + new TransactionReceipt( + TransactionType.EIP1559, 1, 1000L, List.of(), java.util.Optional.empty()), + new TransactionReceipt( + TransactionType.ACCESS_LIST, 1, 2000L, List.of(), java.util.Optional.empty())); + + List syncReceipts = + transactionReceipts.stream() + .map( + tr -> { + // Encode as it would appear on the wire (eth/68): an RLP bytes element + BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); + TransactionReceiptEncoder.writeTo( + tr, rlpOut, TransactionReceiptEncodingConfiguration.DEFAULT); + // Simulate ReceiptsMessage.deserializeReceiptLists() calling readBytes(), + // which strips the outer RLP bytes-element wrapper and returns the raw + // typeCode||rlp_body content (first byte in 0x01-0x7f) + final Bytes rawWireContent = + new BytesValueRLPInput(rlpOut.encoded(), false).readBytes(); + return syncReceiptDecoder.decode(rawWireContent); + }) + .toList(); + + Updater updater = blockchainStorage.updater(); + updater.putSyncTransactionReceipts(blockHash, syncReceipts); + updater.commit(); + + List loadedReceipts = + blockchainStorage.getTransactionReceipts(blockHash).get(); + Assertions.assertEquals(2, loadedReceipts.size()); + Assertions.assertTrue(loadedReceipts.containsAll(transactionReceipts)); + } } From 23774233350da9314939dd41b5566c7e275603ac Mon Sep 17 00:00:00 2001 From: Qian Cheng <91401632+Qian-Cheng-nju@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:07:10 +0800 Subject: [PATCH 55/77] fix: add blockchain-head guard to handleBlockTimerExpiry (#10052) * fix: add blockchain-head guard to handleBlockTimerExpiry Signed-off-by: Qian-Cheng-nju Signed-off-by: Qian Cheng <91401632+Qian-Cheng-nju@users.noreply.github.com> --- .../core/statemachine/QbftController.java | 6 +++++ .../core/statemachine/QbftControllerTest.java | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java index d43e9de4521..c049ccfe9eb 100644 --- a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java +++ b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java @@ -260,6 +260,12 @@ public void handleNewBlockEvent(final QbftNewChainHead newChainHead) { @Override public void handleBlockTimerExpiry(final BlockTimerExpiry blockTimerExpiry) { final ConsensusRoundIdentifier roundIdentifier = blockTimerExpiry.getRoundIdentifier(); + // Discard block timer events that target a height already on the blockchain (e.g., block + // was imported via peer sync while the timer was pending). Same guard as handleRoundExpiry. + if (roundIdentifier.getSequenceNumber() <= blockchain.getChainHeadBlockNumber()) { + LOG.debug("Discarding a block-timer which targets a height not above current chain height."); + return; + } if (isMsgForCurrentHeight(roundIdentifier, getCurrentChainHeight())) { getCurrentHeightManager().handleBlockTimerExpiry(roundIdentifier); } else { diff --git a/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftControllerTest.java b/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftControllerTest.java index 0339133de3b..ecfdbe65a39 100644 --- a/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftControllerTest.java +++ b/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftControllerTest.java @@ -341,6 +341,28 @@ public void blockTimerForPastHeightIsDiscarded() { verify(blockHeightManager, never()).handleBlockTimerExpiry(any()); } + @Test + public void blockTimerForAlreadyImportedHeightIsDiscarded() { + // Simulate peer sync: blockchain head advances to current height while block timer is pending + when(blockChain.getChainHeadBlockNumber()).thenReturn(4L); + final BlockTimerExpiry blockTimerExpiry = new BlockTimerExpiry(roundIdentifier); + constructQbftController(); + qbftController.start(); + qbftController.handleBlockTimerExpiry(blockTimerExpiry); + verify(blockHeightManager, never()).handleBlockTimerExpiry(any()); + } + + @Test + public void blockTimerForHeightBelowChainHeadIsDiscarded() { + // Blockchain head has advanced beyond the timer's target height + when(blockChain.getChainHeadBlockNumber()).thenReturn(5L); + final BlockTimerExpiry blockTimerExpiry = new BlockTimerExpiry(roundIdentifier); + constructQbftController(); + qbftController.start(); + qbftController.handleBlockTimerExpiry(blockTimerExpiry); + verify(blockHeightManager, never()).handleBlockTimerExpiry(any()); + } + @Test public void proposalForUnknownValidatorIsDiscarded() { setupProposal(roundIdentifier, unknownValidator); From e16f1725b8e4fa476c1d94604371ab9542834220 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 18 Mar 2026 15:16:35 +1000 Subject: [PATCH 56/77] Fix eth/69 receipt encoding for Frontier tx (#9900) * test: combine snap fixes and enable Besu-to-Besu snap sync test Cherry-pick all snap fixes (#9876, #9877, #9855) and Fabio's CliqueToPoSTest from #9852 with the snap sync section uncommented. * allow pivot distance to be customized for test * request tracking bug * Frontier receipts were never detected as Frontier Signed-off-by: Sally MacFarlane * fix: use getEthSerializedType in SyncTransactionReceiptDecoder for Frontier receipts The decoder was storing Frontier type code as getSerializedType() (0xf8) but the encoder checks getEthSerializedType() (0x00). When a peer sends Frontier receipts with empty type bytes (e.g. reth), the decoder stored 0xf8, the encoder didn't recognize it as Frontier, and encoded it as a typed receipt - producing wrong receipt roots and disconnecting the peer. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Sally MacFarlane * logging improvements for breach of protocol for rlp * Fix typed receipt encoding in putSyncTransactionReceipts Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Claude Opus 4.6 --- CHANGELOG.md | 2 + .../dsl/node/ProcessBesuNodeRunner.java | 2 + .../acceptance/clique/CliqueToPoSTest.java | 35 ++++++--- .../SyncTransactionReceiptDecoder.java | 4 +- .../SyncTransactionReceiptEncoder.java | 7 +- .../ethereum/eth/manager/RequestManager.java | 40 ++++++++-- .../ethereum/eth/manager/EthPeerTest.java | 12 ++- .../eth/manager/RequestManagerTest.java | 77 +++++++++++++++++++ .../task/GetSyncReceiptsFromPeerTaskTest.java | 31 ++++++++ 9 files changed, 182 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 790d2be303f..15c0c748db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ - `PluginTransactionSelectorFactory.create(final SelectorsStateManager selectorsStateManager)` is deprecated for removal ### Bug fixes +- Fix eth/69 snap sync receipt root mismatch by correctly identifying Frontier transaction type in SyncTransactionReceiptEncoder [#9900](https://github.com/hyperledger/besu/pull/9900) +- Fix outstanding request counter leak in RequestManager that could cause peers to appear at capacity [#9900](https://github.com/hyperledger/besu/pull/9900) - BFT forks that change block period on time-based forks don't take effect [9681](https://github.com/hyperledger/besu/issues/9681) - Fix QBFT `RLPException` when decoding proposals from pre-26.1.0 nodes that do not include the `blockAccessList` field [#9977](https://github.com/hyperledger/besu/pull/9977) - Fix eth_simulateV1 discrepancy [9960] (https://github.com/besu-eth/besu/issues/9960) eth_simulateV1 now accepts calls where both input and data diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java index 729b68001ba..421b4219563 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java @@ -162,6 +162,8 @@ private List commandlineArgs(final BesuNode node, final Path dataDir) { } params.add("--sync-min-peers"); params.add(Integer.toString(node.getSynchronizerConfiguration().getSyncMinimumPeerCount())); + params.add("--Xsynchronizer-pivot-distance"); + params.add(Integer.toString(node.getSynchronizerConfiguration().getSyncPivotDistance())); } else { params.add("--sync-mode"); params.add("FULL"); diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueToPoSTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueToPoSTest.java index 913173442fb..c79746302d4 100644 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueToPoSTest.java +++ b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueToPoSTest.java @@ -1,5 +1,5 @@ /* - * Copyright contributors to Besu. + * 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 @@ -19,6 +19,8 @@ import org.hyperledger.besu.ethereum.eth.sync.SyncMode; import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration; +import org.hyperledger.besu.ethereum.eth.sync.snapsync.ImmutableSnapSyncConfiguration; +import org.hyperledger.besu.ethereum.eth.sync.snapsync.SnapSyncConfiguration; import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; @@ -89,6 +91,9 @@ public static void runBesuCommand(final Path dataPath) throws IOException, Inter @Test public void blocksAreBuiltAndNodesSyncAfterSwitchingToPoS() throws Exception { + final SnapSyncConfiguration snapServerEnabledConfig = + ImmutableSnapSyncConfiguration.builder().isSnapServerEnabled(true).build(); + final BesuNode minerNode = besu.createNode( "miner", @@ -103,6 +108,13 @@ public void blocksAreBuiltAndNodesSyncAfterSwitchingToPoS() throws Exception { .engineRpcEnabled(true) .miningEnabled()); + minerNode.setSynchronizerConfiguration( + SynchronizerConfiguration.builder() + .syncMode(SyncMode.FULL) + .syncMinimumPeerCount(1) + .snapSyncConfiguration(snapServerEnabledConfig) + .build()); + // First sync node uses full sync and starts fresh; it does not produce blocks final BesuNode syncNodeFull = besu.createNode( @@ -119,6 +131,7 @@ public void blocksAreBuiltAndNodesSyncAfterSwitchingToPoS() throws Exception { SynchronizerConfiguration.builder() .syncMode(SyncMode.FULL) .syncMinimumPeerCount(1) + .snapSyncConfiguration(snapServerEnabledConfig) .build()) .engineRpcEnabled(true)); @@ -140,6 +153,8 @@ public void blocksAreBuiltAndNodesSyncAfterSwitchingToPoS() throws Exception { SynchronizerConfiguration.builder() .syncMode(SyncMode.SNAP) .syncMinimumPeerCount(1) + .syncPivotDistance(2) + .snapSyncConfiguration(snapServerEnabledConfig) .build()); // Copy key files to the miner node datadir @@ -161,36 +176,32 @@ public void blocksAreBuiltAndNodesSyncAfterSwitchingToPoS() throws Exception { } minerNode.verify(blockchain.currentHeight(10)); - final String block10Hash = latestPayload.get("blockHash").asText(); + final String headHash = latestPayload.get("blockHash").asText(); // Add the full sync node to the cluster so it peers with minerNode cluster.addNode(syncNodeFull); // ensure the node reached TTD first - syncNodeFull.verify(blockchain.minimumHeight(4, 10)); + syncNodeFull.verify(blockchain.minimumHeight(4, 30)); - // A single forkchoiceUpdatedV1 pointing at block 10 kicks off backward sync; + // A single forkchoiceUpdatedV1 pointing at head kicks off backward sync; // syncNodeFull does not have the block yet so it responds SYNCING and begins downloading - triggerSyncViaForkchoiceUpdate(syncNodeFull, block10Hash); + triggerSyncViaForkchoiceUpdate(syncNodeFull, headHash); // Wait for full sync to complete and verify the full chain is present syncNodeFull.verify(blockchain.minimumHeight(10, 30)); - /* TODO: Uncomment when snap sync is fixed to work for small networks - // Add the snap sync node to the cluster so it peers with minerNode cluster.addNode(syncNodeSnap); syncNodeSnap.awaitPeerDiscovery(net.awaitPeerCount(2)); - // A single forkchoiceUpdatedV1 pointing at block 10 kicks off snap sync to pivot; + // A single forkchoiceUpdatedV1 pointing at head kicks off snap sync to pivot; // syncNodeSnap does not have the block yet so it responds SYNCING and begins downloading - triggerSyncViaForkchoiceUpdate(syncNodeSnap, block10Hash); + triggerSyncViaForkchoiceUpdate(syncNodeSnap, headHash); // Wait for snap sync to complete and verify the full chain is present - syncNodeSnap.verify(blockchain.minimumHeight(10, 60)); - - */ + syncNodeSnap.verify(blockchain.minimumHeight(10, 120)); } /** diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/SyncTransactionReceiptDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/SyncTransactionReceiptDecoder.java index 88a9beacfe4..5ccf8155634 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/SyncTransactionReceiptDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/SyncTransactionReceiptDecoder.java @@ -98,7 +98,7 @@ private SyncTransactionReceipt decodeEth69Receipt( final Bytes statusOrStateRoot) { Bytes transactionTypeCode = transactionByteRlp.isEmpty() - ? Bytes.of(TransactionType.FRONTIER.getSerializedType()) + ? Bytes.of(TransactionType.FRONTIER.getEthSerializedType()) : transactionByteRlp; Bytes cumulativeGasUsed = input.readBytes(); List> logs = parseLogs(input); @@ -117,7 +117,7 @@ private SyncTransactionReceipt decodeLegacyReceipt( if (bloomFilter != null) { syncTransactionReceipt = new SyncTransactionReceipt(rawRlp); } else { - Bytes transactionTypeCode = Bytes.of(TransactionType.FRONTIER.getSerializedType()); + Bytes transactionTypeCode = Bytes.of(TransactionType.FRONTIER.getEthSerializedType()); List> logs = parseLogs(input); syncTransactionReceipt = new SyncTransactionReceipt( diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/SyncTransactionReceiptEncoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/SyncTransactionReceiptEncoder.java index 12ccce4d162..a5aaa4c48bb 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/SyncTransactionReceiptEncoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/receipt/SyncTransactionReceiptEncoder.java @@ -32,10 +32,11 @@ public SyncTransactionReceiptEncoder(final SimpleNoCopyRlpEncoder rlpEncoder) { } public Bytes encodeForRootCalculation(final SyncTransactionReceipt receipt) { + final Bytes typeCode = receipt.getTransactionTypeCode(); final boolean isFrontier = - !receipt.getTransactionTypeCode().isEmpty() - && receipt.getTransactionTypeCode().get(0) - == TransactionType.FRONTIER.getSerializedType(); + typeCode.isEmpty() + || typeCode.get(0) == TransactionType.FRONTIER.getEthSerializedType() + || typeCode.get(0) == TransactionType.FRONTIER.getSerializedType(); List encodedLogs = receipt.getLogs().stream() diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/RequestManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/RequestManager.java index 20d7c31dfdd..89f43346253 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/RequestManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/RequestManager.java @@ -27,6 +27,7 @@ import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -67,14 +68,15 @@ public ResponseStream dispatchRequest(final RequestSender sender, final MessageD } public void dispatchResponse(final EthMessage ethMessage) { - final Collection streams = List.copyOf(responseStreams.values()); - final int count = outstandingRequests.decrementAndGet(); try { final Map.Entry requestIdAndEthMessage = ethMessage.getData().unwrapMessageData(); Optional.ofNullable(responseStreams.get(requestIdAndEthMessage.getKey())) .ifPresentOrElse( - responseStream -> responseStream.processMessage(requestIdAndEthMessage.getValue()), + responseStream -> { + responseStream.releaseOutstandingRequest(); + responseStream.processMessage(requestIdAndEthMessage.getValue()); + }, // Consider incorrect requestIds to be a useless response; too // many of these and we will disconnect. () -> peer.recordUselessResponse("Request ID incorrect")); @@ -90,9 +92,9 @@ public void dispatchResponse(final EthMessage ethMessage) { DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); } - if (count == 0) { + if (outstandingRequests.get() == 0) { // No possibility of any remaining outstanding messages - closeOutstandingStreams(streams); + closeOutstandingStreams(List.copyOf(responseStreams.values())); } } @@ -101,7 +103,15 @@ public void close() { } private ResponseStream createStream(final BigInteger requestId) { - final ResponseStream stream = new ResponseStream(peer, () -> deregisterStream(requestId)); + final AtomicBoolean released = new AtomicBoolean(false); + final Runnable releaser = + () -> { + if (released.compareAndSet(false, true)) { + outstandingRequests.decrementAndGet(); + } + }; + final ResponseStream stream = + new ResponseStream(peer, () -> deregisterStream(requestId), releaser); responseStreams.put(requestId, stream); return stream; } @@ -152,13 +162,18 @@ private Response(final boolean closed, final MessageData message) { public static class ResponseStream { private final EthPeer peer; private final DeregistrationProcessor deregisterCallback; + private final Runnable outstandingRequestReleaser; private final Queue bufferedResponses = new ConcurrentLinkedQueue<>(); private volatile boolean closed = false; private volatile ResponseCallback responseCallback = null; - public ResponseStream(final EthPeer peer, final DeregistrationProcessor deregisterCallback) { + public ResponseStream( + final EthPeer peer, + final DeregistrationProcessor deregisterCallback, + final Runnable outstandingRequestReleaser) { this.peer = peer; this.deregisterCallback = deregisterCallback; + this.outstandingRequestReleaser = outstandingRequestReleaser; } public ResponseStream then(final ResponseCallback callback) { @@ -172,12 +187,23 @@ public ResponseStream then(final ResponseCallback callback) { return this; } + /** + * Releases the outstanding request capacity for this stream. Uses CAS to ensure exactly-once + * semantics: either dispatchResponse() or close() will release the capacity, but not both. + */ + void releaseOutstandingRequest() { + outstandingRequestReleaser.run(); + } + public void close() { if (closed) { return; } closed = true; deregisterCallback.exec(); + // Release outstanding request capacity if not already released by dispatchResponse(). + // This handles the case where a request times out without receiving a response. + outstandingRequestReleaser.run(); bufferedResponses.add(new Response(true, null)); dispatchBufferedResponses(); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthPeerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthPeerTest.java index 611d4a72cd2..5ba50aab9a7 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthPeerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthPeerTest.java @@ -123,7 +123,11 @@ public void shouldHaveAvailableCapacityUntilOutstandingRequestLimitIsReached() assertThat(peer.hasAvailableRequestCapacity()).isFalse(); assertThat(peer.outstandingRequests()).isEqualTo(5); - peer.dispatch(new EthMessage(peer, BlockBodiesMessage.create(emptyList()))); + peer.dispatch( + new EthMessage( + peer, + BlockBodiesMessage.create(emptyList()) + .wrapMessageData(java.math.BigInteger.valueOf(1)))); assertThat(peer.hasAvailableRequestCapacity()).isTrue(); assertThat(peer.outstandingRequests()).isEqualTo(4); } @@ -424,7 +428,7 @@ private void messageStream( // Dispatch unrelated message and check that it is not process EthMessage otherEthMessage = - new EthMessage(peer, otherMessage.wrapMessageData(BigInteger.valueOf(requestIdCounter++))); + new EthMessage(peer, otherMessage.wrapMessageData(BigInteger.valueOf(999))); peer.dispatch(otherEthMessage); assertThat(messageCount.get()).isEqualTo(1); assertThat(closedCount.get()).isEqualTo(0); @@ -433,7 +437,7 @@ private void messageStream( new EthMessage(peer, targetMessage.wrapMessageData(BigInteger.valueOf(requestIdCounter++))); // Dispatch last outstanding message and check that streams are closed peer.dispatch(targetEthMessage); - assertThat(messageCount.get()).isEqualTo(1); + assertThat(messageCount.get()).isEqualTo(2); assertThat(closedCount.get()).isEqualTo(2); targetEthMessage = @@ -441,7 +445,7 @@ private void messageStream( // Check that no new messages are delivered getStream.get(peer); peer.dispatch(targetEthMessage); - assertThat(messageCount.get()).isEqualTo(1); + assertThat(messageCount.get()).isEqualTo(2); assertThat(closedCount.get()).isEqualTo(2); targetEthMessage = diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RequestManagerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RequestManagerTest.java index 8bb81467bb6..04ec6c00dc6 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RequestManagerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RequestManagerTest.java @@ -230,6 +230,20 @@ private EthPeer createPeer() { Bytes.random(64)); } + @Test + public void recordsUselessResponseWhenRequestIdDoesNotMatchOutstandingRequest() throws Exception { + final EthPeer peer = createPeer(); + final RequestManager requestManager = new RequestManager(peer, EthProtocol.NAME); + + // Dispatch responses with request IDs that were never issued - each should record a useless + // response. After USELESS_RESPONSE_THRESHOLD useless responses the peer should be disconnected. + for (int i = 0; i < PeerReputation.USELESS_RESPONSE_THRESHOLD; i++) { + requestManager.dispatchResponse(mockMessage(peer)); + } + + assertThat(peer.isDisconnected()).isTrue(); + } + @Test public void disconnectsPeerOnBadMessage() throws Exception { final EthPeer peer = createPeer(); @@ -251,4 +265,67 @@ public void disconnectsPeerOnBadMessage() throws Exception { requestManager.dispatchResponse(mockMessage); assertThat(peer.isDisconnected()).isTrue(); } + + @Test + public void closingStreamWithoutResponseReleasesOutstandingRequest() throws Exception { + final EthPeer peer = createPeer(); + final RequestManager requestManager = new RequestManager(peer, EthProtocol.NAME); + + final RequestManager.RequestSender sender = __ -> {}; + // Send a request - outstanding should go to 1 + final RequestManager.ResponseStream stream = + requestManager.dispatchRequest(sender, new RawMessage(0x01, Bytes.EMPTY)); + assertThat(requestManager.outstandingRequests()).isEqualTo(1); + + // Close the stream without dispatching a response (simulates timeout) + stream.close(); + + // Outstanding requests should be released back to 0 + assertThat(requestManager.outstandingRequests()).isEqualTo(0); + } + + @Test + public void closingStreamAfterResponseDoesNotDoubleDecrement() throws Exception { + final EthPeer peer = createPeer(); + final RequestManager requestManager = new RequestManager(peer, EthProtocol.NAME); + + final RequestManager.RequestSender sender = __ -> {}; + // Send a request + final RequestManager.ResponseStream stream = + requestManager.dispatchRequest(sender, new RawMessage(0x01, Bytes.EMPTY)); + assertThat(requestManager.outstandingRequests()).isEqualTo(1); + + // Dispatch response - outstanding should go to 0 + final EthMessage mockMessage = mockMessage(peer); + requestManager.dispatchResponse(mockMessage); + assertThat(requestManager.outstandingRequests()).isEqualTo(0); + + // Close the stream after response was received - should not go negative + stream.close(); + assertThat(requestManager.outstandingRequests()).isEqualTo(0); + } + + @Test + public void multipleTimedOutRequestsAllReleaseCapacity() throws Exception { + final EthPeer peer = createPeer(); + final RequestManager requestManager = new RequestManager(peer, EthProtocol.NAME); + + final RequestManager.RequestSender sender = __ -> {}; + // Send 3 requests + final RequestManager.ResponseStream stream1 = + requestManager.dispatchRequest(sender, new RawMessage(0x01, Bytes.EMPTY)); + final RequestManager.ResponseStream stream2 = + requestManager.dispatchRequest(sender, new RawMessage(0x01, Bytes.EMPTY)); + final RequestManager.ResponseStream stream3 = + requestManager.dispatchRequest(sender, new RawMessage(0x01, Bytes.EMPTY)); + assertThat(requestManager.outstandingRequests()).isEqualTo(3); + + // Close all streams without responses (simulates 3 timeouts) + stream1.close(); + assertThat(requestManager.outstandingRequests()).isEqualTo(2); + stream2.close(); + assertThat(requestManager.outstandingRequests()).isEqualTo(1); + stream3.close(); + assertThat(requestManager.outstandingRequests()).isEqualTo(0); + } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java index f8da0991440..2737cc15a67 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java @@ -34,7 +34,9 @@ import org.hyperledger.besu.ethereum.core.SyncBlockBody; import org.hyperledger.besu.ethereum.core.SyncTransactionReceipt; import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.core.encoding.receipt.SyncTransactionReceiptDecoder; import org.hyperledger.besu.ethereum.core.encoding.receipt.SyncTransactionReceiptEncoder; +import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.core.Utils; @@ -424,6 +426,35 @@ public void validateResultFailsReceiptRootDoesNotMatch() { List.of()))); } + @Test + public void validateResultPassesForFrontierReceiptsAfterDecoderRoundTrip() { + // Regression test for eth/69 Frontier receipt root calculation. + // Exercises the full decode path (encodeForRootCalculation is invoked) rather than + // the single-arg constructor path used by toResponseReceipt() in other tests. + final MockedBlock block = mockBlock(1, 2); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(block.block), List.of()), protocolSchedule); + + final SyncTransactionReceiptDecoder decoder = new SyncTransactionReceiptDecoder(); + final List decodedReceipts = + block.receipts.stream() + .map( + receipt -> { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + TransactionReceiptEncoder.writeTo( + receipt, + out, + TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + return decoder.decode(out.encoded()); + }) + .toList(); + + assertEquals( + PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD, + task.validateResult(new Response(Map.of(block.block, decodedReceipts), List.of()))); + } + @Test public void validateResultSuccessWhenPartialBlockIsIncomplete() { final MockedBlock mockedBlock = mockBlock(1, 3); From 2ea5ad96e80f8451c0c30308e8508f43f440abb0 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 18 Mar 2026 15:54:04 +1000 Subject: [PATCH 57/77] fallback to resync (#10019) Signed-off-by: Sally MacFarlane --- .../eth/sync/DefaultSynchronizer.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DefaultSynchronizer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DefaultSynchronizer.java index 937e95ee3e0..f91facb5151 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DefaultSynchronizer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DefaultSynchronizer.java @@ -20,6 +20,7 @@ import org.hyperledger.besu.consensus.merge.UnverifiedForkchoiceListener; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.Synchronizer; import org.hyperledger.besu.ethereum.eth.manager.ChainHeadEstimate; import org.hyperledger.besu.ethereum.eth.manager.EthContext; @@ -236,11 +237,24 @@ private CompletableFuture handleSyncResult(final PivotSyncState result) { syncState.markInitialSyncPhaseAsDone(); } else { fastSyncDownloader.ifPresent(PivotSyncDownloader::deletePivotSyncState); - result - .getPivotBlockHeader() - .ifPresent( - blockHeader -> - protocolContext.getWorldStateArchive().resetArchiveStateTo(blockHeader)); + final Optional maybePivotHeader = result.getPivotBlockHeader(); + maybePivotHeader.ifPresent( + blockHeader -> protocolContext.getWorldStateArchive().resetArchiveStateTo(blockHeader)); + + if (maybePivotHeader + .map( + bh -> + !protocolContext + .getWorldStateArchive() + .isWorldStateAvailable(bh.getStateRoot(), bh.getHash())) + .orElse(false)) { + LOG.warn( + "World state not available for pivot block {} after snap sync completion, resyncing", + maybePivotHeader.map(BlockHeader::toLogString).orElse("unknown")); + resyncWorldState(); + return CompletableFuture.completedFuture(null); + } + if (result.hasPivotBlockHash()) LOG.info( "Sync completed successfully with pivot block {}", From 473613f0c20cc95c598619ffa07e63efad9b0b17 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 18 Mar 2026 16:16:51 +1000 Subject: [PATCH 58/77] Revert tracer aggregator (#10068) * Revert "Tracer aggregator (#9745)" This reverts commit 219b28ea029ce99596fc0c56f1d026793704e6b5. TracerAggregator iterates a List on every per-opcode call (tracePreExecution, tracePostExecution, traceContextEnter, etc.), which is a performance hit on the hot path. The InterruptibleOperationTracer single-delegate pattern is preferred for tracer composition. EthTransferLogOperationTracer and custom tracers remain mutually exclusive in BlockSimulator for now (isTraceTransfers will go away with Glamsterdam). Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .../ethereum/transaction/BlockSimulator.java | 17 +- .../BlockStateCallSimulationResult.java | 16 +- .../transaction/TransactionSimulator.java | 5 +- .../besu/evm/tracing/TracerAggregator.java | 282 -------------- .../evm/tracing/TracerAggregatorTest.java | 349 ------------------ 5 files changed, 20 insertions(+), 649 deletions(-) delete mode 100644 evm/src/main/java/org/hyperledger/besu/evm/tracing/TracerAggregator.java delete mode 100644 evm/src/test/java/org/hyperledger/besu/evm/tracing/TracerAggregatorTest.java diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java index 585c3fff670..d7dc6e0967b 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java @@ -63,7 +63,6 @@ import org.hyperledger.besu.evm.blockhash.BlockHashLookup; import org.hyperledger.besu.evm.tracing.EthTransferLogOperationTracer; import org.hyperledger.besu.evm.tracing.OperationTracer; -import org.hyperledger.besu.evm.tracing.TracerAggregator; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import org.hyperledger.besu.plugin.data.BlockOverrides; @@ -365,13 +364,17 @@ protected BlockStateCallSimulationResult processTransactions( final WorldUpdater transactionUpdater = blockUpdater.updater(); final CallParameter callParameter = blockStateCall.getCalls().get(transactionLocation); - // Always use TracerAggregator, optionally adding EthTransferLogOperationTracer - final TracerAggregator finalOperationTracer; + // Custom tracer and EthTraceTransfers are mutually exclusive + OperationTracer finalOperationTracer = operationTracer; if (isTraceTransfers) { - finalOperationTracer = - TracerAggregator.combining(operationTracer, new EthTransferLogOperationTracer()); - } else { - finalOperationTracer = TracerAggregator.combining(operationTracer); + if (finalOperationTracer == OperationTracer.NO_TRACING) { + finalOperationTracer = new EthTransferLogOperationTracer(); + } else { + // this shouldn't happen, and isTraceTransfers will go away with Glamsterdam + throw new IllegalArgumentException( + "A custom tracer and traceTransfers cannot be used together." + + " Disable traceTransfers or omit the custom tracer."); + } } long gasLimit = diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCallSimulationResult.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCallSimulationResult.java index fa40c9055b6..78ead789146 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCallSimulationResult.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCallSimulationResult.java @@ -23,7 +23,7 @@ import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.tracing.EthTransferLogOperationTracer; -import org.hyperledger.besu.evm.tracing.TracerAggregator; +import org.hyperledger.besu.evm.tracing.OperationTracer; import java.util.ArrayList; import java.util.List; @@ -68,12 +68,13 @@ public long getCumulativeBlobGasUsed() { * * @param result the transaction simulation result * @param worldState the world state after the transaction - * @param tracerAggregator the tracer aggregator used for the transaction + * @param operationTracer the tracer used for the transaction; if it is an {@link + * EthTransferLogOperationTracer}, its logs are used in place of the receipt logs */ public void add( final TransactionSimulatorResult result, final MutableWorldState worldState, - final TracerAggregator tracerAggregator) { + final OperationTracer operationTracer) { Objects.requireNonNull(result, "TransactionSimulatorResult cannot be null"); Objects.requireNonNull(worldState, "WorldState cannot be null"); @@ -88,11 +89,10 @@ public void add( transactionReceiptFactory.create( result.transaction().getType(), result.result(), worldState, cumulativeGasUsed); - final List logs = - tracerAggregator - .findTracer(EthTransferLogOperationTracer.class) - .map(EthTransferLogOperationTracer::getLogs) - .orElse(transactionReceipt.getLogsList()); + List logs = + (operationTracer instanceof EthTransferLogOperationTracer) + ? ((EthTransferLogOperationTracer) operationTracer).getLogs() + : transactionReceipt.getLogsList(); transactionSimulatorResults.add( new TransactionSimulatorResultWithMetadata( diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java index 8e0572bb6df..69cb298220c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java @@ -48,7 +48,6 @@ 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.tracing.TracerAggregator; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.math.BigInteger; @@ -153,7 +152,7 @@ public Optional processOnPending( // in order to trace the state diff we need to make sure that // the world updater always has a parent - if (TracerAggregator.hasTracer(operationTracer, DebugOperationTracer.class)) { + if (operationTracer instanceof DebugOperationTracer) { updater = updater.parentUpdater().isPresent() ? updater : updater.updater(); } @@ -290,7 +289,7 @@ public Optional process( } // in order to trace the state diff we need to make sure that // the world updater always has a parent - if (TracerAggregator.hasTracer(operationTracer, DebugOperationTracer.class)) { + if (operationTracer instanceof DebugOperationTracer) { updater = updater.parentUpdater().isPresent() ? updater : updater.updater(); } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/tracing/TracerAggregator.java b/evm/src/main/java/org/hyperledger/besu/evm/tracing/TracerAggregator.java deleted file mode 100644 index 4e408513e14..00000000000 --- a/evm/src/main/java/org/hyperledger/besu/evm/tracing/TracerAggregator.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * 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.evm.tracing; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Log; -import org.hyperledger.besu.datatypes.Transaction; -import org.hyperledger.besu.datatypes.Wei; -import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; -import org.hyperledger.besu.evm.frame.MessageFrame; -import org.hyperledger.besu.evm.operation.Operation.OperationResult; -import org.hyperledger.besu.evm.worldstate.WorldView; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import org.apache.tuweni.bytes.Bytes; - -/** - * A standard OperationTracer implementation that aggregates multiple tracers and delegates all - * tracing calls to them in sequence. - * - *

    This provides a consistent way to compose multiple tracers throughout Besu, replacing ad-hoc - * composite tracer implementations with a standardized solution. - */ -public class TracerAggregator implements OperationTracer { - - private final List tracers; - - /** - * Create a TracerAggregator from a list of tracers. - * - * @param tracers the tracers to aggregate - */ - public TracerAggregator(final List tracers) { - this.tracers = Collections.unmodifiableList(tracers); - } - - /** - * Create a TracerAggregator from an array of tracers. - * - * @param tracers the tracers to aggregate - */ - public TracerAggregator(final OperationTracer... tracers) { - this.tracers = Collections.unmodifiableList(Arrays.asList(tracers)); - } - - /** - * Create a TracerAggregator that combines an existing tracer with additional tracers. This is - * useful for adding tracers to an existing tracing setup. - * - * @param baseTracer the base tracer to extend - * @param additionalTracers additional tracers to add - * @return a new TracerAggregator combining all tracers - */ - public static TracerAggregator combining( - final OperationTracer baseTracer, final OperationTracer... additionalTracers) { - if (baseTracer == OperationTracer.NO_TRACING) { - return new TracerAggregator(additionalTracers); - } - - final OperationTracer[] allTracers = new OperationTracer[additionalTracers.length + 1]; - allTracers[0] = baseTracer; - System.arraycopy(additionalTracers, 0, allTracers, 1, additionalTracers.length); - - return new TracerAggregator(allTracers); - } - - /** - * Create a TracerAggregator from multiple tracers, filtering out NO_TRACING instances. - * - * @param tracers the tracers to aggregate - * @return a TracerAggregator, or OperationTracer.NO_TRACING if no actual tracers are provided - */ - public static OperationTracer of(final OperationTracer... tracers) { - final OperationTracer[] filteredTracers = - Arrays.stream(tracers) - .filter(tracer -> tracer != OperationTracer.NO_TRACING) - .toArray(OperationTracer[]::new); - - if (filteredTracers.length == 0) { - return OperationTracer.NO_TRACING; - } else if (filteredTracers.length == 1) { - return filteredTracers[0]; - } else { - return new TracerAggregator(filteredTracers); - } - } - - @Override - public void tracePreExecution(final MessageFrame frame) { - for (final OperationTracer tracer : tracers) { - tracer.tracePreExecution(frame); - } - } - - @Override - public void tracePostExecution(final MessageFrame frame, final OperationResult operationResult) { - for (final OperationTracer tracer : tracers) { - tracer.tracePostExecution(frame, operationResult); - } - } - - @Override - public void tracePrecompileCall( - final MessageFrame frame, final long gasRequirement, final Bytes output) { - for (final OperationTracer tracer : tracers) { - tracer.tracePrecompileCall(frame, gasRequirement, output); - } - } - - @Override - public void traceAccountCreationResult( - final MessageFrame frame, final Optional haltReason) { - for (final OperationTracer tracer : tracers) { - tracer.traceAccountCreationResult(frame, haltReason); - } - } - - @Override - public void tracePrepareTransaction(final WorldView worldView, final Transaction transaction) { - for (final OperationTracer tracer : tracers) { - tracer.tracePrepareTransaction(worldView, transaction); - } - } - - @Override - public void traceStartTransaction(final WorldView worldView, final Transaction transaction) { - for (final OperationTracer tracer : tracers) { - tracer.traceStartTransaction(worldView, transaction); - } - } - - @Override - public void traceBeforeRewardTransaction( - final WorldView worldView, final Transaction tx, final Wei miningReward) { - for (final OperationTracer tracer : tracers) { - tracer.traceBeforeRewardTransaction(worldView, tx, miningReward); - } - } - - @Override - public void traceEndTransaction( - final WorldView worldView, - final Transaction tx, - final boolean status, - final Bytes output, - final List logs, - final long gasUsed, - final Set

    selfDestructs, - final long timeNs) { - for (final OperationTracer tracer : tracers) { - tracer.traceEndTransaction( - worldView, tx, status, output, logs, gasUsed, selfDestructs, timeNs); - } - } - - @Override - public void traceContextEnter(final MessageFrame frame) { - for (final OperationTracer tracer : tracers) { - tracer.traceContextEnter(frame); - } - } - - @Override - public void traceContextReEnter(final MessageFrame frame) { - for (final OperationTracer tracer : tracers) { - tracer.traceContextReEnter(frame); - } - } - - @Override - public void traceContextExit(final MessageFrame frame) { - for (final OperationTracer tracer : tracers) { - tracer.traceContextExit(frame); - } - } - - @Override - public boolean isExtendedTracing() { - for (final OperationTracer tracer : tracers) { - if (tracer.isExtendedTracing()) { - return true; - } - } - return false; - } - - @Override - public List getTraceFrames() { - // Return trace frames from the first tracer that provides them - for (final OperationTracer tracer : tracers) { - final List frames = tracer.getTraceFrames(); - if (!frames.isEmpty()) { - return frames; - } - } - return Collections.emptyList(); - } - - /** - * Get the list of aggregated tracers. - * - * @return an unmodifiable list of the tracers - */ - public List getTracers() { - return tracers; - } - - /** - * Find the first tracer of the specified type. - * - * @param the tracer type - * @param tracerClass the class of the tracer to find - * @return an Optional containing the tracer if found - */ - @SuppressWarnings("unchecked") - public Optional findTracer(final Class tracerClass) { - for (final OperationTracer tracer : tracers) { - if (tracerClass.isInstance(tracer)) { - return Optional.of((T) tracer); - } - // If this is another TracerAggregator, search recursively - if (tracer instanceof TracerAggregator) { - final Optional found = ((TracerAggregator) tracer).findTracer(tracerClass); - if (found.isPresent()) { - return found; - } - } - } - return Optional.empty(); - } - - /** - * Check if this aggregator contains a tracer of the specified type. - * - * @param tracerClass the class of the tracer to check for - * @return true if a tracer of the specified type is present - */ - public boolean hasTracer(final Class tracerClass) { - return findTracer(tracerClass).isPresent(); - } - - /** - * Static utility method to check if an operation tracer contains a tracer of the specified type, - * either directly or within an aggregated tracer. - * - * @param operationTracer the tracer to check - * @param tracerClass the class of the tracer to check for - * @return true if a tracer of the specified type is present - */ - public static boolean hasTracer( - final OperationTracer operationTracer, final Class tracerClass) { - // Check if the tracer is directly of the specified type - if (tracerClass.isInstance(operationTracer)) { - return true; - } - - // Check if it's a TracerAggregator that might contain the specified tracer type - if (operationTracer instanceof TracerAggregator) { - final TracerAggregator aggregator = (TracerAggregator) operationTracer; - return aggregator.hasTracer(tracerClass); - } - - return false; - } -} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/tracing/TracerAggregatorTest.java b/evm/src/test/java/org/hyperledger/besu/evm/tracing/TracerAggregatorTest.java deleted file mode 100644 index 84c131538fd..00000000000 --- a/evm/src/test/java/org/hyperledger/besu/evm/tracing/TracerAggregatorTest.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * 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.evm.tracing; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Log; -import org.hyperledger.besu.datatypes.Transaction; -import org.hyperledger.besu.evm.frame.MessageFrame; -import org.hyperledger.besu.evm.operation.Operation.OperationResult; -import org.hyperledger.besu.evm.worldstate.WorldView; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.Test; - -public class TracerAggregatorTest { - - @Test - void shouldCreateEmptyAggregatorFromNoTracers() { - final OperationTracer aggregator = TracerAggregator.of(); - - assertThat(aggregator).isEqualTo(OperationTracer.NO_TRACING); - } - - @Test - void shouldReturnSingleTracerWhenOnlyOneProvided() { - final OperationTracer mockTracer = mock(OperationTracer.class); - - final OperationTracer result = TracerAggregator.of(mockTracer); - - assertThat(result).isSameAs(mockTracer); - } - - @Test - void shouldCreateAggregatorFromMultipleTracers() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - - final OperationTracer result = TracerAggregator.of(tracer1, tracer2); - - assertThat(result).isInstanceOf(TracerAggregator.class); - final TracerAggregator aggregator = (TracerAggregator) result; - assertThat(aggregator.getTracers()).containsExactly(tracer1, tracer2); - } - - @Test - void shouldFilterOutNoTracingInstances() { - final OperationTracer mockTracer = mock(OperationTracer.class); - - final OperationTracer result = - TracerAggregator.of(OperationTracer.NO_TRACING, mockTracer, OperationTracer.NO_TRACING); - - assertThat(result).isSameAs(mockTracer); - } - - @Test - void shouldDelegateTracePreExecutionToAllTracers() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - final MessageFrame frame = mock(MessageFrame.class); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - aggregator.tracePreExecution(frame); - - verify(tracer1).tracePreExecution(frame); - verify(tracer2).tracePreExecution(frame); - } - - @Test - void shouldDelegateTracePostExecutionToAllTracers() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - final MessageFrame frame = mock(MessageFrame.class); - final OperationResult result = mock(OperationResult.class); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - aggregator.tracePostExecution(frame, result); - - verify(tracer1).tracePostExecution(frame, result); - verify(tracer2).tracePostExecution(frame, result); - } - - @Test - void shouldDelegateTracePrecompileCallToAllTracers() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - final MessageFrame frame = mock(MessageFrame.class); - final long gasRequirement = 100L; - final Bytes output = Bytes.of(1, 2, 3); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - aggregator.tracePrecompileCall(frame, gasRequirement, output); - - verify(tracer1).tracePrecompileCall(frame, gasRequirement, output); - verify(tracer2).tracePrecompileCall(frame, gasRequirement, output); - } - - @Test - void shouldDelegateTracePrepareTransactionToAllTracers() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - final WorldView worldView = mock(WorldView.class); - final Transaction transaction = mock(Transaction.class); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - aggregator.tracePrepareTransaction(worldView, transaction); - - verify(tracer1).tracePrepareTransaction(worldView, transaction); - verify(tracer2).tracePrepareTransaction(worldView, transaction); - } - - @Test - void shouldDelegateTraceStartTransactionToAllTracers() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - final WorldView worldView = mock(WorldView.class); - final Transaction transaction = mock(Transaction.class); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - aggregator.traceStartTransaction(worldView, transaction); - - verify(tracer1).traceStartTransaction(worldView, transaction); - verify(tracer2).traceStartTransaction(worldView, transaction); - } - - @Test - void shouldDelegateTraceEndTransactionToAllTracers() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - final WorldView worldView = mock(WorldView.class); - final Transaction transaction = mock(Transaction.class); - final boolean status = true; - final Bytes output = Bytes.of(1, 2, 3); - final List logs = Collections.emptyList(); - final long gasUsed = 1000L; - final Set
    selfDestructs = Set.of(); - final long timeNs = 123456L; - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - aggregator.traceEndTransaction( - worldView, transaction, status, output, logs, gasUsed, selfDestructs, timeNs); - - verify(tracer1) - .traceEndTransaction( - worldView, transaction, status, output, logs, gasUsed, selfDestructs, timeNs); - verify(tracer2) - .traceEndTransaction( - worldView, transaction, status, output, logs, gasUsed, selfDestructs, timeNs); - } - - @Test - void shouldReturnTrueForExtendedTracingIfAnyTracerReturnsTrue() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - - when(tracer1.isExtendedTracing()).thenReturn(false); - when(tracer2.isExtendedTracing()).thenReturn(true); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - assertThat(aggregator.isExtendedTracing()).isTrue(); - } - - @Test - void shouldReturnFalseForExtendedTracingIfNoTracerReturnsTrue() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - - when(tracer1.isExtendedTracing()).thenReturn(false); - when(tracer2.isExtendedTracing()).thenReturn(false); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - assertThat(aggregator.isExtendedTracing()).isFalse(); - } - - @Test - void shouldReturnTraceFramesFromFirstTracerThatProvidesNonEmptyFrames() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - final OperationTracer tracer3 = mock(OperationTracer.class); - - final List expectedFrames = Arrays.asList(mock(TraceFrame.class)); - - when(tracer1.getTraceFrames()).thenReturn(Collections.emptyList()); - when(tracer2.getTraceFrames()).thenReturn(expectedFrames); - when(tracer3.getTraceFrames()).thenReturn(Collections.emptyList()); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2, tracer3); - - assertThat(aggregator.getTraceFrames()).isSameAs(expectedFrames); - } - - @Test - void shouldFindTracerByType() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final EthTransferLogOperationTracer tracer2 = mock(EthTransferLogOperationTracer.class); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - final Optional found = - aggregator.findTracer(EthTransferLogOperationTracer.class); - - assertThat(found).isPresent(); - assertThat(found.get()).isSameAs(tracer2); - } - - @Test - void shouldReturnEmptyWhenTracerNotFound() { - final OperationTracer tracer1 = mock(OperationTracer.class); - - final TracerAggregator aggregator = new TracerAggregator(tracer1); - - final Optional found = - aggregator.findTracer(EthTransferLogOperationTracer.class); - - assertThat(found).isEmpty(); - } - - @Test - void shouldFindTracerRecursivelyInNestedAggregators() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final EthTransferLogOperationTracer tracer2 = mock(EthTransferLogOperationTracer.class); - final OperationTracer tracer3 = mock(OperationTracer.class); - - final TracerAggregator nestedAggregator = new TracerAggregator(tracer2, tracer3); - final TracerAggregator mainAggregator = new TracerAggregator(tracer1, nestedAggregator); - - final Optional found = - mainAggregator.findTracer(EthTransferLogOperationTracer.class); - - assertThat(found).isPresent(); - assertThat(found.get()).isSameAs(tracer2); - } - - @Test - void shouldCheckHasTracerCorrectly() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final EthTransferLogOperationTracer tracer2 = mock(EthTransferLogOperationTracer.class); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - assertThat(aggregator.hasTracer(EthTransferLogOperationTracer.class)).isTrue(); - // Test for a tracer type that doesn't exist in this aggregator - abstract class NonExistentTracer implements OperationTracer {} - assertThat(aggregator.hasTracer(NonExistentTracer.class)).isFalse(); - } - - @Test - void shouldCombineTracersCorrectlyWithNoTracing() { - final EthTransferLogOperationTracer tracer = mock(EthTransferLogOperationTracer.class); - - final TracerAggregator result = TracerAggregator.combining(OperationTracer.NO_TRACING, tracer); - - assertThat(result.getTracers()).containsExactly(tracer); - } - - @Test - void shouldCombineTracersCorrectlyWithExistingTracer() { - final OperationTracer baseTracer = mock(OperationTracer.class); - final EthTransferLogOperationTracer additionalTracer = - mock(EthTransferLogOperationTracer.class); - - final TracerAggregator result = TracerAggregator.combining(baseTracer, additionalTracer); - - assertThat(result.getTracers()).containsExactly(baseTracer, additionalTracer); - } - - @Test - void shouldMaintainTracerOrder() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final OperationTracer tracer2 = mock(OperationTracer.class); - final OperationTracer tracer3 = mock(OperationTracer.class); - - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2, tracer3); - final MessageFrame frame = mock(MessageFrame.class); - - aggregator.tracePreExecution(frame); - - // Verify that tracers are called in the correct order - final var inOrder = org.mockito.Mockito.inOrder(tracer1, tracer2, tracer3); - inOrder.verify(tracer1).tracePreExecution(frame); - inOrder.verify(tracer2).tracePreExecution(frame); - inOrder.verify(tracer3).tracePreExecution(frame); - } - - @Test - void staticHasTracerShouldCheckDirectTracer() { - final EthTransferLogOperationTracer tracer = mock(EthTransferLogOperationTracer.class); - - assertThat(TracerAggregator.hasTracer(tracer, EthTransferLogOperationTracer.class)).isTrue(); - assertThat(TracerAggregator.hasTracer(tracer, OperationTracer.class)).isTrue(); - - // Test for a tracer type that doesn't match - abstract class NonExistentTracer implements OperationTracer {} - assertThat(TracerAggregator.hasTracer(tracer, NonExistentTracer.class)).isFalse(); - } - - @Test - void staticHasTracerShouldCheckAggregatedTracer() { - final OperationTracer tracer1 = mock(OperationTracer.class); - final EthTransferLogOperationTracer tracer2 = mock(EthTransferLogOperationTracer.class); - final TracerAggregator aggregator = new TracerAggregator(tracer1, tracer2); - - assertThat(TracerAggregator.hasTracer(aggregator, EthTransferLogOperationTracer.class)) - .isTrue(); - assertThat(TracerAggregator.hasTracer(aggregator, OperationTracer.class)).isTrue(); - - // Test for a tracer type that doesn't exist in the aggregator - abstract class NonExistentTracer implements OperationTracer {} - assertThat(TracerAggregator.hasTracer(aggregator, NonExistentTracer.class)).isFalse(); - } - - @Test - void staticHasTracerShouldReturnFalseForNoTracing() { - assertThat( - TracerAggregator.hasTracer( - OperationTracer.NO_TRACING, EthTransferLogOperationTracer.class)) - .isFalse(); - } -} From 0d1e19ce9696502f190a9203ccf38c3832521998 Mon Sep 17 00:00:00 2001 From: Karim Taam Date: Wed, 18 Mar 2026 10:43:29 +0400 Subject: [PATCH 59/77] Add tests for parallel block processing (optimistic and BAL) (#10010) Signed-off-by: Karim Taam --- .github/workflows/acceptance-tests.yml | 2 +- .github/workflows/pre-review.yml | 2 +- .github/workflows/splitTestsByTime.sh | 2 +- .../MainnetParallelBlockProcessor.java | 1 - ...lelizedConcurrentTransactionProcessor.java | 27 +- .../AbstractContractStorageTest.java | 166 ++++++ .../AbstractMiningBeneficiaryBalTest.java | 114 ++++ ...ParallelBlockProcessorIntegrationTest.java | 424 ++++++++++++++ .../AbstractSimpleTransferTest.java | 200 +++++++ .../AbstractStorageDependencyTest.java | 164 ++++++ ...ParallelBlockProcessorIntegrationTest.java | 291 ++++++++++ .../BalTransactionProcessorUnitTest.java | 456 +++++++++++++++ ...ParallelBlockProcessorIntegrationTest.java | 125 ++++ ...ptimisticTransactionProcessorUnitTest.java | 495 ++++++++++++++++ .../ParallelBlockProcessorTestSupport.java | 59 ++ ...ParallelBlockTransactionProcessorTest.java | 542 ------------------ .../mainnet/parallelization/genesis-it.json | 264 +++++++++ 17 files changed, 2776 insertions(+), 558 deletions(-) create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractContractStorageTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractMiningBeneficiaryBalTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractParallelBlockProcessorIntegrationTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractSimpleTransferTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractStorageDependencyTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalParallelBlockProcessorIntegrationTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalTransactionProcessorUnitTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticParallelBlockProcessorIntegrationTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticTransactionProcessorUnitTest.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockProcessorTestSupport.java delete mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockTransactionProcessorTest.java create mode 100644 ethereum/core/src/test/resources/org/hyperledger/besu/ethereum/mainnet/parallelization/genesis-it.json diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 178f9446aec..e53c2d67130 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -47,7 +47,7 @@ jobs: - name: List all acceptance tests run: ./gradlew acceptanceTest --test-dry-run -Dorg.gradle.parallel=true -Dorg.gradle.caching=true - name: Extract current test list - run: mkdir tmp; find . -type f -name TEST-*.xml | xargs -I{} bash -c "xmlstarlet sel -t -v '/testsuite/@name' '{}'; echo ' acceptanceTest'" | tee tmp/currentTests.list + run: mkdir tmp; find . -type f -name TEST-*.xml | xargs -I{} bash -c "xmlstarlet sel -t -v '/testsuite/testcase[1]/@classname' '{}'; echo ' acceptanceTest'" | tee tmp/currentTests.list - name: Get acceptance test reports uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d continue-on-error: true diff --git a/.github/workflows/pre-review.yml b/.github/workflows/pre-review.yml index 3f36fb28883..cc9d5cd9090 100644 --- a/.github/workflows/pre-review.yml +++ b/.github/workflows/pre-review.yml @@ -114,7 +114,7 @@ jobs: - name: List unit tests run: ./gradlew test --test-dry-run -Dorg.gradle.parallel=true -Dorg.gradle.caching=true - name: Extract current test list - run: mkdir tmp; find . -type f -name TEST-*.xml | xargs -I{} bash -c "xmlstarlet sel -t -v '/testsuite/@name' '{}'; echo '{}' | sed 's#\./\(.*\)/build/test-results/.*# \1#'" | tee tmp/currentTests.list + run: mkdir tmp; find . -type f -name TEST-*.xml | xargs -I{} bash -c "xmlstarlet sel -t -v '/testsuite/testcase[1]/@classname' '{}'; echo '{}' | sed 's#\./\(.*\)/build/test-results/.*# \1#'" | tee tmp/currentTests.list - name: Get unit test reports uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d continue-on-error: true diff --git a/.github/workflows/splitTestsByTime.sh b/.github/workflows/splitTestsByTime.sh index c77c70d4bbd..27281b199f0 100755 --- a/.github/workflows/splitTestsByTime.sh +++ b/.github/workflows/splitTestsByTime.sh @@ -21,7 +21,7 @@ SPLIT_COUNT=$4 SPLIT_INDEX=$5 # extract tests time from Junit XML reports -find "$REPORTS_DIR" -type f -name TEST-*.xml | xargs -I{} bash -c "xmlstarlet sel -t -v 'concat(sum(//testcase/@time), \" \", //testsuite/@name)' '{}'; echo '{}' | sed \"s#${REPORT_STRIP_PREFIX}/\(.*\)/${REPORT_STRIP_SUFFIX}.*# \1#\"" > tmp/timing.tsv +find "$REPORTS_DIR" -type f -name TEST-*.xml | xargs -I{} bash -c "xmlstarlet sel -t -v 'concat(sum(//testcase/@time), \" \", //testsuite/testcase[1]/@classname)' '{}'; echo '{}' | sed \"s#${REPORT_STRIP_PREFIX}/\(.*\)/${REPORT_STRIP_SUFFIX}.*# \1#\"" > tmp/timing.tsv # Sort times in descending order IFS=$'\n' sorted=($(sort -nr tmp/timing.tsv)) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/MainnetParallelBlockProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/MainnetParallelBlockProcessor.java index 4819c8a4d9c..3582d1d8c27 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/MainnetParallelBlockProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/MainnetParallelBlockProcessor.java @@ -151,7 +151,6 @@ public BlockProcessingResult processBlock( block, blockAccessList, new ParallelTransactionPreprocessing(transactionProcessor, executor, balConfiguration)); - if (blockProcessingResult.isFailed()) { // Fallback to non-parallel processing if there is a block processing exception . LOG.info( diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelizedConcurrentTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelizedConcurrentTransactionProcessor.java index 1d0fd5319cd..a32e19b768d 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelizedConcurrentTransactionProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelizedConcurrentTransactionProcessor.java @@ -216,18 +216,21 @@ public Optional getProcessingResult( miningBeneficiaryAccount.incrementBalance(reward); } - final Wei miningBeneficiaryPostBalance = miningBeneficiaryAccount.getBalance(); - transactionProcessingResult - .getPartialBlockAccessView() - .ifPresent( - partialBlockAccessView -> - partialBlockAccessView.accountChanges().stream() - .filter( - accountChanges -> accountChanges.getAddress().equals(miningBeneficiary)) - .findFirst() - .ifPresent( - accountChanges -> - accountChanges.setPostBalance(miningBeneficiaryPostBalance))); + if (!reward.isZero()) { + final Wei miningBeneficiaryPostBalance = miningBeneficiaryAccount.getBalance(); + transactionProcessingResult + .getPartialBlockAccessView() + .ifPresent( + partialBlockAccessView -> + partialBlockAccessView.accountChanges().stream() + .filter( + accountChanges -> + accountChanges.getAddress().equals(miningBeneficiary)) + .findFirst() + .ifPresent( + accountChanges -> + accountChanges.setPostBalance(miningBeneficiaryPostBalance))); + } blockAccumulator.importStateChangesFromSource(transactionAccumulator); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractContractStorageTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractContractStorageTest.java new file mode 100644 index 00000000000..6648c210f53 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractContractStorageTest.java @@ -0,0 +1,166 @@ +/* + * 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.parallelization; + +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_GENESIS_1; +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; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_GENESIS_2_KEYPAIR; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.CONTRACT_ADDRESS; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Integration tests for contract storage operations. */ +public abstract class AbstractContractStorageTest + extends AbstractParallelBlockProcessorIntegrationTest { + + private final Address contractAddr = Address.fromHexStringStrict(CONTRACT_ADDRESS); + + @Test + @DisplayName("Writing different storage slots from the same sender produces matching state") + void writeMultipleSlotsSameSender() { + final Transaction txSetSlot1 = + createContractCallTransaction( + 0, contractAddr, "setSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(100)); + final Transaction txSetSlot2 = + createContractCallTransaction( + 1, contractAddr, "setSlot2", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(200)); + final Transaction txSetSlot3 = + createContractCallTransaction( + 2, contractAddr, "setSlot3", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(300)); + + final ComparisonResult result = + executeAndCompare(Wei.of(5), txSetSlot1, txSetSlot2, txSetSlot3); + + assertContractStorage(result.seqWorldState(), contractAddr, 0, 100); + assertContractStorage(result.seqWorldState(), contractAddr, 1, 200); + assertContractStorage(result.seqWorldState(), contractAddr, 2, 300); + + assertContractStorageMatches(result.seqWorldState(), result.parWorldState(), contractAddr, 0); + assertContractStorageMatches(result.seqWorldState(), result.parWorldState(), contractAddr, 1); + assertContractStorageMatches(result.seqWorldState(), result.parWorldState(), contractAddr, 2); + assertAccountsMatch( + result.seqWorldState(), + result.parWorldState(), + Address.fromHexStringStrict(ACCOUNT_GENESIS_1)); + } + + @Test + @DisplayName("Writing then reading the same storage slot produces matching state") + void writeThenReadSameSlot() { + final Transaction txSetSlot1 = + createContractCallTransaction( + 0, contractAddr, "setSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(999)); + final Transaction txGetSlot1 = + createContractCallTransaction( + 0, contractAddr, "getSlot1", ACCOUNT_GENESIS_2_KEYPAIR, Optional.empty()); + + final ComparisonResult result = executeAndCompare(Wei.of(5), txSetSlot1, txGetSlot1); + + assertContractStorage(result.seqWorldState(), contractAddr, 0, 999); + assertContractStorageMatches(result.seqWorldState(), result.parWorldState(), contractAddr, 0); + assertAccountsMatch( + result.seqWorldState(), + result.parWorldState(), + Address.fromHexStringStrict(ACCOUNT_GENESIS_1)); + assertAccountsMatch( + result.seqWorldState(), + result.parWorldState(), + Address.fromHexStringStrict(ACCOUNT_GENESIS_2)); + } + + @Test + @DisplayName("Reading then writing the same storage slot produces matching state") + void readThenWriteSameSlot() { + final Transaction txGetSlot1 = + createContractCallTransaction( + 0, contractAddr, "getSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.empty()); + final Transaction txSetSlot1 = + createContractCallTransaction( + 0, contractAddr, "setSlot1", ACCOUNT_GENESIS_2_KEYPAIR, Optional.of(777)); + + final ComparisonResult result = executeAndCompare(Wei.of(5), txGetSlot1, txSetSlot1); + + assertContractStorage(result.seqWorldState(), contractAddr, 0, 777); + assertContractStorageMatches(result.seqWorldState(), result.parWorldState(), contractAddr, 0); + } + + @Test + @DisplayName("Write-read-write across two senders produces matching state") + void writeReadWriteAcrossSenders() { + final Transaction txSetSlot1 = + createContractCallTransaction( + 0, contractAddr, "setSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(100)); + final Transaction txGetSlot1 = + createContractCallTransaction( + 0, contractAddr, "getSlot1", ACCOUNT_GENESIS_2_KEYPAIR, Optional.empty()); + final Transaction txSetSlot2 = + createContractCallTransaction( + 1, contractAddr, "setSlot2", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(200)); + final Transaction txSetSlot3 = + createContractCallTransaction( + 2, contractAddr, "setSlot3", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(300)); + + final ComparisonResult result = + executeAndCompare(Wei.of(5), txSetSlot1, txGetSlot1, txSetSlot2, txSetSlot3); + + assertContractStorage(result.seqWorldState(), contractAddr, 0, 100); + assertContractStorage(result.seqWorldState(), contractAddr, 1, 200); + assertContractStorage(result.seqWorldState(), contractAddr, 2, 300); + + for (int slot = 0; slot <= 2; slot++) { + assertContractStorageMatches( + result.seqWorldState(), result.parWorldState(), contractAddr, slot); + } + assertAccountsMatch( + result.seqWorldState(), + result.parWorldState(), + Address.fromHexStringStrict(ACCOUNT_GENESIS_1)); + assertAccountsMatch( + result.seqWorldState(), + result.parWorldState(), + Address.fromHexStringStrict(ACCOUNT_GENESIS_2)); + } + + @Test + @DisplayName("Storage reads from different senders produce matching state") + void readFromDifferentSenders() { + final Transaction txGetSlot1A = + createContractCallTransaction( + 0, contractAddr, "getSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.empty()); + final Transaction txGetSlot1B = + createContractCallTransaction( + 0, contractAddr, "getSlot1", ACCOUNT_GENESIS_2_KEYPAIR, Optional.empty()); + + final ComparisonResult result = executeAndCompare(Wei.of(5), txGetSlot1A, txGetSlot1B); + + assertContractStorageMatches(result.seqWorldState(), result.parWorldState(), contractAddr, 0); + assertAccountsMatch( + result.seqWorldState(), + result.parWorldState(), + Address.fromHexStringStrict(ACCOUNT_GENESIS_1)); + assertAccountsMatch( + result.seqWorldState(), + result.parWorldState(), + Address.fromHexStringStrict(ACCOUNT_GENESIS_2)); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractMiningBeneficiaryBalTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractMiningBeneficiaryBalTest.java new file mode 100644 index 00000000000..699e14f60d4 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractMiningBeneficiaryBalTest.java @@ -0,0 +1,114 @@ +/* + * 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.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.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.MINING_BENEFICIARY; + +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BalanceChange; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Integration tests for mining beneficiary BAL balance tracking. */ +public abstract class AbstractMiningBeneficiaryBalTest + extends AbstractParallelBlockProcessorIntegrationTest { + + @Test + @DisplayName( + "Parallel BAL must record correct cumulative mining beneficiary postBalance with priority fees") + void miningBeneficiaryPostBalanceWithPriorityFees() { + // Two independent transfers with non-zero priority fees. + // baseFee=1 so effectivePriorityFee = min(maxPriorityFeePerGas, maxFeePerGas - baseFee) > 0. + // The mining beneficiary accumulates the priority fee from each transaction. + final Transaction tx1 = + createTransferTransaction( + 0, 1_000_000_000_000_000_000L, 300_000L, 2L, 10L, ACCOUNT_2, ACCOUNT_GENESIS_1_KEYPAIR); + final Transaction tx2 = + createTransferTransaction( + 0, 2_000_000_000_000_000_000L, 300_000L, 3L, 10L, ACCOUNT_3, ACCOUNT_GENESIS_2_KEYPAIR); + + final ComparisonResult result = executeAndCompare(Wei.of(1), tx1, tx2); + + final Optional seqBal = getBlockAccessList(result.seqResult()); + final Optional parBal = getBlockAccessList(result.parResult()); + assertThat(seqBal).as("Sequential BAL should be present").isPresent(); + assertThat(parBal).as("Parallel BAL should be present").isPresent(); + + final List seqBalChanges = + getBalanceChangesFor(seqBal.get(), MINING_BENEFICIARY); + final List parBalChanges = + getBalanceChangesFor(parBal.get(), MINING_BENEFICIARY); + + assertThat(seqBalChanges) + .as("Sequential BAL should have balance changes for mining beneficiary") + .isNotEmpty(); + assertThat(parBalChanges) + .as("Parallel BAL balance changes for mining beneficiary must match sequential") + .isEqualTo(seqBalChanges); + } + + @Test + @DisplayName( + "Multiple transactions with varying priority fees produce correct cumulative beneficiary balances") + void multipleTxsWithVaryingPriorityFees() { + // Three transactions: two from sender A (nonces 0,1) and one from sender B (nonce 0), + // each with a different priority fee. baseFee=1 ensures positive effective priority fees. + final Transaction tx1 = + createTransferTransaction( + 0, 100_000_000_000_000L, 300_000L, 1L, 10L, ACCOUNT_2, ACCOUNT_GENESIS_1_KEYPAIR); + final Transaction tx2 = + createTransferTransaction( + 0, 200_000_000_000_000L, 300_000L, 4L, 10L, ACCOUNT_3, ACCOUNT_GENESIS_2_KEYPAIR); + final Transaction tx3 = + createTransferTransaction( + 1, 300_000_000_000_000L, 300_000L, 2L, 10L, ACCOUNT_4, ACCOUNT_GENESIS_1_KEYPAIR); + + final ComparisonResult result = executeAndCompare(Wei.of(1), tx1, tx2, tx3); + + final Optional seqBal = getBlockAccessList(result.seqResult()); + final Optional parBal = getBlockAccessList(result.parResult()); + assertThat(seqBal).isPresent(); + assertThat(parBal).isPresent(); + + final List seqBalChanges = + getBalanceChangesFor(seqBal.get(), MINING_BENEFICIARY); + final List parBalChanges = + getBalanceChangesFor(parBal.get(), MINING_BENEFICIARY); + + assertThat(seqBalChanges).isNotEmpty(); + assertThat(parBalChanges) + .as("Parallel BAL mining beneficiary balance changes must match sequential") + .isEqualTo(seqBalChanges); + + // Each balance change should be strictly increasing (cumulative rewards) + for (int i = 1; i < seqBalChanges.size(); i++) { + assertThat(seqBalChanges.get(i).postBalance()) + .as("Balance change at index %d must be >= previous", i) + .isGreaterThanOrEqualTo(seqBalChanges.get(i - 1).postBalance()); + } + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractParallelBlockProcessorIntegrationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractParallelBlockProcessorIntegrationTest.java new file mode 100644 index 00000000000..717ebbcf9ec --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractParallelBlockProcessorIntegrationTest.java @@ -0,0 +1,424 @@ +/* + * 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.parallelization; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.GENESIS_CONFIG; +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.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.BlockProcessingOutputs; +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.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.mainnet.BalConfiguration; +import org.hyperledger.besu.ethereum.mainnet.BlockProcessor; +import org.hyperledger.besu.ethereum.mainnet.BodyValidation; +import org.hyperledger.besu.ethereum.mainnet.MainnetBlockProcessor; +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.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BalanceChange; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiAccount; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.web3j.abi.FunctionEncoder; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.generated.Uint256; + +/** + * Abstract base class for parallel block processor integration tests. Provides common utilities for + * block construction, state root discovery, and sequential vs parallel comparison. Subclasses + * provide specific parallel preprocessing implementations (BAL or Optimistic). + */ +@SuppressWarnings("rawtypes") +public abstract class AbstractParallelBlockProcessorIntegrationTest { + + protected static final BalConfiguration SEQUENTIAL_CONFIG = BalConfiguration.DEFAULT; + + protected abstract String getVariantName(); + + protected abstract ParallelTransactionPreprocessing createParallelPreprocessing( + MainnetTransactionProcessor transactionProcessor); + + protected BalConfiguration getBalConfiguration() { + return BalConfiguration.DEFAULT; + } + + // ==================== Context Creation ==================== + + protected ExecutionContextTestFixture createFreshContext() { + return ExecutionContextTestFixture.builder(GenesisConfig.fromResource(GENESIS_CONFIG)) + .dataStorageFormat(DataStorageFormat.BONSAI) + .build(); + } + + protected BlockProcessor createSequentialProcessor(final ExecutionContextTestFixture ctx) { + final ProtocolSpec spec = + ctx.getProtocolSchedule() + .getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader()); + return new MainnetBlockProcessor( + spec.getTransactionProcessor(), + spec.getTransactionReceiptFactory(), + Wei.ZERO, + BlockHeader::getCoinbase, + true, + ctx.getProtocolSchedule(), + SEQUENTIAL_CONFIG); + } + + protected BlockProcessor createParallelProcessor(final ExecutionContextTestFixture ctx) { + final ProtocolSpec spec = + ctx.getProtocolSchedule() + .getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader()); + return new NoBlockFallbackParallelBlockProcessor( + spec.getTransactionProcessor(), + spec.getTransactionReceiptFactory(), + Wei.ZERO, + BlockHeader::getCoinbase, + true, + ctx.getProtocolSchedule(), + getBalConfiguration(), + new NoOpMetricsSystem()); + } + + /** + * Parallel block processor without block-level fallback. When parallel processing produces a + * wrong state root, the result is returned as-is rather than silently falling back to sequential. + */ + static class NoBlockFallbackParallelBlockProcessor extends MainnetParallelBlockProcessor { + + public NoBlockFallbackParallelBlockProcessor( + 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, + final Optional blockAccessList) { + return super.processBlock( + protocolContext, + blockchain, + worldState, + block, + blockAccessList, + new ParallelTransactionPreprocessing( + transactionProcessor, Runnable::run, balConfiguration)); + } + } + + // ==================== Block Construction ==================== + + protected Block createBlock( + final ExecutionContextTestFixture ctx, + final Hash stateRoot, + final Wei baseFee, + final Transaction... txs) { + return createBlock(ctx, stateRoot, baseFee, MINING_BENEFICIARY, txs); + } + + protected Block createBlock( + final ExecutionContextTestFixture ctx, + final Hash stateRoot, + final Wei baseFee, + final Address coinbase, + final Transaction... txs) { + final BlockHeader parentHeader = ctx.getBlockchain().getChainHeadHeader(); + final BlockHeader blockHeader = + new BlockHeaderTestFixture() + .number(parentHeader.getNumber() + 1L) + .parentHash(parentHeader.getHash()) + .coinbase(coinbase) + .stateRoot(stateRoot) + .gasLimit(30_000_000L) + .baseFeePerGas(baseFee) + .buildHeader(); + final BlockBody blockBody = + new BlockBody(Arrays.asList(txs), Collections.emptyList(), Optional.empty()); + return new Block(blockHeader, blockBody); + } + + // ==================== State Root Discovery ==================== + + protected Hash discoverStateRoot(final Wei baseFee, final Transaction... txs) { + return discoverStateRoot(baseFee, MINING_BENEFICIARY, txs); + } + + protected Hash discoverStateRoot( + final Wei baseFee, final Address coinbase, final Transaction... txs) { + final ExecutionContextTestFixture ctx = createFreshContext(); + final MutableWorldState ws = ctx.getStateArchive().getWorldState(); + final Block block = createBlock(ctx, Hash.ZERO, baseFee, coinbase, txs); + final BlockProcessor processor = createSequentialProcessor(ctx); + final BlockProcessingResult result = + processor.processBlock(ctx.getProtocolContext(), ctx.getBlockchain(), ws, block); + + if (result.isSuccessful()) { + return ws.rootHash(); + } + + final String msg = + result.errorMessage.orElseThrow( + () -> new AssertionError("Discovery processing failed without error message")); + final String marker = "calculated "; + final int idx = msg.indexOf(marker); + if (idx < 0) { + throw new AssertionError("Unexpected error message format: " + msg); + } + return Hash.fromHexString(msg.substring(idx + marker.length())); + } + + // ==================== Core Comparison ==================== + + protected ComparisonResult executeAndCompare(final Wei baseFee, final Transaction... txs) { + final Hash stateRoot = discoverStateRoot(baseFee, txs); + + // Sequential processing + final ExecutionContextTestFixture seqCtx = createFreshContext(); + final MutableWorldState seqWs = seqCtx.getStateArchive().getWorldState(); + final Block block = createBlock(seqCtx, stateRoot, baseFee, txs); + final BlockProcessor seqProcessor = createSequentialProcessor(seqCtx); + final BlockProcessingResult seqResult = + seqProcessor.processBlock( + seqCtx.getProtocolContext(), seqCtx.getBlockchain(), seqWs, block); + assertTrue( + seqResult.isSuccessful(), + "Sequential processing failed: " + seqResult.errorMessage.orElse("(no message)")); + + // Parallel processing + 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 = createParallelProcessor(parCtx); + final ParallelTransactionPreprocessing preprocessing = + createParallelPreprocessing(spec.getTransactionProcessor()); + final BlockProcessingResult parResult = + parProcessor.processBlock( + parCtx.getProtocolContext(), parCtx.getBlockchain(), parWs, parBlock, preprocessing); + assertTrue( + parResult.isSuccessful(), + getVariantName() + + " parallel processing failed: " + + parResult.errorMessage.orElse("(no message)")); + + // State root comparison + assertThat(parWs.rootHash()) + .as(getVariantName() + " parallel state root must match sequential") + .isEqualTo(seqWs.rootHash()); + + // BAL hash comparison + final Optional seqBal = getBlockAccessList(seqResult); + final Optional parBal = getBlockAccessList(parResult); + assertThat(seqBal).as("Sequential BAL should be present").isPresent(); + assertThat(parBal).as(getVariantName() + " parallel BAL should be present").isPresent(); + assertThat(BodyValidation.balHash(parBal.get())) + .as(getVariantName() + " parallel BAL hash must match sequential") + .isEqualTo(BodyValidation.balHash(seqBal.get())); + + return new ComparisonResult( + seqWs.rootHash(), parWs.rootHash(), seqResult, parResult, seqWs, parWs); + } + + // ==================== Transaction Builders ==================== + + protected Transaction createTransferTransaction( + final long nonce, + final long value, + final long gasLimit, + final long maxPriorityFeePerGas, + final long maxFeePerGas, + final String toAddress, + final KeyPair keyPair) { + return Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(nonce) + .maxPriorityFeePerGas(Wei.of(maxPriorityFeePerGas)) + .maxFeePerGas(Wei.of(maxFeePerGas)) + .gasLimit(gasLimit) + .to(Address.fromHexStringStrict(toAddress)) + .value(Wei.of(value)) + .payload(Bytes.EMPTY) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(keyPair); + } + + protected Transaction createContractCallTransaction( + final int nonce, + final Address contractAddress, + final String methodName, + final KeyPair keyPair, + final Optional value) { + final Bytes payload = encodeFunctionCall(methodName, value); + return Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(nonce) + .maxPriorityFeePerGas(Wei.of(0)) + .maxFeePerGas(Wei.of(5)) + .gasLimit(3_000_000L) + .to(contractAddress) + .value(Wei.ZERO) + .payload(payload) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(keyPair); + } + + @SuppressWarnings("SameParameterValue") + protected Transaction createContractSendEthTransaction( + final int nonce, + final Address contractAddress, + final String methodName, + final KeyPair keyPair, + final String toAddress, + final long value) { + final List inputParameters = + Arrays.asList(new org.web3j.abi.datatypes.Address(toAddress), new Uint256(value)); + final Function function = new Function(methodName, inputParameters, List.of()); + final Bytes payload = Bytes.fromHexString(FunctionEncoder.encode(function)); + return Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(nonce) + .maxPriorityFeePerGas(Wei.of(0)) + .maxFeePerGas(Wei.of(5)) + .gasLimit(3_000_000L) + .to(contractAddress) + .value(Wei.ZERO) + .payload(payload) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(keyPair); + } + + @SuppressWarnings({"OptionalUsedAsFieldOrParameterType", "OptionalIsPresent"}) + private Bytes encodeFunctionCall(final String methodName, final Optional value) { + final List inputParameters = + value.isPresent() ? Arrays.asList(new Uint256(value.get())) : List.of(); + final Function function = new Function(methodName, inputParameters, List.of()); + return Bytes.fromHexString(FunctionEncoder.encode(function)); + } + + // ==================== Assertions ==================== + + protected void assertAccountsMatch( + final MutableWorldState seqWs, final MutableWorldState parWs, final Address address) { + final BonsaiAccount seqAccount = (BonsaiAccount) seqWs.get(address); + final BonsaiAccount parAccount = (BonsaiAccount) parWs.get(address); + + if (seqAccount == null) { + assertThat(parAccount).as("Account " + address + " should be null in both").isNull(); + return; + } + assertThat(parAccount).as("Account " + address + " should exist in parallel").isNotNull(); + assertThat(parAccount.getBalance()) + .as("Balance mismatch for " + address) + .isEqualTo(seqAccount.getBalance()); + assertThat(parAccount.getNonce()) + .as("Nonce mismatch for " + address) + .isEqualTo(seqAccount.getNonce()); + } + + protected void assertContractStorageMatches( + final MutableWorldState seqWs, + final MutableWorldState parWs, + final Address contractAddress, + final int slot) { + final BonsaiAccount seqAccount = (BonsaiAccount) seqWs.get(contractAddress); + final BonsaiAccount parAccount = (BonsaiAccount) parWs.get(contractAddress); + assertThat(parAccount.getStorageValue(UInt256.valueOf(slot))) + .as("Storage slot " + slot + " mismatch for " + contractAddress) + .isEqualTo(seqAccount.getStorageValue(UInt256.valueOf(slot))); + } + + protected void assertContractStorage( + final MutableWorldState worldState, + final Address contractAddress, + final int slot, + final int expectedValue) { + final BonsaiAccount account = (BonsaiAccount) worldState.get(contractAddress); + assertThat(account).as("Contract account should exist: " + contractAddress).isNotNull(); + assertThat(account.getStorageValue(UInt256.valueOf(slot))) + .as("Storage slot " + slot + " value") + .isEqualTo(UInt256.valueOf(expectedValue)); + } + + protected Optional getBlockAccessList(final BlockProcessingResult result) { + return result.getYield().flatMap(BlockProcessingOutputs::getBlockAccessList); + } + + protected List getBalanceChangesFor( + final BlockAccessList bal, final Address address) { + return bal.accountChanges().stream() + .filter(ac -> ac.address().equals(address)) + .flatMap(ac -> ac.balanceChanges().stream()) + .toList(); + } + + // ==================== Result Record ==================== + + protected record ComparisonResult( + Hash seqStateRoot, + Hash parStateRoot, + BlockProcessingResult seqResult, + BlockProcessingResult parResult, + MutableWorldState seqWorldState, + MutableWorldState parWorldState) {} +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractSimpleTransferTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractSimpleTransferTest.java new file mode 100644 index 00000000000..a8cee7f38f7 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractSimpleTransferTest.java @@ -0,0 +1,200 @@ +/* + * 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.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_5; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_6; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_GENESIS_1; +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; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_GENESIS_2_KEYPAIR; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.MINING_BENEFICIARY; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.MINING_BENEFICIARY_KEYPAIR; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiAccount; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Integration tests for simple ETH transfer scenarios. */ +public abstract class AbstractSimpleTransferTest + extends AbstractParallelBlockProcessorIntegrationTest { + + @Test + @DisplayName("Independent transfers from different senders produce matching state") + void independentTransfers() { + 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 ComparisonResult result = executeAndCompare(Wei.of(5), tx1, tx2); + + final Address addr2 = Address.fromHexStringStrict(ACCOUNT_2); + final Address addr3 = Address.fromHexStringStrict(ACCOUNT_3); + final Address sender1 = Address.fromHexStringStrict(ACCOUNT_GENESIS_1); + final Address sender2 = Address.fromHexStringStrict(ACCOUNT_GENESIS_2); + + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr2); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr3); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), sender1); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), sender2); + + assertThat(((BonsaiAccount) result.seqWorldState().get(addr2)).getBalance()) + .isEqualTo(Wei.of(1_000_000_000_000_000_000L)); + assertThat(((BonsaiAccount) result.seqWorldState().get(addr3)).getBalance()) + .isEqualTo(Wei.of(2_000_000_000_000_000_000L)); + } + + @Test + @DisplayName("Multiple transactions from the same sender produce matching state") + void sameSenderConflict() { + final Transaction tx1 = + createTransferTransaction( + 0, 1_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_4, ACCOUNT_GENESIS_1_KEYPAIR); + final Transaction tx2 = + createTransferTransaction( + 1, 2_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_5, ACCOUNT_GENESIS_1_KEYPAIR); + final Transaction tx3 = + createTransferTransaction( + 2, 3_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_6, ACCOUNT_GENESIS_1_KEYPAIR); + + final ComparisonResult result = executeAndCompare(Wei.of(5), tx1, tx2, tx3); + + final Address addr4 = Address.fromHexStringStrict(ACCOUNT_4); + final Address addr5 = Address.fromHexStringStrict(ACCOUNT_5); + final Address addr6 = Address.fromHexStringStrict(ACCOUNT_6); + final Address sender = Address.fromHexStringStrict(ACCOUNT_GENESIS_1); + + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr4); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr5); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr6); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), sender); + + assertThat(((BonsaiAccount) result.seqWorldState().get(addr4)).getBalance()) + .isEqualTo(Wei.of(1_000_000_000_000_000_000L)); + assertThat(((BonsaiAccount) result.seqWorldState().get(addr5)).getBalance()) + .isEqualTo(Wei.of(2_000_000_000_000_000_000L)); + assertThat(((BonsaiAccount) result.seqWorldState().get(addr6)).getBalance()) + .isEqualTo(Wei.of(3_000_000_000_000_000_000L)); + + assertThat(((BonsaiAccount) result.seqWorldState().get(sender)).getNonce()).isEqualTo(3L); + } + + @Test + @DisplayName("Receiver of tx1 is sender of tx2 produces matching state") + void receiverSenderOverlap() { + final Transaction tx1 = + createTransferTransaction( + 0, + 1_000_000_000_000_000_000L, + 300_000L, + 0L, + 5L, + ACCOUNT_GENESIS_2, + ACCOUNT_GENESIS_1_KEYPAIR); + final Transaction tx2 = + createTransferTransaction( + 0, 2_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_2, ACCOUNT_GENESIS_2_KEYPAIR); + + final ComparisonResult result = executeAndCompare(Wei.of(5), tx1, tx2); + + final Address addr2 = Address.fromHexStringStrict(ACCOUNT_2); + final Address sender1 = Address.fromHexStringStrict(ACCOUNT_GENESIS_1); + final Address sender2 = Address.fromHexStringStrict(ACCOUNT_GENESIS_2); + + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr2); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), sender1); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), sender2); + + assertThat(((BonsaiAccount) result.seqWorldState().get(addr2)).getBalance()) + .isEqualTo(Wei.of(2_000_000_000_000_000_000L)); + assertThat(((BonsaiAccount) result.seqWorldState().get(sender1)).getNonce()).isEqualTo(1L); + assertThat(((BonsaiAccount) result.seqWorldState().get(sender2)).getNonce()).isEqualTo(1L); + } + + @Test + @DisplayName("Single transaction produces matching state") + void singleTransaction() { + final Transaction tx1 = + createTransferTransaction( + 0, 5_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_2, ACCOUNT_GENESIS_1_KEYPAIR); + + final ComparisonResult result = executeAndCompare(Wei.of(5), tx1); + + final Address addr2 = Address.fromHexStringStrict(ACCOUNT_2); + final Address sender = Address.fromHexStringStrict(ACCOUNT_GENESIS_1); + + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr2); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), sender); + + assertThat(((BonsaiAccount) result.seqWorldState().get(addr2)).getBalance()) + .isEqualTo(Wei.of(5_000_000_000_000_000_000L)); + } + + @Test + @DisplayName( + "Sender of tx2 is the mining beneficiary triggers collision and produces matching state") + void senderIsMiningBeneficiary() { + final Transaction tx1 = + createTransferTransaction( + 0, + 1_000_000_000_000_000_000L, + 300_000L, + 0L, + 5L, + MINING_BENEFICIARY.toHexString(), + ACCOUNT_GENESIS_1_KEYPAIR); + final Transaction tx2 = + createTransferTransaction( + 0, 2_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_3, MINING_BENEFICIARY_KEYPAIR); + + final ComparisonResult result = executeAndCompare(Wei.of(5), tx1, tx2); + + final Address addr3 = Address.fromHexStringStrict(ACCOUNT_3); + final Address sender1 = Address.fromHexStringStrict(ACCOUNT_GENESIS_1); + + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), MINING_BENEFICIARY); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr3); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), sender1); + } + + @Test + @DisplayName("Both senders send to the same recipient produce matching state") + void sameRecipientFromDifferentSenders() { + 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, 3_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_2, ACCOUNT_GENESIS_2_KEYPAIR); + + final ComparisonResult result = executeAndCompare(Wei.of(5), tx1, tx2); + + final Address addr2 = Address.fromHexStringStrict(ACCOUNT_2); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr2); + assertThat(((BonsaiAccount) result.seqWorldState().get(addr2)).getBalance()) + .isEqualTo(Wei.of(4_000_000_000_000_000_000L)); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractStorageDependencyTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractStorageDependencyTest.java new file mode 100644 index 00000000000..dc7807edabf --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractStorageDependencyTest.java @@ -0,0 +1,164 @@ +/* + * 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.parallelization; + +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_GENESIS_1; +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; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_GENESIS_2_KEYPAIR; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.CONTRACT_ADDRESS; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.MINING_BENEFICIARY_KEYPAIR; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.PARALLEL_TEST_CONTRACT; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for storage dependency scenarios that validate collision detection. These tests + * MUST fail if collision detection is disabled because transactions would see stale state. + * + *

    Uses the ParallelTestStorage contract at 0x...eeeee which has incrementSlot1() that reads slot + * 0 and writes slot0 + 1. Initial slot 0 value = 10. + */ +public abstract class AbstractStorageDependencyTest + extends AbstractParallelBlockProcessorIntegrationTest { + + private final Address parallelContract = Address.fromHexStringStrict(PARALLEL_TEST_CONTRACT); + + @Test + @DisplayName("setSlot then increment from different sender must detect collision") + void setSlotThenIncrementFromDifferentSender() { + // Tx1: sender A sets slot 0 = 100 + // Tx2: sender B increments slot 0 (reads slot 0, writes slot 0 + 1) + // Sequential result: slot 0 = 101 (set to 100, then 100 + 1) + // Without collision detection: slot 0 = 11 (base=10, pre-computed 10+1, overwrites 100) + final Transaction txSet = + createContractCallTransaction( + 0, parallelContract, "setSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(100)); + final Transaction txIncrement = + createContractCallTransaction( + 0, parallelContract, "incrementSlot1", ACCOUNT_GENESIS_2_KEYPAIR, Optional.empty()); + + final ComparisonResult result = executeAndCompare(Wei.of(5), txSet, txIncrement); + + assertContractStorage(result.seqWorldState(), parallelContract, 0, 101); + assertContractStorageMatches( + result.seqWorldState(), result.parWorldState(), parallelContract, 0); + } + + @Test + @DisplayName("Two increments from different senders must detect collision") + void twoIncrementsFromDifferentSenders() { + // Tx1: sender A increments slot 0 (10 → 11) + // Tx2: sender B increments slot 0 (11 → 12) + // Sequential result: slot 0 = 12 + // Without collision detection: both see base=10, both write 11, last import wins → 11 + final Transaction txInc1 = + createContractCallTransaction( + 0, parallelContract, "incrementSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.empty()); + final Transaction txInc2 = + createContractCallTransaction( + 0, parallelContract, "incrementSlot1", ACCOUNT_GENESIS_2_KEYPAIR, Optional.empty()); + + final ComparisonResult result = executeAndCompare(Wei.of(5), txInc1, txInc2); + + assertContractStorage(result.seqWorldState(), parallelContract, 0, 12); + assertContractStorageMatches( + result.seqWorldState(), result.parWorldState(), parallelContract, 0); + } + + @Test + @DisplayName("Increment then set from different sender must detect collision") + void incrementThenSetFromDifferentSender() { + // Tx1: sender A increments slot 0 (10 → 11) + // Tx2: sender B sets slot 0 = 500 + // Sequential result: slot 0 = 500 + final Transaction txInc = + createContractCallTransaction( + 0, parallelContract, "incrementSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.empty()); + final Transaction txSet = + createContractCallTransaction( + 0, parallelContract, "setSlot1", ACCOUNT_GENESIS_2_KEYPAIR, Optional.of(500)); + + final ComparisonResult result = executeAndCompare(Wei.of(5), txInc, txSet); + + assertContractStorage(result.seqWorldState(), parallelContract, 0, 500); + assertContractStorageMatches( + result.seqWorldState(), result.parWorldState(), parallelContract, 0); + assertAccountsMatch( + result.seqWorldState(), + result.parWorldState(), + Address.fromHexStringStrict(ACCOUNT_GENESIS_1)); + assertAccountsMatch( + result.seqWorldState(), + result.parWorldState(), + Address.fromHexStringStrict(ACCOUNT_GENESIS_2)); + } + + @Test + @DisplayName("Set slot + increment + set another slot must detect collision on shared slot") + void setIncrementAndWriteOtherSlot() { + // Tx1: sender A sets slot 0 = 200 on ParallelTestStorage + // Tx2: sender B increments slot 0 (should read 200, write 201) + // Tx3: sender A sets slot 1 = 42 on the other contract (no conflict) + final Transaction txSet = + createContractCallTransaction( + 0, parallelContract, "setSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(200)); + final Transaction txIncrement = + createContractCallTransaction( + 0, parallelContract, "incrementSlot1", ACCOUNT_GENESIS_2_KEYPAIR, Optional.empty()); + final Address contractAddr = Address.fromHexStringStrict(CONTRACT_ADDRESS); + final Transaction txOther = + createContractCallTransaction( + 1, contractAddr, "setSlot2", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(42)); + + final ComparisonResult result = executeAndCompare(Wei.of(5), txSet, txIncrement, txOther); + + assertContractStorage(result.seqWorldState(), parallelContract, 0, 201); + assertContractStorageMatches( + result.seqWorldState(), result.parWorldState(), parallelContract, 0); + assertContractStorage(result.seqWorldState(), contractAddr, 1, 42); + assertContractStorageMatches(result.seqWorldState(), result.parWorldState(), contractAddr, 1); + } + + @Test + @DisplayName("Three-way storage dependency chain produces correct sequential result") + void threeWayStorageDependencyChain() { + // Tx1: set slot 0 = 50 + // Tx2: increment slot 0 (should read 50, write 51) + // Tx3: increment slot 0 (should read 51, write 52) + final Transaction txSet = + createContractCallTransaction( + 0, parallelContract, "setSlot1", ACCOUNT_GENESIS_1_KEYPAIR, Optional.of(50)); + final Transaction txInc1 = + createContractCallTransaction( + 0, parallelContract, "incrementSlot1", ACCOUNT_GENESIS_2_KEYPAIR, Optional.empty()); + final Transaction txInc2 = + createContractCallTransaction( + 0, parallelContract, "incrementSlot1", MINING_BENEFICIARY_KEYPAIR, Optional.empty()); + + final ComparisonResult result = executeAndCompare(Wei.of(5), txSet, txInc1, txInc2); + + assertContractStorage(result.seqWorldState(), parallelContract, 0, 52); + assertContractStorageMatches( + result.seqWorldState(), result.parWorldState(), parallelContract, 0); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalParallelBlockProcessorIntegrationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalParallelBlockProcessorIntegrationTest.java new file mode 100644 index 00000000000..e89e85fa0dd --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalParallelBlockProcessorIntegrationTest.java @@ -0,0 +1,291 @@ +/* + * 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.parallelization; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.BlockProcessingResult; +import org.hyperledger.besu.ethereum.ProtocolContext; +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.BalConfiguration; +import org.hyperledger.besu.ethereum.mainnet.BlockProcessor; +import org.hyperledger.besu.ethereum.mainnet.BodyValidation; +import org.hyperledger.besu.ethereum.mainnet.MainnetBlockProcessor; +import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.evm.blockhash.BlockHashLookup; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; + +/** + * Integration tests for BAL (Block Access List) based parallel block processing. Uses + * BalConcurrentTransactionProcessor under the hood. + * + *

    BAL-specific flow: + * + *

      + *
    1. Sequential execution generates the BAL + reference state root + *
    2. Parallel import uses the BAL from step 1 with BalConcurrentTransactionProcessor + *
    3. State roots are compared + *
    + */ +class BalParallelBlockProcessorIntegrationTest { + + private static String getVariant() { + return "BAL"; + } + + private static ParallelTransactionPreprocessing createPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return new ParallelTransactionPreprocessing( + transactionProcessor, Runnable::run, BalConfiguration.DEFAULT); + } + + /** Base class for BAL tests that overrides executeAndCompare with BAL-specific logic. */ + abstract static class BalTestBase extends AbstractParallelBlockProcessorIntegrationTest { + + @Override + protected String getVariantName() { + return getVariant(); + } + + @Override + protected ParallelTransactionPreprocessing createParallelPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return createPreprocessing(transactionProcessor); + } + + /** + * BAL-specific comparison: + * + *
      + *
    1. Discover the correct state root + *
    2. Sequential execution generates the BAL + *
    3. Parallel import uses the generated BAL with BalConcurrentTransactionProcessor + *
    4. Compare state roots + *
    + */ + @Override + protected ComparisonResult executeAndCompare(final Wei baseFee, final Transaction... txs) { + final Hash stateRoot = discoverStateRoot(baseFee, txs); + + // ========== Step 1: Sequential execution ========== + final ExecutionContextTestFixture seqCtx = createFreshContext(); + final MutableWorldState seqWs = seqCtx.getStateArchive().getWorldState(); + final Block block = createBlock(seqCtx, stateRoot, baseFee, txs); + final ProtocolSpec spec = + seqCtx + .getProtocolSchedule() + .getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader()); + final MainnetTransactionProcessor txProcessor = spec.getTransactionProcessor(); + + final BlockProcessor seqProcessor = + new MainnetBlockProcessor( + txProcessor, + spec.getTransactionReceiptFactory(), + Wei.ZERO, + BlockHeader::getCoinbase, + true, + seqCtx.getProtocolSchedule(), + SEQUENTIAL_CONFIG); + + final BlockProcessingResult seqResult = + seqProcessor.processBlock( + seqCtx.getProtocolContext(), seqCtx.getBlockchain(), seqWs, block); + assertTrue( + seqResult.isSuccessful(), + "Sequential execution failed: " + seqResult.errorMessage.orElse("(no message)")); + + final Optional generatedBal = getBlockAccessList(seqResult); + assertThat(generatedBal).as("Sequential execution should produce a BAL").isPresent(); + + // ========== Step 2: Parallel import using BAL ========== + final ExecutionContextTestFixture parCtx = createFreshContext(); + final MutableWorldState parWs = parCtx.getStateArchive().getWorldState(); + final Block parBlock = createBlock(parCtx, stateRoot, baseFee, txs); + final ProtocolSpec parSpec = + parCtx + .getProtocolSchedule() + .getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader()); + final MainnetTransactionProcessor parTxProcessor = parSpec.getTransactionProcessor(); + + final BlockProcessor parProcessor = createParallelProcessor(parCtx); + final ParallelTransactionPreprocessing balImportPreprocessing = + new ParallelTransactionPreprocessingWithBal( + parTxProcessor, generatedBal.get(), BalConfiguration.DEFAULT); + + final BlockProcessingResult parResult = + parProcessor.processBlock( + parCtx.getProtocolContext(), + parCtx.getBlockchain(), + parWs, + parBlock, + balImportPreprocessing); + assertTrue( + parResult.isSuccessful(), + "BAL parallel import failed: " + parResult.errorMessage.orElse("(no message)")); + + // ========== Step 3: Compare state roots ========== + assertThat(parWs.rootHash()) + .as("BAL parallel import state root must match sequential reference") + .isEqualTo(seqWs.rootHash()); + + final Optional parBal = getBlockAccessList(parResult); + assertThat(parBal).as("Parallel import should also produce a BAL").isPresent(); + + final Hash seqBalHash = BodyValidation.balHash(generatedBal.get()); + final Hash parBalHash = BodyValidation.balHash(parBal.get()); + assertThat(parBalHash) + .as("BAL hash from parallel import must match sequential") + .isEqualTo(seqBalHash); + + return new ComparisonResult( + seqWs.rootHash(), parWs.rootHash(), seqResult, parResult, seqWs, parWs); + } + } + + /** + * Custom preprocessing that injects a pre-computed BAL to force use of + * BalConcurrentTransactionProcessor with applyWritesFromPriorTransactions. + */ + private static class ParallelTransactionPreprocessingWithBal + extends ParallelTransactionPreprocessing { + + private final BlockAccessList preComputedBal; + + ParallelTransactionPreprocessingWithBal( + final MainnetTransactionProcessor transactionProcessor, + final BlockAccessList preComputedBal, + final BalConfiguration balConfiguration) { + super(transactionProcessor, Runnable::run, balConfiguration); + this.preComputedBal = preComputedBal; + } + + @Override + public Optional run( + final ProtocolContext protocolContext, + final BlockHeader blockHeader, + final List transactions, + final Address miningBeneficiary, + final BlockHashLookup blockHashLookup, + final Wei blobGasPrice, + final Optional blockAccessListBuilder, + final Optional maybeBlockBal) { + return super.run( + protocolContext, + blockHeader, + transactions, + miningBeneficiary, + blockHashLookup, + blobGasPrice, + blockAccessListBuilder, + Optional.of(preComputedBal)); + } + } + + @Nested + @DisplayName("Simple Transfers") + class SimpleTransfers extends AbstractSimpleTransferTest { + @Override + protected String getVariantName() { + return getVariant(); + } + + @Override + protected ParallelTransactionPreprocessing createParallelPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return createPreprocessing(transactionProcessor); + } + + @Override + protected ComparisonResult executeAndCompare(final Wei baseFee, final Transaction... txs) { + return new BalTestBase() {}.executeAndCompare(baseFee, txs); + } + } + + @Nested + @DisplayName("Contract Storage") + class ContractStorage extends AbstractContractStorageTest { + @Override + protected String getVariantName() { + return getVariant(); + } + + @Override + protected ParallelTransactionPreprocessing createParallelPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return createPreprocessing(transactionProcessor); + } + + @Override + protected ComparisonResult executeAndCompare(final Wei baseFee, final Transaction... txs) { + return new BalTestBase() {}.executeAndCompare(baseFee, txs); + } + } + + @Nested + @DisplayName("Storage Dependency (Collision Detection)") + class StorageDependency extends AbstractStorageDependencyTest { + @Override + protected String getVariantName() { + return getVariant(); + } + + @Override + protected ParallelTransactionPreprocessing createParallelPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return createPreprocessing(transactionProcessor); + } + + @Override + protected ComparisonResult executeAndCompare(final Wei baseFee, final Transaction... txs) { + return new BalTestBase() {}.executeAndCompare(baseFee, txs); + } + } + + @Nested + @DisplayName("Mining Beneficiary BAL") + class MiningBeneficiaryBal extends AbstractMiningBeneficiaryBalTest { + @Override + protected String getVariantName() { + return getVariant(); + } + + @Override + protected ParallelTransactionPreprocessing createParallelPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return createPreprocessing(transactionProcessor); + } + + @Override + protected ComparisonResult executeAndCompare(final Wei baseFee, final Transaction... txs) { + return new BalTestBase() {}.executeAndCompare(baseFee, txs); + } + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalTransactionProcessorUnitTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalTransactionProcessorUnitTest.java new file mode 100644 index 00000000000..59bbd46c95f --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalTransactionProcessorUnitTest.java @@ -0,0 +1,456 @@ +/* + * 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.parallelization; + +import static org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig.createStatefulConfigWithTrie; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.StorageSlotKey; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.chain.MutableBlockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; +import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; +import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; +import org.hyperledger.besu.ethereum.mainnet.ValidationResult; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.PartialBlockAccessView; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.CodeCache; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoOpBonsaiCachedWorldStorageManager; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoopBonsaiCachedMerkleTrieLoader; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldState; +import org.hyperledger.besu.ethereum.trie.pathbased.common.trielog.NoOpTrieLogManager; +import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; +import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.blockhash.BlockHashLookup; +import org.hyperledger.besu.evm.internal.EvmConfiguration; +import org.hyperledger.besu.evm.tracing.OperationTracer; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit tests for BalConcurrentTransactionProcessor (BAL strategy). Tests verify: - Pre-state setup + * from BlockAccessList - Balance, nonce, code, and storage slot values are correctly applied - + * Transaction processing with BAL-based world state + */ +@ExtendWith(MockitoExtension.class) +class BalTransactionProcessorUnitTest { + + private static final Address MINING_BENEFICIARY = Address.fromHexString("0x1"); + private static final Wei BLOB_GAS_PRICE = Wei.ZERO; + private final Executor sameThreadExecutor = Runnable::run; + + @Mock private MainnetTransactionProcessor transactionProcessor; + + private record TestEnvironment( + ProtocolContext protocolContext, BlockHeader blockHeader, BonsaiWorldState worldState) {} + + private BonsaiWorldState createEmptyWorldState() { + final BonsaiWorldStateKeyValueStorage storage = + new BonsaiWorldStateKeyValueStorage( + new InMemoryKeyValueStorageProvider(), + new NoOpMetricsSystem(), + DataStorageConfiguration.DEFAULT_BONSAI_CONFIG); + + return new BonsaiWorldState( + storage, + new NoopBonsaiCachedMerkleTrieLoader(), + new NoOpBonsaiCachedWorldStorageManager(storage, EvmConfiguration.DEFAULT, new CodeCache()), + new NoOpTrieLogManager(), + EvmConfiguration.DEFAULT, + createStatefulConfigWithTrie(), + new CodeCache()); + } + + private TestEnvironment createTestEnvironment() { + final ProtocolContext protocolContext = mock(ProtocolContext.class); + final MutableBlockchain blockchain = mock(MutableBlockchain.class); + final BlockHeader chainHeadBlockHeader = mock(BlockHeader.class); + final BlockHeader blockHeader = mock(BlockHeader.class); + final WorldStateArchive worldStateArchive = mock(WorldStateArchive.class); + final BonsaiWorldState worldState = createEmptyWorldState(); + + when(protocolContext.getBlockchain()).thenReturn(blockchain); + when(blockchain.getChainHeadHeader()).thenReturn(chainHeadBlockHeader); + when(chainHeadBlockHeader.getHash()).thenReturn(Hash.ZERO); + when(chainHeadBlockHeader.getStateRoot()).thenReturn(Hash.EMPTY_TRIE_HASH); + when(blockHeader.getParentHash()).thenReturn(Hash.ZERO); + when(protocolContext.getWorldStateArchive()).thenReturn(worldStateArchive); + when(worldStateArchive.getWorldState(any())).thenReturn(Optional.of(worldState)); + + return new TestEnvironment(protocolContext, blockHeader, worldState); + } + + private Transaction mockTransaction() { + final Transaction transaction = mock(Transaction.class); + when(transaction.detachedCopy()).thenReturn(transaction); + return transaction; + } + + private BlockAccessList mockEmptyBlockAccessList() { + final BlockAccessList blockAccessList = mock(BlockAccessList.class); + when(blockAccessList.accountChanges()).thenReturn(Collections.emptyList()); + return blockAccessList; + } + + private void stubSuccessfulTransaction() { + when(transactionProcessor.processTransaction( + any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn( + TransactionProcessingResult.successful( + Collections.emptyList(), + 0, + 0, + Bytes.EMPTY, + Optional.empty(), + ValidationResult.valid())); + } + + @Nested + @DisplayName("Transaction Processing") + class TransactionProcessingTests { + + @Test + @DisplayName("Transaction processor is called with correct parameters") + void transactionProcessorCalledWithCorrectParams() { + final TestEnvironment env = createTestEnvironment(); + final BlockAccessList blockAccessList = mockEmptyBlockAccessList(); + final Transaction transaction = mockTransaction(); + stubSuccessfulTransaction(); + + final BalConcurrentTransactionProcessor processor = + new BalConcurrentTransactionProcessor( + transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + verify(transactionProcessor, times(1)) + .processTransaction( + any(WorldUpdater.class), + eq(env.blockHeader()), + eq(transaction), + eq(MINING_BENEFICIARY), + any(OperationTracer.class), + any(BlockHashLookup.class), + eq(TransactionValidationParams.processingBlock()), + eq(BLOB_GAS_PRICE), + any()); + } + + @Test + @DisplayName("All transactions are processed") + void allTransactionsProcessed() { + final TestEnvironment env = createTestEnvironment(); + final BlockAccessList blockAccessList = mockEmptyBlockAccessList(); + final Transaction tx1 = mockTransaction(); + final Transaction tx2 = mockTransaction(); + final Transaction tx3 = mockTransaction(); + stubSuccessfulTransaction(); + + final BalConcurrentTransactionProcessor processor = + new BalConcurrentTransactionProcessor( + transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + List.of(tx1, tx2, tx3), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + verify(transactionProcessor, times(3)) + .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); + } + + @Test + @DisplayName("Processing result is returned for successful transaction") + void processingResultReturnedForSuccessfulTransaction() { + final TestEnvironment env = createTestEnvironment(); + final BlockAccessList blockAccessList = mockEmptyBlockAccessList(); + final Transaction transaction = mockTransaction(); + stubSuccessfulTransaction(); + + final BalConcurrentTransactionProcessor processor = + new BalConcurrentTransactionProcessor( + transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + final Optional result = + processor.getProcessingResult( + env.worldState(), + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()); + + assertTrue(result.isPresent(), "Expected processing result to be present"); + assertTrue(result.get().isSuccessful(), "Expected successful result"); + } + } + + @Nested + @DisplayName("Pre-State Setup") + class PreStateSetupTests { + + @Test + @DisplayName("Pre-state is correctly set up from BlockAccessList") + void preStateSetupFromBlockAccessList() { + final TestEnvironment env = createTestEnvironment(); + + final Address accountAddress = + Address.fromHexString("0x1000000000000000000000000000000000000001"); + + final UInt256 slot1Key = UInt256.ONE; + final UInt256 slot2Key = UInt256.valueOf(2); + final StorageSlotKey slot1 = new StorageSlotKey(slot1Key); + final StorageSlotKey slot2 = new StorageSlotKey(slot2Key); + + final Wei tx0Balance = Wei.of(100); + final long tx0Nonce = 1L; + final Bytes tx0Code = Bytes.fromHexString("0xAA"); + final UInt256 tx0Slot1Value = UInt256.valueOf(1); + final UInt256 tx0Slot2Value = UInt256.valueOf(3); + + final Wei tx1Balance = Wei.of(200); + final long tx1Nonce = 2L; + final Bytes tx1Code = Bytes.fromHexString("0xBB"); + final UInt256 tx1Slot1Value = UInt256.valueOf(5); + final UInt256 tx1Slot2Value = UInt256.ZERO; + + final Wei tx2Balance = Wei.of(300); + final long tx2Nonce = 3L; + final Bytes tx2Code = Bytes.fromHexString("0xCC"); + final UInt256 tx2Slot1Value = UInt256.valueOf(7); + + final BlockAccessList.BlockAccessListBuilder balBuilder = BlockAccessList.builder(); + + final PartialBlockAccessView.PartialBlockAccessViewBuilder p0 = + new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(0); + final PartialBlockAccessView.AccountChangesBuilder a0 = + p0.getOrCreateAccountBuilder(accountAddress); + a0.withPostBalance(tx0Balance); + a0.withNonceChange(tx0Nonce); + a0.withNewCode(tx0Code); + a0.addStorageChange(slot1, tx0Slot1Value); + a0.addStorageChange(slot2, tx0Slot2Value); + balBuilder.apply(p0.build()); + + final PartialBlockAccessView.PartialBlockAccessViewBuilder p1 = + new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(1); + final PartialBlockAccessView.AccountChangesBuilder a1 = + p1.getOrCreateAccountBuilder(accountAddress); + a1.withPostBalance(tx1Balance); + a1.withNonceChange(tx1Nonce); + a1.withNewCode(tx1Code); + a1.addStorageChange(slot1, tx1Slot1Value); + a1.addStorageChange(slot2, null); + balBuilder.apply(p1.build()); + + final PartialBlockAccessView.PartialBlockAccessViewBuilder p2 = + new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(2); + final PartialBlockAccessView.AccountChangesBuilder a2 = + p2.getOrCreateAccountBuilder(accountAddress); + a2.withPostBalance(tx2Balance); + a2.withNonceChange(tx2Nonce); + a2.withNewCode(tx2Code); + a2.addStorageChange(slot1, tx2Slot1Value); + balBuilder.apply(p2.build()); + + final BlockAccessList blockAccessList = balBuilder.build(); + + final AtomicInteger locationCounter = new AtomicInteger(0); + when(transactionProcessor.processTransaction( + any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenAnswer( + invocation -> { + final int transactionLocation = locationCounter.getAndIncrement(); + final WorldUpdater worldUpdater = invocation.getArgument(0, WorldUpdater.class); + final Account account = worldUpdater.get(accountAddress); + + assertTrue(account != null, "Expected account to exist in world updater"); + + switch (transactionLocation) { + case 0 -> { + assertEquals(tx0Balance, account.getBalance(), "Balance mismatch for tx0"); + assertEquals(tx0Nonce, account.getNonce(), "Nonce mismatch for tx0"); + assertEquals(tx0Code, account.getCode(), "Code mismatch for tx0"); + assertEquals( + tx0Slot1Value, account.getStorageValue(slot1Key), "Slot1 mismatch for tx0"); + assertEquals( + tx0Slot2Value, account.getStorageValue(slot2Key), "Slot2 mismatch for tx0"); + } + case 1 -> { + assertEquals(tx1Balance, account.getBalance(), "Balance mismatch for tx1"); + assertEquals(tx1Nonce, account.getNonce(), "Nonce mismatch for tx1"); + assertEquals(tx1Code, account.getCode(), "Code mismatch for tx1"); + assertEquals( + tx1Slot1Value, account.getStorageValue(slot1Key), "Slot1 mismatch for tx1"); + assertEquals( + tx1Slot2Value, account.getStorageValue(slot2Key), "Slot2 mismatch for tx1"); + } + case 2 -> { + assertEquals(tx2Balance, account.getBalance(), "Balance mismatch for tx2"); + assertEquals(tx2Nonce, account.getNonce(), "Nonce mismatch for tx2"); + assertEquals(tx2Code, account.getCode(), "Code mismatch for tx2"); + assertEquals( + tx2Slot1Value, account.getStorageValue(slot1Key), "Slot1 mismatch for tx2"); + } + default -> + throw new IllegalStateException( + "Unexpected transactionLocation " + transactionLocation); + } + + return TransactionProcessingResult.successful( + Collections.emptyList(), + 0, + 0, + Bytes.EMPTY, + Optional.empty(), + ValidationResult.valid()); + }); + + final BalConcurrentTransactionProcessor processor = + new BalConcurrentTransactionProcessor( + transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); + + final Transaction tx0 = mockTransaction(); + final Transaction tx1 = mockTransaction(); + final Transaction tx2 = mockTransaction(); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + List.of(tx0, tx1, tx2), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + final Transaction[] txs = new Transaction[] {tx0, tx1, tx2}; + for (int i = 0; i < txs.length; i++) { + final Optional maybeResult = + processor.getProcessingResult( + env.worldState(), + MINING_BENEFICIARY, + txs[i], + i, + Optional.empty(), + Optional.empty()); + + assertTrue( + maybeResult.isPresent(), + "Expected processing result for transaction " + i + " to be present"); + assertTrue( + maybeResult.get().isSuccessful(), + "Expected processing result for transaction " + i + " to be successful"); + } + } + } + + @Nested + @DisplayName("Fallback Behavior") + class FallbackBehaviorTests { + + @Test + @DisplayName("Returns empty when parallel context is null") + void returnsEmptyWhenParallelContextIsNull() { + final TestEnvironment env = createTestEnvironment(); + final BlockAccessList blockAccessList = mockEmptyBlockAccessList(); + final Transaction transaction = mockTransaction(); + + when(transactionProcessor.processTransaction( + any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(new RuntimeException("Simulated failure")); + + final BalConcurrentTransactionProcessor processor = + new BalConcurrentTransactionProcessor( + transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + final Optional result = + processor.getProcessingResult( + env.worldState(), + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()); + + assertTrue( + result.isEmpty(), "Expected empty result when context is null - triggers fallback"); + } + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticParallelBlockProcessorIntegrationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticParallelBlockProcessorIntegrationTest.java new file mode 100644 index 00000000000..b6d1fa28fbb --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticParallelBlockProcessorIntegrationTest.java @@ -0,0 +1,125 @@ +/* + * 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.parallelization; + +import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; +import org.hyperledger.besu.ethereum.mainnet.ImmutableBalConfiguration; +import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; + +/** + * Integration tests for optimistic (collision-detection based) parallel block processing. Uses + * ParallelizedConcurrentTransactionProcessor under the hood. + * + *

    Tests are organized into nested classes by category, each extending the appropriate abstract + * test class to inherit the test methods while providing the optimistic-specific configuration. + */ +class OptimisticParallelBlockProcessorIntegrationTest { + + private static final BalConfiguration OPTIMISTIC_CONFIG = + ImmutableBalConfiguration.builder().isPerfectParallelizationEnabled(false).build(); + + private static String getVariant() { + return "Optimistic (Collision Detection)"; + } + + private static ParallelTransactionPreprocessing createPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return new ParallelTransactionPreprocessing( + transactionProcessor, Runnable::run, OPTIMISTIC_CONFIG); + } + + @Nested + @DisplayName("Simple Transfers") + class SimpleTransfers extends AbstractSimpleTransferTest { + @Override + protected String getVariantName() { + return getVariant(); + } + + @Override + protected BalConfiguration getBalConfiguration() { + return OPTIMISTIC_CONFIG; + } + + @Override + protected ParallelTransactionPreprocessing createParallelPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return createPreprocessing(transactionProcessor); + } + } + + @Nested + @DisplayName("Contract Storage") + class ContractStorage extends AbstractContractStorageTest { + @Override + protected String getVariantName() { + return getVariant(); + } + + @Override + protected BalConfiguration getBalConfiguration() { + return OPTIMISTIC_CONFIG; + } + + @Override + protected ParallelTransactionPreprocessing createParallelPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return createPreprocessing(transactionProcessor); + } + } + + @Nested + @DisplayName("Storage Dependency (Collision Detection)") + class StorageDependency extends AbstractStorageDependencyTest { + @Override + protected String getVariantName() { + return getVariant(); + } + + @Override + protected BalConfiguration getBalConfiguration() { + return OPTIMISTIC_CONFIG; + } + + @Override + protected ParallelTransactionPreprocessing createParallelPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return createPreprocessing(transactionProcessor); + } + } + + @Nested + @DisplayName("Mining Beneficiary BAL") + class MiningBeneficiaryBal extends AbstractMiningBeneficiaryBalTest { + @Override + protected String getVariantName() { + return getVariant(); + } + + @Override + protected BalConfiguration getBalConfiguration() { + return OPTIMISTIC_CONFIG; + } + + @Override + protected ParallelTransactionPreprocessing createParallelPreprocessing( + final MainnetTransactionProcessor transactionProcessor) { + return createPreprocessing(transactionProcessor); + } + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticTransactionProcessorUnitTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticTransactionProcessorUnitTest.java new file mode 100644 index 00000000000..c0f081b8c30 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticTransactionProcessorUnitTest.java @@ -0,0 +1,495 @@ +/* + * 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.parallelization; + +import static org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig.createStatefulConfigWithTrie; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.chain.MutableBlockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; +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.ValidationResult; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BlockAccessListBuilder; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.PartialBlockAccessView; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.CodeCache; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoOpBonsaiCachedWorldStorageManager; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoopBonsaiCachedMerkleTrieLoader; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldState; +import org.hyperledger.besu.ethereum.trie.pathbased.common.trielog.NoOpTrieLogManager; +import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; +import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.evm.blockhash.BlockHashLookup; +import org.hyperledger.besu.evm.internal.EvmConfiguration; +import org.hyperledger.besu.evm.tracing.OperationTracer; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executor; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit tests for ParallelizedConcurrentTransactionProcessor (Optimistic strategy). Tests verify: - + * Collision detector is called with correct arguments - Fallback to sequential execution when + * collision is detected - Access location tracker integration - Partial block access view updates + */ +@ExtendWith(MockitoExtension.class) +class OptimisticTransactionProcessorUnitTest { + + private static final Address MINING_BENEFICIARY = Address.fromHexString("0x1"); + private static final Wei BLOB_GAS_PRICE = Wei.ZERO; + private final Executor sameThreadExecutor = Runnable::run; + + @Mock private MainnetTransactionProcessor transactionProcessor; + @Mock private TransactionCollisionDetector collisionDetector; + + private ParallelizedConcurrentTransactionProcessor processor; + private TestEnvironment env; + + @BeforeEach + void setUp() { + processor = + new ParallelizedConcurrentTransactionProcessor(transactionProcessor, collisionDetector); + env = createTestEnvironment(); + } + + private record TestEnvironment( + ProtocolContext protocolContext, BlockHeader blockHeader, BonsaiWorldState worldState) {} + + private BonsaiWorldState createEmptyWorldState() { + final BonsaiWorldStateKeyValueStorage storage = + new BonsaiWorldStateKeyValueStorage( + new InMemoryKeyValueStorageProvider(), + new NoOpMetricsSystem(), + DataStorageConfiguration.DEFAULT_BONSAI_CONFIG); + + return new BonsaiWorldState( + storage, + new NoopBonsaiCachedMerkleTrieLoader(), + new NoOpBonsaiCachedWorldStorageManager(storage, EvmConfiguration.DEFAULT, new CodeCache()), + new NoOpTrieLogManager(), + EvmConfiguration.DEFAULT, + createStatefulConfigWithTrie(), + new CodeCache()); + } + + private TestEnvironment createTestEnvironment() { + final ProtocolContext protocolContext = mock(ProtocolContext.class); + final MutableBlockchain blockchain = mock(MutableBlockchain.class); + final BlockHeader chainHeadBlockHeader = mock(BlockHeader.class); + final BlockHeader blockHeader = mock(BlockHeader.class); + final WorldStateArchive worldStateArchive = mock(WorldStateArchive.class); + final BonsaiWorldState worldState = createEmptyWorldState(); + + when(protocolContext.getBlockchain()).thenReturn(blockchain); + when(blockchain.getChainHeadHeader()).thenReturn(chainHeadBlockHeader); + when(chainHeadBlockHeader.getHash()).thenReturn(Hash.ZERO); + when(chainHeadBlockHeader.getStateRoot()).thenReturn(Hash.EMPTY_TRIE_HASH); + when(blockHeader.getParentHash()).thenReturn(Hash.ZERO); + when(protocolContext.getWorldStateArchive()).thenReturn(worldStateArchive); + when(worldStateArchive.getWorldState(any())).thenReturn(Optional.of(worldState)); + + return new TestEnvironment(protocolContext, blockHeader, worldState); + } + + private Transaction mockTransaction() { + final Transaction transaction = mock(Transaction.class); + when(transaction.detachedCopy()).thenReturn(transaction); + return transaction; + } + + private void stubSuccessfulTransaction(final Optional partialView) { + when(transactionProcessor.processTransaction( + any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn( + TransactionProcessingResult.successful( + Collections.emptyList(), 0, 0, Bytes.EMPTY, partialView, ValidationResult.valid())); + } + + @Nested + @DisplayName("Transaction Processing") + class TransactionProcessingTests { + + @Test + @DisplayName("Transaction processor is called with correct parameters") + void transactionProcessorCalledWithCorrectParams() { + final Transaction transaction = mockTransaction(); + stubSuccessfulTransaction(Optional.empty()); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + verify(transactionProcessor, times(1)) + .processTransaction( + any(WorldUpdater.class), + eq(env.blockHeader()), + eq(transaction), + eq(MINING_BENEFICIARY), + any(OperationTracer.class), + any(BlockHashLookup.class), + eq(TransactionValidationParams.processingBlock()), + eq(BLOB_GAS_PRICE), + eq(Optional.empty())); + } + + @Test + @DisplayName("Transaction processor is called once per transaction") + void transactionProcessorCalledOncePerTransaction() { + final Transaction tx1 = mockTransaction(); + final Transaction tx2 = mockTransaction(); + final Transaction tx3 = mockTransaction(); + stubSuccessfulTransaction(Optional.empty()); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + List.of(tx1, tx2, tx3), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + verify(transactionProcessor, times(3)) + .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); + } + } + + @Nested + @DisplayName("Collision Detection") + class CollisionDetectionTests { + + @Test + @DisplayName("Collision detector is called with correct transaction") + void collisionDetectorCalledWithCorrectTransaction() { + final Transaction transaction = mockTransaction(); + stubSuccessfulTransaction(Optional.empty()); + when(collisionDetector.hasCollision(any(), any(), any(), any())).thenReturn(false); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + processor.getProcessingResult( + env.worldState(), MINING_BENEFICIARY, transaction, 0, Optional.empty(), Optional.empty()); + + verify(collisionDetector, times(1)) + .hasCollision( + eq(transaction), + eq(MINING_BENEFICIARY), + any(ParallelizedTransactionContext.class), + any()); + } + + @Test + @DisplayName("Collision detector is called with correct mining beneficiary") + void collisionDetectorCalledWithCorrectMiningBeneficiary() { + final Transaction transaction = mockTransaction(); + final Address customBeneficiary = Address.fromHexString("0xABCDEF"); + stubSuccessfulTransaction(Optional.empty()); + when(collisionDetector.hasCollision(any(), any(), any(), any())).thenReturn(false); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + customBeneficiary, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + processor.getProcessingResult( + env.worldState(), customBeneficiary, transaction, 0, Optional.empty(), Optional.empty()); + + verify(collisionDetector, times(1)) + .hasCollision(eq(transaction), eq(customBeneficiary), any(), any()); + } + + @Test + @DisplayName("Collision detector is called for each transaction") + void collisionDetectorCalledForEachTransaction() { + final Transaction tx1 = mockTransaction(); + final Transaction tx2 = mockTransaction(); + final Transaction tx3 = mockTransaction(); + stubSuccessfulTransaction(Optional.empty()); + when(collisionDetector.hasCollision(any(), any(), any(), any())).thenReturn(false); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + List.of(tx1, tx2, tx3), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + processor.getProcessingResult( + env.worldState(), MINING_BENEFICIARY, tx1, 0, Optional.empty(), Optional.empty()); + processor.getProcessingResult( + env.worldState(), MINING_BENEFICIARY, tx2, 1, Optional.empty(), Optional.empty()); + processor.getProcessingResult( + env.worldState(), MINING_BENEFICIARY, tx3, 2, Optional.empty(), Optional.empty()); + + verify(collisionDetector, times(1)) + .hasCollision(eq(tx1), eq(MINING_BENEFICIARY), any(), any()); + verify(collisionDetector, times(1)) + .hasCollision(eq(tx2), eq(MINING_BENEFICIARY), any(), any()); + verify(collisionDetector, times(1)) + .hasCollision(eq(tx3), eq(MINING_BENEFICIARY), any(), any()); + } + } + + @Nested + @DisplayName("Fallback Behavior") + class FallbackBehaviorTests { + + @Test + @DisplayName("Returns empty when collision detected - triggers sequential fallback") + void returnsEmptyWhenCollisionDetected() { + final Transaction transaction = mockTransaction(); + stubSuccessfulTransaction(Optional.empty()); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + when(collisionDetector.hasCollision(any(), any(), any(), any())).thenReturn(true); + + final Optional result = + processor.getProcessingResult( + env.worldState(), + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()); + + assertTrue(result.isEmpty(), "Expected empty result due to collision - triggers fallback"); + } + + @Test + @DisplayName("Returns empty when parallel context is null") + void returnsEmptyWhenParallelContextIsNull() { + final Transaction transaction = mockTransaction(); + + when(transactionProcessor.processTransaction( + any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(new RuntimeException("Simulated failure")); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + final Optional result = + processor.getProcessingResult( + env.worldState(), + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()); + + assertTrue( + result.isEmpty(), "Expected empty result when context is null - triggers fallback"); + } + + @Test + @DisplayName("Returns result when no collision detected") + void returnsResultWhenNoCollision() { + final Transaction transaction = mockTransaction(); + stubSuccessfulTransaction(Optional.empty()); + when(collisionDetector.hasCollision(any(), any(), any(), any())).thenReturn(false); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + final Optional result = + processor.getProcessingResult( + env.worldState(), + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()); + + assertTrue(result.isPresent(), "Expected result when no collision"); + assertTrue(result.get().isSuccessful(), "Expected successful result"); + } + + @Test + @DisplayName("First transaction succeeds, second triggers fallback") + void partialFallbackOnSecondTransaction() { + final Transaction tx1 = mockTransaction(); + final Transaction tx2 = mockTransaction(); + stubSuccessfulTransaction(Optional.empty()); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + List.of(tx1, tx2), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty()); + + when(collisionDetector.hasCollision(eq(tx1), any(), any(), any())).thenReturn(false); + when(collisionDetector.hasCollision(eq(tx2), any(), any(), any())).thenReturn(true); + + final Optional result1 = + processor.getProcessingResult( + env.worldState(), MINING_BENEFICIARY, tx1, 0, Optional.empty(), Optional.empty()); + final Optional result2 = + processor.getProcessingResult( + env.worldState(), MINING_BENEFICIARY, tx2, 1, Optional.empty(), Optional.empty()); + + assertTrue(result1.isPresent(), "First transaction should succeed"); + assertTrue(result2.isEmpty(), "Second transaction should trigger fallback"); + } + } + + @Nested + @DisplayName("Access Location Tracker Integration") + class AccessLocationTrackerTests { + + @Test + @DisplayName("Access location tracker is passed when BAL builder is provided") + void accessLocationTrackerPassedWithBalBuilder() { + final Transaction transaction = mockTransaction(); + stubSuccessfulTransaction(Optional.empty()); + + final BlockAccessListBuilder balBuilder = mock(BlockAccessListBuilder.class); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.of(balBuilder)); + + verify(transactionProcessor) + .processTransaction( + any(WorldUpdater.class), + eq(env.blockHeader()), + eq(transaction), + eq(MINING_BENEFICIARY), + any(OperationTracer.class), + any(BlockHashLookup.class), + eq(TransactionValidationParams.processingBlock()), + eq(BLOB_GAS_PRICE), + argThat(Optional::isPresent)); + } + + @Test + @DisplayName("Partial block access view is preserved in result") + void partialBlockAccessViewPreservedInResult() { + final Transaction transaction = mockTransaction(); + + final PartialBlockAccessView partialView = mock(PartialBlockAccessView.class); + + stubSuccessfulTransaction(Optional.of(partialView)); + when(collisionDetector.hasCollision(any(), any(), any(), any())).thenReturn(false); + + final BlockAccessListBuilder balBuilder = mock(BlockAccessListBuilder.class); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.of(balBuilder)); + + final Optional maybeResult = + processor.getProcessingResult( + env.worldState(), + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()); + + assertTrue(maybeResult.isPresent(), "Expected result to be applied"); + assertTrue( + maybeResult.get().getPartialBlockAccessView().isPresent(), + "Expected BAL view to be present"); + } + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockProcessorTestSupport.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockProcessorTestSupport.java new file mode 100644 index 00000000000..fa3035d0fed --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockProcessorTestSupport.java @@ -0,0 +1,59 @@ +/* + * 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.parallelization; + +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPPrivateKey; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; + +import org.apache.tuweni.bytes.Bytes32; + +/** Shared constants and utilities for parallel block processor integration tests. */ +public final class ParallelBlockProcessorTestSupport { + + private ParallelBlockProcessorTestSupport() {} + + public static final String GENESIS_CONFIG = + "/org/hyperledger/besu/ethereum/mainnet/parallelization/genesis-it.json"; + + public static final Address MINING_BENEFICIARY = + Address.fromHexStringStrict("0xa05b21E5186Ce93d2a226722b85D6e550Ac7D6E3"); + + public static final String ACCOUNT_GENESIS_1 = "0x627306090abab3a6e1400e9345bc60c78a8bef57"; + public static final String ACCOUNT_GENESIS_2 = "0x7f2d653f56ea8de6ffa554c7a0cd4e03af79f3eb"; + public static final String ACCOUNT_2 = "0x0000000000000000000000000000000000000002"; + public static final String ACCOUNT_3 = "0x0000000000000000000000000000000000000003"; + public static final String ACCOUNT_4 = "0x0000000000000000000000000000000000000004"; + public static final String ACCOUNT_5 = "0x0000000000000000000000000000000000000005"; + public static final String ACCOUNT_6 = "0x0000000000000000000000000000000000000006"; + public static final String CONTRACT_ADDRESS = "0x00000000000000000000000000000000000fffff"; + public static final String PARALLEL_TEST_CONTRACT = "0x00000000000000000000000000000000000eeeee"; + + public static final KeyPair ACCOUNT_GENESIS_1_KEYPAIR = + generateKeyPair("c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"); + public static final KeyPair ACCOUNT_GENESIS_2_KEYPAIR = + generateKeyPair("fc5141e75bf622179f8eedada7fab3e2e6b3e3da8eb9df4f46d84df22df7430e"); + public static final KeyPair MINING_BENEFICIARY_KEYPAIR = + generateKeyPair("3a4ff6d22d7502ef2452368165422861c01a0f72f851793b372b87888dc3c453"); + + public static KeyPair generateKeyPair(final String privateKeyHex) { + return SignatureAlgorithmFactory.getInstance() + .createKeyPair( + SECPPrivateKey.create( + Bytes32.fromHexString(privateKeyHex), SignatureAlgorithm.ALGORITHM)); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockTransactionProcessorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockTransactionProcessorTest.java deleted file mode 100644 index 4443a1179ef..00000000000 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockTransactionProcessorTest.java +++ /dev/null @@ -1,542 +0,0 @@ -/* - * 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 - */ -package org.hyperledger.besu.ethereum.mainnet.parallelization; - -import static org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig.createStatefulConfigWithTrie; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.datatypes.StorageSlotKey; -import org.hyperledger.besu.datatypes.Wei; -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.chain.MutableBlockchain; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; -import org.hyperledger.besu.ethereum.core.Transaction; -import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; -import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; -import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; -import org.hyperledger.besu.ethereum.mainnet.ValidationResult; -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.block.access.list.PartialBlockAccessView; -import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; -import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.CodeCache; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoOpBonsaiCachedWorldStorageManager; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoopBonsaiCachedMerkleTrieLoader; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldState; -import org.hyperledger.besu.ethereum.trie.pathbased.common.trielog.NoOpTrieLogManager; -import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; -import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; -import org.hyperledger.besu.evm.account.Account; -import org.hyperledger.besu.evm.blockhash.BlockHashLookup; -import org.hyperledger.besu.evm.internal.EvmConfiguration; -import org.hyperledger.besu.evm.tracing.OperationTracer; -import org.hyperledger.besu.evm.worldstate.WorldUpdater; -import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; - -import java.util.Collections; -import java.util.Optional; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; - -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.units.bigints.UInt256; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ParallelBlockTransactionProcessorTest { - - private enum ProcessorVariant { - PARALLELIZED, - BAL - } - - private final Executor sameThreadExecutor = Runnable::run; - private static final Address MINING_BENEFICIARY = Address.fromHexString("0x1"); - private static final Wei BLOB_GAS_PRICE = Wei.ZERO; - - private Stream processorVariants() { - return Stream.of(ProcessorVariant.PARALLELIZED, ProcessorVariant.BAL); - } - - private BonsaiWorldState createEmptyWorldState() { - final BonsaiWorldStateKeyValueStorage bonsaiWorldStateKeyValueStorage = - new BonsaiWorldStateKeyValueStorage( - new InMemoryKeyValueStorageProvider(), - new NoOpMetricsSystem(), - DataStorageConfiguration.DEFAULT_BONSAI_CONFIG); - - return new BonsaiWorldState( - bonsaiWorldStateKeyValueStorage, - new NoopBonsaiCachedMerkleTrieLoader(), - new NoOpBonsaiCachedWorldStorageManager( - bonsaiWorldStateKeyValueStorage, EvmConfiguration.DEFAULT, new CodeCache()), - new NoOpTrieLogManager(), - EvmConfiguration.DEFAULT, - createStatefulConfigWithTrie(), - new CodeCache()); - } - - private ParallelBlockTransactionProcessor createProcessor( - final ProcessorVariant variant, - final MainnetTransactionProcessor transactionProcessor, - final TransactionCollisionDetector collisionDetector, - final BlockAccessList blockAccessList) { - - return switch (variant) { - case PARALLELIZED -> - new ParallelizedConcurrentTransactionProcessor(transactionProcessor, collisionDetector); - case BAL -> - new BalConcurrentTransactionProcessor( - transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); - }; - } - - private record TestEnvironment( - ProtocolContext protocolContext, BlockHeader blockHeader, BonsaiWorldState worldState) {} - - private TestEnvironment createTestEnvironment() { - final ProtocolContext protocolContext = mock(ProtocolContext.class); - final MutableBlockchain blockchain = mock(MutableBlockchain.class); - final BlockHeader chainHeadBlockHeader = mock(BlockHeader.class); - final BlockHeader blockHeader = mock(BlockHeader.class); - final WorldStateArchive worldStateArchive = mock(WorldStateArchive.class); - final BonsaiWorldState worldState = createEmptyWorldState(); - - when(protocolContext.getBlockchain()).thenReturn(blockchain); - when(blockchain.getChainHeadHeader()).thenReturn(chainHeadBlockHeader); - when(chainHeadBlockHeader.getHash()).thenReturn(Hash.ZERO); - when(chainHeadBlockHeader.getStateRoot()).thenReturn(Hash.EMPTY_TRIE_HASH); - - when(blockHeader.getParentHash()).thenReturn(Hash.ZERO); - - when(protocolContext.getWorldStateArchive()).thenReturn(worldStateArchive); - when(worldStateArchive.getWorldState(any())).thenReturn(Optional.of(worldState)); - - return new TestEnvironment(protocolContext, blockHeader, worldState); - } - - private BlockAccessList mockEmptyBlockAccessList() { - final BlockAccessList blockAccessList = mock(BlockAccessList.class); - when(blockAccessList.accountChanges()).thenReturn(Collections.emptyList()); - return blockAccessList; - } - - private Transaction mockTransaction() { - final Transaction transaction = mock(Transaction.class); - when(transaction.detachedCopy()).thenReturn(transaction); - return transaction; - } - - private record ProcessorTestFixture( - ProcessorVariant variant, - MainnetTransactionProcessor transactionProcessor, - TransactionCollisionDetector collisionDetector, - BlockAccessList blockAccessList, - Transaction transaction, - TestEnvironment env, - ParallelBlockTransactionProcessor processor) {} - - private ProcessorTestFixture createFixture(final ProcessorVariant variant) { - final MainnetTransactionProcessor transactionProcessor = - mock(MainnetTransactionProcessor.class); - final TransactionCollisionDetector collisionDetector = mock(TransactionCollisionDetector.class); - final BlockAccessList blockAccessList = mockEmptyBlockAccessList(); - final Transaction transaction = mockTransaction(); - final TestEnvironment env = createTestEnvironment(); - final ParallelBlockTransactionProcessor processor = - createProcessor(variant, transactionProcessor, collisionDetector, blockAccessList); - - if (variant == ProcessorVariant.PARALLELIZED) { - when(collisionDetector.hasCollision(any(), any(), any(), any())).thenReturn(false); - } - - return new ProcessorTestFixture( - variant, - transactionProcessor, - collisionDetector, - blockAccessList, - transaction, - env, - processor); - } - - private void stubSuccessfulTransaction( - final MainnetTransactionProcessor transactionProcessor, - final Optional partialView) { - - when(transactionProcessor.processTransaction( - any(), any(), any(), any(), any(), any(), any(), any(), any())) - .thenReturn( - TransactionProcessingResult.successful( - Collections.emptyList(), 0, 0, Bytes.EMPTY, partialView, ValidationResult.valid())); - } - - @ParameterizedTest(name = "{index}: {0}") - @MethodSource("processorVariants") - void testRunTransaction(final ProcessorVariant variant) { - final ProcessorTestFixture f = createFixture(variant); - stubSuccessfulTransaction(f.transactionProcessor(), Optional.empty()); - - f.processor() - .runAsyncBlock( - f.env().protocolContext(), - f.env().blockHeader(), - Collections.singletonList(f.transaction()), - MINING_BENEFICIARY, - (__, ___) -> Hash.EMPTY, - BLOB_GAS_PRICE, - sameThreadExecutor, - Optional.empty()); - - verify(f.transactionProcessor(), times(1)) - .processTransaction( - any(WorldUpdater.class), - eq(f.env().blockHeader()), - eq(f.transaction()), - eq(MINING_BENEFICIARY), - any(OperationTracer.class), - any(BlockHashLookup.class), - eq(TransactionValidationParams.processingBlock()), - eq(BLOB_GAS_PRICE), - eq(Optional.empty())); - - final Optional maybeResult = - f.processor() - .getProcessingResult( - f.env().worldState(), - MINING_BENEFICIARY, - f.transaction(), - 0, - Optional.empty(), - Optional.empty()); - - assertTrue(maybeResult.isPresent(), "Expected the transaction result to be present"); - assertTrue(maybeResult.get().isSuccessful(), "Expected the processing to be successful"); - } - - @ParameterizedTest(name = "{index}: {0}") - @MethodSource("processorVariants") - void testRunTransactionWithFailure(final ProcessorVariant variant) { - final ProcessorTestFixture f = createFixture(variant); - when(f.transactionProcessor() - .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any())) - .thenReturn( - TransactionProcessingResult.failed( - 0, - 0, - ValidationResult.invalid( - TransactionInvalidReason.BLOB_GAS_PRICE_BELOW_CURRENT_BLOB_BASE_FEE), - Optional.of(Bytes.EMPTY), - Optional.empty(), - Optional.empty())); - - f.processor() - .runAsyncBlock( - f.env().protocolContext(), - f.env().blockHeader(), - Collections.singletonList(f.transaction()), - MINING_BENEFICIARY, - (__, ___) -> Hash.EMPTY, - BLOB_GAS_PRICE, - sameThreadExecutor, - Optional.empty()); - - final Optional maybeResult = - f.processor() - .getProcessingResult( - f.env().worldState(), - MINING_BENEFICIARY, - f.transaction(), - 0, - Optional.empty(), - Optional.empty()); - - if (variant == ProcessorVariant.BAL) { - assertFalse(maybeResult.isEmpty(), "Expected non-empty result for the BAL variant"); - assertTrue(maybeResult.get().getStatus() == TransactionProcessingResult.Status.FAILED); - } else { - assertTrue( - maybeResult.isEmpty(), - "Expected empty result so the block processor re-executes the transaction"); - } - } - - @Test - void testRunTransactionWithConflict() { - final ProcessorTestFixture f = createFixture(ProcessorVariant.PARALLELIZED); - stubSuccessfulTransaction(f.transactionProcessor(), Optional.empty()); - - f.processor() - .runAsyncBlock( - f.env().protocolContext(), - f.env().blockHeader(), - Collections.singletonList(f.transaction()), - MINING_BENEFICIARY, - (__, ___) -> Hash.EMPTY, - BLOB_GAS_PRICE, - sameThreadExecutor, - Optional.empty()); - - verify(f.transactionProcessor(), times(1)) - .processTransaction( - any(WorldUpdater.class), - eq(f.env().blockHeader()), - eq(f.transaction()), - eq(MINING_BENEFICIARY), - any(OperationTracer.class), - any(BlockHashLookup.class), - eq(TransactionValidationParams.processingBlock()), - eq(BLOB_GAS_PRICE), - eq(Optional.empty())); - - // simulate a conflict - when(f.collisionDetector().hasCollision(any(), any(), any(), any())).thenReturn(true); - - final Optional maybeResult = - f.processor() - .getProcessingResult( - f.env().worldState(), - MINING_BENEFICIARY, - f.transaction(), - 0, - Optional.empty(), - Optional.empty()); - - assertTrue( - maybeResult.isEmpty(), "Expected no transaction result to be applied due to conflict"); - } - - @Test - void testApplyResultUsesAccessLocationTrackerAndUpdatesPartialBlockAccessView() { - final ProcessorTestFixture f = createFixture(ProcessorVariant.PARALLELIZED); - - final PartialBlockAccessView partialView = mock(PartialBlockAccessView.class); - final PartialBlockAccessView.AccountChanges beneficiaryChanges = - mock(PartialBlockAccessView.AccountChanges.class); - when(beneficiaryChanges.getAddress()).thenReturn(MINING_BENEFICIARY); - when(partialView.accountChanges()).thenReturn(Collections.singletonList(beneficiaryChanges)); - - stubSuccessfulTransaction(f.transactionProcessor(), Optional.of(partialView)); - - final BlockAccessListBuilder balBuilder = mock(BlockAccessListBuilder.class); - - f.processor() - .runAsyncBlock( - f.env().protocolContext(), - f.env().blockHeader(), - Collections.singletonList(f.transaction()), - MINING_BENEFICIARY, - (__, ___) -> Hash.EMPTY, - BLOB_GAS_PRICE, - sameThreadExecutor, - Optional.of(balBuilder)); - - verify(f.transactionProcessor()) - .processTransaction( - any(WorldUpdater.class), - eq(f.env().blockHeader()), - eq(f.transaction()), - eq(MINING_BENEFICIARY), - any(OperationTracer.class), - any(BlockHashLookup.class), - eq(TransactionValidationParams.processingBlock()), - eq(BLOB_GAS_PRICE), - argThat(Optional::isPresent)); - - final Optional maybeResult = - f.processor() - .getProcessingResult( - f.env().worldState(), - MINING_BENEFICIARY, - f.transaction(), - 0, - Optional.empty(), - Optional.empty()); - - assertTrue( - maybeResult.isPresent(), "Expected the parallelized transaction result to be applied"); - final TransactionProcessingResult result = maybeResult.get(); - assertTrue(result.getPartialBlockAccessView().isPresent(), "Expected BAL view to be present"); - verify(beneficiaryChanges).setPostBalance(any(Wei.class)); - } - - @Test - void testPreStateSetup() { - final TestEnvironment env = createTestEnvironment(); - - final Address accountAddress = - Address.fromHexString("0x1000000000000000000000000000000000000001"); - - final UInt256 slot1Key = UInt256.ONE; - final UInt256 slot2Key = UInt256.valueOf(2); - final StorageSlotKey slot1 = new StorageSlotKey(slot1Key); - final StorageSlotKey slot2 = new StorageSlotKey(slot2Key); - - final Wei tx0Balance = Wei.of(100); - final long tx0Nonce = 1L; - final Bytes tx0Code = Bytes.fromHexString("0xAA"); - final UInt256 tx0Slot1Value = UInt256.valueOf(1); - final UInt256 tx0Slot2Value = UInt256.valueOf(3); - - final Wei tx1Balance = Wei.of(200); - final long tx1Nonce = 2L; - final Bytes tx1Code = Bytes.fromHexString("0xBB"); - final UInt256 tx1Slot1Value = UInt256.valueOf(5); - final UInt256 tx1Slot2Value = UInt256.ZERO; - - final Wei tx2Balance = Wei.of(300); - final long tx2Nonce = 3L; - final Bytes tx2Code = Bytes.fromHexString("0xCC"); - final UInt256 tx2Slot1Value = UInt256.valueOf(7); - - final BlockAccessList.BlockAccessListBuilder balBuilder = BlockAccessList.builder(); - - final PartialBlockAccessView.PartialBlockAccessViewBuilder p0 = - new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(0); - final PartialBlockAccessView.AccountChangesBuilder a0 = - p0.getOrCreateAccountBuilder(accountAddress); - a0.withPostBalance(tx0Balance); - a0.withNonceChange(tx0Nonce); - a0.withNewCode(tx0Code); - a0.addStorageChange(slot1, tx0Slot1Value); - a0.addStorageChange(slot2, tx0Slot2Value); - balBuilder.apply(p0.build()); - - final PartialBlockAccessView.PartialBlockAccessViewBuilder p1 = - new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(1); - final PartialBlockAccessView.AccountChangesBuilder a1 = - p1.getOrCreateAccountBuilder(accountAddress); - a1.withPostBalance(tx1Balance); - a1.withNonceChange(tx1Nonce); - a1.withNewCode(tx1Code); - a1.addStorageChange(slot1, tx1Slot1Value); - a1.addStorageChange(slot2, null); - balBuilder.apply(p1.build()); - - final PartialBlockAccessView.PartialBlockAccessViewBuilder p2 = - new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(2); - final PartialBlockAccessView.AccountChangesBuilder a2 = - p2.getOrCreateAccountBuilder(accountAddress); - a2.withPostBalance(tx2Balance); - a2.withNonceChange(tx2Nonce); - a2.withNewCode(tx2Code); - a2.addStorageChange(slot1, tx2Slot1Value); - balBuilder.apply(p2.build()); - - final BlockAccessList blockAccessList = balBuilder.build(); - - final MainnetTransactionProcessor transactionProcessor = - mock(MainnetTransactionProcessor.class); - - final AtomicInteger locationCounter = new AtomicInteger(0); - when(transactionProcessor.processTransaction( - any(), any(), any(), any(), any(), any(), any(), any(), any())) - .thenAnswer( - invocation -> { - final int transactionLocation = locationCounter.getAndIncrement(); - final WorldUpdater worldUpdater = invocation.getArgument(0, WorldUpdater.class); - final Account account = worldUpdater.get(accountAddress); - - assertTrue(account != null, "Expected account to exist in world updater"); - - switch (transactionLocation) { - case 0 -> { - // transactionLocation = 0 -> balIndex = 1 -> latest < 1 is tx0 - assertEquals(tx0Balance, account.getBalance()); - assertEquals(tx0Nonce, account.getNonce()); - assertEquals(tx0Code, account.getCode()); - assertEquals(tx0Slot1Value, account.getStorageValue(slot1Key)); - assertEquals(tx0Slot2Value, account.getStorageValue(slot2Key)); - } - case 1 -> { - // transactionLocation = 1 -> balIndex = 2 -> latest < 2 is tx1 - assertEquals(tx1Balance, account.getBalance()); - assertEquals(tx1Nonce, account.getNonce()); - assertEquals(tx1Code, account.getCode()); - assertEquals(tx1Slot1Value, account.getStorageValue(slot1Key)); - assertEquals(tx1Slot2Value, account.getStorageValue(slot2Key)); - } - case 2 -> { - // transactionLocation = 2 -> balIndex = 3 -> latest < 3 is tx2 - assertEquals(tx2Balance, account.getBalance()); - assertEquals(tx2Nonce, account.getNonce()); - assertEquals(tx2Code, account.getCode()); - assertEquals(tx2Slot1Value, account.getStorageValue(slot1Key)); - } - default -> - throw new IllegalStateException( - "Unexpected transactionLocation " + transactionLocation); - } - - return TransactionProcessingResult.successful( - Collections.emptyList(), - 0, - 0, - Bytes.EMPTY, - Optional.empty(), - ValidationResult.valid()); - }); - - final BalConcurrentTransactionProcessor processor = - new BalConcurrentTransactionProcessor( - transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); - - final Transaction tx0 = mockTransaction(); - final Transaction tx1 = mockTransaction(); - final Transaction tx2 = mockTransaction(); - - processor.runAsyncBlock( - env.protocolContext(), - env.blockHeader(), - java.util.List.of(tx0, tx1, tx2), - MINING_BENEFICIARY, - (__, ___) -> Hash.EMPTY, - BLOB_GAS_PRICE, - sameThreadExecutor, - Optional.empty()); - - final Transaction[] txs = new Transaction[] {tx0, tx1, tx2}; - for (int i = 0; i < txs.length; i++) { - final Optional maybeResult = - processor.getProcessingResult( - env.worldState(), MINING_BENEFICIARY, txs[i], i, Optional.empty(), Optional.empty()); - - assertTrue( - maybeResult.isPresent(), - "Expected processing result for transactionLocation " + i + " to be present"); - assertTrue( - maybeResult.get().isSuccessful(), - "Expected processing result for transactionLocation " + i + " to be successful"); - } - } -} diff --git a/ethereum/core/src/test/resources/org/hyperledger/besu/ethereum/mainnet/parallelization/genesis-it.json b/ethereum/core/src/test/resources/org/hyperledger/besu/ethereum/mainnet/parallelization/genesis-it.json new file mode 100644 index 00000000000..8e216a4e5ad --- /dev/null +++ b/ethereum/core/src/test/resources/org/hyperledger/besu/ethereum/mainnet/parallelization/genesis-it.json @@ -0,0 +1,264 @@ +{ + "config": { + "chainId":42, + "homesteadBlock":0, + "eip150Block":0, + "eip155Block":0, + "eip158Block":0, + "byzantiumBlock":0, + "constantinopleBlock":0, + "petersburgBlock":0, + "istanbulBlock":0, + "muirGlacierBlock":0, + "berlinBlock":0, + "londonBlock":0, + "terminalTotalDifficulty":0, + "cancunTime":0, + "pragueTime":0, + "osakaTime":0, + "amsterdamTime":0, + "blobSchedule": { + "cancun": { + "target": 3, + "max": 6, + "baseFeeUpdateFraction": 3338477 + }, + "prague": { + "target": 6, + "max": 9, + "baseFeeUpdateFraction": 5007716 + }, + "osaka": { + "target": 9, + "max": 12, + "baseFeeUpdateFraction": 5007716 + } + }, + "ethash": {}, + "depositContractAddress": "0x4242424242424242424242424242424242424242", + "withdrawalRequestContractAddress": "0x00A3ca265EBcb825B45F985A16CEFB49958cE017", + "consolidationRequestContractAddress": "0x00b42dbF2194e931E80326D950320f7d9Dbeac02" + }, + "nonce":"0x42", + "timestamp":"0x0", + "extraData":"0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit":"0x1C9C380", + "difficulty":"0x400000000", + "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase":"0x0000000000000000000000000000000000000000", + "alloc":{ + "a05b21E5186Ce93d2a226722b85D6e550Ac7D6E3": { + "privateKey": "3a4ff6d22d7502ef2452368165422861c01a0f72f851793b372b87888dc3c453", + "balance": "90000000000000000000000" + }, + "8da48afC965480220a3dB9244771bd3afcB5d895": { + "comment": "This account has signed a authorization for contract 0x0000000000000000000000000000000000009999 to send a 7702 transaction", + "privateKey": "11f2e7b6a734ab03fa682450e0d4681d18a944f8b83c99bf7b9b4de6c0f35ea1", + "balance": "90000000000000000000000" + }, + "0x0000000000000000000000000000000000000666": { + "comment": "Contract reverts immediately when called", + "balance": "0", + "code": "5F5FFD", + "codeDecompiled": "PUSH0 PUSH0 REVERT", + "storage": {} + }, + "0x0000000000000000000000000000000000009999": { + "comment": "Contract sends all its Ether to the address provided as a call data.", + "balance": "0", + "code": "5F5F5F5F475F355AF100", + "codeDecompiled": "PUSH0 PUSH0 PUSH0 PUSH0 SELFBALANCE PUSH0 CALLDATALOAD GAS CALL STOP", + "storage": {} + }, + "0x0000000000000000000000000000000000007708": { + "comment": "Contract that self-destructs and sends balance to address in calldata", + "balance": "1000000000000000000", + "code": "0x5F35FF", + "codeDecompiled": "PUSH0 CALLDATALOAD SELFDESTRUCT", + "storage": {} + }, + "0x0000000000000000000000000000000000007709": { + "comment": "Contract that self-destructs to ITSELF (for testing Selfdestruct LOG2)", + "balance": "1000000000000000000", + "code": "0x30FF", + "codeDecompiled": "ADDRESS SELFDESTRUCT", + "storage": {} + }, + "0x0000000000000000000000000000000000007700": { + "comment": "Destroyer contract - calls 7702, 7703, 7701 to test lexicographical sorting", + "balance": "0", + "code": "0x5F5F5F5F5F7300000000000000000000000000000000000077025AF15F5F5F5F5F7300000000000000000000000000000000000077035AF15F5F5F5F5F7300000000000000000000000000000000000077015AF100", + "codeDecompiled": "PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH20<0x7702> GAS CALL PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH20<0x7703> GAS CALL PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH20<0x7701> GAS CALL STOP", + "storage": {} + }, + "0x0000000000000000000000000000000000007701": { + "comment": "Selfdestruct-to-self contract 1 (lowest address)", + "balance": "100000000000000000", + "code": "0x30FF", + "codeDecompiled": "ADDRESS SELFDESTRUCT", + "storage": {} + }, + "0x0000000000000000000000000000000000007702": { + "comment": "Selfdestruct-to-self contract 2 (middle address)", + "balance": "200000000000000000", + "code": "0x30FF", + "codeDecompiled": "ADDRESS SELFDESTRUCT", + "storage": {} + }, + "0x0000000000000000000000000000000000007703": { + "comment": "Selfdestruct-to-self contract 3 (highest address)", + "balance": "300000000000000000", + "code": "0x30FF", + "codeDecompiled": "ADDRESS SELFDESTRUCT", + "storage": {} + }, + "0xa4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb": { + "balance": "1000000000000000000000000000" + }, + "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f": { + "comment": "This is the account used to sign the transaction that creates a validator exit", + "balance": "1000000000000000000000000000" + }, + "0x00A3ca265EBcb825B45F985A16CEFB49958cE017": { + "comment": "This is the runtime bytecode for the Withdrawal Request Smart Contract. It was created from the generated alloc section of fork_Prague_blockchain_test_engine_single_block_single_withdrawal_request_from_contract spec test", + "balance": "0", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460c7573615156028575f545f5260205ff35b36603814156101f05760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f057600182026001905f5b5f821115608057810190830284830290049160010191906065565b9093900434106101f057600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160db575060105b5f5b81811461017f5780604c02838201600302600401805490600101805490600101549160601b83528260140152807fffffffffffffffffffffffffffffffff0000000000000000000000000000000016826034015260401c906044018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160dd565b9101809214610191579060025561019c565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101c957505f5b6001546002828201116101de5750505f6101e4565b01600290035b5f555f600155604c025ff35b5f5ffd", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002": "0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000004": "000000000000000000000000a4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb", + "0x0000000000000000000000000000000000000000000000000000000000000005": "b10a4a15bf67b328c9b101d09e5c6ee6672978fdad9ef0d9e2ceffaee9922355", + "0x0000000000000000000000000000000000000000000000000000000000000006": "5d8601f0cb3bcc4ce1af9864779a416e00000000000000000000000000000000" + } + }, + "0x4242424242424242424242424242424242424242": { + "comment": "This is the runtime bytecode for the Deposit Request Smart Contract. It was created from the generated alloc section of fork_Prague_blockchain_test_engine_single_deposit_from_contract spec test", + "balance": "0", + "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a2646970667358221220dceca8706b29e917dacf25fceef95acac8d90d765ac926663ce4096195952b6164736f6c634300060b0033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000022": "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0x0000000000000000000000000000000000000000000000000000000000000023": "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x0000000000000000000000000000000000000000000000000000000000000024": "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x0000000000000000000000000000000000000000000000000000000000000025": "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x0000000000000000000000000000000000000000000000000000000000000026": "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0x0000000000000000000000000000000000000000000000000000000000000027": "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x0000000000000000000000000000000000000000000000000000000000000028": "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x0000000000000000000000000000000000000000000000000000000000000029": "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x000000000000000000000000000000000000000000000000000000000000002a": "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0x000000000000000000000000000000000000000000000000000000000000002b": "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x000000000000000000000000000000000000000000000000000000000000002c": "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0x000000000000000000000000000000000000000000000000000000000000002d": "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0x000000000000000000000000000000000000000000000000000000000000002e": "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0x000000000000000000000000000000000000000000000000000000000000002f": "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0x0000000000000000000000000000000000000000000000000000000000000030": "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x0000000000000000000000000000000000000000000000000000000000000031": "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x0000000000000000000000000000000000000000000000000000000000000032": "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x0000000000000000000000000000000000000000000000000000000000000033": "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0x0000000000000000000000000000000000000000000000000000000000000034": "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0x0000000000000000000000000000000000000000000000000000000000000035": "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x0000000000000000000000000000000000000000000000000000000000000036": "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0x0000000000000000000000000000000000000000000000000000000000000037": "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0x0000000000000000000000000000000000000000000000000000000000000038": "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x0000000000000000000000000000000000000000000000000000000000000039": "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x000000000000000000000000000000000000000000000000000000000000003a": "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x000000000000000000000000000000000000000000000000000000000000003b": "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x000000000000000000000000000000000000000000000000000000000000003c": "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x000000000000000000000000000000000000000000000000000000000000003d": "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x000000000000000000000000000000000000000000000000000000000000003e": "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0x000000000000000000000000000000000000000000000000000000000000003f": "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x0000000000000000000000000000000000000000000000000000000000000040": "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7" + } + }, + "0x00b42dbF2194e931E80326D950320f7d9Dbeac02": { + "comment": "This is the runtime bytecode for the Consolidation Request Smart Contract", + "nonce": "0x01", + "balance": "0x00", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500", + "storage": {} + }, + "0x000000000000000000000000000000000000784b": { + "comment": "SLOTNUM test contract - stores current slot number at storage key 0 (EIP-7843). Bytecode: SLOTNUM PUSH0 SSTORE STOP", + "balance": "0x00", + "code": "0x4b5f5500", + "codeDecompiled": "SLOTNUM PUSH0 SSTORE STOP", + "storage": {} + }, + "0x8024000000000000000000000000000000000001": { + "comment": "EIP-8024 DUPN test contract: PUSH1 1, PUSH1 0, 16xDUP1, DUPN 0, PUSH0, SSTORE", + "balance": "0x0", + "code": "0x6001600080808080808080808080808080808080e6005f55", + "codeDecompiled": "PUSH1 1 PUSH1 0 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUP1 DUPN 0 PUSH0 SSTORE", + "storage": {} + }, + "0x8024000000000000000000000000000000000002": { + "comment": "EIP-8024 SWAPN test contract: builds stack and swaps positions using SWAPN", + "balance": "0x0", + "code": "0x6001600260008080808080808080808080808080808080e7005f555f545f01015f55", + "codeDecompiled": "PUSH1 1 PUSH1 2 PUSH1 0 DUP1x17 SWAPN 0 PUSH0 SSTORE PUSH0 SLOAD PUSH0 ADD ADD PUSH0 SSTORE", + "storage": {} + }, + "0x8024000000000000000000000000000000000003": { + "comment": "EIP-8024 EXCHANGE test contract: PUSH1 0, PUSH1 1, PUSH1 2, EXCHANGE 01, stores result", + "balance": "0x0", + "code": "0x600060016002e8015f555f545f01015f55", + "codeDecompiled": "PUSH1 0 PUSH1 1 PUSH1 2 EXCHANGE 01 PUSH0 SSTORE PUSH0 SLOAD PUSH0 ADD ADD PUSH0 SSTORE", + "storage": {} + }, + "0x8024000000000000000000000000000000000004": { + "comment": "EIP-8024 invalid immediate test: DUPN with 0x5b (invalid range)", + "balance": "0x0", + "code": "0xe65f", + "codeDecompiled": "DUPN 0x5f", + "storage": {} + }, + "0x0000000000000000000000000000000000007778": { + "comment": "EIP-7778 test contract: Clears storage slot 0 when called, generating SSTORE refund. Pre-loaded with non-zero storage.", + "balance": "0", + "code": "0x5F5F5500", + "codeDecompiled": "PUSH0 PUSH0 SSTORE STOP", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + }, + "0x0000000000000000000000000000000000007710": { + "comment": "Factory that CREATEs a child contract (30FF = ADDRESS SELFDESTRUCT) with its balance, then CALLs it. Used to test EIP-7708 Selfdestruct logs for same-tx-created contracts.", + "balance": "1000000000000000000", + "code": "0x6130FF5F526002601E47F05F5F5F5F5F945AF100", + "codeDecompiled": "PUSH2 0x30FF PUSH0 MSTORE PUSH1 2 PUSH1 30 SELFBALANCE CREATE PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 SWAP5 GAS CALL STOP", + "storage": {} + }, + "627306090abab3a6e1400e9345bc60c78a8bef57": { + "balance": "0x3635C9ADC5DEA00000", + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored" + }, + "7f2d653f56ea8de6ffa554c7a0cd4e03af79f3eb": { + "balance": "0x3635C9ADC5DEA00000", + "privateKey": "fc5141e75bf622179f8eedada7fab3e2e6b3e3da8eb9df4f46d84df22df7430e", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored" + },"0x00000000000000000000000000000000000fffff": { + "balance": "0", + "code": "60806040526004361061007e575f3560e01c8063713ba21b1161004d578063713ba21b14610121578063bad4ba0b14610149578063cda7be6114610171578063f8b2cb4f1461019b57610085565b8063250976e7146100895780632ccb1b30146100b35780635e8ad6c7146100cf5780636c190cd1146100f757610085565b3661008557005b5f80fd5b348015610094575f80fd5b5061009d6101d7565b6040516100aa91906102d1565b60405180910390f35b6100cd60048036038101906100c89190610372565b6101e0565b005b3480156100da575f80fd5b506100f560048036038101906100f091906103b0565b61026b565b005b348015610102575f80fd5b5061010b610274565b60405161011891906102d1565b60405180910390f35b34801561012c575f80fd5b50610147600480360381019061014291906103b0565b61027d565b005b348015610154575f80fd5b5061016f600480360381019061016a91906103b0565b610287565b005b34801561017c575f80fd5b50610185610291565b60405161019291906102d1565b60405180910390f35b3480156101a6575f80fd5b506101c160048036038101906101bc9190610416565b610299565b6040516101ce91906102d1565b60405180910390f35b5f600254905090565b47811115610223576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161021a9061049b565b60405180910390fd5b8173ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f19350505050158015610266573d5f803e3d5ffd5b505050565b805f8190555050565b5f600154905090565b8060018190555050565b8060028190555050565b5f8054905090565b5f8173ffffffffffffffffffffffffffffffffffffffff16319050919050565b5f819050919050565b6102cb816102b9565b82525050565b5f6020820190506102e45f8301846102c2565b92915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610317826102ee565b9050919050565b6103278161030d565b8114610331575f80fd5b50565b5f813590506103428161031e565b92915050565b610351816102b9565b811461035b575f80fd5b50565b5f8135905061036c81610348565b92915050565b5f8060408385031215610388576103876102ea565b5b5f61039585828601610334565b92505060206103a68582860161035e565b9150509250929050565b5f602082840312156103c5576103c46102ea565b5b5f6103d28482850161035e565b91505092915050565b5f6103e5826102ee565b9050919050565b6103f5816103db565b81146103ff575f80fd5b50565b5f81359050610410816103ec565b92915050565b5f6020828403121561042b5761042a6102ea565b5b5f61043884828501610402565b91505092915050565b5f82825260208201905092915050565b7f496e73756666696369656e742062616c616e636520696e20636f6e74726163745f82015250565b5f610485602083610441565b915061049082610451565b602082019050919050565b5f6020820190508181035f8301526104b281610479565b905091905056fea264697066735822122023baf1931f9dfdc95c00818838ff0de5f17ac26981187c1407ae4f1d111ff57464736f6c634300081a0033", + "comment": "This bytecode corresponds to the smart contract contractUsedInBlockProcessingIT.sol, visible in the resource files", + "storage": { + "0x0": "0x2a", + "0x1": "0x54", + "0x2": "0x7e" + } + }, + "0x00000000000000000000000000000000000eeeee": { + "balance": "0", + "code": "608060405234801561000f575f80fd5b50600436106100a6575f3560e01c8063713ba21b1161006e578063713ba21b146100f0578063924fe31514610103578063bad4ba0b1461010c578063cda7be611461011f578063d987e6b514610126578063f8b2cb4f1461012f575f80fd5b80631f457cb5146100aa578063250976e7146100c4578063519c962e146100cc5780635e8ad6c7146100d65780636c190cd1146100e8575b5f80fd5b6100b25f5481565b60405190815260200160405180910390f35b6002546100b2565b6100d461014a565b005b6100d46100e4366004610157565b5f55565b6001546100b2565b6100d46100fe366004610157565b600155565b6100b260025481565b6100d461011a366004610157565b600255565b5f546100b2565b6100b260015481565b6100b261013d36600461016e565b6001600160a01b03163190565b5f546100e490600161019b565b5f60208284031215610167575f80fd5b5035919050565b5f6020828403121561017e575f80fd5b81356001600160a01b0381168114610194575f80fd5b9392505050565b808201808211156101ba57634e487b7160e01b5f52601160045260245ffd5b9291505056fea2646970667358221220b1be20967fb448131fa8be6ddeaa677d8478c967c86425e52c77837dcf6aab6264736f6c63430008190033", + "comment": "ParallelTestStorage runtime bytecode (solc 0.8.25 --bin-runtime). incrementSlot1()=519c962e, setSlot1()=5e8ad6c7. slot1 initial=10.", + "storage": { + "0x0": "0xa" + } + } + }, + "number":"0x0", + "gasUsed":"0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas":"0x7" +} From aa9bdb255cf3f245ba60745207a74e73b7840343 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 18 Mar 2026 17:12:07 +1000 Subject: [PATCH 60/77] report 10 slowest tests (#10063) * report slowest tests once from unittests-passed, not per-runner * report total test and class counts in summary * suppress Gradle job summaries from unit test matrix runners on success Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .github/workflows/pre-review.yml | 15 ++++++ .github/workflows/reportSlowestTests.sh | 71 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100755 .github/workflows/reportSlowestTests.sh diff --git a/.github/workflows/pre-review.yml b/.github/workflows/pre-review.yml index cc9d5cd9090..54c0951bf1a 100644 --- a/.github/workflows/pre-review.yml +++ b/.github/workflows/pre-review.yml @@ -111,6 +111,7 @@ jobs: uses: gradle/actions/setup-gradle@9e899d11ad247ec76be7a60bc1cf9d3abbb9e7f1 with: cache-disabled: true + add-job-summary: on-failure - name: List unit tests run: ./gradlew test --test-dry-run -Dorg.gradle.parallel=true -Dorg.gradle.caching=true - name: Extract current test list @@ -177,3 +178,17 @@ jobs: || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }} + - name: Checkout Repo + if: always() + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + - name: Download all unit test results + if: always() + uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe + with: + pattern: unit-*-test-results + merge-multiple: true + - name: Report 10 slowest tests + if: always() + run: python3 .github/workflows/reportSlowestTests.sh 10 diff --git a/.github/workflows/reportSlowestTests.sh b/.github/workflows/reportSlowestTests.sh new file mode 100755 index 00000000000..36d8d009398 --- /dev/null +++ b/.github/workflows/reportSlowestTests.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +## +## 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 +## + +import glob +import os +import sys +import xml.etree.ElementTree as ET + +count = int(sys.argv[1]) if len(sys.argv) > 1 else 10 +runner = sys.argv[2] if len(sys.argv) > 2 else None + +results = [] +total_tests = 0 +for f in glob.glob('**/build/test-results/**/TEST-*.xml', recursive=True): + try: + tree = ET.parse(f) + except ET.ParseError: + continue + root = tree.getroot() + suites = root.findall('testsuite') if root.tag == 'testsuites' else [root] + for suite in suites: + name = suite.get('name') + time = suite.get('time') + if name and time: + try: + results.append((float(time), name)) + total_tests += int(suite.get('tests', 0)) + except ValueError: + pass + +if not results: + sys.exit(0) + +results.sort(reverse=True) +total_classes = len(results) + +heading = f'{count} Slowest Test Classes (runner {runner})' if runner else f'{count} Slowest Test Classes' +summary_line = f'{total_tests:,} tests across {total_classes} classes' +lines = [ + f'

    \n', + f'{heading} — {summary_line}\n\n', + '| Rank | Class | Time |\n', + '|------|-------|------|\n', +] +for i, (t, name) in enumerate(results[:count], 1): + mins = int(t // 60) + secs = t % 60 + time_str = f'{mins}m {secs:.0f}s' if mins else f'{secs:.1f}s' + short = name.split('.')[-1] + lines.append(f'| {i} | `{short}` | {time_str} |\n') +lines.append('\n
    \n') + +summary_path = os.environ.get('GITHUB_STEP_SUMMARY') +if summary_path: + with open(summary_path, 'a', encoding='utf-8') as f: + f.writelines(lines) +else: + sys.stdout.writelines(lines) From c7d95ccdca5e5844852b4cb6c6db352c6fa89ad1 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 18 Mar 2026 17:37:33 +1000 Subject: [PATCH 61/77] reduce volume of chain data pruner test (#10062) * reduce volume of chain data pruner test * address matkt review: restore timeline comments and fix loop boundary - Restore the detailed pruning timeline explanation in pruningWithFrequency and balPruningWithTwoBatches, adapted to use derived variable names - Fix forkBlocksRemovedInBalOnlyMode: the second loop was starting at 45 instead of 25; pruningMark = 280 - 256 = 24, so block 25 is the first unpruned block. Also fix stale comment "i <= 44" -> "i <= 24" and extend the upper bound to 280 (the full canonical chain length) Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .../ethereum/chain/ChainDataPrunerTest.java | 297 ++++++++++-------- 1 file changed, 167 insertions(+), 130 deletions(-) diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/ChainDataPrunerTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/ChainDataPrunerTest.java index 6fb3ded3c10..7c9de85a6f9 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/ChainDataPrunerTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/ChainDataPrunerTest.java @@ -74,8 +74,10 @@ public void singleChainPruning() { gen.setBlockOptionsSupplier( () -> BlockDataGenerator.BlockOptions.create().withGeneratedBlockAccessList()); - // Generate & Import 1000 blocks with BAL - gen.blockSequenceWithAccessList(genesisBlock, 1000) + final int retention = 512; + final int chainLength = retention + 8; // just past the pruning threshold + + gen.blockSequenceWithAccessList(genesisBlock, chainLength) .forEach( blockWithBal -> { final Block blk = blockWithBal.getBlock(); @@ -101,13 +103,13 @@ public void singleChainPruning() { // Genesis block (block 0) is always kept assertThat(blockchain.getBlockHeader(0)).isPresent(); - if (number <= 512) { + if (number <= retention) { // No pruning has occurred yet assertThat(blockchain.getBlockHeader(1)).isPresent(); } else { - // Prune block number - 512 only - assertThat(blockchain.getBlockHeader(number - 512)).isEmpty(); - assertThat(blockchain.getBlockHeader(number - 511)).isPresent(); + // Prune block number - retention only + assertThat(blockchain.getBlockHeader(number - retention)).isEmpty(); + assertThat(blockchain.getBlockHeader(number - retention + 1)).isPresent(); } }); } @@ -141,10 +143,15 @@ public void forkPruning() { gen.setBlockOptionsSupplier( () -> BlockDataGenerator.BlockOptions.create().withGeneratedBlockAccessList()); + final int retention = 512; + final int forkLength = 16; + // need retention + forkLength + 1 canonical blocks so pruning mark covers all fork blocks + final int canonicalLength = retention + forkLength + 3; + List canonicalChain = - gen.blockSequenceWithAccessList(genesisBlock, 1000); + gen.blockSequenceWithAccessList(genesisBlock, canonicalLength); List forkChain = - gen.blockSequenceWithAccessList(genesisBlock, 16); + gen.blockSequenceWithAccessList(genesisBlock, forkLength); // Store fork blocks with their BAL for (BlockDataGenerator.BlockWithAccessList blockWithBal : forkChain) { @@ -160,8 +167,8 @@ public void forkPruning() { blockchain.storeBlock(blk, gen.receipts(blk)); } - // Import first 512 blocks of canonical chain - for (int i = 0; i < 512; i++) { + // Import first retention blocks of canonical chain + for (int i = 0; i < retention; i++) { BlockDataGenerator.BlockWithAccessList blockWithBal = canonicalChain.get(i); final Block blk = blockWithBal.getBlock(); blockWithBal @@ -180,8 +187,8 @@ public void forkPruning() { assertThat(blockchain.getBlockHeader(canonicalChain.get(0).getBlock().getHash())).isPresent(); assertThat(blockchain.getBlockHeader(forkChain.get(0).getBlock().getHash())).isPresent(); - // Continue importing canonical chain from block 512 to 527 - for (int i = 512; i < 527; i++) { + // Continue importing canonical chain past the retention threshold + for (int i = retention; i < retention + forkLength - 1; i++) { BlockDataGenerator.BlockWithAccessList blockWithBal = canonicalChain.get(i); final Block blk = blockWithBal.getBlock(); blockWithBal @@ -197,17 +204,19 @@ public void forkPruning() { // Genesis is always kept assertThat(blockchain.getBlockHeader(0)).isPresent(); - if (i > 512) { - // Prune block on canonical chain and fork for i - 512 only - assertThat(blockchain.getBlockHeader(canonicalChain.get(i - 512).getBlock().getHash())) + if (i > retention) { + // Prune block on canonical chain and fork for i - retention only + assertThat( + blockchain.getBlockHeader(canonicalChain.get(i - retention).getBlock().getHash())) .isEmpty(); - assertThat(blockchain.getBlockHeader(forkChain.get(i - 512).getBlock().getHash())) + assertThat(blockchain.getBlockHeader(forkChain.get(i - retention).getBlock().getHash())) .isEmpty(); } - assertThat(blockchain.getBlockHeader(canonicalChain.get(i - 511).getBlock().getHash())) + assertThat( + blockchain.getBlockHeader(canonicalChain.get(i - retention + 1).getBlock().getHash())) .isPresent(); - assertThat(blockchain.getBlockHeader(forkChain.get(i - 511).getBlock().getHash())) + assertThat(blockchain.getBlockHeader(forkChain.get(i - retention + 1).getBlock().getHash())) .isPresent(); } } @@ -246,7 +255,10 @@ public void balOnlyPruning() { gen.setBlockOptionsSupplier( () -> BlockDataGenerator.BlockOptions.create().withGeneratedBlockAccessList()); - gen.blockSequenceWithAccessList(genesisBlock, 1000) + final int retention = 512; + final int chainLength = retention + 8; // just past the pruning threshold + + gen.blockSequenceWithAccessList(genesisBlock, chainLength) .forEach( blockWithBal -> { final Block blk = blockWithBal.getBlock(); @@ -280,10 +292,10 @@ public void balOnlyPruning() { .isPresent(); }); - if (number > 512) { - assertThat(blockchain.getBlockHeader(number - 512)).isPresent(); + if (number > retention) { + assertThat(blockchain.getBlockHeader(number - retention)).isPresent(); blockchain - .getBlockHeader(number - 512) + .getBlockHeader(number - retention) .ifPresent( oldHeader -> { // Block data still exists @@ -297,9 +309,9 @@ public void balOnlyPruning() { assertThat(blockchainStorage.getBlockAccessList(oldHeader.getBlockHash())) .isEmpty(); }); - assertThat(blockchain.getBlockHeader(number - 511)).isPresent(); + assertThat(blockchain.getBlockHeader(number - retention + 1)).isPresent(); blockchain - .getBlockHeader(number - 511) + .getBlockHeader(number - retention + 1) .ifPresent( recentHeader -> { // Recent blocks should have BAL @@ -336,45 +348,58 @@ public void pruningWithFrequency() { genesisBlock, blockchainStorage, new NoOpMetricsSystem(), 0); blockchain.observeBlockAdded(chainDataPruner); - // Generate 400 blocks - List blocks = gen.blockSequence(genesisBlock, 400); + // Batch boundary derivation: + // storedMark starts at 1 (genesis always kept). First prune fires when + // (blockNumber - retention) - storedMark >= frequency, i.e. blockNumber >= retention + + // frequency + 1 + final int retention = 256; + final int frequency = 100; + final int firstBatchAt = retention + frequency + 1; // 357 + final int firstBatchPrunedUpTo = firstBatchAt - retention; // 101 + final int chainLength = + firstBatchAt + + 13; // past first batch, not enough for second (needs retention + 2*freq + 1 = 457) + + List blocks = gen.blockSequence(genesisBlock, chainLength); for (Block blk : blocks) { blockchain.appendBlock(blk, gen.receipts(blk)); } - // At block 400: - // - We want to keep 256 blocks (blocks 145-400) - // - blockPruningMark = 400 - 256 = 144 + // At block chainLength (= firstBatchAt + 13 = 370): + // - blockPruningMark = chainLength - retention (= 114) // - Genesis (block 0) is ALWAYS kept, pruning starts from block 1 // - // Timeline: - // Block 0 (genesis): ALWAYS kept - // Block 1-256: No pruning (retention = 256) - // Block 257-356: < 100 accumulated - // Block 357: First batch prunes blocks 1-101 - // Block 400: blockPruningMark = 144 - // blocksToBePruned = 144 - 101 = 43 < 100 - // Only blocks 1-101 are pruned, blocks 102-144 wait for next batch + // Pruning timeline: + // block firstBatchAt (= 357): blockPruningMark = firstBatchPrunedUpTo (= 101) + // blocksToBePruned = firstBatchPrunedUpTo - 1 = frequency + // → first batch fires, prunes blocks 1..firstBatchPrunedUpTo + // block chainLength (= 370): blockPruningMark = chainLength - retention (= 114) + // blocksToBePruned = 13 < frequency + // → second batch not fired; needs block retention + 2*freq + 1 + // (= 457) // Genesis (block 0) is ALWAYS kept assertThat(blockchain.getBlockHeader(0)).as("Genesis block should always be kept").isPresent(); - // Blocks 1-101: Should be pruned (first batch at block 357) - for (int i = 1; i <= 101; i++) { + // Blocks pruned in first batch + for (int i = 1; i <= firstBatchPrunedUpTo; i++) { assertThat(blockchain.getBlockHeader(i)) - .as("Block %d should be pruned (first batch at block 357)", i) + .as("Block %d should be pruned (first batch at block %d)", i, firstBatchAt) .isEmpty(); } - // Blocks 102-144: Should NOT be pruned yet (waiting for second batch at block 457) - for (int i = 102; i <= 144; i++) { + // Blocks past first batch mark but below pruning mark: pending second batch + final int pruningMark = chainLength - retention; + for (int i = firstBatchPrunedUpTo + 1; i <= pruningMark; i++) { assertThat(blockchain.getBlockHeader(i)) - .as("Block %d should exist (second batch needs block 457)", i) + .as( + "Block %d should exist (second batch needs block %d)", + i, retention + 2 * frequency + 1) .isPresent(); } - // Blocks 145-400: Should exist (within retention window of 256) - for (int i = 145; i <= 400; i++) { + // Blocks within retention window + for (int i = pruningMark + 1; i <= chainLength; i++) { assertThat(blockchain.getBlockHeader(i)) .as("Block %d should exist (within retention)", i) .isPresent(); @@ -415,8 +440,8 @@ public void balRetentionLowerThanBlockRetentionInBalMode() { gen.setBlockOptionsSupplier( () -> BlockDataGenerator.BlockOptions.create().withGeneratedBlockAccessList()); - // Generate 600 blocks with BAL - gen.blockSequenceWithAccessList(genesisBlock, 600) + // Generate 270 blocks with BAL - just past the 256-block BAL retention threshold + gen.blockSequenceWithAccessList(genesisBlock, 270) .forEach( blockWithBal -> { final Block blk = blockWithBal.getBlock(); @@ -509,7 +534,10 @@ public void equalBalAndBlockRetention() { gen.setBlockOptionsSupplier( () -> BlockDataGenerator.BlockOptions.create().withGeneratedBlockAccessList()); - gen.blockSequence(genesisBlock, 1000) + final int retention = 512; + final int chainLength = retention + 8; // just past the pruning threshold + + gen.blockSequence(genesisBlock, chainLength) .forEach( blk -> { blockchain.appendBlock(blk, gen.receipts(blk)); @@ -519,9 +547,9 @@ public void equalBalAndBlockRetention() { assertThat(blockchain.getBlockHeader(0)) .as("Genesis block should always be kept") .isPresent(); - if (number > 512) { - assertThat(blockchain.getBlockHeader(number - 512)).isEmpty(); - assertThat(blockchain.getBlockHeader(number - 511)).isPresent(); + if (number > retention) { + assertThat(blockchain.getBlockHeader(number - retention)).isEmpty(); + assertThat(blockchain.getBlockHeader(number - retention + 1)).isPresent(); } }); } @@ -551,7 +579,7 @@ public void noPruningWhenBelowRetentionThreshold() { genesisBlock, blockchainStorage, new NoOpMetricsSystem(), 0); blockchain.observeBlockAdded(chainDataPruner); - gen.blockSequence(genesisBlock, 500) + gen.blockSequence(genesisBlock, 50) .forEach( blk -> { blockchain.appendBlock(blk, gen.receipts(blk)); @@ -600,8 +628,14 @@ public void balPruningWithDifferentFrequency() { genesisUpdater.putBlockAccessList(genesisBlock.getHash(), genesisBal); genesisUpdater.commit(); + final int retention = 256; + final int frequency = 100; + final int firstBatchAt = retention + frequency + 1; // 357 + final int firstBatchPrunedUpTo = firstBatchAt - retention; // 101 + final int chainLength = firstBatchAt + 13; // past first batch, not enough for second + List blocks = - gen.blockSequenceWithAccessList(genesisBlock, 400); + gen.blockSequenceWithAccessList(genesisBlock, chainLength); for (BlockDataGenerator.BlockWithAccessList blockWithBal : blocks) { final Block blk = blockWithBal.getBlock(); @@ -619,16 +653,7 @@ public void balPruningWithDifferentFrequency() { blockchain.appendBlock(blk, gen.receipts(blk)); } - // At block 400: - // - balPruningMark = 400 - 256 = 144 - // - Genesis (block 0) BAL is ALWAYS kept, pruning starts from block 1 - // - storedBalPruningMark starts at 1 (after genesis) - // - First pruning at block 357: balPruningMark = 101 - // blocksToBePruned = 101 - 1 = 100 - // Prunes BALs for blocks 1-101 - // - At block 400: balPruningMark = 144 - // blocksToBePruned = 144 - 101 = 43 < 100 - // Only BALs 1-101 are pruned, BALs 102-144 wait for next batch + final int secondBatchAt = retention + 2 * frequency + 1; // 457 // Genesis (block 0): Block and BAL should ALWAYS exist assertThat(blockchain.getBlockHeader(0)) @@ -638,23 +663,23 @@ public void balPruningWithDifferentFrequency() { .as("Genesis BAL should always be kept") .isPresent(); - // Blocks 1-101: BALs should be pruned (first batch at block 357) - for (int i = 1; i <= 101; i++) { + // Blocks 1..firstBatchPrunedUpTo: BALs pruned in first batch + for (int i = 1; i <= firstBatchPrunedUpTo; i++) { final Block block = blocks.get(i - 1).getBlock(); assertThat(blockchain.getBlockHeader(i)) .as("Block %d should exist (BAL mode never prunes blocks)", i) .isPresent(); assertThat(blockchainStorage.getBlockAccessList(block.getHash())) - .as("BAL for block %d should be pruned (first batch at block 357)", i) + .as("BAL for block %d should be pruned (first batch at block %d)", i, firstBatchAt) .isEmpty(); } - // Blocks 102-400: BALs should still exist (not enough accumulated to trigger next pruning) - for (int i = 102; i <= 400; i++) { + // Remaining blocks: BALs still exist (second batch needs block secondBatchAt) + for (int i = firstBatchPrunedUpTo + 1; i <= chainLength; i++) { final Block block = blocks.get(i - 1).getBlock(); assertThat(blockchain.getBlockHeader(i)).as("Block %d should exist", i).isPresent(); assertThat(blockchainStorage.getBlockAccessList(block.getHash())) - .as("BAL for block %d should exist (next pruning at block 457)", i) + .as("BAL for block %d should exist (next pruning at block %d)", i, secondBatchAt) .isPresent(); } } @@ -699,9 +724,17 @@ public void balPruningWithTwoBatches() { genesisUpdater.putBlockAccessList(genesisBlock.getHash(), genesisBal); genesisUpdater.commit(); - // Generate 500 blocks to trigger two pruning batches + final int retention = 256; + final int frequency = 100; + final int firstBatchAt = retention + frequency + 1; // 357 + final int firstBatchPrunedUpTo = firstBatchAt - retention; // 101 + final int secondBatchAt = firstBatchAt + frequency; // 457 + final int secondBatchPrunedUpTo = secondBatchAt - retention; // 201 + final int chainLength = secondBatchAt + 13; // past second batch, not enough for third + + // Generate blocks to trigger two pruning batches List blocks = - gen.blockSequenceWithAccessList(genesisBlock, 500); + gen.blockSequenceWithAccessList(genesisBlock, chainLength); for (BlockDataGenerator.BlockWithAccessList blockWithBal : blocks) { final Block blk = blockWithBal.getBlock(); @@ -719,23 +752,26 @@ public void balPruningWithTwoBatches() { blockchain.appendBlock(blk, gen.receipts(blk)); } - // At block 500: - // - We want to keep 256 BALs (blocks 245-500) - // - balPruningMark = 500 - 256 = 244 + final int pruningMark = chainLength - retention; + final int thirdBatchAt = secondBatchAt + frequency; // 557 + + // At block chainLength (= secondBatchAt + 13 = 470): + // - balPruningMark = chainLength - retention (= 214) // - Genesis (block 0) BAL is ALWAYS kept, pruning starts from block 1 // - // Timeline of pruning: - // Block 357: First batch prunes BALs 1-101 (100 BALs accumulated) - // balPruningMark = 357 - 256 = 101 - // blocksToBePruned = 101 - 1 = 100 - // - // Block 457: Second batch prunes BALs 102-201 (100 more BALs accumulated) - // balPruningMark = 457 - 256 = 201 - // blocksToBePruned = 201 - 101 = 100 - // - // Block 500: Third batch would need block 557 (not reached yet) - // balPruningMark = 500 - 256 = 244 - // blocksToBePruned = 244 - 201 = 43 < 100 + // Pruning timeline: + // block firstBatchAt (= 357): balPruningMark = firstBatchPrunedUpTo (= 101) + // blocksToBePruned = firstBatchPrunedUpTo - 1 = frequency + // → first batch fires, prunes BALs 1..firstBatchPrunedUpTo + // block secondBatchAt (= 457): balPruningMark = secondBatchPrunedUpTo (= 201) + // blocksToBePruned = secondBatchPrunedUpTo - firstBatchPrunedUpTo + // = frequency + // → second batch fires, prunes BALs + // firstBatchPrunedUpTo+1..secondBatchPrunedUpTo + // block chainLength (= 470): balPruningMark = pruningMark (= 214) + // blocksToBePruned = pruningMark - secondBatchPrunedUpTo = 13 < + // frequency + // → third batch not fired; needs block thirdBatchAt (= 557) // Genesis (block 0): Block and BAL should ALWAYS exist assertThat(blockchain.getBlockHeader(0)) @@ -745,43 +781,43 @@ public void balPruningWithTwoBatches() { .as("Genesis BAL should always be kept") .isPresent(); - // First batch (blocks 1-101): BALs should be pruned - for (int i = 1; i <= 101; i++) { + // First batch: BALs pruned + for (int i = 1; i <= firstBatchPrunedUpTo; i++) { final Block block = blocks.get(i - 1).getBlock(); assertThat(blockchain.getBlockHeader(i)) .as("Block %d should exist (BAL mode never prunes blocks)", i) .isPresent(); assertThat(blockchainStorage.getBlockAccessList(block.getHash())) - .as("BAL for block %d should be pruned (first batch at block 357)", i) + .as("BAL for block %d should be pruned (first batch at block %d)", i, firstBatchAt) .isEmpty(); } - // Second batch (blocks 102-201): BALs should be pruned - for (int i = 102; i <= 201; i++) { + // Second batch: BALs pruned + for (int i = firstBatchPrunedUpTo + 1; i <= secondBatchPrunedUpTo; i++) { final Block block = blocks.get(i - 1).getBlock(); assertThat(blockchain.getBlockHeader(i)) .as("Block %d should exist (BAL mode never prunes blocks)", i) .isPresent(); assertThat(blockchainStorage.getBlockAccessList(block.getHash())) - .as("BAL for block %d should be pruned (second batch at block 457)", i) + .as("BAL for block %d should be pruned (second batch at block %d)", i, secondBatchAt) .isEmpty(); } - // Blocks 202-244: Should be pruned eventually but frequency not reached yet - for (int i = 202; i <= 244; i++) { + // Past second batch mark but not enough for third: pending + for (int i = secondBatchPrunedUpTo + 1; i <= pruningMark; i++) { final Block block = blocks.get(i - 1).getBlock(); assertThat(blockchain.getBlockHeader(i)).as("Block %d should exist", i).isPresent(); assertThat(blockchainStorage.getBlockAccessList(block.getHash())) - .as("BAL for block %d should exist (third batch needs block 557)", i) + .as("BAL for block %d should exist (third batch needs block %d)", i, thirdBatchAt) .isPresent(); } - // Blocks 245-500: Should be kept (within retention window) - for (int i = 245; i <= 500; i++) { + // Within retention window + for (int i = pruningMark + 1; i <= chainLength; i++) { final Block block = blocks.get(i - 1).getBlock(); assertThat(blockchain.getBlockHeader(i)).as("Block %d should exist", i).isPresent(); assertThat(blockchainStorage.getBlockAccessList(block.getHash())) - .as("BAL for block %d should exist (within retention of 256 blocks)", i) + .as("BAL for block %d should exist (within retention of %d blocks)", i, retention) .isPresent(); } } @@ -823,9 +859,10 @@ public void forkBlocksRemovedInBalOnlyMode() { gen.setBlockOptionsSupplier( () -> BlockDataGenerator.BlockOptions.create().withGeneratedBlockAccessList()); - // Create canonical chain and fork + // Create canonical chain and fork - need 272+ blocks so pruningMark(=chain-256) covers all 16 + // fork blocks List canonicalChain = - gen.blockSequenceWithAccessList(genesisBlock, 300); + gen.blockSequenceWithAccessList(genesisBlock, 280); List forkChain = gen.blockSequenceWithAccessList(genesisBlock, 16); @@ -857,13 +894,13 @@ public void forkBlocksRemovedInBalOnlyMode() { blockchain.appendBlock(blk, gen.receipts(blk)); } - // At block 300, balPruningMark = 300 - 256 = 44 + // At block 280, balPruningMark = 280 - 256 = 24 // Both canonical and fork blocks should have: // - Blocks still present (BAL mode never prunes blocks) - // - BALs pruned for blocks <= 44 - // - Fork blocks metadata removed for blocks <= 44 + // - BALs pruned for blocks <= 24 + // - Fork blocks metadata removed for blocks <= 24 - // Verify fork blocks 1-16 (all should be pruned since 16 < 44) + // Verify fork blocks 1-16 (all should be pruned since 16 < 24) for (int i = 1; i <= 16; i++) { final Block forkBlock = forkChain.get(i - 1).getBlock(); final Block canonicalBlock = canonicalChain.get(i - 1).getBlock(); @@ -876,7 +913,7 @@ public void forkBlocksRemovedInBalOnlyMode() { .as("Canonical block %d should still exist", i) .isPresent(); - // BALs should be pruned (since i <= 44) + // BALs should be pruned (since i <= 24, the pruning mark) assertThat(blockchainStorage.getBlockAccessList(forkBlock.getHash())) .as("Fork block %d BAL should be pruned", i) .isEmpty(); @@ -890,8 +927,8 @@ public void forkBlocksRemovedInBalOnlyMode() { .isEmpty(); } - // Verify blocks 45-256 still have BALs and fork blocks metadata - for (int i = 45; i <= 256; i++) { + // Verify blocks 25-280 still have BALs and fork blocks metadata + for (int i = 25; i <= 280; i++) { final Block canonicalBlock = canonicalChain.get(i - 1).getBlock(); assertThat(blockchainStorage.getBlockAccessList(canonicalBlock.getHash())) .as("Canonical block %d BAL should exist (within retention)", i) @@ -1065,8 +1102,8 @@ public void pruningBeforeBalActivation() { gen.setBlockOptionsSupplier( () -> BlockDataGenerator.BlockOptions.create().withoutGeneratedBlockAccessList()); - // Generate 300 blocks WITHOUT BAL (pre-activation) - List blocks = gen.blockSequence(genesisBlock, 300); + // Generate 50 blocks WITHOUT BAL (pre-activation) + List blocks = gen.blockSequence(genesisBlock, 50); for (Block blk : blocks) { // No BAL added - simulating pre-BAL activation blockchain.appendBlock(blk, gen.receipts(blk)); @@ -1076,7 +1113,7 @@ public void pruningBeforeBalActivation() { } // All blocks should exist (BAL mode never prunes blocks) - for (int i = 0; i <= 300; i++) { + for (int i = 0; i <= 50; i++) { assertThat(blockchain.getBlockHeader(i)) .as("Block %d should exist (BAL mode never prunes blocks)", i) .isPresent(); @@ -1113,20 +1150,20 @@ public void balPruningTransitionFromNoBalToBalActivated() { genesisBlock, blockchainStorage, new NoOpMetricsSystem(), 0); blockchain.observeBlockAdded(chainDataPruner); - // Phase 1: Generate 200 blocks WITHOUT BAL (simulating pre-activation) + // Phase 1: Generate 20 blocks WITHOUT BAL (simulating pre-activation) gen.setBlockOptionsSupplier( () -> BlockDataGenerator.BlockOptions.create().withoutGeneratedBlockAccessList()); - List preBalBlocks = gen.blockSequence(genesisBlock, 200); + List preBalBlocks = gen.blockSequence(genesisBlock, 20); for (Block blk : preBalBlocks) { blockchain.appendBlock(blk, gen.receipts(blk)); assertThat(blk.getHeader().getBalHash()).isEmpty(); } - // Phase 2: BAL activation at block 201 - generate 200 more blocks WITH BAL + // Phase 2: BAL activation at block 21 - generate 130 more blocks WITH BAL gen.setBlockOptionsSupplier( () -> BlockDataGenerator.BlockOptions.create().withGeneratedBlockAccessList()); List postBalBlocks = - gen.blockSequenceWithAccessList(preBalBlocks.get(199), 200); + gen.blockSequenceWithAccessList(preBalBlocks.get(19), 130); for (BlockDataGenerator.BlockWithAccessList blockWithBal : postBalBlocks) { final Block blk = blockWithBal.getBlock(); @@ -1148,37 +1185,37 @@ public void balPruningTransitionFromNoBalToBalActivated() { assertThat(blk.getHeader().getBalHash()).isPresent(); } - // At block 400: - // - We want to keep 100 BALs (blocks 301-400) - // - Blocks 1-200 never had BAL (should be skipped) - // - Blocks 201-300 had BAL but should be pruned + // At block 150 (20 pre-BAL + 130 post-BAL): + // - We want to keep 100 BALs (blocks 51-150) + // - Blocks 1-20 never had BAL (should be skipped) + // - Blocks 21-50 had BAL but should be pruned (balPruningMark = 150 - 100 = 50) // All blocks should still exist (BAL mode never prunes blocks) - for (int i = 0; i <= 400; i++) { + for (int i = 0; i <= 150; i++) { assertThat(blockchain.getBlockHeader(i)) .as("Block %d should exist (BAL mode never prunes blocks)", i) .isPresent(); } - // Blocks 1-200: No BAL to prune (never existed) - for (int i = 1; i <= 200; i++) { + // Blocks 1-20: No BAL to prune (never existed) + for (int i = 1; i <= 20; i++) { final Block block = preBalBlocks.get(i - 1); assertThat(blockchainStorage.getBlockAccessList(block.getHash())) .as("Block %d never had BAL", i) .isEmpty(); } - // Blocks 201-300: BAL should be pruned (outside retention window) - for (int i = 201; i <= 300; i++) { - final Block block = postBalBlocks.get(i - 201).getBlock(); + // Blocks 21-50: BAL should be pruned (outside retention window) + for (int i = 21; i <= 50; i++) { + final Block block = postBalBlocks.get(i - 21).getBlock(); assertThat(blockchainStorage.getBlockAccessList(block.getHash())) .as("BAL for block %d should be pruned (outside retention)", i) .isEmpty(); } - // Blocks 301-400: BAL should exist (within retention window) - for (int i = 301; i <= 400; i++) { - final Block block = postBalBlocks.get(i - 201).getBlock(); + // Blocks 51-150: BAL should exist (within retention window) + for (int i = 51; i <= 150; i++) { + final Block block = postBalBlocks.get(i - 21).getBlock(); assertThat(blockchainStorage.getBlockAccessList(block.getHash())) .as("BAL for block %d should exist (within retention)", i) .isPresent(); @@ -1368,8 +1405,8 @@ public void balPruningMarkAdvancesAutomaticallyWhenNoBalPresent() { blockchain.observeBlockAdded(chainDataPruner); // Add blocks without BAL - marker should advance automatically - List blocks = gen.blockSequence(genesisBlock, 500); - for (int i = 0; i < 500; i++) { + List blocks = gen.blockSequence(genesisBlock, 50); + for (int i = 0; i < 50; i++) { Block blk = blocks.get(i); blockchain.appendBlock(blk, gen.receipts(blk)); @@ -1381,7 +1418,7 @@ public void balPruningMarkAdvancesAutomaticallyWhenNoBalPresent() { } // All blocks should still exist - for (int i = 0; i <= 500; i++) { + for (int i = 0; i <= 50; i++) { assertThat(blockchain.getBlockHeader(i)).isPresent(); } } From d943e08acef44ca30c401802f55321448a9d394f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Kov=C3=A1=C5=99?= Date: Wed, 18 Mar 2026 10:13:20 +0100 Subject: [PATCH 62/77] EIP-8159: eth/71 - Block Access List Exchange (#9966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Kovar Signed-off-by: Miroslav Kovář --- CHANGELOG.md | 1 + .../api/jsonrpc/eth/eth_protocolVersion.json | 4 +- .../besu/ethereum/eth/EthProtocol.java | 15 +- .../eth/EthProtocolConfiguration.java | 6 + .../besu/ethereum/eth/EthProtocolVersion.java | 22 +++ .../besu/ethereum/eth/manager/EthPeer.java | 5 + .../eth/manager/EthProtocolManager.java | 1 + .../besu/ethereum/eth/manager/EthServer.java | 54 ++++++ .../task/GetBlockAccessListsFromPeerTask.java | 123 ++++++++++++ .../eth/messages/BlockAccessListsMessage.java | 72 +++++++ .../eth/messages/EthProtocolMessages.java | 5 + .../messages/GetBlockAccessListsMessage.java | 87 +++++++++ .../eth/manager/EthProtocolManagerTest.java | 21 ++ .../ethereum/eth/manager/EthServerTest.java | 145 ++++++++++++++ .../eth/manager/RespondingEthPeer.java | 8 + .../GetBlockAccessListsFromPeerTaskTest.java | 183 ++++++++++++++++++ .../messages/BlockAccessListsMessageTest.java | 52 +++++ .../GetBlockAccessListsMessageTest.java | 62 ++++++ 18 files changed, 862 insertions(+), 4 deletions(-) create mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBlockAccessListsFromPeerTask.java create mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/BlockAccessListsMessage.java create mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetBlockAccessListsMessage.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBlockAccessListsFromPeerTaskTest.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/BlockAccessListsMessageTest.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetBlockAccessListsMessageTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 15c0c748db8..2937208350f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ are provided with different values, using input as per the execution-apis spec i - Use JDK SHA-256 provider to leverage hardware SHA-NI instructions instead of BouncyCastle [#9924](https://github.com/hyperledger/besu/pull/9924) - Implement `txpool_status` RPC method [#10002](https://github.com/hyperledger/besu/pull/10002) - Support [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975): eth/70 - partial block receipt lists +- Support [EIP-8159](https://eips.ethereum.org/EIPS/eip-8159): eth/71 - block access list exchange - Limit pooled tx requests by size and remove pre-eth/68 transaction announcement support [#9990](https://github.com/besu-eth/besu/pull/9990) - Use cache locality to improve Shift opcodes [#9878](https://github.com/besu-eth/besu/pull/9878) - Add maxUsedGas field to eth_simulateV1 results [#10066](https://github.com/besu-eth/besu/pull/10066) diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json index 2fe0c58d880..aa3360b3729 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json @@ -8,7 +8,7 @@ "response": { "jsonrpc": "2.0", "id": 2, - "result": "0x46" + "result": "0x47" }, "statusCode": 200 -} \ No newline at end of file +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java index f2b4a4ba141..bbaa64936cc 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java @@ -32,6 +32,7 @@ public class EthProtocol implements SubProtocol { public static final Capability ETH68 = Capability.create(NAME, EthProtocolVersion.V68); public static final Capability ETH69 = Capability.create(NAME, EthProtocolVersion.V69); public static final Capability ETH70 = Capability.create(NAME, EthProtocolVersion.V70); + public static final Capability ETH71 = Capability.create(NAME, EthProtocolVersion.V71); public static final BitSet REQUEST_ID_MESSAGES; static { @@ -46,14 +47,16 @@ public class EthProtocol implements SubProtocol { EthProtocolMessages.GET_NODE_DATA, EthProtocolMessages.NODE_DATA, EthProtocolMessages.GET_RECEIPTS, - EthProtocolMessages.RECEIPTS); + EthProtocolMessages.RECEIPTS, + EthProtocolMessages.GET_BLOCK_ACCESS_LISTS, + EthProtocolMessages.BLOCK_ACCESS_LISTS); REQUEST_ID_MESSAGES = new BitSet(requestIdMessages.stream().mapToInt(i -> i).max().getAsInt() + 1); requestIdMessages.forEach(REQUEST_ID_MESSAGES::set); } // Latest version of the Eth protocol - public static final Capability LATEST = ETH70; + public static final Capability LATEST = ETH71; public static boolean requestIdCompatible(final int code) { return REQUEST_ID_MESSAGES.get(code); @@ -69,6 +72,7 @@ public int messageSpace(final int protocolVersion) { return switch (protocolVersion) { case EthProtocolVersion.V68 -> 17; case EthProtocolVersion.V69, EthProtocolVersion.V70 -> 18; + case EthProtocolVersion.V71 -> 20; default -> 0; }; } @@ -96,6 +100,9 @@ public String messageName(final int protocolVersion, final int code) { case EthProtocolMessages.NODE_DATA -> "NodeData"; case EthProtocolMessages.GET_RECEIPTS -> "GetReceipts"; case EthProtocolMessages.RECEIPTS -> "Receipts"; + case EthProtocolMessages.BLOCK_RANGE_UPDATE -> "BlockRangeUpdate"; + case EthProtocolMessages.GET_BLOCK_ACCESS_LISTS -> "GetBlockAccessLists"; + case EthProtocolMessages.BLOCK_ACCESS_LISTS -> "BlockAccessLists"; default -> INVALID_MESSAGE_NAME; }; } @@ -111,4 +118,8 @@ public static boolean isEth69Compatible(final Capability capability) { public static boolean isEth70Compatible(final Capability capability) { return NAME.equals(capability.getName()) && capability.getVersion() >= ETH70.getVersion(); } + + public static boolean isEth71Compatible(final Capability capability) { + return NAME.equals(capability.getName()) && capability.getVersion() >= ETH71.getVersion(); + } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolConfiguration.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolConfiguration.java index 5196c00dbf3..eccd50607f9 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolConfiguration.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolConfiguration.java @@ -24,6 +24,7 @@ public interface EthProtocolConfiguration { int DEFAULT_MAX_TRANSACTIONS_MESSAGE_SIZE = ByteUnits.MEGABYTE; int DEFAULT_MAX_GET_BLOCK_HEADERS = 512; int DEFAULT_MAX_GET_BLOCK_BODIES = 128; + int DEFAULT_MAX_GET_BLOCK_ACCESS_LISTS = 128; int DEFAULT_MAX_GET_RECEIPTS = 256; int DEFAULT_MAX_GET_NODE_DATA = 384; int DEFAULT_MAX_GET_POOLED_TRANSACTIONS = 256; @@ -52,6 +53,11 @@ default int getMaxGetBlockBodies() { return DEFAULT_MAX_GET_BLOCK_BODIES; } + @Value.Default + default int getMaxGetBlockAccessLists() { + return DEFAULT_MAX_GET_BLOCK_ACCESS_LISTS; + } + @Value.Default default int getMaxGetReceipts() { return DEFAULT_MAX_GET_RECEIPTS; diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java index 555334df314..ec0757864f5 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java @@ -28,6 +28,7 @@ public class EthProtocolVersion { public static final int V68 = 68; public static final int V69 = 69; public static final int V70 = 70; + public static final int V71 = 71; /** eth/68 */ private static final List eth68Messages = @@ -68,6 +69,26 @@ public class EthProtocolVersion { EthProtocolMessages.POOLED_TRANSACTIONS, EthProtocolMessages.BLOCK_RANGE_UPDATE); + /** eth/71 */ + private static final List eth71Messages = + List.of( + EthProtocolMessages.STATUS, + EthProtocolMessages.NEW_BLOCK_HASHES, + EthProtocolMessages.TRANSACTIONS, + EthProtocolMessages.GET_BLOCK_HEADERS, + EthProtocolMessages.BLOCK_HEADERS, + EthProtocolMessages.GET_BLOCK_BODIES, + EthProtocolMessages.BLOCK_BODIES, + EthProtocolMessages.NEW_BLOCK, + EthProtocolMessages.GET_RECEIPTS, + EthProtocolMessages.RECEIPTS, + EthProtocolMessages.NEW_POOLED_TRANSACTION_HASHES, + EthProtocolMessages.GET_POOLED_TRANSACTIONS, + EthProtocolMessages.POOLED_TRANSACTIONS, + EthProtocolMessages.BLOCK_RANGE_UPDATE, + EthProtocolMessages.GET_BLOCK_ACCESS_LISTS, + EthProtocolMessages.BLOCK_ACCESS_LISTS); + /** * Returns a list of integers containing the supported messages given the protocol version * @@ -78,6 +99,7 @@ public static List getSupportedMessages(final int protocolVersion) { return switch (protocolVersion) { case EthProtocolVersion.V68 -> eth68Messages; case EthProtocolVersion.V69, EthProtocolVersion.V70 -> eth69Messages; + case EthProtocolVersion.V71 -> eth71Messages; default -> Collections.emptyList(); }; } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeer.java index 0d8d9229339..6235c5291a5 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeer.java @@ -112,6 +112,8 @@ protected boolean removeEldestEntry(final Map.Entry eldest) { roundMessages.put(EthProtocolMessages.NODE_DATA, EthProtocolMessages.GET_NODE_DATA); roundMessages.put( EthProtocolMessages.POOLED_TRANSACTIONS, EthProtocolMessages.GET_POOLED_TRANSACTIONS); + roundMessages.put( + EthProtocolMessages.BLOCK_ACCESS_LISTS, EthProtocolMessages.GET_BLOCK_ACCESS_LISTS); roundMessages.put(SnapV1.ACCOUNT_RANGE, SnapV1.GET_ACCOUNT_RANGE); roundMessages.put(SnapV1.STORAGE_RANGE, SnapV1.GET_STORAGE_RANGE); @@ -158,6 +160,9 @@ EthProtocolMessages.GET_BLOCK_BODIES, new RequestManager(this, EthProtocol.NAME) EthProtocolMessages.GET_NODE_DATA, new RequestManager(this, EthProtocol.NAME)), Map.entry( EthProtocolMessages.GET_POOLED_TRANSACTIONS, + new RequestManager(this, EthProtocol.NAME)), + Map.entry( + EthProtocolMessages.GET_BLOCK_ACCESS_LISTS, new RequestManager(this, EthProtocol.NAME)))); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java index c9cc17317c9..080e1f97366 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java @@ -177,6 +177,7 @@ private List calculateCapabilities( capabilities.add(EthProtocol.ETH68); capabilities.add(EthProtocol.ETH69); capabilities.add(EthProtocol.ETH70); + capabilities.add(EthProtocol.ETH71); capabilities.removeIf(cap -> cap.getVersion() > ethProtocolConfiguration.getMaxEthCapability()); capabilities.removeIf(cap -> cap.getVersion() < ethProtocolConfiguration.getMinEthCapability()); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java index 0578a4c9c97..4a86b2b0f5a 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java @@ -22,6 +22,7 @@ import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.core.encoding.BlockAccessListEncoder; import org.hyperledger.besu.ethereum.core.encoding.EncodingContext; import org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder; import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; @@ -29,9 +30,11 @@ import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; import org.hyperledger.besu.ethereum.eth.manager.exceptions.ProtocolViolationException; +import org.hyperledger.besu.ethereum.eth.messages.BlockAccessListsMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; +import org.hyperledger.besu.ethereum.eth.messages.GetBlockAccessListsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetNodeDataMessage; @@ -43,6 +46,7 @@ import org.hyperledger.besu.ethereum.eth.messages.PooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; @@ -134,6 +138,14 @@ private void registerResponseConstructors() { messageData, ethereumWireProtocolConfiguration.getMaxGetPooledTransactions(), maxMessageSize)); + ethMessages.registerResponseConstructor( + EthProtocolMessages.GET_BLOCK_ACCESS_LISTS, + (peer, messageData, capability) -> + constructGetBlockAccessListsResponse( + blockchain, + messageData, + ethereumWireProtocolConfiguration.getMaxGetBlockAccessLists(), + maxMessageSize)); } static MessageData constructGetHeadersResponse( @@ -376,6 +388,48 @@ static MessageData constructGetPaginatedReceiptsResponse( return PaginatedReceiptsMessage.createUnsafe(encodedResponse, lastBlockIncomplete); } + static MessageData constructGetBlockAccessListsResponse( + final Blockchain blockchain, + final MessageData message, + final int requestLimit, + final int maxMessageSize) { + final GetBlockAccessListsMessage getBlockAccessLists = + GetBlockAccessListsMessage.readFrom(message); + final Iterable blockHashes = getBlockAccessLists.blockHashes(); + + int responseSizeEstimate = RLP.MAX_PREFIX_SIZE; + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + rlp.startList(); + int count = 0; + for (final Hash blockHash : blockHashes) { + if (count >= requestLimit) { + break; + } + count++; + + final Optional maybeBlockAccessList = + blockchain.getBlockAccessList(blockHash); + final BytesValueRLPOutput balOutput = new BytesValueRLPOutput(); + if (maybeBlockAccessList.isPresent()) { + BlockAccessListEncoder.encode(maybeBlockAccessList.get(), balOutput); + } else { + // EIP-8159: Empty lists are returned for blocks where the BAL is unavailable. + balOutput.startList(); + balOutput.endList(); + } + + final int encodedSize = balOutput.encodedSize(); + if (responseSizeEstimate + encodedSize > maxMessageSize) { + break; + } + responseSizeEstimate += encodedSize; + rlp.writeRaw(balOutput.encoded()); + } + rlp.endList(); + + return BlockAccessListsMessage.createUnsafe(rlp.encoded()); + } + static MessageData constructGetPooledTransactionsResponse( final TransactionPool transactionPool, final EthPeer peer, diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBlockAccessListsFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBlockAccessListsFromPeerTask.java new file mode 100644 index 00000000000..a91d6cd0967 --- /dev/null +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBlockAccessListsFromPeerTask.java @@ -0,0 +1,123 @@ +/* + * 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.eth.manager.peertask.task; + +import static com.google.common.base.Preconditions.checkArgument; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.eth.EthProtocol; +import org.hyperledger.besu.ethereum.eth.manager.EthPeerImmutableAttributes; +import org.hyperledger.besu.ethereum.eth.manager.peertask.InvalidPeerTaskResponseException; +import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTask; +import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskValidationResponse; +import org.hyperledger.besu.ethereum.eth.messages.BlockAccessListsMessage; +import org.hyperledger.besu.ethereum.eth.messages.GetBlockAccessListsMessage; +import org.hyperledger.besu.ethereum.mainnet.BodyValidation; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** PeerTask for requesting and validating block access lists against block headers. */ +public class GetBlockAccessListsFromPeerTask implements PeerTask> { + + private static final Logger LOG = LoggerFactory.getLogger(GetBlockAccessListsFromPeerTask.class); + + private final List blockHeaders; + private final long requiredBlockchainHeight; + + public GetBlockAccessListsFromPeerTask(final List blockHeaders) { + checkArgument( + blockHeaders != null && !blockHeaders.isEmpty(), "Block headers must not be empty"); + checkArgument( + blockHeaders.stream().allMatch(header -> header.getBalHash().isPresent()), + "All headers must contain a block access list hash"); + this.blockHeaders = blockHeaders; + this.requiredBlockchainHeight = + blockHeaders.stream() + .mapToLong(BlockHeader::getNumber) + .max() + .orElse(BlockHeader.GENESIS_BLOCK_NUMBER); + } + + @Override + public SubProtocol getSubProtocol() { + return EthProtocol.get(); + } + + @Override + public MessageData getRequestMessage(final Set agreedCapabilities) { + return GetBlockAccessListsMessage.create( + blockHeaders.stream().map(BlockHeader::getBlockHash).toList()); + } + + @Override + public List processResponse( + final MessageData messageData, final Set agreedCapabilities) + throws InvalidPeerTaskResponseException { + if (messageData == null) { + LOG.atDebug().setMessage("Received null response while waiting for block access lists").log(); + throw new InvalidPeerTaskResponseException("Null message data"); + } + final BlockAccessListsMessage balMessage = BlockAccessListsMessage.readFrom(messageData); + return balMessage.blockAccessLists(); + } + + @Override + public PeerTaskValidationResponse validateResult(final List result) { + if (result.isEmpty()) { + return PeerTaskValidationResponse.NO_RESULTS_RETURNED; + } + + if (result.size() > blockHeaders.size()) { + LOG.atDebug() + .setMessage("Received invalid block access list response size: received={}, requested={}") + .addArgument(result::size) + .addArgument(blockHeaders::size) + .log(); + return PeerTaskValidationResponse.TOO_MANY_RESULTS_RETURNED; + } + + for (int i = 0; i < result.size(); i++) { + final Hash expectedBalHash = blockHeaders.get(i).getBalHash().orElse(null); + final Hash actualBalHash = BodyValidation.balHash(result.get(i)); + if (expectedBalHash == null || !expectedBalHash.equals(actualBalHash)) { + LOG.atDebug() + .setMessage( + "Received mismatched block access list at index {}: expected hash {}, actual hash {}") + .addArgument(i) + .addArgument(expectedBalHash) + .addArgument(actualBalHash) + .log(); + return PeerTaskValidationResponse.RESULTS_DO_NOT_MATCH_QUERY; + } + } + + return PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD; + } + + @Override + public Predicate getPeerRequirementFilter() { + return ethPeer -> ethPeer.estimatedChainHeight() >= requiredBlockchainHeight; + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/BlockAccessListsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/BlockAccessListsMessage.java new file mode 100644 index 00000000000..28dbfd749f1 --- /dev/null +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/BlockAccessListsMessage.java @@ -0,0 +1,72 @@ +/* + * 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.eth.messages; + +import org.hyperledger.besu.ethereum.core.encoding.BlockAccessListDecoder; +import org.hyperledger.besu.ethereum.core.encoding.BlockAccessListEncoder; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.AbstractMessageData; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.util.List; + +import org.apache.tuweni.bytes.Bytes; + +public final class BlockAccessListsMessage extends AbstractMessageData { + + public static BlockAccessListsMessage readFrom(final MessageData message) { + if (message instanceof BlockAccessListsMessage) { + return (BlockAccessListsMessage) message; + } + final int code = message.getCode(); + if (code != EthProtocolMessages.BLOCK_ACCESS_LISTS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a BlockAccessListsMessage.", code)); + } + return new BlockAccessListsMessage(message.getData()); + } + + public static BlockAccessListsMessage create(final Iterable blockAccessLists) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.writeList(blockAccessLists, BlockAccessListEncoder::encode); + return new BlockAccessListsMessage(tmp.encoded()); + } + + /** + * Create a message with raw, already encoded data. No checks are performed to validate the + * rlp-encoded data. + * + * @param data An rlp-encoded list of block access lists + * @return A new BlockAccessListsMessage + */ + public static BlockAccessListsMessage createUnsafe(final Bytes data) { + return new BlockAccessListsMessage(data); + } + + private BlockAccessListsMessage(final Bytes data) { + super(data); + } + + @Override + public int getCode() { + return EthProtocolMessages.BLOCK_ACCESS_LISTS; + } + + public List blockAccessLists() { + return new BytesValueRLPInput(data, false).readList(BlockAccessListDecoder::decode); + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/EthProtocolMessages.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/EthProtocolMessages.java index f23ef2ab1ed..98bf3a083f6 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/EthProtocolMessages.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/EthProtocolMessages.java @@ -51,6 +51,11 @@ public final class EthProtocolMessages { // Eth69 messages public static final int BLOCK_RANGE_UPDATE = 0x11; + // Eth71 messages + public static final int GET_BLOCK_ACCESS_LISTS = 0x12; + + public static final int BLOCK_ACCESS_LISTS = 0x13; + private EthProtocolMessages() { // Holder for constants only } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetBlockAccessListsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetBlockAccessListsMessage.java new file mode 100644 index 00000000000..dae7fcbe3f3 --- /dev/null +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetBlockAccessListsMessage.java @@ -0,0 +1,87 @@ +/* + * 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.eth.messages; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.AbstractMessageData; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; +import org.hyperledger.besu.ethereum.rlp.RLPInput; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.apache.tuweni.bytes.Bytes; + +public final class GetBlockAccessListsMessage extends AbstractMessageData { + + public static GetBlockAccessListsMessage readFrom(final MessageData message) { + if (message instanceof GetBlockAccessListsMessage) { + return (GetBlockAccessListsMessage) message; + } + final int code = message.getCode(); + if (code != EthProtocolMessages.GET_BLOCK_ACCESS_LISTS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a GetBlockAccessListsMessage.", code)); + } + return new GetBlockAccessListsMessage(message.getData()); + } + + public static GetBlockAccessListsMessage create(final Iterable blockHashes) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + blockHashes.forEach(hash -> tmp.writeBytes(hash.getBytes())); + tmp.endList(); + return new GetBlockAccessListsMessage(tmp.encoded()); + } + + private GetBlockAccessListsMessage(final Bytes data) { + super(data); + } + + @Override + public int getCode() { + return EthProtocolMessages.GET_BLOCK_ACCESS_LISTS; + } + + public Iterable blockHashes() { + return () -> + new Iterator<>() { + private final RLPInput input = new BytesValueRLPInput(data, false); + private boolean initialized = false; + + private void ensureInitialized() { + if (!initialized) { + input.enterList(); + initialized = true; + } + } + + @Override + public boolean hasNext() { + ensureInitialized(); + return !input.isEndOfCurrentList(); + } + + @Override + public Hash next() { + ensureInitialized(); + if (!hasNext()) throw new NoSuchElementException(); + return Hash.wrap(input.readBytes32()); + } + }; + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java index 3571f054c19..20525bb55f2 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java @@ -406,6 +406,27 @@ public void doNotDisconnectOnValidMessage() { } } + @Test + public void disconnectOnMalformedGetBlockAccessListsMessage() { + try (final EthProtocolManager ethManager = + EthProtocolManagerTestBuilder.builder() + .setProtocolSchedule(protocolSchedule) + .setBlockchain(blockchain) + .setEthScheduler(new DeterministicEthScheduler(() -> false)) + .setWorldStateArchive(protocolContext.getWorldStateArchive()) + .setTransactionPool(transactionPool) + .setEthereumWireProtocolConfiguration(EthProtocolConfiguration.DEFAULT) + .build()) { + final MessageData malformedMessageData = + new RawMessage(EthProtocolMessages.GET_BLOCK_ACCESS_LISTS, Bytes.fromHexString("0xc1ff")); + final MockPeerConnection peer = setupPeer(ethManager, (cap, msg, conn) -> {}); + + ethManager.processMessage(EthProtocol.LATEST, new DefaultMessage(peer, malformedMessageData)); + + assertThat(peer.isDisconnected()).isTrue(); + } + } + @Test public void respondToGetHeaders() throws ExecutionException, InterruptedException { final CompletableFuture done = new CompletableFuture<>(); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java index 67cf81fe3e9..7c6992f2d59 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java @@ -20,6 +20,8 @@ import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Hash; @@ -30,14 +32,17 @@ import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.core.encoding.BlockAccessListEncoder; import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; import org.hyperledger.besu.ethereum.eth.ImmutableEthProtocolConfiguration; import org.hyperledger.besu.ethereum.eth.manager.exceptions.ProtocolViolationException; +import org.hyperledger.besu.ethereum.eth.messages.BlockAccessListsMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; +import org.hyperledger.besu.ethereum.eth.messages.GetBlockAccessListsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetNodeDataMessage; @@ -49,6 +54,7 @@ import org.hyperledger.besu.ethereum.eth.messages.PooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.rlp.RLP; @@ -554,6 +560,139 @@ public void shouldTruncateResponseWhenFirstBlockIndexAndMessageSizeLimitInteract assertThat(result).contains(expectedMsg); } + @Test + public void shouldIncludeEmptyEntryForUnavailableBlockAccessList() { + setupEthServer(); + + final Hash availableHash = dataGenerator.hash(); + final Hash unavailableHash = dataGenerator.hash(); + final BlockAccessList available = dataGenerator.blockAccessList(); + + when(blockchain.getBlockAccessList(availableHash)).thenReturn(Optional.of(available)); + when(blockchain.getBlockAccessList(unavailableHash)).thenReturn(Optional.empty()); + + final GetBlockAccessListsMessage request = + GetBlockAccessListsMessage.create(List.of(availableHash, unavailableHash)); + + final BlockAccessListsMessage expected = + BlockAccessListsMessage.create(List.of(available, new BlockAccessList(List.of()))); + + assertThat(ethMessages.dispatch(new EthMessage(ethPeer, request), EthProtocol.LATEST)) + .contains(expected); + } + + @Test + public void shouldLimitBlockAccessListsByCount() { + final int count = 10; + final int limit = 6; + setupEthServer(b -> b.maxGetBlockAccessLists(limit)); + + final List hashes = new ArrayList<>(count); + final List accessLists = new ArrayList<>(count); + + for (int i = 0; i < count; i++) { + final Hash h = dataGenerator.hash(); + final BlockAccessList bal = dataGenerator.blockAccessList(); + hashes.add(h); + accessLists.add(bal); + when(blockchain.getBlockAccessList(h)).thenReturn(Optional.of(bal)); + } + + final GetBlockAccessListsMessage request = GetBlockAccessListsMessage.create(hashes); + + final BlockAccessListsMessage expected = + BlockAccessListsMessage.create(accessLists.subList(0, limit)); + + assertThat(ethMessages.dispatch(new EthMessage(ethPeer, request), EthProtocol.LATEST)) + .contains(expected); + } + + @Test + public void shouldLimitBlockAccessListsByMessageSize() { + final int count = 10; + setupEthServer(); + + final List hashes = new ArrayList<>(count); + final List accessLists = new ArrayList<>(count); + + for (int i = 0; i < count; i++) { + final Hash h = dataGenerator.hash(); + final BlockAccessList bal = dataGenerator.blockAccessList(); + hashes.add(h); + accessLists.add(bal); + when(blockchain.getBlockAccessList(h)).thenReturn(Optional.of(bal)); + } + + int sizeLimit = RLP.MAX_PREFIX_SIZE; + final List expectedAccessLists = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + final BlockAccessList bal = accessLists.get(i); + expectedAccessLists.add(bal); + sizeLimit += calculateRlpEncodedSize(bal); + } + + final int messageSizeLimit = sizeLimit; + setupEthServer(b -> b.maxMessageSize(messageSizeLimit)); + + final GetBlockAccessListsMessage request = GetBlockAccessListsMessage.create(hashes); + final BlockAccessListsMessage expected = BlockAccessListsMessage.create(expectedAccessLists); + + assertThat(ethMessages.dispatch(new EthMessage(ethPeer, request), EthProtocol.LATEST)) + .contains(expected); + } + + @Test + public void + shouldLimitTheNumberOfBlockAccessListsLookedUpByRequestLimitEvenWhenSomeAreUnavailable() { + final int requestLimit = 2; + setupEthServer(b -> b.maxGetBlockAccessLists(requestLimit)); + + final Hash firstAvailableHash = dataGenerator.hash(); + final Hash unavailableHash = dataGenerator.hash(); + final Hash secondAvailableHash = dataGenerator.hash(); // should NOT be processed + + final BlockAccessList firstAvailable = dataGenerator.blockAccessList(); + final BlockAccessList secondAvailable = dataGenerator.blockAccessList(); + + when(blockchain.getBlockAccessList(firstAvailableHash)).thenReturn(Optional.of(firstAvailable)); + when(blockchain.getBlockAccessList(unavailableHash)).thenReturn(Optional.empty()); + when(blockchain.getBlockAccessList(secondAvailableHash)) + .thenReturn(Optional.of(secondAvailable)); + + final GetBlockAccessListsMessage request = + GetBlockAccessListsMessage.create( + List.of(firstAvailableHash, unavailableHash, secondAvailableHash)); + + // With requestLimit=2, the 3rd hash must not be looked up or included. + final BlockAccessListsMessage expected = + BlockAccessListsMessage.create(List.of(firstAvailable, new BlockAccessList(List.of()))); + + assertThat(ethMessages.dispatch(new EthMessage(ethPeer, request), EthProtocol.LATEST)) + .contains(expected); + verify(blockchain, never()).getBlockAccessList(secondAvailableHash); + } + + @Test + public void shouldReturnEmptyResponseWhenFirstBlockAccessListWouldExceedMessageSize() { + setupEthServer(b -> b.maxMessageSize(RLP.MAX_PREFIX_SIZE)); + + final Hash firstHash = dataGenerator.hash(); + final Hash secondHash = dataGenerator.hash(); + + final BlockAccessList firstBal = dataGenerator.blockAccessList(); + final BlockAccessList secondBal = dataGenerator.blockAccessList(); + + when(blockchain.getBlockAccessList(firstHash)).thenReturn(Optional.of(firstBal)); + when(blockchain.getBlockAccessList(secondHash)).thenReturn(Optional.of(secondBal)); + + final GetBlockAccessListsMessage request = + GetBlockAccessListsMessage.create(List.of(firstHash, secondHash)); + + assertThat(ethMessages.dispatch(new EthMessage(ethPeer, request), EthProtocol.LATEST)) + .contains(BlockAccessListsMessage.create(List.of())); + verify(blockchain, never()).getBlockAccessList(secondHash); + } + private void setupEthServer() { setupEthServer(Function.identity()); } @@ -676,4 +815,10 @@ private Bytes serializePaginatedReceiptsList( rlp.endList(); return rlp.encoded(); } + + private int calculateRlpEncodedSize(final BlockAccessList blockAccessList) { + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + BlockAccessListEncoder.encode(blockAccessList, rlp); + return rlp.encodedSize(); + } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RespondingEthPeer.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RespondingEthPeer.java index 24e31d10ebb..9e9ec336a1d 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RespondingEthPeer.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RespondingEthPeer.java @@ -34,6 +34,7 @@ import org.hyperledger.besu.ethereum.eth.EthProtocolVersion; import org.hyperledger.besu.ethereum.eth.core.Utils; import org.hyperledger.besu.ethereum.eth.manager.snap.SnapProtocolManager; +import org.hyperledger.besu.ethereum.eth.messages.BlockAccessListsMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; @@ -329,6 +330,10 @@ public static Responder blockchainResponder( response = EthServer.constructGetPooledTransactionsResponse( transactionPool, peer, msg, 200, maxMsgSize); + break; + case EthProtocolMessages.GET_BLOCK_ACCESS_LISTS: + response = + EthServer.constructGetBlockAccessListsResponse(blockchain, msg, 200, maxMsgSize); } return Optional.ofNullable(response); }; @@ -438,6 +443,9 @@ public static Responder emptyResponder() { case EthProtocolMessages.GET_POOLED_TRANSACTIONS: response = PooledTransactionsMessage.create(Collections.emptyList()); break; + case EthProtocolMessages.GET_BLOCK_ACCESS_LISTS: + response = BlockAccessListsMessage.create(Collections.emptyList()); + break; } return Optional.ofNullable(response); }; diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBlockAccessListsFromPeerTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBlockAccessListsFromPeerTaskTest.java new file mode 100644 index 00000000000..a635bf20984 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBlockAccessListsFromPeerTaskTest.java @@ -0,0 +1,183 @@ +/* + * 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.eth.manager.peertask.task; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +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.Difficulty; +import org.hyperledger.besu.ethereum.eth.EthProtocol; +import org.hyperledger.besu.ethereum.eth.manager.ChainState; +import org.hyperledger.besu.ethereum.eth.manager.EthPeer; +import org.hyperledger.besu.ethereum.eth.manager.EthPeerImmutableAttributes; +import org.hyperledger.besu.ethereum.eth.manager.PeerReputation; +import org.hyperledger.besu.ethereum.eth.manager.peertask.InvalidPeerTaskResponseException; +import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskValidationResponse; +import org.hyperledger.besu.ethereum.eth.messages.BlockAccessListsMessage; +import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; +import org.hyperledger.besu.ethereum.eth.messages.GetBlockAccessListsMessage; +import org.hyperledger.besu.ethereum.mainnet.BodyValidation; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; + +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class GetBlockAccessListsFromPeerTaskTest { + + private static final Set + AGREED_CAPABILITIES = Set.of(EthProtocol.LATEST); + + @Test + void testGetRequestMessage() { + final Hash hash1 = Hash.fromHexString(StringUtils.repeat("00", 31) + "11"); + final Hash hash2 = Hash.fromHexString(StringUtils.repeat("00", 31) + "21"); + + final BlockHeader header1 = mockBlockHeader(1, new BlockAccessList(List.of()), hash1); + final BlockHeader header2 = mockBlockHeader(2, new BlockAccessList(List.of()), hash2); + + final GetBlockAccessListsFromPeerTask task = + new GetBlockAccessListsFromPeerTask(List.of(header1, header2)); + + final MessageData messageData = task.getRequestMessage(AGREED_CAPABILITIES); + final GetBlockAccessListsMessage message = GetBlockAccessListsMessage.readFrom(messageData); + + assertThat(message.getCode()).isEqualTo(EthProtocolMessages.GET_BLOCK_ACCESS_LISTS); + assertThat(message.blockHashes()) + .containsExactly(Hash.fromHexStringLenient("0x11"), Hash.fromHexStringLenient("0x21")); + } + + @Test + void testProcessResponseWithMatchingData() throws InvalidPeerTaskResponseException { + final BlockAccessList blockAccessList = new BlockAccessList(List.of()); + final BlockHeader header = mockBlockHeader(1, blockAccessList); + + final GetBlockAccessListsFromPeerTask task = + new GetBlockAccessListsFromPeerTask(List.of(header)); + + final List response = + task.processResponse(BlockAccessListsMessage.create(List.of(blockAccessList)), Set.of()); + + assertThat(response).containsExactly(blockAccessList); + } + + @Test + void testValidateResultWithMismatchedBal() { + final BlockAccessList expectedBal = new BlockAccessList(List.of()); + final BlockHeader header = mockBlockHeader(1, expectedBal); + + final BlockAccessList mismatchingBal = + new BlockAccessList( + List.of( + new BlockAccessList.AccountChanges( + Address.fromHexString("0x0000000000000000000000000000000000000001"), + List.of(), + List.of(), + List.of(), + List.of(), + List.of()))); + + final GetBlockAccessListsFromPeerTask task = + new GetBlockAccessListsFromPeerTask(List.of(header)); + + assertThat(task.validateResult(List.of(mismatchingBal))) + .isEqualTo(PeerTaskValidationResponse.RESULTS_DO_NOT_MATCH_QUERY); + } + + @Test + void testRequiresNonEmptyHeaders() { + assertThrows( + IllegalArgumentException.class, () -> new GetBlockAccessListsFromPeerTask(List.of())); + } + + @Test + void testRequiresHeadersWithBalHash() { + final BlockHeader headerWithoutBalHash = Mockito.mock(BlockHeader.class); + Mockito.when(headerWithoutBalHash.getBalHash()).thenReturn(java.util.Optional.empty()); + + assertThrows( + IllegalArgumentException.class, + () -> new GetBlockAccessListsFromPeerTask(List.of(headerWithoutBalHash))); + } + + @Test + void testGetPeerRequirementFilter() { + final BlockHeader header = mockBlockHeader(3, new BlockAccessList(List.of())); + final GetBlockAccessListsFromPeerTask task = + new GetBlockAccessListsFromPeerTask(List.of(header)); + + final EthPeer successfulCandidate = mockPeer(5); + final EthPeer failedCandidate = mockPeer(2); + + assertThat( + task.getPeerRequirementFilter() + .test(EthPeerImmutableAttributes.from(successfulCandidate))) + .isTrue(); + assertThat( + task.getPeerRequirementFilter().test(EthPeerImmutableAttributes.from(failedCandidate))) + .isFalse(); + } + + @Test + void testValidateResult() { + final BlockHeader header = mockBlockHeader(1, new BlockAccessList(List.of())); + final GetBlockAccessListsFromPeerTask task = + new GetBlockAccessListsFromPeerTask(List.of(header)); + + assertThat(task.validateResult(List.of())) + .isEqualTo(PeerTaskValidationResponse.NO_RESULTS_RETURNED); + assertThat(task.validateResult(List.of(new BlockAccessList(List.of())))) + .isEqualTo(PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD); + assertThat( + task.validateResult( + List.of(new BlockAccessList(List.of()), new BlockAccessList(List.of())))) + .isEqualTo(PeerTaskValidationResponse.TOO_MANY_RESULTS_RETURNED); + } + + private BlockHeader mockBlockHeader(final long blockNumber, final BlockAccessList bal) { + return mockBlockHeader(blockNumber, bal, Hash.ZERO); + } + + private BlockHeader mockBlockHeader( + final long blockNumber, final BlockAccessList bal, final Hash hash) { + final BlockHeader blockHeader = Mockito.mock(BlockHeader.class); + Mockito.when(blockHeader.getNumber()).thenReturn(blockNumber); + Mockito.when(blockHeader.getBlockHash()).thenReturn(hash); + Mockito.when(blockHeader.getBalHash()) + .thenReturn(java.util.Optional.of(BodyValidation.balHash(bal))); + return blockHeader; + } + + private EthPeer mockPeer(final long chainHeight) { + final EthPeer ethPeer = Mockito.mock(EthPeer.class); + final ChainState chainState = Mockito.mock(ChainState.class); + + Mockito.when(ethPeer.chainState()).thenReturn(chainState); + Mockito.when(chainState.getEstimatedHeight()).thenReturn(chainHeight); + Mockito.when(chainState.getEstimatedTotalDifficulty()).thenReturn(Difficulty.of(0)); + Mockito.when(ethPeer.getReputation()).thenReturn(new PeerReputation()); + final PeerConnection connection = Mockito.mock(PeerConnection.class); + Mockito.when(ethPeer.getConnection()).thenReturn(connection); + return ethPeer; + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/BlockAccessListsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/BlockAccessListsMessageTest.java new file mode 100644 index 00000000000..8fd3875847c --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/BlockAccessListsMessageTest.java @@ -0,0 +1,52 @@ +/* + * 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.eth.messages; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.ethereum.core.BlockDataGenerator; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class BlockAccessListsMessageTest { + + @Test + public void roundTripTest() { + final BlockDataGenerator generator = new BlockDataGenerator(1); + final List expected = + List.of(generator.blockAccessList(), new BlockAccessList(List.of())); + + final BlockAccessListsMessage initialMessage = BlockAccessListsMessage.create(expected); + final RawMessage raw = + new RawMessage(EthProtocolMessages.BLOCK_ACCESS_LISTS, initialMessage.getData()); + + final BlockAccessListsMessage message = BlockAccessListsMessage.readFrom(raw); + assertThat(message.blockAccessLists()).isEqualTo(expected); + } + + @Test + public void roundTripWithNoBlockAccessLists() { + final BlockAccessListsMessage initialMessage = BlockAccessListsMessage.create(List.of()); + final RawMessage raw = + new RawMessage(EthProtocolMessages.BLOCK_ACCESS_LISTS, initialMessage.getData()); + + final BlockAccessListsMessage message = BlockAccessListsMessage.readFrom(raw); + assertThat(message.blockAccessLists()).isEmpty(); + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetBlockAccessListsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetBlockAccessListsMessageTest.java new file mode 100644 index 00000000000..4fc3fa26cd0 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetBlockAccessListsMessageTest.java @@ -0,0 +1,62 @@ +/* + * 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.eth.messages; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.BlockDataGenerator; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class GetBlockAccessListsMessageTest { + + @Test + public void roundTripTest() { + final BlockDataGenerator generator = new BlockDataGenerator(1); + final List blockHashes = new ArrayList<>(); + final int hashCount = 20; + for (int i = 0; i < hashCount; i++) { + blockHashes.add(generator.hash()); + } + + final MessageData initialMessage = GetBlockAccessListsMessage.create(blockHashes); + final MessageData raw = + new RawMessage(EthProtocolMessages.GET_BLOCK_ACCESS_LISTS, initialMessage.getData()); + final GetBlockAccessListsMessage message = GetBlockAccessListsMessage.readFrom(raw); + + final Iterator readBlockHashes = message.blockHashes().iterator(); + for (int i = 0; i < hashCount; i++) { + assertThat(readBlockHashes.next()).isEqualTo(blockHashes.get(i)); + } + assertThat(readBlockHashes.hasNext()).isFalse(); + } + + @Test + public void createWithEmptyHashes() { + final MessageData initialMessage = GetBlockAccessListsMessage.create(List.of()); + final MessageData raw = + new RawMessage(EthProtocolMessages.GET_BLOCK_ACCESS_LISTS, initialMessage.getData()); + final GetBlockAccessListsMessage message = GetBlockAccessListsMessage.readFrom(raw); + + assertThat(message.blockHashes()).isEmpty(); + } +} From 34c63aa6fff1104d8bf761cad392e503a9db6bc7 Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Wed, 18 Mar 2026 10:47:16 +0000 Subject: [PATCH 63/77] Implement div sdiv with long limbs (#9923) Signed-off-by: Luis Pinto --- .gitignore | 1 + CHANGELOG.md | 7 + .../vm/operations/DivOperationBenchmark.java | 110 +- .../vm/operations/SDivOperationBenchmark.java | 121 +- .../java/org/hyperledger/besu/evm/EVM.java | 12 +- .../org/hyperledger/besu/evm/MainnetEVMs.java | 8 +- .../org/hyperledger/besu/evm/UInt256.java | 1131 +++++++++-------- .../evm/operation/DivOperationOptimized.java | 65 + .../evm/operation/SDivOperationOptimized.java | 63 + .../besu/evm/UInt256PropertyBasedTest.java | 122 ++ .../org/hyperledger/besu/evm/UInt256Test.java | 158 +++ 11 files changed, 1269 insertions(+), 529 deletions(-) create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/operation/DivOperationOptimized.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/operation/SDivOperationOptimized.java diff --git a/.gitignore b/.gitignore index dde45916f4e..1bb281256b5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.iml *.launch *.log +*.orig .classpath .claude .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 2937208350f..13d78dc36ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,13 @@ are provided with different values, using input as per the execution-apis spec i - Add maxUsedGas field to eth_simulateV1 results [#10066](https://github.com/besu-eth/besu/pull/10066) - Plugin API: pass pending block header when creating selectors [#10034](https://github.com/besu-eth/besu/pull/10034) +### Performance +- UInt256 arithmetics with long limbs [#9677](https://github.com/besu-eth/besu/pull/9677) +- Fix edge case in MOD variant operations regarding multiply subtract step [#9934](https://github.com/besu-eth/besu/pull/9934) +- Fix addMod case with 256bit moduluses [#10001](https://github.com/besu-eth/besu/pull/10001) +- Performance improvements on MOD variant instructions while converting from byte[] to longs [#9976](https://github.com/besu-eth/besu/pull/9976) +- Implement DIV and SDIV with long limbs [#9923](https://github.com/besu-eth/besu/pull/9923) + ## 26.2.0 ### Breaking Changes diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/DivOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/DivOperationBenchmark.java index c7c3108c62c..5e561891d3c 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/DivOperationBenchmark.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/DivOperationBenchmark.java @@ -15,13 +15,119 @@ package org.hyperledger.besu.ethereum.vm.operations; import org.hyperledger.besu.evm.frame.MessageFrame; -import org.hyperledger.besu.evm.operation.DivOperation; +import org.hyperledger.besu.evm.operation.DivOperationOptimized; import org.hyperledger.besu.evm.operation.Operation; +import java.math.BigInteger; +import java.util.Random; + +import org.apache.tuweni.bytes.Bytes; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; + public class DivOperationBenchmark extends BinaryOperationBenchmark { + public enum Case { + DIV_32_32(1, 1), + DIV_64_32(2, 1), + DIV_64_64(2, 2), + DIV_128_32(4, 1), + DIV_128_64(4, 2), + DIV_128_128(4, 4), + DIV_192_32(6, 1), + DIV_192_64(6, 2), + DIV_192_128(6, 4), + DIV_192_192(6, 6), + DIV_256_32(8, 1), + DIV_256_64(8, 2), + DIV_256_128(8, 4), + DIV_256_192(8, 6), + DIV_256_256(8, 8), + DIV_ZERO_QUOTIENT_0_256(0, 8), + DIV_ZERO_QUOTIENT_64_256(2, 8), + DIV_ZERO_QUOTIENT_128_256(4, 8), + DIV_ZERO_QUOTIENT_192_256(6, 8), + DIV_FULL_RANDOM(-1, -1); + + final int numSize; + final int denomSize; + + Case(final int numSize, final int denomSize) { + this.numSize = numSize; + this.denomSize = denomSize; + } + } + + @Param({ + "DIV_32_32", + "DIV_64_32", + "DIV_64_64", + "DIV_128_32", + "DIV_128_64", + "DIV_128_128", + "DIV_192_32", + "DIV_192_64", + "DIV_192_128", + "DIV_192_192", + "DIV_256_32", + "DIV_256_64", + "DIV_256_128", + "DIV_256_192", + "DIV_256_256", + "DIV_ZERO_QUOTIENT_0_256", + "DIV_ZERO_QUOTIENT_64_256", + "DIV_ZERO_QUOTIENT_128_256", + "DIV_ZERO_QUOTIENT_192_256", + "DIV_FULL_RANDOM" + }) + private String caseName; + + @Setup(Level.Iteration) + @Override + public void setUp() { + frame = BenchmarkHelper.createMessageCallFrame(); + + Case scenario = Case.valueOf(caseName); + aPool = new Bytes[SAMPLE_SIZE]; + bPool = new Bytes[SAMPLE_SIZE]; + + final Random random = new Random(); + int aSize; + int bSize; + + for (int i = 0; i < SAMPLE_SIZE; i++) { + if (scenario.numSize < 0) aSize = random.nextInt(1, 33); + else aSize = scenario.numSize * 4; + if (scenario.denomSize < 0) bSize = random.nextInt(1, 33); + else bSize = scenario.denomSize * 4; + + final byte[] a = new byte[aSize]; + final byte[] b = new byte[bSize]; + random.nextBytes(a); + random.nextBytes(b); + + // Swap a and b if necessary + if ((scenario.numSize != scenario.denomSize)) { + aPool[i] = Bytes.wrap(a); + bPool[i] = Bytes.wrap(b); + } else { + BigInteger aInt = new BigInteger(1, a); + BigInteger bInt = new BigInteger(1, b); + if ((aInt.compareTo(bInt) < 0)) { + aPool[i] = Bytes.wrap(b); + bPool[i] = Bytes.wrap(a); + } else { + aPool[i] = Bytes.wrap(a); + bPool[i] = Bytes.wrap(b); + } + } + } + index = 0; + } + @Override protected Operation.OperationResult invoke(final MessageFrame frame) { - return DivOperation.staticOperation(frame); + return DivOperationOptimized.staticOperation(frame); } } diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SDivOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SDivOperationBenchmark.java index 42d016559fe..6ed31ab2385 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SDivOperationBenchmark.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SDivOperationBenchmark.java @@ -16,11 +16,128 @@ import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.operation.Operation; -import org.hyperledger.besu.evm.operation.SDivOperation; +import org.hyperledger.besu.evm.operation.SDivOperationOptimized; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Random; + +import org.apache.tuweni.bytes.Bytes; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; public class SDivOperationBenchmark extends BinaryOperationBenchmark { + + public enum Case { + SDIV_32_32(1, 1), + SDIV_64_32(2, 1), + SDIV_64_64(2, 2), + SDIV_128_32(4, 1), + SDIV_128_64(4, 2), + SDIV_128_128(4, 4), + SDIV_192_32(6, 1), + SDIV_192_64(6, 2), + SDIV_192_128(6, 4), + SDIV_192_192(6, 6), + SDIV_256_32(8, 1), + SDIV_256_64(8, 2), + SDIV_256_128(8, 4), + SDIV_256_192(8, 6), + SDIV_256_256(8, 8), + SDIV_ZERO_QUOTIENT_0_256(0, 8), + SDIV_ZERO_QUOTIENT_64_256(2, 8), + SDIV_ZERO_QUOTIENT_128_256(4, 8), + SDIV_ZERO_QUOTIENT_192_256(6, 8), + SDIV_FULL_RANDOM(-1, -1); + + final int numSize; + final int denomSize; + + Case(final int numSize, final int denomSize) { + this.numSize = numSize; + this.denomSize = denomSize; + } + } + + @Param({ + "SDIV_32_32", + "SDIV_64_32", + "SDIV_64_64", + "SDIV_128_32", + "SDIV_128_64", + "SDIV_128_128", + "SDIV_192_32", + "SDIV_192_64", + "SDIV_192_128", + "SDIV_192_192", + "SDIV_256_32", + "SDIV_256_64", + "SDIV_256_128", + "SDIV_256_192", + "SDIV_256_256", + "SDIV_ZERO_QUOTIENT_0_256", + "SDIV_ZERO_QUOTIENT_64_256", + "SDIV_ZERO_QUOTIENT_128_256", + "SDIV_ZERO_QUOTIENT_192_256", + "SDIV_FULL_RANDOM" + }) + private String caseName; + + @Setup(Level.Iteration) + @Override + public void setUp() { + frame = BenchmarkHelper.createMessageCallFrame(); + + Case scenario = Case.valueOf(caseName); + aPool = new Bytes[SAMPLE_SIZE]; + bPool = new Bytes[SAMPLE_SIZE]; + + final Random random = new Random(); + int aSize; + int bSize; + + for (int i = 0; i < SAMPLE_SIZE; i++) { + if (scenario.numSize < 0) aSize = random.nextInt(1, 33); + else aSize = scenario.numSize * 4; + if (scenario.denomSize < 0) bSize = random.nextInt(1, 33); + else bSize = scenario.denomSize * 4; + + byte[] a = new byte[aSize]; + byte[] b = new byte[bSize]; + random.nextBytes(a); + random.nextBytes(b); + a = negate(a); + b = negate(b); + + // Swap a and b if necessary + if ((scenario.numSize != scenario.denomSize)) { + aPool[i] = Bytes.wrap(a); + bPool[i] = Bytes.wrap(b); + } else { + BigInteger aInt = new BigInteger(a); + BigInteger bInt = new BigInteger(b); + if ((aInt.abs().compareTo(bInt.abs()) < 0)) { + aPool[i] = Bytes.wrap(b); + bPool[i] = Bytes.wrap(a); + } else { + aPool[i] = Bytes.wrap(a); + bPool[i] = Bytes.wrap(b); + } + } + } + index = 0; + } + + private static byte[] negate(final byte[] array) { + byte[] tmp = new byte[32]; + Arrays.fill(tmp, (byte) 0xFF); + System.arraycopy(array, 0, tmp, 32 - array.length, array.length); + return tmp; + } + @Override protected Operation.OperationResult invoke(final MessageFrame frame) { - return SDivOperation.staticOperation(frame); + return SDivOperationOptimized.staticOperation(frame); } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/EVM.java b/evm/src/main/java/org/hyperledger/besu/evm/EVM.java index 21ead3136a4..9752e7b841b 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/EVM.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/EVM.java @@ -37,6 +37,7 @@ import org.hyperledger.besu.evm.operation.ChainIdOperation; import org.hyperledger.besu.evm.operation.CountLeadingZerosOperation; import org.hyperledger.besu.evm.operation.DivOperation; +import org.hyperledger.besu.evm.operation.DivOperationOptimized; import org.hyperledger.besu.evm.operation.DupNOperation; import org.hyperledger.besu.evm.operation.DupOperation; import org.hyperledger.besu.evm.operation.ExchangeOperation; @@ -64,6 +65,7 @@ import org.hyperledger.besu.evm.operation.Push0Operation; import org.hyperledger.besu.evm.operation.PushOperation; import org.hyperledger.besu.evm.operation.SDivOperation; +import org.hyperledger.besu.evm.operation.SDivOperationOptimized; import org.hyperledger.besu.evm.operation.SGtOperation; import org.hyperledger.besu.evm.operation.SLtOperation; import org.hyperledger.besu.evm.operation.SModOperation; @@ -246,8 +248,14 @@ public void runToHalt(final MessageFrame frame, final OperationTracer tracing) { : AddOperation.staticOperation(frame); case 0x02 -> MulOperation.staticOperation(frame); case 0x03 -> SubOperation.staticOperation(frame); - case 0x04 -> DivOperation.staticOperation(frame); - case 0x05 -> SDivOperation.staticOperation(frame); + case 0x04 -> + evmConfiguration.enableOptimizedOpcodes() + ? DivOperationOptimized.staticOperation(frame) + : DivOperation.staticOperation(frame); + case 0x05 -> + evmConfiguration.enableOptimizedOpcodes() + ? SDivOperationOptimized.staticOperation(frame) + : SDivOperation.staticOperation(frame); case 0x06 -> evmConfiguration.enableOptimizedOpcodes() ? ModOperationOptimized.staticOperation(frame) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java b/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java index 120893515f7..278bee68dd0 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java @@ -61,6 +61,7 @@ import org.hyperledger.besu.evm.operation.DelegateCallOperation; import org.hyperledger.besu.evm.operation.DifficultyOperation; import org.hyperledger.besu.evm.operation.DivOperation; +import org.hyperledger.besu.evm.operation.DivOperationOptimized; import org.hyperledger.besu.evm.operation.DupNOperation; import org.hyperledger.besu.evm.operation.DupOperation; import org.hyperledger.besu.evm.operation.EqOperation; @@ -109,6 +110,7 @@ import org.hyperledger.besu.evm.operation.ReturnOperation; import org.hyperledger.besu.evm.operation.RevertOperation; import org.hyperledger.besu.evm.operation.SDivOperation; +import org.hyperledger.besu.evm.operation.SDivOperationOptimized; import org.hyperledger.besu.evm.operation.SGtOperation; import org.hyperledger.besu.evm.operation.SLoadOperation; import org.hyperledger.besu.evm.operation.SLtOperation; @@ -205,8 +207,6 @@ private static void registerFrontierOperations( } registry.put(new MulOperation(gasCalculator)); registry.put(new SubOperation(gasCalculator)); - registry.put(new DivOperation(gasCalculator)); - registry.put(new SDivOperation(gasCalculator)); if (evmConfiguration.enableOptimizedOpcodes()) { registry.put(new AddOperationOptimized(gasCalculator)); registry.put(new ModOperationOptimized(gasCalculator)); @@ -217,6 +217,8 @@ private static void registerFrontierOperations( registry.put(new XorOperationOptimized(gasCalculator)); registry.put(new OrOperationOptimized(gasCalculator)); registry.put(new NotOperationOptimized(gasCalculator)); + registry.put(new DivOperationOptimized(gasCalculator)); + registry.put(new SDivOperationOptimized(gasCalculator)); } else { registry.put(new AddOperation(gasCalculator)); registry.put(new ModOperation(gasCalculator)); @@ -227,6 +229,8 @@ private static void registerFrontierOperations( registry.put(new XorOperation(gasCalculator)); registry.put(new OrOperation(gasCalculator)); registry.put(new NotOperation(gasCalculator)); + registry.put(new DivOperation(gasCalculator)); + registry.put(new SDivOperation(gasCalculator)); } registry.put(new ExpOperation(gasCalculator)); registry.put(new SignExtendOperation(gasCalculator)); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java index a4dc23dae2f..15f20924670 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java @@ -45,6 +45,8 @@ public record UInt256(long u3, long u2, long u1, long u0) { /** The constant 0. */ public static final UInt256 ZERO = new UInt256(0, 0, 0, 0); + public static final UInt256 ONE = new UInt256(0, 0, 0, 1); + private static final byte[] ZERO_BYTES = new byte[BYTESIZE]; /** The constant All ones */ @@ -223,20 +225,16 @@ private UInt320 UInt320Value() { return new UInt320(0, u3, u2, u1, u0); } - private Modulus64 asModulus64() { - return new Modulus64(u0); - } - - private Modulus128 asModulus128() { - return new Modulus128(u1, u0); + private UInt64 asUInt64() { + return new UInt64(u0); } - private Modulus192 asModulus192() { - return new Modulus192(u2, u1, u0); + private UInt128 asUInt128() { + return new UInt128(u1, u0); } - private Modulus256 asModulus256() { - return new Modulus256(u3, u2, u1, u0); + private UInt192 asUInt192() { + return new UInt192(u2, u1, u0); } // -------------------------------------------------------------------------- @@ -309,17 +307,38 @@ public boolean isUInt192() { } /** - * Compares two UInt256. + * Compares two UInt256. If any of the values is `null` it is considered smaller than the other. + * + * @param u1 UInt256 value + * @param u2 UInt256 value + * @return 0 if this == that, negative if this < that and positive if this > that. + */ + public static int compare(final UInt256 u1, final UInt256 u2) { + if (u1 == null || u2 == null) { + return Boolean.compare(u2 == null, u1 == null); + } + return u1.compareTo(u2); + } + + private int compareTo(final UInt256 that) { + if (u3 != that.u3) return Long.compareUnsigned(u3, that.u3); + if (u2 != that.u2) return Long.compareUnsigned(u2, that.u2); + if (u1 != that.u1) return Long.compareUnsigned(u1, that.u1); + return Long.compareUnsigned(u0, that.u0); + } + + /** + * Compares a UInt256 value with a UInt512 value. * - * @param a left UInt256 - * @param b right UInt256 - * @return 0 if a == b, negative if a < b and positive if a > b. + * @param that UInt512 value + * @return 0 if this == that, negative if this < that and positive if this > that. */ - public static int compare(final UInt256 a, final UInt256 b) { - if (a.u3 != b.u3) return Long.compareUnsigned(a.u3, b.u3); - if (a.u2 != b.u2) return Long.compareUnsigned(a.u2, b.u2); - if (a.u1 != b.u1) return Long.compareUnsigned(a.u1, b.u1); - return Long.compareUnsigned(a.u0, b.u0); + private int compareTo(final UInt512 that) { + if ((that.u7 | that.u6 | that.u5 | that.u4) != 0) return -1; + if (that.u3 != u3) return Long.compareUnsigned(u3, that.u3); + if (that.u2 != u2) return Long.compareUnsigned(u2, that.u2); + if (that.u1 != u1) return Long.compareUnsigned(u1, that.u1); + return Long.compareUnsigned(u0, that.u0); } // -------------------------------------------------------------------------- @@ -474,11 +493,11 @@ public UInt256 mul(final UInt256 other) { */ public UInt256 mod(final UInt256 modulus) { if (isZero()) return ZERO; - if (modulus.u3 != 0) return modulus.asModulus256().reduce(this); - if (modulus.u2 != 0) return modulus.asModulus192().reduce(this); - if (modulus.u1 != 0) return modulus.asModulus128().reduce(this); + if (modulus.u3 != 0) return modulus.modReduce(this); + if (modulus.u2 != 0) return modulus.asUInt192().modReduce(this); + if (modulus.u1 != 0) return modulus.asUInt128().modReduce(this); if ((modulus.u0 == 0) || (modulus.u0 == 1)) return ZERO; - return modulus.asModulus64().reduce(this); + return modulus.asUInt64().modReduce(this); } /** @@ -499,6 +518,41 @@ public UInt256 signedMod(final UInt256 modulus) { return r; } + /** + * Unsigned division. + * + *

    Compute this / divisor as unsigned big-endian integer. + * + * @param divisor The divisor. + * @return The quotient. + */ + public UInt256 div(final UInt256 divisor) { + if (isZero()) return ZERO; + if (divisor.u3 != 0) return divisor.divReduce(this); + if (divisor.u2 != 0) return divisor.asUInt192().divReduce(this); + if (divisor.u1 != 0) return divisor.asUInt128().divReduce(this); + if ((divisor.u0 == 0) || (divisor.u0 == 1)) return (divisor.u0 == 1) ? this : ZERO; + return divisor.asUInt64().divReduce(this); + } + + /** + * Signed division. + * + *

    In signed division, integers are interpreted as fixed 256 bits width two's complement signed + * integers. + * + * @param divisor The divisor. + * @return The quotient. + */ + public UInt256 signedDiv(final UInt256 divisor) { + if (isZero() || divisor.isZero()) return ZERO; + UInt256 a = abs(); + UInt256 d = divisor.abs(); + UInt256 q = a.div(d); + if (isNegative() != divisor.isNegative()) q = q.neg(); + return q; + } + /** * Modular addition. * @@ -510,10 +564,10 @@ public UInt256 addMod(final UInt256 other, final UInt256 modulus) { if (isZero()) return other.mod(modulus); if (other.isZero()) return this.mod(modulus); if (modulus.isZeroOrOne()) return ZERO; - if (modulus.u3 != 0) return modulus.asModulus256().sum(this, other); - if (modulus.u2 != 0) return modulus.asModulus192().sum(this, other); - if (modulus.u1 != 0) return modulus.asModulus128().sum(this, other); - return modulus.asModulus64().sum(this, other); + if (modulus.u3 != 0) return modulus.sum(this, other); + if (modulus.u2 != 0) return modulus.asUInt192().sum(this, other); + if (modulus.u1 != 0) return modulus.asUInt128().sum(this, other); + return modulus.asUInt64().sum(this, other); } /** @@ -527,10 +581,10 @@ public UInt256 mulMod(final UInt256 other, final UInt256 modulus) { if (this.isZero() || other.isZero() || modulus.isZeroOrOne()) return ZERO; if (this.isOne()) return other.mod(modulus); if (other.isOne()) return this.mod(modulus); - if (modulus.u3 != 0) return modulus.asModulus256().mul(this, other); - if (modulus.u2 != 0) return modulus.asModulus192().mul(this, other); - if (modulus.u1 != 0) return modulus.asModulus128().mul(this, other); - return modulus.asModulus64().mul(this, other); + if (modulus.u3 != 0) return modulus.mul(this, other); + if (modulus.u2 != 0) return modulus.asUInt192().mul(this, other); + if (modulus.u1 != 0) return modulus.asUInt128().mul(this, other); + return modulus.asUInt64().mul(this, other); } // -------------------------------------------------------------------------- @@ -830,8 +884,223 @@ private UInt512 mul256(final UInt256 v) { // -------------------------------------------------------------------------- // endregion - // region private quotient estimation + // region private division // -------------------------------------------------------------------------- + private UInt256 modReduce(final UInt256 that) { + int cmp = compareTo(that); + if (cmp == 0) return ZERO; + if (cmp > 0) return that; + int shift = Long.numberOfLeadingZeros(u3); + UInt256 m = shiftLeft(shift); + long inv = reciprocal(m.u3); + return m.modReduceNormalised(that, shift, inv); + } + + private UInt256 modReduce(final UInt512 that) { + int cmp = compareTo(that); + if (cmp == 0) return ZERO; + if (cmp > 0) return that.UInt256Value(); + int shift = Long.numberOfLeadingZeros(u3); + UInt256 m = shiftLeft(shift); + long inv = reciprocal(m.u3); + return m.modReduceNormalised(that, shift, inv); + } + + private UInt256 sum(final UInt256 a, final UInt256 b) { + UInt257 sum = a.adc(b); + if (!sum.carry()) { + int cmp = compareTo(sum.UInt256Value()); + if (cmp == 0) return ZERO; + if (cmp > 0) return sum.UInt256Value(); + } + int shift = Long.numberOfLeadingZeros(u3); + UInt256 m = shiftLeft(shift); + long inv = reciprocal(m.u3); + return m.modReduceNormalised(sum, shift, inv); + } + + private UInt256 mul(final UInt256 a, final UInt256 b) { + // multiply-reduce + UInt512 prod = a.mul256(b); + int cmp = compareTo(prod); + if (cmp == 0) return ZERO; + if (cmp > 0) return prod.UInt256Value(); + return modReduce(prod); + } + + private QR256 addBack(final long v3, final long v2, final long v1, final long v0, final long q) { + // Add back + long z0 = v0 + u0; + long carry = ((v0 & u0) | ((v0 | u0) & ~z0)) >>> 63; + + long z1 = v1 + u1 + carry; + long overflow1 = (Long.compareUnsigned(z1, v1) < 0) ? 1 : 0; + long overflow2 = (Long.compareUnsigned(z1, v1) == 0) ? 1 : 0; + carry = overflow1 | (overflow2 & carry); + + long z2 = v2 + u2 + carry; + overflow1 = (Long.compareUnsigned(z2, v2) < 0) ? 1 : 0; + overflow2 = (Long.compareUnsigned(z2, v2) == 0) ? 1 : 0; + carry = overflow1 | (overflow2 & carry); + + long z3 = v3 + u3 + carry; + overflow1 = (Long.compareUnsigned(z3, v3) < 0) ? 1 : 0; + overflow2 = (Long.compareUnsigned(z3, v3) == 0) ? 1 : 0; + carry = overflow1 | (overflow2 & carry); + + if (carry == 0) { // unlikely: add back again + // Proper quotient estimation guarantees recursion max-depth <= 2 + // Unbounded recursion only if there's a bug - fail fast is better than give wrong result + return addBack(z3, z2, z1, z0, q - 1); + } + return new QR256(q, new UInt256(z3, z2, z1, z0)); + } + + private QR256 mulSub(final long v3, final long v2, final long v1, final long v0, final long q) { + // Multiply-subtract: already have highest 1 limbs + // = * q + long p0 = u0 * q; + long p1 = Math.unsignedMultiplyHigh(u0, q); + long z0 = v0 - p0; + long carry = p1 + (((Long.compareUnsigned(v0, z0) < 0) ? 1 : 0)); + + p0 = u1 * q; + p1 = Math.unsignedMultiplyHigh(u1, q); + long res = v1 - p0; + long z1 = res - carry; + long borrow = (Long.compareUnsigned(res, z1) < 0) ? 1 : 0; + carry = p1 + ((Long.compareUnsigned(v1, res) < 0) ? 1 : 0); + + p0 = u2 * q; + p1 = Math.unsignedMultiplyHigh(u2, q); + long t2 = v2 - p0; + res = t2 - borrow; + long z2 = res - carry; + borrow = (Long.compareUnsigned(res, z2) < 0) ? 1 : 0; + carry = + p1 + + ((Long.compareUnsigned(v2, t2) < 0) ? 1 : 0) + + ((Long.compareUnsigned(t2, res) < 0) ? 1 : 0); + + // Propagate overflows (borrows) + long t3 = v3 - carry; + long z3 = t3 - borrow; + borrow = + ((Long.compareUnsigned(v3, t3) < 0) ? 1 : 0) | ((Long.compareUnsigned(t3, z3) < 0) ? 1 : 0); + + if (borrow != 0) return addBack(z3, z2, z1, z0, q - 1); + return new QR256(q, new UInt256(z3, z2, z1, z0)); + } + + private UInt256 mulSubOverflow(final long v3, final long v2, final long v1, final long v0) { + // Overflow case: div2by1 quotient would be <1, 0>, but adjusts to <0, -1> + // = -1 * u0 = + long res, borrow; + + long z0 = v0 + u0; + long carry = u0 - 1 + ((Long.compareUnsigned(v0, z0) <= 0) ? 1 : 0); + + res = v1 - carry; + long z1 = res + u1; + borrow = (Long.compareUnsigned(res, z1) <= 0) ? 1 : 0; + carry = u1 - 1 + ((Long.compareUnsigned(v1, res) < 0) ? 1 : 0); + + res = v2 - carry - borrow; + long z2 = res + u2; + borrow = (Long.compareUnsigned(res, z2) <= 0) ? 1 : 0; + carry = u2 - 1 + ((Long.compareUnsigned(v2, res) < 0) ? 1 : 0); + + long z3 = v3 + u3 - carry - borrow; + // q = MAX may still be 1 too high; check if result >= modulus (i.e. negative wrapped) + if (Long.compareUnsigned(z3, u3) > 0 + || (z3 == u3 + && (Long.compareUnsigned(z2, u2) > 0 + || (z2 == u2 + && (Long.compareUnsigned(z1, u1) > 0 + || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)))))) { + return addBack(z3, z2, z1, z0, 1L).r; + } + return new UInt256(z3, z2, z1, z0); + } + + private QR256 reduceStep( + final long v4, final long v3, final long v2, final long v1, final long v0, final long inv) { + if (v4 == u3) return new QR256(-1L, mulSubOverflow(v3, v2, v1, v0)); + QR64 qr = div2by1(v4, v3, u3, inv); + if (qr.q != 0) return mulSub(qr.r, v2, v1, v0, qr.q); + return new QR256(0, new UInt256(v3, v2, v1, v0)); + } + + private UInt256 modReduceNormalised(final UInt256 that, final int shift, final long inv) { + UInt320 v = that.shiftLeftWide(shift); + QR256 qr; + if (Long.compareUnsigned(v.u4, u3) >= 0) { + qr = reduceStep(0, v.u4, v.u3, v.u2, v.u1, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); + } else { + qr = reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv); + } + return qr.r.shiftRight(shift); + } + + private UInt256 modReduceNormalised(final UInt257 that, final int shift, final long inv) { + UInt320 v = that.shiftLeftWide(shift); + QR256 qr; + if (Long.compareUnsigned(v.u4, u3) >= 0) { + qr = reduceStep(0, v.u4, v.u3, v.u2, v.u1, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); + } else { + qr = reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv); + } + return qr.r.shiftRight(shift); + } + + private UInt256 modReduceNormalised(final UInt512 that, final int shift, final long inv) { + UInt576 v = that.shiftLeftWide(shift); + if ((v.u8 | v.u7 | v.u6) == 0 && Long.compareUnsigned(v.u5, u3) < 0) { + QR256 qr; + if (v.u5 != 0 || Long.compareUnsigned(v.u4, u3) >= 0) { + qr = reduceStep(v.u5, v.u4, v.u3, v.u2, v.u1, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); + } else { + qr = reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv); + } + return qr.r.shiftRight(shift); + } + return modReduceNormalisedSlowPath(v, shift, inv); + } + + private UInt256 modReduceNormalisedSlowPath(final UInt576 v, final int shift, final long inv) { + QR256 qr; + if (v.u8 != 0 || Long.compareUnsigned(v.u7, u3) >= 0) { + qr = reduceStep(v.u8, v.u7, v.u6, v.u5, v.u4, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u3, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u2, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u1, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); + } else if (v.u7 != 0 || Long.compareUnsigned(v.u6, u3) >= 0) { + qr = reduceStep(v.u7, v.u6, v.u5, v.u4, v.u3, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u2, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u1, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); + } else { + qr = reduceStep(v.u6, v.u5, v.u4, v.u3, v.u2, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u1, inv); + qr = reduceStep(qr.r.u3, qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); + } + return qr.r.shiftRight(shift); + } + + private UInt256 divReduce(final UInt256 that) { + int cmp = compareTo(that); + if (cmp == 0) return ONE; + if (cmp > 0) return ZERO; + int shift = Long.numberOfLeadingZeros(u3); + UInt256 m = shiftLeft(shift); + long inv = reciprocal(m.u3); + UInt320 v = that.shiftLeftWide(shift); + return fromLong(m.reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv).q); + } // Lookup table for $\floor{\frac{2^{19} -3 ⋅ 2^8}{d_9 - 256}}$ private static final short[] LUT = @@ -876,11 +1145,10 @@ private static long reciprocal(final long x) { t0 += x; t1 += Long.compareUnsigned(t0, x) < 0 ? 1 : 0; t1 += x; - long v4 = v3 - t1; - return v4; + return v3 - t1; } - private static DivEstimate div2by1(final long x1, final long x0, final long y, final long yInv) { + private static QR64 div2by1(final long x1, final long x0, final long y, final long yInv) { // wrapping umul z1 * yInv long q0 = x1 * yInv; long q1 = Math.unsignedMultiplyHigh(x1, yInv); @@ -901,29 +1169,7 @@ private static DivEstimate div2by1(final long x1, final long x0, final long y, f q1 += adjust; r -= y * adjust; - return new DivEstimate(q1, r); - } - - private static long mod2by1(final long x1, final long x0, final long y, final long yInv) { - // wrapping umul z1 * yInv - long q0 = x1 * yInv; - long q1 = Math.unsignedMultiplyHigh(x1, yInv); - - // wrapping uadd + + <1, 0> - long sum = q0 + x0; - long carry = ((q0 & x0) | ((q0 | x0) & ~sum)) >>> 63; - q0 = sum; - q1 += x1 + carry + 1; - - long r = x0 - q1 * y; - - long adjust = Long.compareUnsigned(q0, r) < 0 ? 1 : 0; - r += y * adjust; - - adjust = Long.compareUnsigned(y, r) <= 0 ? 1 : 0; - r -= y * adjust; - - return r; + return new QR64(q1, r); } // -------------------------------------------------------------------------- @@ -931,116 +1177,36 @@ private static long mod2by1(final long x1, final long x0, final long y, final lo // region Records // -------------------------------------------------------------------------- - record UInt257(boolean carry, UInt256 u) { - boolean isUInt64() { - return !carry && u.isUInt64(); - } + private record QR64(long q, long r) {} - boolean isUInt256() { - return !carry; - } + private record QR128(long q, UInt128 r) {} - UInt256 UInt256Value() { - return u; - } + private record QR192(long q, UInt192 r) {} - UInt320 shiftLeftWide(final int shift) { - long u4 = (carry ? 1L : 0L); - if (shift == 0) return new UInt320(u4, u.u3, u.u2, u.u1, u.u0); - int invShift = (N_BITS_PER_LIMB - shift); - long z0 = (u.u0 << shift); - long z1 = (u.u1 << shift) | u.u0 >>> invShift; - long z2 = (u.u2 << shift) | u.u1 >>> invShift; - long z3 = (u.u3 << shift) | u.u2 >>> invShift; - long z4 = (u4 << shift) | u.u3 >>> invShift; - return new UInt320(z4, z3, z2, z1, z0); - } - } - - record UInt128(long u1, long u0) {} - - record UInt192(long u2, long u1, long u0) {} - - record UInt320(long u4, long u3, long u2, long u1, long u0) { - static final UInt320 ZERO = new UInt320(0, 0, 0, 0, 0); - - UInt320 shiftDigitsRight() { - return new UInt320(0, u4, u3, u2, u1); - } - } - - record UInt448(long u6, long u5, long u4, long u3, long u2, long u1, long u0) { - UInt256 UInt256Value() { - return new UInt256(u3, u2, u1, u0); - } - - UInt512 shiftLeftWide(final int shift) { - if (shift == 0) return new UInt512(0, u6, u5, u4, u3, u2, u1, u0); - int invShift = (N_BITS_PER_LIMB - shift); - long z0 = (u0 << shift); - long z1 = (u1 << shift) | u0 >>> invShift; - long z2 = (u2 << shift) | u1 >>> invShift; - long z3 = (u3 << shift) | u2 >>> invShift; - long z4 = (u4 << shift) | u3 >>> invShift; - long z5 = (u5 << shift) | u4 >>> invShift; - long z6 = (u6 << shift) | u5 >>> invShift; - long z7 = u6 >>> invShift; - return new UInt512(z7, z6, z5, z4, z3, z2, z1, z0); - } - } - - record UInt512(long u7, long u6, long u5, long u4, long u3, long u2, long u1, long u0) { - UInt256 UInt256Value() { - return new UInt256(u3, u2, u1, u0); - } - - UInt576 shiftLeftWide(final int shift) { - if (shift == 0) return new UInt576(0, u7, u6, u5, u4, u3, u2, u1, u0); - int invShift = (N_BITS_PER_LIMB - shift); - long z0 = (u0 << shift); - long z1 = (u1 << shift) | u0 >>> invShift; - long z2 = (u2 << shift) | u1 >>> invShift; - long z3 = (u3 << shift) | u2 >>> invShift; - long z4 = (u4 << shift) | u3 >>> invShift; - long z5 = (u5 << shift) | u4 >>> invShift; - long z6 = (u6 << shift) | u5 >>> invShift; - long z7 = (u7 << shift) | u6 >>> invShift; - long z8 = u7 >>> invShift; - return new UInt576(z8, z7, z6, z5, z4, z3, z2, z1, z0); - } - } - - record UInt576(long u8, long u7, long u6, long u5, long u4, long u3, long u2, long u1, long u0) {} + private record QR256(long q, UInt256 r) {} - private record DivEstimate(long q, long r) {} - - // -------------------------------------------------------------------------- - // endregion - - // region 64bits Modulus - // -------------------------------------------------------------------------- - record Modulus64(long u0) { - Modulus64 shiftLeft(final int shift) { - return (shift == 0) ? this : new Modulus64(u0 << shift); + record UInt64(long u0) { + UInt64 shiftLeft(final int shift) { + return (shift == 0) ? this : new UInt64(u0 << shift); } - UInt256 reduce(final UInt256 that) { + UInt256 modReduce(final UInt256 that) { if (that.isUInt64()) { return UInt256.fromLong(Long.remainderUnsigned(that.u0, u0)); } int shift = Long.numberOfLeadingZeros(u0); - Modulus64 m = shiftLeft(shift); + UInt64 m = shiftLeft(shift); long inv = reciprocal(m.u0); - return m.reduceNormalised(that, shift, inv); + return m.modReduceNormalised(that, shift, inv); } UInt256 sum(final UInt256 a, final UInt256 b) { UInt257 sum = a.adc(b); if (sum.isUInt64()) return UInt256.fromLong(Long.remainderUnsigned(sum.u().u0, u0)); int shift = Long.numberOfLeadingZeros(u0); - Modulus64 m = shiftLeft(shift); + UInt64 m = shiftLeft(shift); long inv = reciprocal(m.u0); - return m.reduceNormalised(sum, shift, inv); + return m.modReduceNormalised(sum, shift, inv); } UInt256 mul(final UInt256 a, final UInt256 b) { @@ -1048,87 +1214,120 @@ UInt256 mul(final UInt256 a, final UInt256 b) { if (a.isUInt64() && b.isUInt64()) { UInt256 prod = a.mul64(b); if (prod.isUInt64()) return UInt256.fromLong(Long.remainderUnsigned(prod.u0, u0)); - return reduce(prod); + return modReduce(prod); } // reduce-multiply-reduce int shift = Long.numberOfLeadingZeros(u0); - Modulus64 m = shiftLeft(shift); + UInt64 m = shiftLeft(shift); long inv = reciprocal(m.u0); - UInt256 x = (a.isUInt64()) ? a : m.reduceNormalised(a, shift, inv); - UInt256 y = (b.isUInt64()) ? b : m.reduceNormalised(b, shift, inv); + UInt256 x = (a.isUInt64()) ? a : m.modReduceNormalised(a, shift, inv); + UInt256 y = (b.isUInt64()) ? b : m.modReduceNormalised(b, shift, inv); UInt256 prod = x.mul64(y); return prod.isUInt64() ? UInt256.fromLong(Long.remainderUnsigned(prod.u0, u0)) - : m.reduceNormalised(prod, shift, inv); + : m.modReduceNormalised(prod, shift, inv); } - private long reduceStep(final long v1, final long v0, final long inv) { - return mod2by1(v1, v0, u0, inv); + private QR64 reduceStep(final long v1, final long v0, final long inv) { + return div2by1(v1, v0, u0, inv); } - private UInt256 reduceNormalised(final UInt256 that, final int shift, final long inv) { + private UInt256 modReduceNormalised(final UInt256 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); if ((v.u4 | v.u3) == 0 && Long.compareUnsigned(v.u2, u0) < 0) { - long r; + QR64 qr; if (v.u2 != 0 || Long.compareUnsigned(v.u1, u0) >= 0) { - r = reduceStep(v.u2, v.u1, inv); - r = reduceStep(r, v.u0, inv); + qr = reduceStep(v.u2, v.u1, inv); + qr = reduceStep(qr.r, v.u0, inv); } else { - r = reduceStep(v.u1, v.u0, inv); + qr = reduceStep(v.u1, v.u0, inv); } - return UInt256.fromLong(r >>> shift); + return UInt256.fromLong(qr.r >>> shift); } return reduceNormalisedSlowPath(v, shift, inv); } - private UInt256 reduceNormalised(final UInt257 that, final int shift, final long inv) { + private UInt256 modReduceNormalised(final UInt257 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); if ((v.u4 | v.u3) == 0 && Long.compareUnsigned(v.u2, u0) < 0) { - long r; + QR64 qr; if (v.u2 != 0 || Long.compareUnsigned(v.u1, u0) >= 0) { - r = reduceStep(v.u2, v.u1, inv); - r = reduceStep(r, v.u0, inv); + qr = reduceStep(v.u2, v.u1, inv); + qr = reduceStep(qr.r, v.u0, inv); } else { - r = reduceStep(v.u1, v.u0, inv); + qr = reduceStep(v.u1, v.u0, inv); } - return UInt256.fromLong(r >>> shift); + return UInt256.fromLong(qr.r >>> shift); } return reduceNormalisedSlowPath(v, shift, inv); } private UInt256 reduceNormalisedSlowPath(final UInt320 v, final int shift, final long inv) { - long r; + QR64 qr; if (Long.compareUnsigned(v.u4, u0) >= 0) { - r = reduceStep(0, v.u4, inv); - r = reduceStep(r, v.u3, inv); - r = reduceStep(r, v.u2, inv); - r = reduceStep(r, v.u1, inv); - r = reduceStep(r, v.u0, inv); - + qr = reduceStep(0, v.u4, inv); + qr = reduceStep(qr.r, v.u3, inv); + qr = reduceStep(qr.r, v.u2, inv); + qr = reduceStep(qr.r, v.u1, inv); + qr = reduceStep(qr.r, v.u0, inv); } else if (v.u4 != 0 || Long.compareUnsigned(v.u3, u0) >= 0) { - r = reduceStep(v.u4, v.u3, inv); - r = reduceStep(r, v.u2, inv); - r = reduceStep(r, v.u1, inv); - r = reduceStep(r, v.u0, inv); + qr = reduceStep(v.u4, v.u3, inv); + qr = reduceStep(qr.r, v.u2, inv); + qr = reduceStep(qr.r, v.u1, inv); + qr = reduceStep(qr.r, v.u0, inv); } else { - r = reduceStep(v.u3, v.u2, inv); - r = reduceStep(r, v.u1, inv); - r = reduceStep(r, v.u0, inv); + qr = reduceStep(v.u3, v.u2, inv); + qr = reduceStep(qr.r, v.u1, inv); + qr = reduceStep(qr.r, v.u0, inv); } - return UInt256.fromLong(r >>> shift); + return UInt256.fromLong(qr.r >>> shift); } - } - // -------------------------------------------------------------------------- - // endregion 64bits Modulus + UInt256 divReduce(final UInt256 that) { + if (that.isUInt64()) { + return UInt256.fromLong(Long.divideUnsigned(that.u0, u0)); + } + int shift = Long.numberOfLeadingZeros(u0); + UInt64 m = shiftLeft(shift); + long inv = reciprocal(m.u0); + return m.divReduceNormalised(that, shift, inv); + } - // region 128bits Modulus - // -------------------------------------------------------------------------- - record Modulus128(long u1, long u0) { - Modulus128 shiftLeft(final int shift) { + private UInt256 divReduceNormalised(final UInt256 that, final int shift, final long inv) { + UInt320 v = that.shiftLeftWide(shift); + if ((v.u4 | v.u3) == 0 && Long.compareUnsigned(v.u2, u0) < 0) { + if (v.u2 != 0 || Long.compareUnsigned(v.u1, u0) >= 0) { + QR64 qr1 = reduceStep(v.u2, v.u1, inv); + QR64 qr0 = reduceStep(qr1.r, v.u0, inv); + return new UInt256(0, 0, qr1.q, qr0.q); + } else { + QR64 qr0 = reduceStep(v.u1, v.u0, inv); + return new UInt256(0, 0, 0, qr0.q); + } + } + return divReduceNormalisedSlowPath(v, inv); + } + + private UInt256 divReduceNormalisedSlowPath(final UInt320 v, final long inv) { + if (v.u4 == 0 && Long.compareUnsigned(v.u3, u0) < 0) { + QR64 qr2 = reduceStep(v.u3, v.u2, inv); + QR64 qr1 = reduceStep(qr2.r, v.u1, inv); + QR64 qr0 = reduceStep(qr1.r, v.u0, inv); + return new UInt256(0, qr2.q, qr1.q, qr0.q); + } + QR64 qr3 = reduceStep(v.u4, v.u3, inv); + QR64 qr2 = reduceStep(qr3.r, v.u2, inv); + QR64 qr1 = reduceStep(qr2.r, v.u1, inv); + QR64 qr0 = reduceStep(qr1.r, v.u0, inv); + return new UInt256(qr3.q, qr2.q, qr1.q, qr0.q); + } + } + + record UInt128(long u1, long u0) { + UInt128 shiftLeft(final int shift) { if (shift == 0) return this; int invShift = N_BITS_PER_LIMB - shift; - return new Modulus128((u1 << shift) | (u0 >>> invShift), u0 << shift); + return new UInt128((u1 << shift) | (u0 >>> invShift), u0 << shift); } int compareTo(final UInt256 v) { @@ -1137,14 +1336,14 @@ int compareTo(final UInt256 v) { return Long.compareUnsigned(u0, v.u0); } - UInt256 reduce(final UInt256 that) { + UInt256 modReduce(final UInt256 that) { int cmp = compareTo(that); if (cmp == 0) return ZERO; if (cmp > 0) return that; int shift = Long.numberOfLeadingZeros(u1); - Modulus128 m = shiftLeft(shift); + UInt128 m = shiftLeft(shift); long inv = reciprocal(m.u1); - return m.reduceNormalised(that, shift, inv); + return m.modReduceNormalised(that, shift, inv); } UInt256 sum(final UInt256 a, final UInt256 b) { @@ -1153,9 +1352,9 @@ UInt256 sum(final UInt256 a, final UInt256 b) { if (cmp == 0) return ZERO; if (cmp > 0) return sum.UInt256Value(); int shift = Long.numberOfLeadingZeros(u1); - Modulus128 m = shiftLeft(shift); + UInt128 m = shiftLeft(shift); long inv = reciprocal(m.u1); - return m.reduceNormalised(sum, shift, inv); + return m.modReduceNormalised(sum, shift, inv); } UInt256 mul(final UInt256 a, final UInt256 b) { @@ -1165,22 +1364,22 @@ UInt256 mul(final UInt256 a, final UInt256 b) { int cmp = compareTo(prod); if (cmp == 0) return ZERO; if (cmp > 0) return prod; - return reduce(prod); + return modReduce(prod); } // reduce-multiply-reduce int shift = Long.numberOfLeadingZeros(u1); - Modulus128 m = shiftLeft(shift); + UInt128 m = shiftLeft(shift); long inv = reciprocal(m.u1); - UInt256 x = (a.isUInt128()) ? a : m.reduceNormalised(a, shift, inv); - UInt256 y = (b.isUInt128()) ? b : m.reduceNormalised(b, shift, inv); + UInt256 x = (a.isUInt128()) ? a : m.modReduceNormalised(a, shift, inv); + UInt256 y = (b.isUInt128()) ? b : m.modReduceNormalised(b, shift, inv); UInt256 prod = x.mul128(y); int cmp = compareTo(prod); if (cmp == 0) return ZERO; if (cmp > 0) return prod; - return m.reduceNormalised(prod, shift, inv); + return m.modReduceNormalised(prod, shift, inv); } - private UInt128 addBack(final long v1, final long v0) { + private QR128 addBack(final long v1, final long v0, final long q) { // Quotient estimate could be 0, +1, +2 of real quotient. // Add back step in case estimate is off. long z0 = v0 + u0; @@ -1194,12 +1393,12 @@ private UInt128 addBack(final long v1, final long v0) { if (carry == 0) { // unlikely: add back again // Proper quotient estimation guarantees recursion max-depth <= 2 // Unbounded recursion only if there's a bug - fail fast is better than give wrong result - return addBack(z1, z0); + return addBack(z1, z0, q - 1); } - return new UInt128(z1, z0); + return new QR128(q, new UInt128(z1, z0)); } - private UInt128 mulSub(final long x1, final long x0, final long q) { + private QR128 mulSub(final long x1, final long x0, final long q) { // Multiply-subtract: highest limb is already substracted // = * q long p0 = u0 * q; @@ -1211,8 +1410,8 @@ private UInt128 mulSub(final long x1, final long x0, final long q) { long z1 = x1 - carry; long borrow = (Long.compareUnsigned(x1, z1) < 0) ? 1 : 0; - if (borrow != 0) return addBack(z1, z0); // less likely - return new UInt128(z1, z0); + if (borrow != 0) return addBack(z1, z0, q - 1); // less likely + return new QR128(q, new UInt128(z1, z0)); } private UInt128 mulSubOverflow(final long v1, final long v0) { @@ -1224,77 +1423,103 @@ private UInt128 mulSubOverflow(final long v1, final long v0) { long z1 = v1 + u1 - carry; // q = MAX may still be 1 too high; check if result >= modulus (i.e. negative wrapped) if (Long.compareUnsigned(z1, u1) > 0 || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)) { - return addBack(z1, z0); + return addBack(z1, z0, 1L).r; } return new UInt128(z1, z0); } - private UInt128 reduceStep(final long v2, final long v1, final long v0, final long inv) { - if (v2 == u1) return mulSubOverflow(v1, v0); - DivEstimate qr = div2by1(v2, v1, u1, inv); + private QR128 reduceStep(final long v2, final long v1, final long v0, final long inv) { + if (v2 == u1) return new QR128(-1L, mulSubOverflow(v1, v0)); + QR64 qr = div2by1(v2, v1, u1, inv); if (qr.q != 0) return mulSub(qr.r, v0, qr.q); - return new UInt128(qr.r, v0); + return new QR128(0, new UInt128(qr.r, v0)); } - private UInt256 reduceNormalised(final UInt256 that, final int shift, final long inv) { + private UInt256 modReduceNormalised(final UInt256 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); if (v.u4 == 0 && Long.compareUnsigned(v.u3, u1) < 0) { - UInt128 r; + QR128 qr; if (v.u3 != 0 || Long.compareUnsigned(v.u2, u1) >= 0) { - r = reduceStep(v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u1, r.u0, v.u0, inv); + qr = reduceStep(v.u3, v.u2, v.u1, inv); + qr = reduceStep(qr.r.u1, qr.r.u0, v.u0, inv); } else { - r = reduceStep(v.u2, v.u1, v.u0, inv); + qr = reduceStep(v.u2, v.u1, v.u0, inv); } - return new UInt256(0, 0, r.u1, r.u0).shiftRight(shift); + return new UInt256(0, 0, qr.r.u1, qr.r.u0).shiftRight(shift); } - return reduceNormalisedSlowPath(v, shift, inv); + return modReduceNormalisedSlowPath(v, shift, inv); } - private UInt256 reduceNormalised(final UInt257 that, final int shift, final long inv) { + private UInt256 modReduceNormalised(final UInt257 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); if (v.u4 == 0 && Long.compareUnsigned(v.u3, u1) < 0) { - UInt128 r; + QR128 qr; if (v.u3 != 0 || Long.compareUnsigned(v.u2, u1) >= 0) { - r = reduceStep(v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u1, r.u0, v.u0, inv); + qr = reduceStep(v.u3, v.u2, v.u1, inv); + qr = reduceStep(qr.r.u1, qr.r.u0, v.u0, inv); } else { - r = reduceStep(v.u2, v.u1, v.u0, inv); + qr = reduceStep(v.u2, v.u1, v.u0, inv); } - return new UInt256(0, 0, r.u1, r.u0).shiftRight(shift); + return new UInt256(0, 0, qr.r.u1, qr.r.u0).shiftRight(shift); } - return reduceNormalisedSlowPath(v, shift, inv); + return modReduceNormalisedSlowPath(v, shift, inv); } - private UInt256 reduceNormalisedSlowPath(final UInt320 v, final int shift, final long inv) { - UInt128 r; + private UInt256 modReduceNormalisedSlowPath(final UInt320 v, final int shift, final long inv) { + QR128 qr; if (Long.compareUnsigned(v.u4, u1) >= 0) { - r = reduceStep(0, v.u4, v.u3, inv); - r = reduceStep(r.u1, r.u0, v.u2, inv); - r = reduceStep(r.u1, r.u0, v.u1, inv); - r = reduceStep(r.u1, r.u0, v.u0, inv); + qr = reduceStep(0, v.u4, v.u3, inv); + qr = reduceStep(qr.r.u1, qr.r.u0, v.u2, inv); + qr = reduceStep(qr.r.u1, qr.r.u0, v.u1, inv); + qr = reduceStep(qr.r.u1, qr.r.u0, v.u0, inv); } else { - r = reduceStep(v.u4, v.u3, v.u2, inv); - r = reduceStep(r.u1, r.u0, v.u1, inv); - r = reduceStep(r.u1, r.u0, v.u0, inv); + qr = reduceStep(v.u4, v.u3, v.u2, inv); + qr = reduceStep(qr.r.u1, qr.r.u0, v.u1, inv); + qr = reduceStep(qr.r.u1, qr.r.u0, v.u0, inv); } - return new UInt256(0, 0, r.u1, r.u0).shiftRight(shift); + return new UInt256(0, 0, qr.r.u1, qr.r.u0).shiftRight(shift); } - } - // -------------------------------------------------------------------------- - // endregion 128bits Modulus + UInt256 divReduce(final UInt256 that) { + int cmp = compareTo(that); + if (cmp == 0) return ONE; + if (cmp > 0) return ZERO; + int shift = Long.numberOfLeadingZeros(u1); + UInt128 m = shiftLeft(shift); + long inv = reciprocal(m.u1); + return m.divReduceNormalised(that, shift, inv); + } - // region 192bits Modulus - // -------------------------------------------------------------------------- - record Modulus192(long u2, long u1, long u0) { - Modulus192 shiftLeft(final int shift) { + private UInt256 divReduceNormalised(final UInt256 that, final int shift, final long inv) { + UInt320 v = that.shiftLeftWide(shift); + if (v.u4 == 0 && Long.compareUnsigned(v.u3, u1) < 0) { + if (v.u3 != 0 || Long.compareUnsigned(v.u2, u1) >= 0) { + QR128 qr1 = reduceStep(v.u3, v.u2, v.u1, inv); + QR128 qr0 = reduceStep(qr1.r.u1, qr1.r.u0, v.u0, inv); + return new UInt256(0, 0, qr1.q, qr0.q); + } + QR128 qr0 = reduceStep(v.u2, v.u1, v.u0, inv); + return new UInt256(0, 0, 0, qr0.q); + } + return divReduceNormalisedSlowPath(v, inv); + } + + private UInt256 divReduceNormalisedSlowPath(final UInt320 v, final long inv) { + QR128 qr2 = reduceStep(v.u4, v.u3, v.u2, inv); + QR128 qr1 = reduceStep(qr2.r.u1, qr2.r.u0, v.u1, inv); + QR128 qr0 = reduceStep(qr1.r.u1, qr1.r.u0, v.u0, inv); + return new UInt256(0, qr2.q, qr1.q, qr0.q); + } + } + + record UInt192(long u2, long u1, long u0) { + UInt192 shiftLeft(final int shift) { if (shift == 0) return this; int invShift = N_BITS_PER_LIMB - shift; long z0 = u0 << shift; long z1 = (u1 << shift) | (u0 >>> invShift); long z2 = (u2 << shift) | (u1 >>> invShift); - return new Modulus192(z2, z1, z0); + return new UInt192(z2, z1, z0); } int compareTo(final UInt256 v) { @@ -1311,24 +1536,24 @@ int compareTo(final UInt448 v) { return Long.compareUnsigned(u0, v.u0); } - UInt256 reduce(final UInt256 that) { + UInt256 modReduce(final UInt256 that) { int cmp = compareTo(that); if (cmp == 0) return ZERO; if (cmp > 0) return that; int shift = Long.numberOfLeadingZeros(u2); - Modulus192 m = shiftLeft(shift); + UInt192 m = shiftLeft(shift); long inv = reciprocal(m.u2); - return m.reduceNormalised(that, shift, inv); + return m.modReduceNormalised(that, shift, inv); } - UInt256 reduce(final UInt448 that) { + UInt256 modReduce(final UInt448 that) { int cmp = compareTo(that); if (cmp == 0) return ZERO; if (cmp > 0) return that.UInt256Value(); int shift = Long.numberOfLeadingZeros(u2); - Modulus192 m = shiftLeft(shift); + UInt192 m = shiftLeft(shift); long inv = reciprocal(m.u2); - return m.reduceNormalised(that, shift, inv); + return m.modReduceNormalised(that, shift, inv); } UInt256 sum(final UInt256 a, final UInt256 b) { @@ -1339,9 +1564,9 @@ UInt256 sum(final UInt256 a, final UInt256 b) { if (cmp > 0) return sum.UInt256Value(); } int shift = Long.numberOfLeadingZeros(u2); - Modulus192 m = shiftLeft(shift); + UInt192 m = shiftLeft(shift); long inv = reciprocal(m.u2); - return m.reduceNormalised(sum, shift, inv); + return m.modReduceNormalised(sum, shift, inv); } UInt256 mul(final UInt256 a, final UInt256 b) { @@ -1351,22 +1576,22 @@ UInt256 mul(final UInt256 a, final UInt256 b) { int cmp = compareTo(prod); if (cmp == 0) return ZERO; if (cmp > 0) return prod.UInt256Value(); - return reduce(prod); + return modReduce(prod); } // reduce-multiply-reduce int shift = Long.numberOfLeadingZeros(u2); - Modulus192 m = shiftLeft(shift); + UInt192 m = shiftLeft(shift); long inv = reciprocal(m.u2); - UInt256 x = (a.isUInt192()) ? a : m.reduceNormalised(a, shift, inv); - UInt256 y = (b.isUInt192()) ? b : m.reduceNormalised(b, shift, inv); + UInt256 x = (a.isUInt192()) ? a : m.modReduceNormalised(a, shift, inv); + UInt256 y = (b.isUInt192()) ? b : m.modReduceNormalised(b, shift, inv); UInt448 prod = x.mul192(y); int cmp = compareTo(prod); if (cmp == 0) return ZERO; if (cmp > 0) return prod.UInt256Value(); - return m.reduceNormalised(prod, shift, inv); + return m.modReduceNormalised(prod, shift, inv); } - private UInt192 addBack(final long v2, final long v1, final long v0) { + private QR192 addBack(final long v2, final long v1, final long v0, final long q) { // Add back long z0 = v0 + u0; long carry = ((v0 & u0) | ((v0 | u0) & ~z0)) >>> 63; @@ -1384,12 +1609,12 @@ private UInt192 addBack(final long v2, final long v1, final long v0) { if (carry == 0) { // unlikely: add back again // Proper quotient estimation guarantees recursion max-depth <= 2 // Unbounded recursion only if there's a bug - fail fast is better than give wrong result - return addBack(z2, z1, z0); + return addBack(z2, z1, z0, q - 1); } - return new UInt192(z2, z1, z0); + return new QR192(q, new UInt192(z2, z1, z0)); } - private UInt192 mulSub(final long v2, final long v1, final long v0, final long q) { + private QR192 mulSub(final long v2, final long v1, final long v0, final long q) { // Multiply-subtract: already have highest 2 limbs // = * q long p0 = u0 * q; @@ -1411,8 +1636,8 @@ private UInt192 mulSub(final long v2, final long v1, final long v0, final long q ((Long.compareUnsigned(v2, t2) < 0) ? 1 : 0) | ((Long.compareUnsigned(t2, z2) < 0) ? 1 : 0); - if (borrow != 0) return addBack(z2, z1, z0); // unlikely - return new UInt192(z2, z1, z0); + if (borrow != 0) return addBack(z2, z1, z0, q - 1); // unlikely + return new QR192(q, new UInt192(z2, z1, z0)); } private UInt192 mulSubOverflow(final long v2, final long v1, final long v0) { @@ -1432,331 +1657,195 @@ private UInt192 mulSubOverflow(final long v2, final long v1, final long v0) { || (z2 == u2 && (Long.compareUnsigned(z1, u1) > 0 || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)))) { - return addBack(z2, z1, z0); + return addBack(z2, z1, z0, 1L).r; } return new UInt192(z2, z1, z0); } - private UInt192 reduceStep( + private QR192 reduceStep( final long v3, final long v2, final long v1, final long v0, final long inv) { - if (v3 == u2) return mulSubOverflow(v2, v1, v0); - DivEstimate qr = div2by1(v3, v2, u2, inv); + if (v3 == u2) return new QR192(-1L, mulSubOverflow(v2, v1, v0)); + QR64 qr = div2by1(v3, v2, u2, inv); if (qr.q != 0) return mulSub(qr.r, v1, v0, qr.q); - return new UInt192(v2, v1, v0); + return new QR192(0, new UInt192(v2, v1, v0)); } - private UInt256 reduceNormalised(final UInt256 that, final int shift, final long inv) { + private UInt256 modReduceNormalised(final UInt256 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); if (Long.compareUnsigned(v.u4, u2) < 0) { - UInt192 r; + QR192 qr; if (v.u4 != 0 || Long.compareUnsigned(v.u3, u2) >= 0) { - r = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); + qr = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); } else { - r = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); + qr = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); } - return new UInt256(0, r.u2, r.u1, r.u0).shiftRight(shift); + return new UInt256(0, qr.r.u2, qr.r.u1, qr.r.u0).shiftRight(shift); } return reduceNormalisedSlowPath(v, shift, inv); } - private UInt256 reduceNormalised(final UInt257 that, final int shift, final long inv) { + private UInt256 modReduceNormalised(final UInt257 that, final int shift, final long inv) { UInt320 v = that.shiftLeftWide(shift); if (Long.compareUnsigned(v.u4, u2) < 0) { - UInt192 r; + QR192 qr; if (v.u4 != 0 || Long.compareUnsigned(v.u3, u2) >= 0) { - r = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); + qr = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); } else { - r = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); + qr = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); } - return new UInt256(0, r.u2, r.u1, r.u0).shiftRight(shift); + return new UInt256(0, qr.r.u2, qr.r.u1, qr.r.u0).shiftRight(shift); } return reduceNormalisedSlowPath(v, shift, inv); } private UInt256 reduceNormalisedSlowPath(final UInt320 v, final int shift, final long inv) { - UInt192 r = reduceStep(0, v.u4, v.u3, v.u2, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u1, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); - return new UInt256(0, r.u2, r.u1, r.u0).shiftRight(shift); + QR192 qr = reduceStep(0, v.u4, v.u3, v.u2, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u1, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); + return new UInt256(0, qr.r.u2, qr.r.u1, qr.r.u0).shiftRight(shift); } - private UInt256 reduceNormalised(final UInt448 that, final int shift, final long inv) { + private UInt256 modReduceNormalised(final UInt448 that, final int shift, final long inv) { UInt512 v = that.shiftLeftWide(shift); if ((v.u7 | v.u6 | v.u5) == 0 && Long.compareUnsigned(v.u4, u2) < 0) { - UInt192 r; + QR192 qr; if (v.u4 != 0 || Long.compareUnsigned(v.u3, u2) >= 0) { - r = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); + qr = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); } else { - r = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); + qr = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); } - return new UInt256(0, r.u2, r.u1, r.u0).shiftRight(shift); + return new UInt256(0, qr.r.u2, qr.r.u1, qr.r.u0).shiftRight(shift); } - return reduceNormalisedSlowPath(v, shift, inv); + return modReduceNormalisedSlowPath(v, shift, inv); } - private UInt256 reduceNormalisedSlowPath(final UInt512 v, final int shift, final long inv) { - UInt192 r; + private UInt256 modReduceNormalisedSlowPath(final UInt512 v, final int shift, final long inv) { + QR192 qr; if (v.u7 != 0 || Long.compareUnsigned(v.u6, u2) >= 0) { - r = reduceStep(v.u7, v.u6, v.u5, v.u4, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u3, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u2, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u1, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); + qr = reduceStep(v.u7, v.u6, v.u5, v.u4, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u3, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u2, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u1, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); } else if (v.u6 != 0 || Long.compareUnsigned(v.u5, u2) >= 0) { - r = reduceStep(v.u6, v.u5, v.u4, v.u3, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u2, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u1, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); + qr = reduceStep(v.u6, v.u5, v.u4, v.u3, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u2, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u1, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); } else { - r = reduceStep(v.u5, v.u4, v.u3, v.u2, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u1, inv); - r = reduceStep(r.u2, r.u1, r.u0, v.u0, inv); + qr = reduceStep(v.u5, v.u4, v.u3, v.u2, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u1, inv); + qr = reduceStep(qr.r.u2, qr.r.u1, qr.r.u0, v.u0, inv); } - return new UInt256(0, r.u2, r.u1, r.u0).shiftRight(shift); + return new UInt256(0, qr.r.u2, qr.r.u1, qr.r.u0).shiftRight(shift); } - } - // -------------------------------------------------------------------------- - // endregion 192bits Modulus - - // region 256bits Modulus - // -------------------------------------------------------------------------- - record Modulus256(long u3, long u2, long u1, long u0) { - Modulus256 shiftLeft(final int shift) { - if (shift == 0) return this; - int invShift = N_BITS_PER_LIMB - shift; - long z0 = u0 << shift; - long z1 = (u1 << shift) | (u0 >>> invShift); - long z2 = (u2 << shift) | (u1 >>> invShift); - long z3 = (u3 << shift) | (u2 >>> invShift); - return new Modulus256(z3, z2, z1, z0); - } - - int compareTo(final UInt256 v) { - if (v.u3 != u3) return Long.compareUnsigned(u3, v.u3); - if (v.u2 != u2) return Long.compareUnsigned(u2, v.u2); - if (v.u1 != u1) return Long.compareUnsigned(u1, v.u1); - return Long.compareUnsigned(u0, v.u0); + UInt256 divReduce(final UInt256 that) { + int cmp = compareTo(that); + if (cmp == 0) return ONE; + if (cmp > 0) return ZERO; + int shift = Long.numberOfLeadingZeros(u2); + UInt192 m = shiftLeft(shift); + long inv = reciprocal(m.u2); + return m.divReduceNormalised(that, shift, inv); } - int compareTo(final UInt512 v) { - if ((v.u7 | v.u6 | v.u5 | v.u4) != 0) return -1; - if (v.u3 != u3) return Long.compareUnsigned(u3, v.u3); - if (v.u2 != u2) return Long.compareUnsigned(u2, v.u2); - if (v.u1 != u1) return Long.compareUnsigned(u1, v.u1); - return Long.compareUnsigned(u0, v.u0); + private UInt256 divReduceNormalised(final UInt256 that, final int shift, final long inv) { + UInt320 v = that.shiftLeftWide(shift); + if (v.u4 == 0 && Long.compareUnsigned(v.u3, u2) < 0) { + QR192 qr0 = reduceStep(v.u3, v.u2, v.u1, v.u0, inv); + return new UInt256(0, 0, 0, qr0.q); + } + return divReduceNormalisedSlowPath(v, inv); } - UInt256 reduce(final UInt256 that) { - int cmp = compareTo(that); - if (cmp == 0) return ZERO; - if (cmp > 0) return that; - int shift = Long.numberOfLeadingZeros(u3); - Modulus256 m = shiftLeft(shift); - long inv = reciprocal(m.u3); - return m.reduceNormalised(that, shift, inv); + private UInt256 divReduceNormalisedSlowPath(final UInt320 v, final long inv) { + QR192 qr1 = reduceStep(v.u4, v.u3, v.u2, v.u1, inv); + QR192 qr0 = reduceStep(qr1.r.u2, qr1.r.u1, qr1.r.u0, v.u0, inv); + return new UInt256(0, 0, qr1.q, qr0.q); } + } - UInt256 reduce(final UInt512 that) { - int cmp = compareTo(that); - if (cmp == 0) return ZERO; - if (cmp > 0) return that.UInt256Value(); - int shift = Long.numberOfLeadingZeros(u3); - Modulus256 m = shiftLeft(shift); - long inv = reciprocal(m.u3); - return m.reduceNormalised(that, shift, inv); + record UInt257(boolean carry, UInt256 u) { + boolean isUInt64() { + return !carry && u.isUInt64(); } - UInt256 sum(final UInt256 a, final UInt256 b) { - UInt257 sum = a.adc(b); - if (!sum.carry()) { - int cmp = compareTo(sum.UInt256Value()); - if (cmp == 0) return ZERO; - if (cmp > 0) return sum.UInt256Value(); - } - int shift = Long.numberOfLeadingZeros(u3); - Modulus256 m = shiftLeft(shift); - long inv = reciprocal(m.u3); - return m.reduceNormalised(sum, shift, inv); + boolean isUInt256() { + return !carry; } - UInt256 mul(final UInt256 a, final UInt256 b) { - // multiply-reduce - UInt512 prod = a.mul256(b); - int cmp = compareTo(prod); - if (cmp == 0) return ZERO; - if (cmp > 0) return prod.UInt256Value(); - return reduce(prod); + UInt256 UInt256Value() { + return u; } - private UInt256 addBack(final long v3, final long v2, final long v1, final long v0) { - // Add back - long z0 = v0 + u0; - long carry = ((v0 & u0) | ((v0 | u0) & ~z0)) >>> 63; - - long z1 = v1 + u1 + carry; - long overflow1 = (Long.compareUnsigned(z1, v1) < 0) ? 1 : 0; - long overflow2 = (Long.compareUnsigned(z1, v1) == 0) ? 1 : 0; - carry = overflow1 | (overflow2 & carry); - - long z2 = v2 + u2 + carry; - overflow1 = (Long.compareUnsigned(z2, v2) < 0) ? 1 : 0; - overflow2 = (Long.compareUnsigned(z2, v2) == 0) ? 1 : 0; - carry = overflow1 | (overflow2 & carry); - - long z3 = v3 + u3 + carry; - overflow1 = (Long.compareUnsigned(z3, v3) < 0) ? 1 : 0; - overflow2 = (Long.compareUnsigned(z3, v3) == 0) ? 1 : 0; - carry = overflow1 | (overflow2 & carry); - - if (carry == 0) { // unlikely: add back again - // Proper quotient estimation guarantees recursion max-depth <= 2 - // Unbounded recursion only if there's a bug - fail fast is better than give wrong result - return addBack(z3, z2, z1, z0); - } - return new UInt256(z3, z2, z1, z0); + UInt320 shiftLeftWide(final int shift) { + long u4 = (carry ? 1L : 0L); + if (shift == 0) return new UInt320(u4, u.u3, u.u2, u.u1, u.u0); + int invShift = (N_BITS_PER_LIMB - shift); + long z0 = (u.u0 << shift); + long z1 = (u.u1 << shift) | u.u0 >>> invShift; + long z2 = (u.u2 << shift) | u.u1 >>> invShift; + long z3 = (u.u3 << shift) | u.u2 >>> invShift; + long z4 = (u4 << shift) | u.u3 >>> invShift; + return new UInt320(z4, z3, z2, z1, z0); } + } - private UInt256 mulSub( - final long v3, final long v2, final long v1, final long v0, final long q) { - // Multiply-subtract: already have highest 1 limbs - // = * q - long p0 = u0 * q; - long p1 = Math.unsignedMultiplyHigh(u0, q); - long z0 = v0 - p0; - long carry = p1 + (((Long.compareUnsigned(v0, z0) < 0) ? 1 : 0)); - - p0 = u1 * q; - p1 = Math.unsignedMultiplyHigh(u1, q); - long res = v1 - p0; - long z1 = res - carry; - long borrow = (Long.compareUnsigned(res, z1) < 0) ? 1 : 0; - carry = p1 + ((Long.compareUnsigned(v1, res) < 0) ? 1 : 0); - - p0 = u2 * q; - p1 = Math.unsignedMultiplyHigh(u2, q); - long t2 = v2 - p0; - res = t2 - borrow; - long z2 = res - carry; - borrow = (Long.compareUnsigned(res, z2) < 0) ? 1 : 0; - carry = - p1 - + ((Long.compareUnsigned(v2, t2) < 0) ? 1 : 0) - + ((Long.compareUnsigned(t2, res) < 0) ? 1 : 0); - - // Propagate overflows (borrows) - long t3 = v3 - carry; - long z3 = t3 - borrow; - borrow = - ((Long.compareUnsigned(v3, t3) < 0) ? 1 : 0) - | ((Long.compareUnsigned(t3, z3) < 0) ? 1 : 0); + record UInt320(long u4, long u3, long u2, long u1, long u0) { + static final UInt320 ZERO = new UInt320(0, 0, 0, 0, 0); - if (borrow != 0) return addBack(z3, z2, z1, z0); - return new UInt256(z3, z2, z1, z0); + UInt320 shiftDigitsRight() { + return new UInt320(0, u4, u3, u2, u1); } + } - private UInt256 mulSubOverflow(final long v3, final long v2, final long v1, final long v0) { - // Overflow case: div2by1 quotient would be <1, 0>, but adjusts to <0, -1> - // = -1 * u0 = - long res, borrow; - - long z0 = v0 + u0; - long carry = u0 - 1 + ((Long.compareUnsigned(v0, z0) <= 0) ? 1 : 0); - - res = v1 - carry; - long z1 = res + u1; - borrow = (Long.compareUnsigned(res, z1) <= 0) ? 1 : 0; - carry = u1 - 1 + ((Long.compareUnsigned(v1, res) < 0) ? 1 : 0); - - res = v2 - carry - borrow; - long z2 = res + u2; - borrow = (Long.compareUnsigned(res, z2) <= 0) ? 1 : 0; - carry = u2 - 1 + ((Long.compareUnsigned(v2, res) < 0) ? 1 : 0); - - long z3 = v3 + u3 - carry - borrow; - // q = MAX may still be 1 too high; check if result >= modulus (i.e. negative wrapped) - if (Long.compareUnsigned(z3, u3) > 0 - || (z3 == u3 - && (Long.compareUnsigned(z2, u2) > 0 - || (z2 == u2 - && (Long.compareUnsigned(z1, u1) > 0 - || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)))))) { - return addBack(z3, z2, z1, z0); - } - return new UInt256(z3, z2, z1, z0); + record UInt448(long u6, long u5, long u4, long u3, long u2, long u1, long u0) { + UInt256 UInt256Value() { + return new UInt256(u3, u2, u1, u0); } - private UInt256 reduceStep( - final long v4, final long v3, final long v2, final long v1, final long v0, final long inv) { - if (v4 == u3) return mulSubOverflow(v3, v2, v1, v0); - DivEstimate qr = div2by1(v4, v3, u3, inv); - if (qr.q != 0) return mulSub(qr.r, v2, v1, v0, qr.q); - return new UInt256(v3, v2, v1, v0); + UInt512 shiftLeftWide(final int shift) { + if (shift == 0) return new UInt512(0, u6, u5, u4, u3, u2, u1, u0); + int invShift = (N_BITS_PER_LIMB - shift); + long z0 = (u0 << shift); + long z1 = (u1 << shift) | u0 >>> invShift; + long z2 = (u2 << shift) | u1 >>> invShift; + long z3 = (u3 << shift) | u2 >>> invShift; + long z4 = (u4 << shift) | u3 >>> invShift; + long z5 = (u5 << shift) | u4 >>> invShift; + long z6 = (u6 << shift) | u5 >>> invShift; + long z7 = u6 >>> invShift; + return new UInt512(z7, z6, z5, z4, z3, z2, z1, z0); } + } - private UInt256 reduceNormalised(final UInt256 that, final int shift, final long inv) { - UInt320 v = that.shiftLeftWide(shift); - UInt256 r; - if (Long.compareUnsigned(v.u4, u3) >= 0) { - r = reduceStep(0, v.u4, v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u0, inv); - } else { - r = reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv); - } - return r.shiftRight(shift); + private record UInt512(long u7, long u6, long u5, long u4, long u3, long u2, long u1, long u0) { + UInt256 UInt256Value() { + return new UInt256(u3, u2, u1, u0); } - private UInt256 reduceNormalised(final UInt257 that, final int shift, final long inv) { - UInt320 v = that.shiftLeftWide(shift); - UInt256 r; - if (Long.compareUnsigned(v.u4, u3) >= 0) { - r = reduceStep(0, v.u4, v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u0, inv); - } else { - r = reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv); - } - return r.shiftRight(shift); + UInt576 shiftLeftWide(final int shift) { + if (shift == 0) return new UInt576(0, u7, u6, u5, u4, u3, u2, u1, u0); + int invShift = (N_BITS_PER_LIMB - shift); + long z0 = (u0 << shift); + long z1 = (u1 << shift) | u0 >>> invShift; + long z2 = (u2 << shift) | u1 >>> invShift; + long z3 = (u3 << shift) | u2 >>> invShift; + long z4 = (u4 << shift) | u3 >>> invShift; + long z5 = (u5 << shift) | u4 >>> invShift; + long z6 = (u6 << shift) | u5 >>> invShift; + long z7 = (u7 << shift) | u6 >>> invShift; + long z8 = u7 >>> invShift; + return new UInt576(z8, z7, z6, z5, z4, z3, z2, z1, z0); } + } - private UInt256 reduceNormalised(final UInt512 that, final int shift, final long inv) { - UInt576 v = that.shiftLeftWide(shift); - if ((v.u8 | v.u7 | v.u6) == 0 && Long.compareUnsigned(v.u5, u3) < 0) { - UInt256 r; - if (v.u5 != 0 || Long.compareUnsigned(v.u4, u3) >= 0) { - r = reduceStep(v.u5, v.u4, v.u3, v.u2, v.u1, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u0, inv); - } else { - r = reduceStep(v.u4, v.u3, v.u2, v.u1, v.u0, inv); - } - return r.shiftRight(shift); - } - return reduceNormalisedSlowPath(v, shift, inv); - } + record UInt576(long u8, long u7, long u6, long u5, long u4, long u3, long u2, long u1, long u0) {} - private UInt256 reduceNormalisedSlowPath(final UInt576 v, final int shift, final long inv) { - UInt256 r; - if (v.u8 != 0 || Long.compareUnsigned(v.u7, u3) >= 0) { - r = reduceStep(v.u8, v.u7, v.u6, v.u5, v.u4, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u3, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u2, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u1, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u0, inv); - } else if (v.u7 != 0 || Long.compareUnsigned(v.u6, u3) >= 0) { - r = reduceStep(v.u7, v.u6, v.u5, v.u4, v.u3, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u2, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u1, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u0, inv); - } else { - r = reduceStep(v.u6, v.u5, v.u4, v.u3, v.u2, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u1, inv); - r = reduceStep(r.u3, r.u2, r.u1, r.u0, v.u0, inv); - } - return r.shiftRight(shift); - } - } // -------------------------------------------------------------------------- - // endregion 256bits Modulus + // endregion } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/DivOperationOptimized.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/DivOperationOptimized.java new file mode 100644 index 00000000000..6190098df60 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/DivOperationOptimized.java @@ -0,0 +1,65 @@ +/* + * 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 + */ +package org.hyperledger.besu.evm.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** The Div operation. */ +public class DivOperationOptimized extends AbstractFixedCostOperation { + + /** The Div success. */ + static final OperationResult divSuccess = new OperationResult(5, null); + + /** + * Instantiates a new Div operation. + * + * @param gasCalculator the gas calculator + */ + public DivOperationOptimized(final GasCalculator gasCalculator) { + super(0x04, "DIV", 2, 1, gasCalculator, gasCalculator.getLowTierGasCost()); + } + + @Override + public OperationResult executeFixedCostOperation(final MessageFrame frame, final EVM evm) { + return staticOperation(frame); + } + + /** + * Performs Div operation. + * + * @param frame the frame + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame) { + + final Bytes value0 = frame.popStackItem(); + final Bytes value1 = frame.popStackItem(); + + if (value1.isZero()) { + frame.pushStackItem(Bytes32.ZERO); + } else { + UInt256 b0 = UInt256.fromBytesBE(value0.toArrayUnsafe()); + UInt256 b1 = UInt256.fromBytesBE(value1.toArrayUnsafe()); + frame.pushStackItem(Bytes.wrap(b0.div(b1).toBytesBE())); + } + return divSuccess; + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/SDivOperationOptimized.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/SDivOperationOptimized.java new file mode 100644 index 00000000000..428c0429598 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/SDivOperationOptimized.java @@ -0,0 +1,63 @@ +/* + * 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 + */ +package org.hyperledger.besu.evm.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** The SDiv operation. */ +public class SDivOperationOptimized extends AbstractFixedCostOperation { + + private static final OperationResult sdivSuccess = new OperationResult(5, null); + + /** + * Instantiates a new SDiv operation. + * + * @param gasCalculator the gas calculator + */ + public SDivOperationOptimized(final GasCalculator gasCalculator) { + super(0x05, "SDIV", 2, 1, gasCalculator, gasCalculator.getLowTierGasCost()); + } + + @Override + public OperationResult executeFixedCostOperation(final MessageFrame frame, final EVM evm) { + return staticOperation(frame); + } + + /** + * Performs SDiv operation. + * + * @param frame the frame + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame) { + final Bytes value0 = frame.popStackItem(); + final Bytes value1 = frame.popStackItem(); + + if (value1.isZero()) { + frame.pushStackItem(Bytes32.ZERO); + } else { + UInt256 b0 = UInt256.fromBytesBE(value0.toArrayUnsafe()); + UInt256 b1 = UInt256.fromBytesBE(value1.toArrayUnsafe()); + frame.pushStackItem(Bytes.wrap(b0.signedDiv(b1).toBytesBE())); + } + return sdivSuccess; + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/UInt256PropertyBasedTest.java b/evm/src/test/java/org/hyperledger/besu/evm/UInt256PropertyBasedTest.java index 99cad626218..b2e352b0e5d 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/UInt256PropertyBasedTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/UInt256PropertyBasedTest.java @@ -328,6 +328,46 @@ void property_divByZero_invariants() { assertThat(x.signedMod(zero).toBytesBE()).containsExactly(Bytes32.ZERO.toArrayUnsafe()); assertThat(x.addMod(x, zero).toBytesBE()).containsExactly(Bytes32.ZERO.toArrayUnsafe()); assertThat(x.mulMod(x, zero).toBytesBE()).containsExactly(Bytes32.ZERO.toArrayUnsafe()); + assertThat(x.div(zero).toBytesBE()).containsExactly(Bytes32.ZERO.toArrayUnsafe()); + assertThat(x.signedDiv(zero).toBytesBE()).containsExactly(Bytes32.ZERO.toArrayUnsafe()); + } + + @Property + void property_div_matchesBigInteger( + @ForAll("unsigned1to32") final byte[] a, @ForAll("unsigned1to32") final byte[] d) { + // Arrange + final UInt256 ua = UInt256.fromBytesBE(a); + final UInt256 ud = UInt256.fromBytesBE(d); + + // Act + byte[] got = ua.div(ud).toBytesBE(); + + // Assert + BigInteger A = toBigUnsigned(a); + BigInteger D = toBigUnsigned(d); + byte[] exp = + (D.signum() == 0) ? Bytes32.ZERO.toArrayUnsafe() : bigUnsignedToBytes32(A.divide(D)); + assertThat(got).containsExactly(exp); + } + + @Property + void property_signedDiv_matchesEvmSemantics( + @ForAll("unsigned1to32") final byte[] a, @ForAll("unsigned1to32") final byte[] d) { + // Arrange + final byte[] a32 = Bytes32.leftPad(Bytes.wrap(a)).toArrayUnsafe(); + final byte[] d32 = Bytes32.leftPad(Bytes.wrap(d)).toArrayUnsafe(); + final BigInteger A = new BigInteger(a32); + final BigInteger D = new BigInteger(d32); + final UInt256 ua = UInt256.fromBytesBE(a32); + final UInt256 ud = UInt256.fromBytesBE(d32); + + // Act + byte[] got = ua.signedDiv(ud).toBytesBE(); + + // Assert + byte[] expected = + (D.signum() == 0) ? Bytes32.ZERO.toArrayUnsafe() : computeSignedDivExpected(A, D); + assertThat(got).containsExactly(expected); } // -------------------------------------------------------------------------- @@ -1803,6 +1843,60 @@ void property_shiftRight_matches_big_integer_mod_2_256( assertThat(got).containsExactly(expected); } + @Property(seed = "314159265") + void property_div_matches_big_integer_unsigned( + @ForAll("bytes0to64_shaped") final byte[] a, @ForAll("bytes0to64_shaped") final byte[] d) { + // Arrange. + final UInt256 ua = UInt256.fromBytesBE(a); + final UInt256 ud = UInt256.fromBytesBE(d); + final BigInteger A = toBigUnsignedMod256(a); + final BigInteger D = toBigUnsignedMod256(d); + final byte[] expected = expectedDiv(A, D); + + // Act. + final byte[] got = ua.div(ud).toBytesBE(); + + // Assert. + assertThat(got).containsExactly(expected); + } + + @Property(seed = "271828182") + void property_signedDiv_matches_evm_semantics( + @ForAll("bytes0to64_shaped") final byte[] a, @ForAll("bytes0to64_shaped") final byte[] d) { + // Arrange. + final byte[] a32 = toBytes32Unsigned(a); + final byte[] d32 = toBytes32Unsigned(d); + final UInt256 ua = UInt256.fromBytesBE(a32); + final UInt256 ud = UInt256.fromBytesBE(d32); + final BigInteger A = new BigInteger(a32); + final BigInteger D = new BigInteger(d32); + final byte[] expected = expectedSignedDiv(A, D); + + // Act. + final byte[] got = ua.signedDiv(ud).toBytesBE(); + + // Assert. + assertThat(got).containsExactly(expected); + } + + @Property(seed = "34243534534") + void property_signedDiv_division_by_minus_one(@ForAll("bytes0to64_shaped") final byte[] a) { + // Arrange. + final byte[] a32 = toBytes32Unsigned(a); + final byte[] d32 = applyPattern(new byte[32], Pattern.ALL_FF); + final UInt256 ua = UInt256.fromBytesBE(a32); + final UInt256 ud = UInt256.fromBytesBE(d32); + final BigInteger A = new BigInteger(a32); + final BigInteger D = new BigInteger(d32); + final byte[] expected = expectedSignedDiv(A, D); + + // Act. + final byte[] got = ua.signedDiv(ud).toBytesBE(); + + // Assert. + assertThat(got).containsExactly(expected); + } + @Property(seed = "123456789") void property_mod_matches_big_integer_unsigned( @ForAll("bytes0to64_shaped") final byte[] a, @ForAll("bytes0to64_shaped") final byte[] m) { @@ -1937,6 +2031,16 @@ private static byte[] computeSignedModExpected(final BigInteger A, final BigInte return bigUnsignedToBytes32(r); } + private static byte[] computeSignedDivExpected(final BigInteger A, final BigInteger D) { + BigInteger q = A.abs().divide(D.abs()); + + if ((A.signum() < 0) != (D.signum() < 0) && q.signum() != 0) { + return padNegative(q); + } + + return bigUnsignedToBytes32(q); + } + private static byte[] padNegative(final BigInteger r) { BigInteger neg = r.negate(); byte[] rb = neg.toByteArray(); @@ -1991,6 +2095,24 @@ private static BigInteger toBigUnsignedMod256(final byte[] be) { return new BigInteger(1, be).mod(TWO_256); } + private static byte[] expectedDiv(final BigInteger A, final BigInteger D) { + if (D.signum() == 0) { + return new byte[32]; + } + return bigUnsignedToBytes32(A.divide(D)); + } + + private static byte[] expectedSignedDiv(final BigInteger A, final BigInteger D) { + if (D.signum() == 0) { + return new byte[32]; + } + BigInteger q = A.abs().divide(D.abs()); + if ((A.signum() < 0) != (D.signum() < 0) && q.signum() != 0) { + return padNegative(q); + } + return bigUnsignedToBytes32(q); + } + private static byte[] expectedMod(final BigInteger A, final BigInteger M) { if (M.signum() == 0) { return new byte[32]; diff --git a/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java b/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java index 5ce51d311f7..0a29d8d2d24 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java @@ -15,10 +15,13 @@ package org.hyperledger.besu.evm; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.array; import java.math.BigInteger; import java.util.Arrays; +import java.util.Collection; import java.util.Random; +import java.util.stream.IntStream; import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; @@ -436,6 +439,10 @@ public void signedModRandom() { byte[] bArray = new byte[bSize]; random.nextBytes(aArray); random.nextBytes(bArray); + + aArray = negate(aArray, random.nextBoolean()); + bArray = negate(bArray, random.nextBoolean()); + if ((aSize < 32) && (neg)) { byte[] tmp = new byte[32]; Arrays.fill(tmp, (byte) 0xFF); @@ -466,4 +473,155 @@ public void signedModRandom() { .isEqualTo(expected); } } + + @Test + public void divRandom() { + final Random random = new Random(45532); + for (int i = 0; i < SAMPLE_SIZE; i++) { + int aSize = random.nextInt(1, 33); + int bSize = random.nextInt(1, 33); + byte[] aArray = new byte[aSize]; + byte[] bArray = new byte[bSize]; + random.nextBytes(aArray); + random.nextBytes(bArray); + UInt256 a = UInt256.fromBytesBE(aArray); + UInt256 b = UInt256.fromBytesBE(bArray); + UInt256 q = a.div(b); + BigInteger aInt = new BigInteger(1, aArray); + BigInteger bInt = new BigInteger(1, bArray); + Bytes32 qBytes = Bytes32.leftPad(Bytes.wrap(q.toBytesBE())); + Bytes32 expected = Bytes32.ZERO; + if (BigInteger.ZERO.compareTo(bInt) != 0) { + BigInteger quotient = aInt.divide(bInt); + expected = bigIntTo32B(quotient, 1); + } + assertThat(qBytes) + .withFailMessage( + String.format("Failure detected:\n%s.DIV(%s)\n", a.toHexString(), b.toHexString())) + .isEqualTo(expected); + } + } + + @Test + public void signedDivRandom() { + final Random random = new Random(957467); + for (int i = 0; i < SAMPLE_SIZE; i++) { + int aSize = random.nextInt(1, 33); + int bSize = random.nextInt(1, 33); + byte[] aArray = new byte[aSize]; + byte[] bArray = new byte[bSize]; + random.nextBytes(aArray); + random.nextBytes(bArray); + + aArray = negate(aArray, random.nextBoolean()); + bArray = negate(bArray, random.nextBoolean()); + + UInt256 a = UInt256.fromBytesBE(aArray); + UInt256 b = UInt256.fromBytesBE(bArray); + UInt256 q = a.signedDiv(b); + BigInteger aInt = a.isNegative() ? new BigInteger(aArray) : new BigInteger(1, aArray); + BigInteger bInt = b.isNegative() ? new BigInteger(bArray) : new BigInteger(1, bArray); + Bytes32 qBytes = Bytes32.leftPad(Bytes.wrap(q.toBytesBE())); + Bytes32 expected = Bytes32.ZERO; + if (BigInteger.ZERO.compareTo(bInt) != 0) { + BigInteger quotient = aInt.divide(bInt); + expected = bigIntTo32B(quotient, quotient.signum()); + } + assertThat(qBytes) + .withFailMessage( + String.format("Failure detected:\n%s.SDIV(%s)\n", a.toHexString(), b.toHexString())) + .isEqualTo(expected); + } + } + + private static byte[] negate(final byte[] array, final boolean negate) { + if (!negate || array.length >= 32) { + return array; + } + byte[] tmp = new byte[32]; + Arrays.fill(tmp, (byte) 0xFF); + System.arraycopy(array, 0, tmp, 32 - array.length, array.length); + return tmp; + } + + @ParameterizedTest + @MethodSource("testCases") + void div_sdiv(final String numerator, final String denominator, final int sign) { + byte[] aArray = Bytes32.leftPad(Bytes.fromHexString(numerator)).toArray(); + byte[] bArray = Bytes32.leftPad(Bytes.fromHexString(denominator)).toArray(); + final UInt256 a = UInt256.fromBytesBE(aArray); + final UInt256 b = UInt256.fromBytesBE(bArray); + + BigInteger aInt = sign < 0 ? new BigInteger(aArray) : new BigInteger(1, aArray); + BigInteger bInt = sign < 0 ? new BigInteger(bArray) : new BigInteger(1, bArray); + + final Bytes32 qBytes = + sign < 0 + ? Bytes32.leftPad(Bytes.wrap(a.signedDiv(b).toBytesBE())) + : Bytes32.leftPad(Bytes.wrap(a.div(b).toBytesBE())); + + Bytes32 expected = Bytes32.ZERO; + if (BigInteger.ZERO.compareTo(bInt) != 0) { + BigInteger quotient = aInt.divide(bInt); + expected = bigIntTo32B(quotient, quotient.signum()); + } + assertThat(qBytes).isEqualTo(expected); + } + + static Collection testCases() { + return Arrays.stream( + new Object[][] { + {"0x00", "0x01"}, + {"0x50", "0x21"}, + { + "0x120d7a733f5016ad9fae51cb9896e15a96147719fe0379d0cb2642a6951e0a5c", + "0x007cdab49aba612fb02bd738a74c76789bc9a911c90296502a35df43e939e6e2" + }, + {"0xa7f576de3a6c", "0xfffffffffef1c296a4c6"}, + {"0xffffffffffffffffffffffff6bacfb1469f9a4d5674a85b75f951d72d7a58e4a", "0x020000"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x01"}, + {"0x01", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + {"0x1598209296af93c13b2f5fde7d8e99", "0x09244c1368"}, + { + "0xfffffffffffffff9309d38241af6a2545b52958d000000000000000000000000", + "0xb17217f7d1cf79abc9e3b398" + }, + {"0xa7f576de3a6c", "0xa7f576de3a6c"}, + {"0x9c2c35e6c180771cda86cde561fe7609b9e89e8e5b", "0x993951396a774e675e93bea2e77c"}, + { + "0xa73fc792edbfb1038115f77a37613b8f5b64837e28768c9dd90828", + "0x0700b2d7adda7612da7f95" + }, + {"0xbf1256135bb3f72de074d0f237", "0x8b63235ac1765530"}, + {"0x5b35862b0027a502b1d4cbc4a09e25", "0x932542f4003763"} + }) + .flatMap( + inputs -> + IntStream.of(-1, 1) + .mapToObj( + sign -> { + Object[] newInputs = Arrays.copyOf(inputs, inputs.length + 1); + newInputs[inputs.length] = sign; + return newInputs; + })) + .toList(); + } + + @Test + void compare() { + assertThat(UInt256.compare(null, UInt256.ONE)).isEqualTo(-1); + assertThat(UInt256.compare(null, null)).isEqualTo(0); + assertThat(UInt256.compare(UInt256.ONE, null)).isEqualTo(1); + assertThat(UInt256.compare(UInt256.ZERO, UInt256.ZERO)).isEqualTo(0); + assertThat(UInt256.compare(UInt256.ONE, UInt256.ZERO)).isEqualTo(1); + assertThat(UInt256.compare(UInt256.ZERO, UInt256.ONE)).isEqualTo(-1); + } } From 976a219418508e4835c7faffcd2b2995b35adf5d Mon Sep 17 00:00:00 2001 From: Jason Frame Date: Thu, 19 Mar 2026 12:51:11 +1000 Subject: [PATCH 64/77] Bonsai archive seperate column families (#10058) Signed-off-by: Jason Frame --- CHANGELOG.md | 1 + .../keyvalue/KeyValueSegmentIdentifier.java | 12 +++ .../flat/BonsaiArchiveFlatDbStrategy.java | 65 ++++++++---- .../PathBasedWorldStateKeyValueStorage.java | 26 ++--- .../BonsaiWorldStateKeyValueStorageTest.java | 41 ++++---- .../flat/BonsaiArchiveFlatDbStrategyTest.java | 99 +++++++++++++++++-- .../worldview/BonsaiArchiverTests.java} | 64 ++++++------ 7 files changed, 216 insertions(+), 92 deletions(-) rename ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/{common/trielog/ArchiverTests.java => bonsai/worldview/BonsaiArchiverTests.java} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13d78dc36ab..7ad0f0eba69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Deprecated `--min-block-occupancy-ratio` for removal and make it noop. That option, that is ignored on PoS networks, is related to the deprecated PoW, and allowed to broadcast a mined block as soon as it reached a satisfying fill threshold. The option is still recognized, but it has no effect and will be completely removed in a future release. [#10036](https://github.com/besu-eth/besu/pull/10036) - Plugin API - Removed `TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD`, in general it could be replaced with `BLOCK_FULL` +- Experimental Bonsai Archive column families have changed to improve performance during bonsai to archive migration. If you are using the Bonsai archive you will need to do a full resync [#10058](https://github.com/besu-eth/besu/pull/10058/changes) ### Upcoming Breaking Changes - RPC changes to enhance compatibility with other ELs diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueSegmentIdentifier.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueSegmentIdentifier.java index c072528fd5e..7968f3cc1d6 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueSegmentIdentifier.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueSegmentIdentifier.java @@ -51,6 +51,18 @@ public enum KeyValueSegmentIdentifier implements SegmentIdentifier { true, false, true), + ACCOUNT_INFO_STATE_FREEZER( + "ACCOUNT_INFO_STATE_FREEZER".getBytes(StandardCharsets.UTF_8), + EnumSet.of(X_BONSAI_ARCHIVE), + true, + false, + true), + ACCOUNT_STORAGE_FREEZER( + "ACCOUNT_STORAGE_FREEZER".getBytes(StandardCharsets.UTF_8), + EnumSet.of(X_BONSAI_ARCHIVE), + true, + false, + true), VARIABLES(new byte[] {11}), // formerly GOQUORUM_PRIVATE_WORLD_STATE // previously supported GoQuorum private states diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/flat/BonsaiArchiveFlatDbStrategy.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/flat/BonsaiArchiveFlatDbStrategy.java index 8e6f33669b1..a9ea82c151b 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/flat/BonsaiArchiveFlatDbStrategy.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/flat/BonsaiArchiveFlatDbStrategy.java @@ -14,10 +14,10 @@ */ package org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.flat; -import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE; +import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_FREEZER; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE; -import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_STORAGE_STORAGE; +import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_STORAGE_FREEZER; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.TRIE_BRANCH_STORAGE; import static org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedWorldStateKeyValueStorage.WORLD_BLOCK_NUMBER_KEY; @@ -133,7 +133,7 @@ public Optional getFlatAccount( // Find the nearest account state for this address and block context Optional nearestAccount = storage - .getNearestBefore(ACCOUNT_INFO_STATE, keyNearest) + .getNearestBefore(ACCOUNT_INFO_STATE_ARCHIVE, keyNearest) .filter( found -> accountHash.getBytes().commonPrefixLength(found.key()) @@ -143,7 +143,7 @@ public Optional getFlatAccount( if (nearestAccount.isEmpty()) { accountFound = storage - .getNearestBefore(ACCOUNT_INFO_STATE_ARCHIVE, keyNearest) + .getNearestBefore(ACCOUNT_INFO_STATE_FREEZER, keyNearest) .filter( found -> accountHash.getBytes().commonPrefixLength(found.key()) @@ -180,7 +180,7 @@ protected Stream> accountsToPairStream( final Stream> stream = storage .streamFromKey( - ACCOUNT_INFO_STATE, + ACCOUNT_INFO_STATE_ARCHIVE, calculateArchiveKeyNoContextMinSuffix(startKeyHash.toArrayUnsafe()), calculateArchiveKeyNoContextMaxSuffix(endKeyHash.toArrayUnsafe())) .map(e -> Bytes.of(calculateArchiveKeyNoContextMaxSuffix(trimSuffix(e.getKey())))) @@ -190,7 +190,11 @@ protected Stream> accountsToPairStream( new Pair<>( Bytes32.wrap(trimSuffix(e.toArrayUnsafe())), Bytes.of( - storage.getNearestBefore(ACCOUNT_INFO_STATE, e).get().value().get()))); + storage + .getNearestBefore(ACCOUNT_INFO_STATE_ARCHIVE, e) + .get() + .value() + .get()))); return stream; } @@ -200,7 +204,7 @@ protected Stream> accountsToPairStream( final Stream> stream = storage .streamFromKey( - ACCOUNT_INFO_STATE, + ACCOUNT_INFO_STATE_ARCHIVE, calculateArchiveKeyNoContextMinSuffix(startKeyHash.toArrayUnsafe())) .map(e -> Bytes.of(calculateArchiveKeyNoContextMaxSuffix(trimSuffix(e.getKey())))) .distinct() @@ -209,7 +213,11 @@ protected Stream> accountsToPairStream( new Pair( Bytes32.wrap(trimSuffix(e.toArrayUnsafe())), Bytes.of( - storage.getNearestBefore(ACCOUNT_INFO_STATE, e).get().value().get()))); + storage + .getNearestBefore(ACCOUNT_INFO_STATE_ARCHIVE, e) + .get() + .value() + .get()))); return stream; } @@ -221,7 +229,7 @@ protected Stream> storageToPairStream( final Function valueMapper) { return storage .streamFromKey( - ACCOUNT_STORAGE_STORAGE, + ACCOUNT_STORAGE_ARCHIVE, calculateArchiveKeyNoContextMinSuffix( calculateNaturalSlotKey(accountHash, Hash.wrap(Bytes32.wrap(startKeyHash))))) .map(e -> Bytes.of(calculateArchiveKeyNoContextMaxSuffix(trimSuffix(e.getKey())))) @@ -234,7 +242,7 @@ protected Stream> storageToPairStream( valueMapper.apply( Bytes.of( storage - .getNearestBefore(ACCOUNT_STORAGE_STORAGE, key) + .getNearestBefore(ACCOUNT_STORAGE_ARCHIVE, key) .get() .value() .get()) @@ -250,7 +258,7 @@ protected Stream> storageToPairStream( final Function valueMapper) { return storage .streamFromKey( - ACCOUNT_STORAGE_STORAGE, + ACCOUNT_STORAGE_ARCHIVE, calculateArchiveKeyNoContextMinSuffix( calculateNaturalSlotKey(accountHash, Hash.wrap(Bytes32.wrap(startKeyHash)))), calculateArchiveKeyNoContextMaxSuffix( @@ -265,7 +273,7 @@ protected Stream> storageToPairStream( valueMapper.apply( Bytes.of( storage - .getNearestBefore(ACCOUNT_STORAGE_STORAGE, key) + .getNearestBefore(ACCOUNT_STORAGE_ARCHIVE, key) .get() .value() .get()) @@ -287,7 +295,7 @@ public void putFlatAccount( calculateArchiveKeyWithMinSuffix( getStateArchiveContextForWrite(storage).get(), accountHash.getBytes().toArrayUnsafe()); - transaction.put(ACCOUNT_INFO_STATE, keySuffixed, accountValue.toArrayUnsafe()); + transaction.put(ACCOUNT_INFO_STATE_ARCHIVE, keySuffixed, accountValue.toArrayUnsafe()); } @Override @@ -301,7 +309,7 @@ public void removeFlatAccount( calculateArchiveKeyWithMinSuffix( getStateArchiveContextForWrite(storage).get(), accountHash.getBytes().toArrayUnsafe()); - transaction.put(ACCOUNT_INFO_STATE, keySuffixed, DELETED_ACCOUNT_VALUE); + transaction.put(ACCOUNT_INFO_STATE_ARCHIVE, keySuffixed, DELETED_ACCOUNT_VALUE); } private byte[] trimSuffix(final byte[] suffixedAddress) { @@ -332,7 +340,7 @@ public Optional getFlatStorageValueByStorageSlotKey( // Find the nearest storage for this address, slot key hash, and block context Optional nearestStorage = storage - .getNearestBefore(ACCOUNT_STORAGE_STORAGE, keyNearest) + .getNearestBefore(ACCOUNT_STORAGE_ARCHIVE, keyNearest) .filter( found -> Bytes.of(naturalKey).commonPrefixLength(found.key()) >= naturalKey.length); @@ -341,7 +349,7 @@ public Optional getFlatStorageValueByStorageSlotKey( // Check the archived storage as old state is moved out of the primary DB segment storageFound = storage - .getNearestBefore(ACCOUNT_STORAGE_ARCHIVE, keyNearest) + .getNearestBefore(ACCOUNT_STORAGE_FREEZER, keyNearest) // don't return accounts that do not have a matching account hash .filter( found -> @@ -389,7 +397,7 @@ public void putFlatAccountStorageValueByStorageSlotHash( byte[] keyNearest = calculateArchiveKeyWithMinSuffix(getStateArchiveContextForWrite(storage).get(), naturalKey); - transaction.put(ACCOUNT_STORAGE_STORAGE, keyNearest, storageValue.toArrayUnsafe()); + transaction.put(ACCOUNT_STORAGE_ARCHIVE, keyNearest, storageValue.toArrayUnsafe()); } /* @@ -408,7 +416,7 @@ public void removeFlatAccountStorageValueByStorageSlotHash( byte[] keySuffixed = calculateArchiveKeyWithMinSuffix(getStateArchiveContextForWrite(storage).get(), naturalKey); - transaction.put(ACCOUNT_STORAGE_STORAGE, keySuffixed, DELETED_STORAGE_VALUE); + transaction.put(ACCOUNT_STORAGE_ARCHIVE, keySuffixed, DELETED_STORAGE_VALUE); } public static byte[] calculateNaturalSlotKey(final Hash accountHash, final Hash slotHash) { @@ -433,6 +441,27 @@ public static Bytes calculateArchiveKeyWithMaxSuffix( return Bytes.of(calculateArchiveKeyWithSuffix(context, naturalKey, MAX_BLOCK_SUFFIX)); } + @Override + public void clearAll(final SegmentedKeyValueStorage storage) { + clearArchiveSegments(storage); + // Then call parent to clear other segments + super.clearAll(storage); + } + + @Override + public void resetOnResync(final SegmentedKeyValueStorage storage) { + clearArchiveSegments(storage); + // Then call parent to reset other segments + super.resetOnResync(storage); + } + + private static void clearArchiveSegments(final SegmentedKeyValueStorage storage) { + storage.clear(ACCOUNT_INFO_STATE_ARCHIVE); + storage.clear(ACCOUNT_STORAGE_ARCHIVE); + storage.clear(ACCOUNT_INFO_STATE_FREEZER); + storage.clear(ACCOUNT_STORAGE_FREEZER); + } + // TODO JF: move this out of this class so can be used with ArchiveCodeStorageStrategy without // being static public static byte[] calculateArchiveKeyWithSuffix( diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/storage/PathBasedWorldStateKeyValueStorage.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/storage/PathBasedWorldStateKeyValueStorage.java index 57274456da8..363a057fdca 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/storage/PathBasedWorldStateKeyValueStorage.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/storage/PathBasedWorldStateKeyValueStorage.java @@ -16,7 +16,9 @@ import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE; +import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_FREEZER; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE; +import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_STORAGE_FREEZER; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_STORAGE_STORAGE; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.CODE_STORAGE; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.TRIE_BRANCH_STORAGE; @@ -216,9 +218,9 @@ public boolean pruneTrieLog(final Hash blockHash) { } /** - * Move old account state from the primary DB segment to the archive segment that will only be - * used for historic state queries. This prevents performance degradation over time for writes to - * the primary DB segments. + * Move old account state from the primary DB archive segment to the archive freezer segment that + * will only be used for historic state queries. This prevents performance degradation over time + * for writes to the primary archive DB segments. * * @param previousBlockHeader the block header for the previous block, used to get the "nearest * before" state @@ -243,7 +245,7 @@ public int archivePreviousAccountState( // Move all entries that match this address hash to the archive DB segment while ((nextMatch = composedWorldStateStorage - .getNearestBefore(ACCOUNT_INFO_STATE, previousKey) + .getNearestBefore(ACCOUNT_INFO_STATE_ARCHIVE, previousKey) .filter( found -> found.value().isPresent() @@ -254,8 +256,8 @@ public int archivePreviousAccountState( .forEach( (nearestKey) -> { moveDBEntry( - ACCOUNT_INFO_STATE, ACCOUNT_INFO_STATE_ARCHIVE, + ACCOUNT_INFO_STATE_FREEZER, nearestKey.key().toArrayUnsafe(), nearestKey.value().get()); archivedStateCount.getAndIncrement(); @@ -287,9 +289,9 @@ public int archivePreviousAccountState( } /** - * Move old storage state from the primary DB segment to the archive segment that will only be - * used for historic state queries. This prevents performance degradation over time for writes to - * the primary DB segments. + * Move old storage state from the primary archive DB segment to the archive freezer segment that + * will only be used for historic state queries. This prevents performance degradation over time + * for writes to the primary DB segments. * * @param previousBlockHeader the block header for the previous block, used to get the "nearest * before" state @@ -315,7 +317,7 @@ public int archivePreviousStorageState( // to the archive DB segment while ((nextMatch = composedWorldStateStorage - .getNearestBefore(ACCOUNT_STORAGE_STORAGE, previousKey) + .getNearestBefore(ACCOUNT_STORAGE_ARCHIVE, previousKey) .filter( found -> found.value().isPresent() @@ -338,8 +340,8 @@ public int archivePreviousStorageState( .log(); } moveDBEntry( - ACCOUNT_STORAGE_STORAGE, ACCOUNT_STORAGE_ARCHIVE, + ACCOUNT_STORAGE_FREEZER, nearestKey.key().toArrayUnsafe(), nearestKey.value().get()); archivedStorageCount.getAndIncrement(); @@ -395,7 +397,7 @@ private void moveDBEntry( public Optional getLatestArchivedBlock() { return composedWorldStateStorage - .get(ACCOUNT_INFO_STATE_ARCHIVE, ARCHIVED_BLOCKS) + .get(ACCOUNT_INFO_STATE_FREEZER, ARCHIVED_BLOCKS) .map(Bytes::wrap) .map(Bytes::toLong); } @@ -403,7 +405,7 @@ public Optional getLatestArchivedBlock() { public void setLatestArchivedBlock(final Long blockNumber) { SegmentedKeyValueStorageTransaction tx = composedWorldStateStorage.startTransaction(); tx.put( - ACCOUNT_INFO_STATE_ARCHIVE, + ACCOUNT_INFO_STATE_FREEZER, ARCHIVED_BLOCKS, Bytes.ofUnsignedLong(blockNumber).toArrayUnsafe()); tx.commit(); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/BonsaiWorldStateKeyValueStorageTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/BonsaiWorldStateKeyValueStorageTest.java index 11c740999a3..944dca41967 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/BonsaiWorldStateKeyValueStorageTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/BonsaiWorldStateKeyValueStorageTest.java @@ -15,7 +15,9 @@ package org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage; import static org.assertj.core.api.Assertions.assertThat; +import static org.bouncycastle.util.Arrays.concatenate; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE; +import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.TRIE_BRANCH_STORAGE; import static org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedWorldStateKeyValueStorage.WORLD_BLOCK_NUMBER_KEY; import static org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedWorldStateKeyValueStorage.WORLD_ROOT_HASH_KEY; @@ -78,18 +80,17 @@ public static Collection flatDbMode() { new Object[][] {{FlatDbMode.FULL}, {FlatDbMode.PARTIAL}, {FlatDbMode.ARCHIVE}}); } - public static Stream flatDbModeAndKeyMapper() { + public static Stream flatDbModeKeyMapperAndSegment() { Function flatDBKey = (key) -> key; // No-op // For archive we want <32-byte-hex>000000000000000n where n is the current archive block number Function flatDBArchiveKey = - (key) -> - org.bouncycastle.util.Arrays.concatenate(key, Bytes.ofUnsignedLong(2).toArrayUnsafe()); + (key) -> concatenate(key, Bytes.ofUnsignedLong(2).toArrayUnsafe()); return Stream.of( - Arguments.of(FlatDbMode.FULL, flatDBKey), - Arguments.of(FlatDbMode.PARTIAL, flatDBKey), - Arguments.of(FlatDbMode.ARCHIVE, flatDBArchiveKey)); + Arguments.of(FlatDbMode.FULL, flatDBKey, ACCOUNT_INFO_STATE), + Arguments.of(FlatDbMode.PARTIAL, flatDBKey, ACCOUNT_INFO_STATE), + Arguments.of(FlatDbMode.ARCHIVE, flatDBArchiveKey, ACCOUNT_INFO_STATE_ARCHIVE)); } public static Collection flatDbModeAndCodeStorageMode() { @@ -461,9 +462,11 @@ void clear_reloadFlatDbStrategy(final FlatDbMode flatDbMode) { } @ParameterizedTest - @MethodSource("flatDbModeAndKeyMapper") + @MethodSource("flatDbModeKeyMapperAndSegment") void clear_putGetAccountFlatDbStrategy( - final FlatDbMode flatDbMode, final Function keyMapper) { + final FlatDbMode flatDbMode, + final Function keyMapper, + final KeyValueSegmentIdentifier segment) { final BonsaiWorldStateKeyValueStorage storage = spy(setUp(flatDbMode)); // save world state root hash @@ -493,9 +496,7 @@ void clear_putGetAccountFlatDbStrategy( // and flat archive DB // and we want to ensure keys put to the archive DB include the archive block context/suffix byte[] lookupKey = keyMapper.apply(account.addressHash().getBytes().toArrayUnsafe()); - assertThat( - Bytes.wrap( - storage.getComposedWorldStateStorage().get(ACCOUNT_INFO_STATE, lookupKey).get())) + assertThat(Bytes.wrap(storage.getComposedWorldStateStorage().get(segment, lookupKey).get())) .isEqualTo( Bytes.fromHexString( "0xF84E823D98887B5E41A364EA8BFCA056E81F171BCC55A6FF8345E692C0F86E5B48E01B996CADC001622FB5E363B421A0C5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470")); @@ -518,9 +519,11 @@ void clear_putGetAccountFlatDbStrategy( } @ParameterizedTest - @MethodSource({"flatDbModeAndKeyMapper"}) + @MethodSource({"flatDbModeKeyMapperAndSegment"}) void clear_streamFlatAccounts( - final FlatDbMode flatDbMode, final Function keyMapper) { + final FlatDbMode flatDbMode, + final Function keyMapper, + final KeyValueSegmentIdentifier segment) { final BonsaiWorldStateKeyValueStorage storage = spy(setUp(flatDbMode)); // save world state root hash @@ -549,19 +552,13 @@ void clear_streamFlatAccounts( // Convert the key to lookup the entry we expect to find in K/V storage. No-op for everything // except ARCHIVE, which needs to append the 000000000000000x suffix to the key byte[] lookupKey = keyMapper.apply(account1.addressHash().getBytes().toArrayUnsafe()); - assertThat( - Bytes32.wrap( - storage.getComposedWorldStateStorage().get(ACCOUNT_INFO_STATE, lookupKey).get())) + assertThat(Bytes32.wrap(storage.getComposedWorldStateStorage().get(segment, lookupKey).get())) .isEqualTo(account1Value); lookupKey = keyMapper.apply(account2.addressHash().getBytes().toArrayUnsafe()); - assertThat( - Bytes32.wrap( - storage.getComposedWorldStateStorage().get(ACCOUNT_INFO_STATE, lookupKey).get())) + assertThat(Bytes32.wrap(storage.getComposedWorldStateStorage().get(segment, lookupKey).get())) .isEqualTo(account2Value); lookupKey = keyMapper.apply(account3.addressHash().getBytes().toArrayUnsafe()); - assertThat( - Bytes32.wrap( - storage.getComposedWorldStateStorage().get(ACCOUNT_INFO_STATE, lookupKey).get())) + assertThat(Bytes32.wrap(storage.getComposedWorldStateStorage().get(segment, lookupKey).get())) .isEqualTo(account3Value); // Streaming the entire range to ensure we get all 3 accounts back diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/flat/BonsaiArchiveFlatDbStrategyTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/flat/BonsaiArchiveFlatDbStrategyTest.java index dabf34d14d7..b3b85185f35 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/flat/BonsaiArchiveFlatDbStrategyTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/storage/flat/BonsaiArchiveFlatDbStrategyTest.java @@ -15,7 +15,9 @@ package org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.flat; import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE; +import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE; +import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_FREEZER; +import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.ACCOUNT_STORAGE_FREEZER; import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.TRIE_BRANCH_STORAGE; import static org.hyperledger.besu.ethereum.trie.pathbased.common.storage.PathBasedWorldStateKeyValueStorage.WORLD_BLOCK_NUMBER_KEY; @@ -30,6 +32,7 @@ import java.util.Optional; import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.util.Arrays; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -57,7 +60,7 @@ public void genesisBlockUsesZeroSuffixWhenWorldBlockNumberKeyNotSet() { final byte[] expectedKey = Bytes.concatenate(accountHash.getBytes(), Bytes.ofUnsignedLong(0)).toArrayUnsafe(); - final Optional storedValue = storage.get(ACCOUNT_INFO_STATE, expectedKey); + final Optional storedValue = storage.get(ACCOUNT_INFO_STATE_ARCHIVE, expectedKey); assertThat(storedValue).isPresent(); assertThat(Bytes.wrap(storedValue.get())).isEqualTo(accountValue); @@ -77,14 +80,14 @@ public void block1UsesOneSuffixWhenWorldBlockNumberKeyIsZero() { final byte[] expectedKey = Bytes.concatenate(accountHash.getBytes(), Bytes.ofUnsignedLong(1)).toArrayUnsafe(); - final Optional storedValue = storage.get(ACCOUNT_INFO_STATE, expectedKey); + final Optional storedValue = storage.get(ACCOUNT_INFO_STATE_ARCHIVE, expectedKey); assertThat(storedValue).isPresent(); assertThat(Bytes.wrap(storedValue.get())).isEqualTo(accountValue); final byte[] genesisKey = Bytes.concatenate(accountHash.getBytes(), Bytes.ofUnsignedLong(0)).toArrayUnsafe(); - assertThat(storage.get(ACCOUNT_INFO_STATE, genesisKey)).isEmpty(); + assertThat(storage.get(ACCOUNT_INFO_STATE_ARCHIVE, genesisKey)).isEmpty(); } @Test @@ -101,7 +104,7 @@ public void block2UsesTwoSuffixWhenWorldBlockNumberKeyIsOne() { final byte[] expectedKey = Bytes.concatenate(accountHash.getBytes(), Bytes.ofUnsignedLong(2)).toArrayUnsafe(); - final Optional storedValue = storage.get(ACCOUNT_INFO_STATE, expectedKey); + final Optional storedValue = storage.get(ACCOUNT_INFO_STATE_ARCHIVE, expectedKey); assertThat(storedValue).isPresent(); assertThat(Bytes.wrap(storedValue.get())).isEqualTo(accountValue); @@ -129,8 +132,8 @@ public void genesisAndBlock1AccountsDoNotOverwrite() { final byte[] block1Key = Bytes.concatenate(accountHash.getBytes(), Bytes.ofUnsignedLong(1)).toArrayUnsafe(); - final Optional genesisValue = storage.get(ACCOUNT_INFO_STATE, genesisKey); - final Optional block1Value = storage.get(ACCOUNT_INFO_STATE, block1Key); + final Optional genesisValue = storage.get(ACCOUNT_INFO_STATE_ARCHIVE, genesisKey); + final Optional block1Value = storage.get(ACCOUNT_INFO_STATE_ARCHIVE, block1Key); assertThat(genesisValue).isPresent(); assertThat(Bytes.wrap(genesisValue.get())).isEqualTo(genesisAccountValue); @@ -176,12 +179,92 @@ public void sequentialBlocksUseIncrementingSuffixes() { for (long blockNum = 0; blockNum <= 3; blockNum++) { final byte[] key = Bytes.concatenate(accountHash.getBytes(), Bytes.ofUnsignedLong(blockNum)).toArrayUnsafe(); - final Optional value = storage.get(ACCOUNT_INFO_STATE, key); + final Optional value = storage.get(ACCOUNT_INFO_STATE_ARCHIVE, key); assertThat(value).as("Block " + blockNum + " should have stored value").isPresent(); assertThat(Bytes.wrap(value.get())).isEqualTo(expectedValues[(int) blockNum]); } } + @Test + public void clearAll_removesDataFromAccountInfoStateFreezer() { + byte[] accountKey = + Hash.fromHexString("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + .getBytes() + .toArrayUnsafe(); + byte[] accountValue = Bytes.fromHexString("0xAABBCCDD").toArrayUnsafe(); + SegmentedKeyValueStorageTransaction tx = storage.startTransaction(); + tx.put(ACCOUNT_INFO_STATE_FREEZER, accountKey, accountValue); + tx.commit(); + + assertThat(storage.get(ACCOUNT_INFO_STATE_FREEZER, accountKey)).isNotEmpty(); + + archiveFlatDbStrategy.clearAll(storage); + + assertThat(storage.get(ACCOUNT_INFO_STATE_FREEZER, accountKey)).isEmpty(); + } + + @Test + public void clearAll_removesDataFromAccountStorageFreezer() { + byte[] storageKey = + Arrays.concatenate( + Hash.fromHexString("0x1111111111111111111111111111111111111111111111111111111111111111") + .getBytes() + .toArrayUnsafe(), + Hash.fromHexString("0x2222222222222222222222222222222222222222222222222222222222222222") + .getBytes() + .toArrayUnsafe()); + byte[] storageValue = Bytes.fromHexString("0xdeadbeef").toArrayUnsafe(); + SegmentedKeyValueStorageTransaction tx = storage.startTransaction(); + tx.put(ACCOUNT_STORAGE_FREEZER, storageKey, storageValue); + tx.commit(); + + assertThat(storage.get(ACCOUNT_STORAGE_FREEZER, storageKey)).isNotEmpty(); + + archiveFlatDbStrategy.clearAll(storage); + + assertThat(storage.get(ACCOUNT_STORAGE_FREEZER, storageKey)).isEmpty(); + } + + @Test + public void resetOnResync_removesDataFromAccountInfoStateFreezer() { + byte[] accountKey = + Hash.fromHexString("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890") + .getBytes() + .toArrayUnsafe(); + byte[] accountValue = Bytes.fromHexString("0x11223344").toArrayUnsafe(); + SegmentedKeyValueStorageTransaction tx = storage.startTransaction(); + tx.put(ACCOUNT_INFO_STATE_FREEZER, accountKey, accountValue); + tx.commit(); + + assertThat(storage.get(ACCOUNT_INFO_STATE_FREEZER, accountKey)).isNotEmpty(); + + archiveFlatDbStrategy.resetOnResync(storage); + + assertThat(storage.get(ACCOUNT_INFO_STATE_FREEZER, accountKey)).isEmpty(); + } + + @Test + public void resetOnResync_removesDataFromAccountStorageFreezer() { + byte[] storageKey = + Arrays.concatenate( + Hash.fromHexString("0x3333333333333333333333333333333333333333333333333333333333333333") + .getBytes() + .toArrayUnsafe(), + Hash.fromHexString("0x4444444444444444444444444444444444444444444444444444444444444444") + .getBytes() + .toArrayUnsafe()); + byte[] storageValue = Bytes.fromHexString("0xcafebabe").toArrayUnsafe(); + SegmentedKeyValueStorageTransaction tx = storage.startTransaction(); + tx.put(ACCOUNT_STORAGE_FREEZER, storageKey, storageValue); + tx.commit(); + + assertThat(storage.get(ACCOUNT_STORAGE_FREEZER, storageKey)).isNotEmpty(); + + archiveFlatDbStrategy.resetOnResync(storage); + + assertThat(storage.get(ACCOUNT_STORAGE_FREEZER, storageKey)).isEmpty(); + } + private void setWorldBlockNumber(final long blockNumber) { final SegmentedKeyValueStorageTransaction tx = storage.startTransaction(); tx.put( diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/common/trielog/ArchiverTests.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiArchiverTests.java similarity index 96% rename from ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/common/trielog/ArchiverTests.java rename to ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiArchiverTests.java index df49f147080..95b381dfb54 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/common/trielog/ArchiverTests.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiArchiverTests.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.ethereum.trie.pathbased.common.trielog; +package org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -41,8 +41,8 @@ import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.CodeCache; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiPreImageProxy; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiArchiver; -import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldState; +import org.hyperledger.besu.ethereum.trie.pathbased.common.trielog.TrieLogLayer; +import org.hyperledger.besu.ethereum.trie.pathbased.common.trielog.TrieLogManager; import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; import org.hyperledger.besu.ethereum.worldstate.FlatDbMode; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; @@ -66,7 +66,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -public class ArchiverTests { +public class BonsaiArchiverTests { // Number of blocks in the chain. This is different to the number of blocks // we have successfully archived state for @@ -109,7 +109,7 @@ public Optional load(final Hash blockHash) { @SuppressWarnings("BannedMethod") @BeforeEach public void setup() { - Configurator.setLevel(LogManager.getLogger(ArchiverTests.class).getName(), Level.TRACE); + Configurator.setLevel(LogManager.getLogger(BonsaiArchiverTests.class).getName(), Level.TRACE); worldStateStorage = Mockito.mock(BonsaiWorldStateKeyValueStorage.class); blockchain = Mockito.mock(Blockchain.class); trieLogManager = Mockito.mock(TrieLogManager.class); @@ -705,8 +705,8 @@ public Optional load(final Long blockNumber) { // Generate some trie logs to return for a specific block - // For state to be moved from the primary DB segment to the archive DB segment, we need the - // primary DB segment to have the account in already + // For state to be moved from the archive DB segment to the archive freezer DB segment, we need + // the archive DB segment to have the account in already SegmentedKeyValueStorageTransaction tx = testWorldStateStorage.getComposedWorldStateStorage().startTransaction(); final BonsaiAccount block150Account = @@ -747,7 +747,7 @@ public Optional load(final Long blockNumber) { BytesValueRLPOutput out = new BytesValueRLPOutput(); block150Account.writeTo(out); tx.put( - KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE, + KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), Bytes.fromHexString("0x0000000000000096").toArrayUnsafe()), @@ -755,7 +755,7 @@ public Optional load(final Long blockNumber) { out = new BytesValueRLPOutput(); block151Account.writeTo(out); tx.put( - KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE, + KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), Bytes.fromHexString("0x0000000000000097").toArrayUnsafe()), @@ -763,7 +763,7 @@ public Optional load(final Long blockNumber) { out = new BytesValueRLPOutput(); block152Account.writeTo(out); tx.put( - KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE, + KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), Bytes.fromHexString("0x0000000000000098").toArrayUnsafe()), @@ -852,18 +852,18 @@ public Optional load(final Hash blockHash) { // We should have marked up to block 200 as archived assertThat(testWorldStateStorage.getLatestArchivedBlock().get()).isEqualTo(200); - // Only the latest/current state of the account should be in the primary DB segment + // Only the latest/current state of the account should be in the archive DB segment assertThat( testWorldStateStorage.getComposedWorldStateStorage().stream( - KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE) + KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE) .count()) .isEqualTo(1); - // Both the previous account states should be in the archive segment, plus the special key that - // records the latest archived block + // Both the previous account states should be in the archive freezer segment, plus the special + // key that records the latest archived block assertThat( testWorldStateStorage.getComposedWorldStateStorage().stream( - KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE) + KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_FREEZER) .count()) .isEqualTo(3); @@ -872,7 +872,7 @@ public Optional load(final Hash blockHash) { testWorldStateStorage .getComposedWorldStateStorage() .containsKey( - KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE, + KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_FREEZER, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), Bytes.fromHexString("0x0000000000000096").toArrayUnsafe()))) @@ -881,7 +881,7 @@ public Optional load(final Hash blockHash) { testWorldStateStorage .getComposedWorldStateStorage() .containsKey( - KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE, + KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_FREEZER, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), Bytes.fromHexString("0x0000000000000097").toArrayUnsafe()))) @@ -890,7 +890,7 @@ public Optional load(final Hash blockHash) { testWorldStateStorage .getComposedWorldStateStorage() .containsKey( - KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE, + KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE_ARCHIVE, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), Bytes.fromHexString("0x0000000000000098").toArrayUnsafe()))) @@ -945,36 +945,36 @@ public Optional load(final Long blockNumber) { // Generate some trie logs to return for a specific block - // For storage to be moved from the primary DB segment to the archive DB segment, we need the - // primary DB segment to have the storage in already + // For storage to be moved from the archive DB segment to the archive freezer DB segment, we + // need the archive DB segment to have the storage in already SegmentedKeyValueStorageTransaction tx = testWorldStateStorage.getComposedWorldStateStorage().startTransaction(); StorageSlotKey slotKey = new StorageSlotKey(UInt256.fromHexString("0x1")); // The key for a bonsai-archive flat DB storage entry is suffixed with the block number where // that state change took place, hence the "0x0000000000000096" suffix to the address hash below tx.put( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_STORAGE, + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), slotKey.getSlotHash().getBytes().toArrayUnsafe(), Bytes.fromHexString("0x0000000000000096").toArrayUnsafe()), Bytes.fromHexString("0x0123").toArrayUnsafe()); tx.put( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_STORAGE, + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), slotKey.getSlotHash().getBytes().toArrayUnsafe(), Bytes.fromHexString("0x0000000000000097").toArrayUnsafe()), Bytes.fromHexString("0x0234").toArrayUnsafe()); tx.put( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_STORAGE, + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), slotKey.getSlotHash().getBytes().toArrayUnsafe(), Bytes.fromHexString("0x0000000000000098").toArrayUnsafe()), Bytes.fromHexString("0x0345").toArrayUnsafe()); tx.put( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_STORAGE, + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), slotKey.getSlotHash().getBytes().toArrayUnsafe(), @@ -1080,17 +1080,17 @@ public Optional load(final Hash blockHash) { // We should have marked up to block 200 as archived assertThat(testWorldStateStorage.getLatestArchivedBlock().get()).isEqualTo(200); - // Only the latest/current state of the account should be in the primary DB segment + // Only the latest/current state of the account should be in the archive DB segment assertThat( testWorldStateStorage.getComposedWorldStateStorage().stream( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_STORAGE) + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE) .count()) .isEqualTo(1); - // All 3 previous storage states should be in the storage archiver + // All 3 previous storage states should be in the storage archive freezer assertThat( testWorldStateStorage.getComposedWorldStateStorage().stream( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE) + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_FREEZER) .count()) .isEqualTo(3); @@ -1099,7 +1099,7 @@ public Optional load(final Hash blockHash) { testWorldStateStorage .getComposedWorldStateStorage() .containsKey( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE, + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_FREEZER, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), slotKey.getSlotHash().getBytes().toArrayUnsafe(), @@ -1109,7 +1109,7 @@ public Optional load(final Hash blockHash) { testWorldStateStorage .getComposedWorldStateStorage() .containsKey( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE, + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_FREEZER, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), slotKey.getSlotHash().getBytes().toArrayUnsafe(), @@ -1119,7 +1119,7 @@ public Optional load(final Hash blockHash) { testWorldStateStorage .getComposedWorldStateStorage() .containsKey( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE, + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_FREEZER, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), slotKey.getSlotHash().getBytes().toArrayUnsafe(), @@ -1129,7 +1129,7 @@ public Optional load(final Hash blockHash) { testWorldStateStorage .getComposedWorldStateStorage() .containsKey( - KeyValueSegmentIdentifier.ACCOUNT_STORAGE_STORAGE, + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_ARCHIVE, Arrays.concatenate( address.addressHash().getBytes().toArrayUnsafe(), slotKey.getSlotHash().getBytes().toArrayUnsafe(), From 57623ff8e18a02313586b042d9a7ab88d21b49c4 Mon Sep 17 00:00:00 2001 From: Cyrus Date: Thu, 19 Mar 2026 08:48:34 +0530 Subject: [PATCH 65/77] fix: prevent unsigned underflow in eth_feeHistory reward bounds (#10060) * fix: prevent unsigned underflow in eth_feeHistory reward bounds Wei.subtract() wraps around on underflow since Wei is unsigned 256-bit. In boundRewards(), when nextBaseFee exceeds gasPriceLowerBound (happens when querying historical blocks from a higher-fee period), the subtraction produces a near-2^256 value. This corrupts every reward entry in the response, causing wallets to suggest absurd gas prices. Floor the priority fee delta at zero when nextBaseFee is larger. Signed-off-by: Shridhar Panigrahi * test: address review feedback on eth_feeHistory underflow fix - Shorten verbose comment in boundRewards to a single line - Replace indirect <= 2L assertion with exact expected reward values - Add equal-case test (nextBaseFee == lowerBoundGasPrice) confirming lowerBoundPriorityFee is Wei.ZERO and rewards are correct Signed-off-by: Shridhar Panigrahi --------- Signed-off-by: Shridhar Panigrahi Co-authored-by: Sally MacFarlane Signed-off-by: Cyrus --- .../internal/methods/EthFeeHistory.java | 6 +- .../internal/methods/EthFeeHistoryTest.java | 58 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistory.java index 0fd00961231..d5e1a6e4e85 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistory.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistory.java @@ -306,7 +306,11 @@ private List calculateRewards( */ private List boundRewards(final List rewards, final Wei nextBaseFee) { final Wei lowerBoundGasPrice = blockchainQueries.gasPriceLowerBound(); - final Wei lowerBoundPriorityFee = lowerBoundGasPrice.subtract(nextBaseFee); + // prevent unsigned underflow when nextBaseFee exceeds lower bound + final Wei lowerBoundPriorityFee = + lowerBoundGasPrice.greaterThan(nextBaseFee) + ? lowerBoundGasPrice.subtract(nextBaseFee) + : Wei.ZERO; final Wei minPriorityFee = miningCoordinator.getMinPriorityFeePerGas(); final Wei forcedMinPriorityFee = UInt256s.max(minPriorityFee, lowerBoundPriorityFee); final Wei lowerBound = diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistoryTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistoryTest.java index f142f9b6bcd..a02d0c839c2 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistoryTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistoryTest.java @@ -236,6 +236,64 @@ public void shouldApplyLowerBoundRewardsCorrectly() { assertThat(rewards).isEqualTo(expectedBoundedRewards); } + @Test + public void shouldNotUnderflowWhenNextBaseFeeExceedsGasPriceLowerBound() { + // gasPriceLowerBound = 10, nextBaseFee = 50: lowerBoundPriorityFee = ZERO (not negative). + // forcedMinPriorityFee = max(minPriorityFee=1, 0) = 1 → lowerBound=1, upperBound=2. + List rewardPercentiles = + Arrays.asList(0.0, 5.0, 10.0, 27.50, 31.0, 59.0, 60.0, 61.0, 100.0); + + Block block = mock(Block.class); + Blockchain blockchain = mockBlockchainTransactionsWithPriorityFee(block); + + ApiConfiguration apiConfiguration = + ImmutableApiConfiguration.builder() + .isGasAndPriorityFeeLimitingEnabled(true) + .lowerBoundGasAndPriorityFeeCoefficient(100L) + .upperBoundGasAndPriorityFeeCoefficient(200L) + .build(); + + final var blockchainQueries = mockBlockchainQueries(blockchain, Wei.of(10)); + when(miningCoordinator.getMinPriorityFeePerGas()).thenReturn(Wei.ONE); + + EthFeeHistory ethFeeHistory = + new EthFeeHistory(null, blockchainQueries, miningCoordinator, apiConfiguration); + + List rewards = ethFeeHistory.computeRewards(rewardPercentiles, block, Wei.of(50)); + + List expectedBoundedRewards = Stream.of(1, 1, 2, 2, 2, 2, 2, 2, 2).map(Wei::of).toList(); + assertThat(rewards).isEqualTo(expectedBoundedRewards); + } + + @Test + public void shouldReturnZeroLowerBoundPriorityFeeWhenNextBaseFeeEqualsGasPriceLowerBound() { + // Equal case: nextBaseFee == lowerBoundGasPrice → lowerBoundPriorityFee must be ZERO, + // not a wraparound. forcedMinPriorityFee = max(1, 0) = 1 → lowerBound=1, upperBound=2. + List rewardPercentiles = + Arrays.asList(0.0, 5.0, 10.0, 27.50, 31.0, 59.0, 60.0, 61.0, 100.0); + + Block block = mock(Block.class); + Blockchain blockchain = mockBlockchainTransactionsWithPriorityFee(block); + + ApiConfiguration apiConfiguration = + ImmutableApiConfiguration.builder() + .isGasAndPriorityFeeLimitingEnabled(true) + .lowerBoundGasAndPriorityFeeCoefficient(100L) + .upperBoundGasAndPriorityFeeCoefficient(200L) + .build(); + + final var blockchainQueries = mockBlockchainQueries(blockchain, Wei.of(50)); + when(miningCoordinator.getMinPriorityFeePerGas()).thenReturn(Wei.ONE); + + EthFeeHistory ethFeeHistory = + new EthFeeHistory(null, blockchainQueries, miningCoordinator, apiConfiguration); + + List rewards = ethFeeHistory.computeRewards(rewardPercentiles, block, Wei.of(50)); + + List expectedBoundedRewards = Stream.of(1, 1, 2, 2, 2, 2, 2, 2, 2).map(Wei::of).toList(); + assertThat(rewards).isEqualTo(expectedBoundedRewards); + } + private Blockchain mockBlockchainTransactionsWithPriorityFee(final Block block) { final Blockchain blockchain = mock(Blockchain.class); From 42e2eef4d0f6f68081f4769954966bfa4ebbb8cd Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Thu, 19 Mar 2026 20:29:54 +1000 Subject: [PATCH 66/77] Log block number/hash on receipts root mismatch for debugging (#10071) When GetSyncReceiptsFromPeerTask returns RESULTS_DO_NOT_MATCH_QUERY, log the block number, hash, expected receiptsRoot, and calculated receiptsRoot at DEBUG level so the failing blocks can be identified without enabling TRACE logging. Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .../peertask/task/GetSyncReceiptsFromPeerTask.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java index c419f1195c9..326c22dd1b8 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java @@ -48,8 +48,11 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes; import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class GetSyncReceiptsFromPeerTask implements PeerTask { + private static final Logger LOG = LoggerFactory.getLogger(GetSyncReceiptsFromPeerTask.class); private final Request request; protected final ProtocolSchedule protocolSchedule; private final List requestedHeaders; @@ -276,7 +279,16 @@ private boolean receiptsRootMatches( }) .toList()); - return calculatedReceiptsRoot.equals(blockHeader.getReceiptsRoot()); + final boolean matches = calculatedReceiptsRoot.equals(blockHeader.getReceiptsRoot()); + if (!matches) { + LOG.debug( + "Receipts root mismatch for block {} ({}): expected={} calculated={}", + blockHeader.getNumber(), + blockHeader.getHash(), + blockHeader.getReceiptsRoot(), + calculatedReceiptsRoot); + } + return matches; } @VisibleForTesting From 3ecda3eaab422afb274c3cce67a639f44d9ab2a1 Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Fri, 20 Mar 2026 11:39:43 +0000 Subject: [PATCH 67/77] fix UInt256: Take result from addBack in mulSubOverflow (#10078) Signed-off-by: Luis Pinto --- .../org/hyperledger/besu/evm/UInt256.java | 45 ++++++++++++------- .../besu/evm/UInt256PropertyBasedTest.java | 32 ++++++++----- .../org/hyperledger/besu/evm/UInt256Test.java | 14 +++++- 3 files changed, 62 insertions(+), 29 deletions(-) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java index 15f20924670..b5d28af181d 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java @@ -992,7 +992,7 @@ private QR256 mulSub(final long v3, final long v2, final long v1, final long v0, return new QR256(q, new UInt256(z3, z2, z1, z0)); } - private UInt256 mulSubOverflow(final long v3, final long v2, final long v1, final long v0) { + private QR256 mulSubOverflow(final long v3, final long v2, final long v1, final long v0) { // Overflow case: div2by1 quotient would be <1, 0>, but adjusts to <0, -1> // = -1 * u0 = long res, borrow; @@ -1011,21 +1011,22 @@ private UInt256 mulSubOverflow(final long v3, final long v2, final long v1, fina carry = u2 - 1 + ((Long.compareUnsigned(v2, res) < 0) ? 1 : 0); long z3 = v3 + u3 - carry - borrow; - // q = MAX may still be 1 too high; check if result >= modulus (i.e. negative wrapped) + // q = MAX may still be 1 too high - need to pass q - 1 (-2L) to addBack; check if result >= + // modulus (i.e. negative wrapped) if (Long.compareUnsigned(z3, u3) > 0 || (z3 == u3 && (Long.compareUnsigned(z2, u2) > 0 || (z2 == u2 && (Long.compareUnsigned(z1, u1) > 0 || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)))))) { - return addBack(z3, z2, z1, z0, 1L).r; + return addBack(z3, z2, z1, z0, -2L); } - return new UInt256(z3, z2, z1, z0); + return new QR256(-1L, new UInt256(z3, z2, z1, z0)); } private QR256 reduceStep( final long v4, final long v3, final long v2, final long v1, final long v0, final long inv) { - if (v4 == u3) return new QR256(-1L, mulSubOverflow(v3, v2, v1, v0)); + if (v4 == u3) return mulSubOverflow(v3, v2, v1, v0); QR64 qr = div2by1(v4, v3, u3, inv); if (qr.q != 0) return mulSub(qr.r, v2, v1, v0, qr.q); return new QR256(0, new UInt256(v3, v2, v1, v0)); @@ -1175,8 +1176,10 @@ private static QR64 div2by1(final long x1, final long x0, final long y, final lo // -------------------------------------------------------------------------- // endregion - // region Records - // -------------------------------------------------------------------------- + // region Quotient / Remainder types + // These types are used to store the result of division. Due to the nature of the algorithm + // (div2by1) quotient (q) can't + // ever exceed 64 bits while remainder (r) can range from 64 to 256 bits. private record QR64(long q, long r) {} private record QR128(long q, UInt128 r) {} @@ -1185,6 +1188,11 @@ private record QR192(long q, UInt192 r) {} private record QR256(long q, UInt256 r) {} + // -------------------------------------------------------------------------- + // endregion + + // region UInt* types + // -------------------------------------------------------------------------- record UInt64(long u0) { UInt64 shiftLeft(final int shift) { return (shift == 0) ? this : new UInt64(u0 << shift); @@ -1414,22 +1422,23 @@ private QR128 mulSub(final long x1, final long x0, final long q) { return new QR128(q, new UInt128(z1, z0)); } - private UInt128 mulSubOverflow(final long v1, final long v0) { + private QR128 mulSubOverflow(final long v1, final long v0) { // Overflow case: div2by1 quotient would be <1, 0>, but adjusts to <0, MAX> // = -1 * u0 = long z0 = v0 + u0; long carry = u0 - 1 + ((Long.compareUnsigned(v0, z0) <= 0) ? 1 : 0); long z1 = v1 + u1 - carry; - // q = MAX may still be 1 too high; check if result >= modulus (i.e. negative wrapped) + // q = MAX may still be 1 too high - need to pass q - 1 (-2L) to addBack; check if result >= + // modulus (i.e. negative wrapped) if (Long.compareUnsigned(z1, u1) > 0 || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)) { - return addBack(z1, z0, 1L).r; + return addBack(z1, z0, -2L); } - return new UInt128(z1, z0); + return new QR128(-1L, new UInt128(z1, z0)); } private QR128 reduceStep(final long v2, final long v1, final long v0, final long inv) { - if (v2 == u1) return new QR128(-1L, mulSubOverflow(v1, v0)); + if (v2 == u1) return mulSubOverflow(v1, v0); QR64 qr = div2by1(v2, v1, u1, inv); if (qr.q != 0) return mulSub(qr.r, v0, qr.q); return new QR128(0, new UInt128(qr.r, v0)); @@ -1640,7 +1649,7 @@ private QR192 mulSub(final long v2, final long v1, final long v0, final long q) return new QR192(q, new UInt192(z2, z1, z0)); } - private UInt192 mulSubOverflow(final long v2, final long v1, final long v0) { + private QR192 mulSubOverflow(final long v2, final long v1, final long v0) { // Overflow case: div2by1 quotient would be <1, 0>, but adjusts to <0, -1> // = -1 * u0 = long z0 = v0 + u0; @@ -1652,19 +1661,21 @@ private UInt192 mulSubOverflow(final long v2, final long v1, final long v0) { carry = u1 - 1 + ((Long.compareUnsigned(v1, res) < 0) ? 1 : 0); long z2 = v2 - carry + u2 - borrow; - // q = MAX may still be 1 too high; check if result >= modulus (i.e. negative wrapped) + // q = MAX may still be 1 too high - need to pass q - 1 (-2L) to addBack; check if result >= + // modulus (i.e. negative wrapped) if (Long.compareUnsigned(z2, u2) > 0 || (z2 == u2 && (Long.compareUnsigned(z1, u1) > 0 || (z1 == u1 && Long.compareUnsigned(z0, u0) >= 0)))) { - return addBack(z2, z1, z0, 1L).r; + + return addBack(z2, z1, z0, -2L); } - return new UInt192(z2, z1, z0); + return new QR192(-1L, new UInt192(z2, z1, z0)); } private QR192 reduceStep( final long v3, final long v2, final long v1, final long v0, final long inv) { - if (v3 == u2) return new QR192(-1L, mulSubOverflow(v2, v1, v0)); + if (v3 == u2) return mulSubOverflow(v2, v1, v0); QR64 qr = div2by1(v3, v2, u2, inv); if (qr.q != 0) return mulSub(qr.r, v1, v0, qr.q); return new QR192(0, new UInt192(v2, v1, v0)); diff --git a/evm/src/test/java/org/hyperledger/besu/evm/UInt256PropertyBasedTest.java b/evm/src/test/java/org/hyperledger/besu/evm/UInt256PropertyBasedTest.java index b2e352b0e5d..e373ebaa58e 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/UInt256PropertyBasedTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/UInt256PropertyBasedTest.java @@ -87,6 +87,7 @@ Arbitrary bytes0to64_shaped() { Tuple.of(4, Arbitraries.of(Pattern.ALL_ZERO)), Tuple.of(4, Arbitraries.of(Pattern.ALL_FF)), Tuple.of(4, Arbitraries.of(Pattern.LIMB_SIGN_BITS)), + Tuple.of(4, Arbitraries.of(Pattern.FIXED_TOP_LIMBS)), Tuple.of(10, Arbitraries.of(Pattern.RANDOM))); return lengths.flatMap( @@ -129,29 +130,38 @@ private enum Pattern { ALL_ZERO, ALL_FF, LIMB_SIGN_BITS, + FIXED_TOP_LIMBS, RANDOM } private static byte[] applyPattern(final byte[] bytes, final Pattern pat) { - switch (pat) { - case ALL_ZERO: + return switch (pat) { + case ALL_ZERO -> { Arrays.fill(bytes, (byte) 0x00); - return bytes; - case ALL_FF: + yield bytes; + } + case ALL_FF -> { Arrays.fill(bytes, (byte) 0xFF); - return bytes; - case LIMB_SIGN_BITS: + yield bytes; + } + case LIMB_SIGN_BITS -> { Arrays.fill(bytes, (byte) 0x00); forceMsbAtIndexIfPresent(bytes, 0); forceMsbAtIndexIfPresent(bytes, 8); forceMsbAtIndexIfPresent(bytes, 16); forceMsbAtIndexIfPresent(bytes, 24); forceMsbAtIndexIfPresent(bytes, bytes.length - 1); - return bytes; - case RANDOM: - default: - return bytes; - } + yield bytes; + } + case FIXED_TOP_LIMBS -> { + int size = (int) Math.ceil(bytes.length / 8D) * 8; + final byte[] newArray = new byte[size]; + System.arraycopy(bytes, 0, newArray, size - bytes.length, bytes.length); + Arrays.fill(newArray, 0, 8, (byte) 0x80); + yield newArray; + } + case RANDOM -> bytes; + }; } private static void forceMsbAtIndexIfPresent(final byte[] bytes, final int index) { diff --git a/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java b/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java index 0a29d8d2d24..9dd578f21f2 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/UInt256Test.java @@ -601,7 +601,19 @@ static Collection testCases() { "0x0700b2d7adda7612da7f95" }, {"0xbf1256135bb3f72de074d0f237", "0x8b63235ac1765530"}, - {"0x5b35862b0027a502b1d4cbc4a09e25", "0x932542f4003763"} + {"0x5b35862b0027a502b1d4cbc4a09e25", "0x932542f4003763"}, + { + // Multiply and subtract overflows and we need to decrement quotient estimation - + // UInt192 case + "0x8200000000000000000000000000000000000000000000000000000000000000", + "0x8200000000000000fe000004000000ffff000000fffff700" + }, + { + // Multiply and subtract overflows and we need to decrement quotient estimation - + // UInt128 case + "0x820000000000000000000000000000000000000000000000", + "0x8200000000000000fe00000000000001" + }, }) .flatMap( inputs -> From 8bb98c61987131f4f9384fe88c8f3440522d6363 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Sat, 21 Mar 2026 06:50:43 +1000 Subject: [PATCH 68/77] BlockchainUtilParameterizedTest - reduce key-pair generations (#10072) * reduce key-pair generations Signed-off-by: Sally MacFarlane * Pre-size ArrayList instances in BlockchainUtilParameterizedTest Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Claude Sonnet 4.6 --- .../util/BlockchainUtilParameterizedTest.java | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/util/BlockchainUtilParameterizedTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/util/BlockchainUtilParameterizedTest.java index af673d10d53..5b4cf38a126 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/util/BlockchainUtilParameterizedTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/util/BlockchainUtilParameterizedTest.java @@ -33,6 +33,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -44,7 +45,13 @@ public class BlockchainUtilParameterizedTest { private static final int chainHeight = 89; private static Block genesisBlock; - private static MutableBlockchain localBlockchain; + // Pre-generated canonical chain data (crypto happens once in @BeforeAll) + private static final List canonicalBlocks = new ArrayList<>(chainHeight); + private static final List> canonicalReceipts = + new ArrayList<>(chainHeight); + + // Rebuilt cheaply from canonical data before each test — never accumulates fork chains + private MutableBlockchain localBlockchain; private MutableBlockchain remoteBlockchain; @@ -54,16 +61,27 @@ public class BlockchainUtilParameterizedTest { @BeforeAll public static void setupClass() { genesisBlock = blockDataGenerator.genesisBlock(); - localBlockchain = InMemoryKeyValueStorageProvider.createInMemoryBlockchain(genesisBlock); - // Setup local chain. + // Use a temporary chain only to thread parent hashes; store blocks for reuse + final MutableBlockchain tempChain = + InMemoryKeyValueStorageProvider.createInMemoryBlockchain(genesisBlock); for (int i = 1; i <= chainHeight; i++) { final BlockDataGenerator.BlockOptions options = new BlockDataGenerator.BlockOptions() .setBlockNumber(i) - .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + .setParentHash(tempChain.getBlockHashByNumber(i - 1).get()); final Block block = blockDataGenerator.block(options); final List receipts = blockDataGenerator.receipts(block); - localBlockchain.appendBlock(block, receipts); + tempChain.appendBlock(block, receipts); + canonicalBlocks.add(block); + canonicalReceipts.add(receipts); + } + } + + @BeforeEach + public void setupInstance() { + localBlockchain = InMemoryKeyValueStorageProvider.createInMemoryBlockchain(genesisBlock); + for (int i = 0; i < canonicalBlocks.size(); i++) { + localBlockchain.appendBlock(canonicalBlocks.get(i), canonicalReceipts.get(i)); } } @@ -78,16 +96,9 @@ public void setup(final int commonAncestorHeight) { final BlockBody commonBody = localBlockchain.getBlockBody(commonHeader.getHash()).get(); remoteBlockchain.appendBlock(new Block(commonHeader, commonBody), receipts); } - // Remaining blocks are disparate. + // Remaining blocks are disparate on the remote chain only. + // Local canonical chain already has blocks up to chainHeight from @BeforeEach. for (long i = commonAncestorHeight + 1L; i <= chainHeight; i++) { - final BlockDataGenerator.BlockOptions localOptions = - new BlockDataGenerator.BlockOptions() - .setBlockNumber(i) - .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); - final Block localBlock = blockDataGenerator.block(localOptions); - final List localReceipts = blockDataGenerator.receipts(localBlock); - localBlockchain.appendBlock(localBlock, localReceipts); - final BlockDataGenerator.BlockOptions remoteOptions = new BlockDataGenerator.BlockOptions() .setDifficulty(Difficulty.ONE) // differentiator @@ -97,7 +108,7 @@ public void setup(final int commonAncestorHeight) { final List remoteReceipts = blockDataGenerator.receipts(remoteBlock); remoteBlockchain.appendBlock(remoteBlock, remoteReceipts); } - headers = new ArrayList<>(); + headers = new ArrayList<>(chainHeight + 1); for (long i = 0L; i <= remoteBlockchain.getChainHeadBlockNumber(); i++) { headers.add(remoteBlockchain.getBlockHeader(i).get()); } From 4c96f478186d6420b290c00818cb73e70f7b5463 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Sat, 21 Mar 2026 14:48:08 +1000 Subject: [PATCH 69/77] minimize setup for BesuCommandTest (#10074) * minimize setup for BesuCommandTest Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane --- .../besu/cli/CommandTestAbstract.java | 104 ++---------------- 1 file changed, 12 insertions(+), 92 deletions(-) diff --git a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index 5580990c2a8..83a0d97ce8c 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -18,10 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.cli.util.CommandLineUtils.DEPENDENCY_WARNING_MSG; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.lenient; @@ -127,6 +123,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -186,12 +183,20 @@ public abstract class CommandTestAbstract { private final HashMap environment = new HashMap<>(); private final List besuCommands = new ArrayList<>(); - private KeyPair keyPair; + + private static final KeyPair keyPair; + + static { + final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); + final Bytes32 keyPairPrvKey = + Bytes32.fromHexString("0xf7a58d5e755d51fa2f6206e91dd574597c73248aaf946ec1964b8c6268d6207b"); + keyPair = signatureAlgorithm.createKeyPair(signatureAlgorithm.createPrivateKey(keyPairPrvKey)); + } protected static final RpcEndpointServiceImpl rpcEndpointServiceImpl = new RpcEndpointServiceImpl(); - @Mock(lenient = true) + @Mock(lenient = true, answer = Answers.RETURNS_SELF) protected RunnerBuilder mockRunnerBuilder; @Mock protected Runner mockRunner; @@ -199,7 +204,7 @@ public abstract class CommandTestAbstract { @Mock(lenient = true) protected BesuController.Builder mockControllerBuilderFactory; - @Mock(lenient = true) + @Mock(lenient = true, answer = Answers.RETURNS_SELF) protected BesuControllerBuilder mockControllerBuilder; @Mock(lenient = true) @@ -272,42 +277,6 @@ public abstract class CommandTestAbstract { public void initMocks() throws Exception { when(mockControllerBuilderFactory.fromEthNetworkConfig(any(), any())) .thenReturn(mockControllerBuilder); - when(mockControllerBuilder.synchronizerConfiguration(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.ethProtocolConfiguration(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.transactionPoolConfiguration(any())) - .thenReturn(mockControllerBuilder); - when(mockControllerBuilder.dataDirectory(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.miningParameters(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.nodeKey(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.metricsSystem(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.messagePermissioningProviders(any())) - .thenReturn(mockControllerBuilder); - when(mockControllerBuilder.clock(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.isRevertReasonEnabled(false)).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.isParallelTxProcessingEnabled(false)) - .thenReturn(mockControllerBuilder); - when(mockControllerBuilder.isEarlyRoundChangeEnabled(false)).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.storageProvider(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.requiredBlocks(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.reorgLoggingThreshold(anyLong())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.dataStorageConfiguration(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.evmConfiguration(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.networkConfiguration(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.randomPeerPriority(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.maxPeers(anyInt())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.chainPruningConfiguration(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.maxPeers(anyInt())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.maxRemotelyInitiatedPeers(anyInt())) - .thenReturn(mockControllerBuilder); - when(mockControllerBuilder.besuComponent(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.balConfiguration(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.cacheLastBlocks(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.cacheLastBlockHeaders(any())).thenReturn(mockControllerBuilder); - when(mockControllerBuilder.isCacheLastBlockHeadersPreloadEnabled(any())) - .thenReturn(mockControllerBuilder); - when(mockControllerBuilder.genesisStateHashCacheEnabled(any())) - .thenReturn(mockControllerBuilder); - when(mockControllerBuilder.apiConfiguration(any())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.build()).thenReturn(mockController); lenient().when(mockController.getProtocolManager()).thenReturn(mockEthProtocolManager); lenient().when(mockController.getProtocolSchedule()).thenReturn(mockProtocolSchedule); @@ -324,56 +293,9 @@ public void initMocks() throws Exception { when(mockController.getTransactionPool()).thenReturn(mockTransactionPool); when(mockController.getStorageProvider()).thenReturn(storageProvider); - when(mockRunnerBuilder.vertx(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.besuController(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.discoveryEnabled(anyBoolean())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.ethNetworkConfig(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.networkingConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.p2pAdvertisedHost(anyString())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.p2pListenPort(anyInt())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.p2pListenInterface(anyString())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.p2pAdvertisedHostIpv6(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.p2pListenInterfaceIpv6(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.p2pListenPortIpv6(anyInt())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.permissioningConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.p2pEnabled(anyBoolean())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.natMethod(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.natMethodFallbackEnabled(anyBoolean())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.jsonRpcConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.engineJsonRpcConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.graphQLConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.webSocketConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.jsonRpcIpcConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.inProcessRpcConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.apiConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.dataDir(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.bannedNodeIds(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.metricsSystem(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.permissioningService(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.transactionValidatorService(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.metricsConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.staticNodes(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.identityString(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.besuPluginContext(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.autoLogBloomCaching(anyBoolean())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.pidPath(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.ethstatsOptions(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.storageProvider(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.rpcEndpointService(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.apiConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.enodeDnsConfiguration(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.allowedSubnets(any())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.poaDiscoveryRetryBootnodes(anyBoolean())).thenReturn(mockRunnerBuilder); - when(mockRunnerBuilder.preferIpv6Outbound(anyBoolean())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.build()).thenReturn(mockRunner); when(mockBesuComponent.getMetricsSystem()).thenReturn(new NoOpMetricsSystem()); - final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); - - final Bytes32 keyPairPrvKey = - Bytes32.fromHexString("0xf7a58d5e755d51fa2f6206e91dd574597c73248aaf946ec1964b8c6268d6207b"); - keyPair = signatureAlgorithm.createKeyPair(signatureAlgorithm.createPrivateKey(keyPairPrvKey)); - lenient().when(nodeKey.getPublicKey()).thenReturn(keyPair.getPublicKey()); lenient() @@ -447,8 +369,6 @@ protected TestBesuCommand parseCommand( final TestType testType, final InputStream in, final String... args) { // turn off ansi usage globally in picocli System.setProperty("picocli.ansi", "false"); - // reset GlobalOpenTelemetry - GlobalOpenTelemetry.resetForTest(); final TestBesuCommand besuCommand = getTestBesuCommand(testType); besuCommands.add(besuCommand); From 87f5dd0c72260cea57953890686b88bcbd6c21ec Mon Sep 17 00:00:00 2001 From: Stefan Pingel <16143240+pinges@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:59:25 +1000 Subject: [PATCH 70/77] Defer Snappy decompression of P2P messages until worker thread processing (#10048) * Defer Snappy decompression of P2P messages until worker thread processing RawMessage now supports a compressed constructor that defers Snappy decompression until getData() is first called on the worker thread. Messages stay in their compressed form while queued in the tx worker pool. Additionally, worker threads now skip processing for messages from already-disconnected peers, and decompression/deserialization failures disconnect the peer with BREACH_OF_PROTOCOL. Signed-off-by: stefan.pingel@consensys.net Co-authored-by: Claude Opus 4.6 Signed-off-by: Stefan Pingel <16143240+pinges@users.noreply.github.com> --- CHANGELOG.md | 2 + .../eth/manager/EthProtocolManager.java | 19 ++- .../eth/manager/snap/SnapProtocolManager.java | 38 +++-- .../manager/task/AbstractPeerRequestTask.java | 11 ++ ...PooledTransactionHashesMessageHandler.java | 34 ++++- .../TransactionsMessageHandler.java | 32 +++- .../eth/manager/EthProtocolManagerTest.java | 22 +++ .../manager/snap/SnapProtocolManagerTest.java | 90 +++++++++++ ...edTransactionHashesMessageHandlerTest.java | 65 ++++++++ .../rlpx/connections/netty/ApiHandler.java | 20 +-- .../p2p/rlpx/connections/netty/DeFramer.java | 6 +- .../ethereum/p2p/rlpx/framing/Framer.java | 33 ++-- .../p2p/rlpx/wire/AbstractMessageData.java | 2 +- .../ethereum/p2p/rlpx/wire/RawMessage.java | 47 ++++++ .../ethereum/p2p/rlpx/framing/FramerTest.java | 57 +++++++ .../p2p/rlpx/wire/RawMessageTest.java | 142 ++++++++++++++++++ 16 files changed, 562 insertions(+), 58 deletions(-) create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManagerTest.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageHandlerTest.java create mode 100644 ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/RawMessageTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad0f0eba69..676f53cfbde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ are provided with different values, using input as per the execution-apis spec i - Wait for peers before starting chain download. Prevents an OutOfMemory (OOM) error when the node has zero peers [#9979](https://github.com/hyperledger/besu/pull/9979) ### Additions and Improvements +- Defer Snappy decompression of inbound P2P messages from the Netty I/O thread to the worker thread, reducing memory held in the transaction worker queue to compressed size [#10048](https://github.com/besu-eth/besu/pull/10048) - Add IPv6 dual-stack support for DiscV5 peer discovery (enabled via `--Xv5-discovery-enabled`): new `--p2p-host-ipv6`, `--p2p-interface-ipv6`, and `--p2p-port-ipv6` CLI options enable a second UDP discovery socket; `--p2p-ipv6-outbound-enabled` controls whether IPv6 is preferred for outbound connections when a peer advertises both address families [#9763](https://github.com/hyperledger/besu/pull/9763); RLPx now also binds a second TCP socket on the IPv6 interface so IPv6-only peers can establish connections [#9873](https://github.com/hyperledger/besu/pull/9873) - `--net-restrict` now supports IPv6 CIDR notation (e.g. `fd00::/64`) in addition to IPv4, enabling subnet-based peer filtering in IPv6 and dual-stack deployments [#10028](https://github.com/besu-eth/besu/pull/10028) - Stop EngineQosTimer as part of shutdown [#9903](https://github.com/hyperledger/besu/pull/9903) @@ -59,6 +60,7 @@ are provided with different values, using input as per the execution-apis spec i - Fix addMod case with 256bit moduluses [#10001](https://github.com/besu-eth/besu/pull/10001) - Performance improvements on MOD variant instructions while converting from byte[] to longs [#9976](https://github.com/besu-eth/besu/pull/9976) - Implement DIV and SDIV with long limbs [#9923](https://github.com/besu-eth/besu/pull/9923) +- Defer Snappy decompression of inbound P2P messages from the Netty I/O thread to the worker thread, reducing memory held in the transaction worker queue to compressed size [#10048](https://github.com/besu-eth/besu/pull/10048) ## 26.2.0 diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java index 080e1f97366..85d2c6df497 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java @@ -37,6 +37,7 @@ import org.hyperledger.besu.ethereum.p2p.network.ProtocolManager; 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.framing.FramingException; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Message; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; @@ -298,12 +299,12 @@ public void processMessage(final Capability capability, final Message message) { return; } - // This will handle responses - ethPeers.dispatchMessage(ethPeer, ethMessage, getSupportedProtocol()); - - // This will handle requests Optional maybeResponseData = Optional.empty(); try { + // This will handle responses + ethPeers.dispatchMessage(ethPeer, ethMessage, getSupportedProtocol()); + + // This will handle requests if (EthProtocol.requestIdCompatible(code)) { final Map.Entry requestIdAndEthMessage = ethMessage.getData().unwrapMessageData(); @@ -314,6 +315,16 @@ public void processMessage(final Capability capability, final Message message) { } else { maybeResponseData = ethMessages.dispatch(ethMessage, capability); } + } catch (final FramingException e) { + LOG.atDebug() + .setMessage("Disconnecting peer {} due to decompression failure for message code {}") + .addArgument(ethPeer::getLoggableId) + .addArgument(code) + .setCause(e) + .log(); + + ethPeer.disconnect( + DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); } catch (final RLPException e) { LOG.atDebug() .setMessage( diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManager.java index 0b42c35a578..f95ac54f15f 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManager.java @@ -24,6 +24,7 @@ import org.hyperledger.besu.ethereum.eth.sync.snapsync.SnapSyncConfiguration; import org.hyperledger.besu.ethereum.p2p.network.ProtocolManager; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; +import org.hyperledger.besu.ethereum.p2p.rlpx.framing.FramingException; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.AbstractSnapMessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Message; @@ -94,8 +95,7 @@ public void awaitStop() throws InterruptedException {} */ @Override public void processMessage(final Capability cap, final Message message) { - final MessageData messageData = AbstractSnapMessageData.create(message); - final int code = messageData.getCode(); + final int code = message.getData().getCode(); LOG.trace("Process snap message {}, {}", cap, code); final EthPeer ethPeer = ethPeers.peer(message.getConnection()); if (ethPeer == null) { @@ -103,33 +103,41 @@ public void processMessage(final Capability cap, final Message message) { "Ignoring message received from unknown peer connection: {}", message.getConnection()); return; } - final EthMessage ethMessage = new EthMessage(ethPeer, messageData); + + final EthMessage ethMessage = new EthMessage(ethPeer, message.getData()); if (!ethPeer.validateReceivedMessage(ethMessage, getSupportedProtocol())) { - LOG.debug( - "Unsolicited message {} received from, disconnecting: {}", - ethMessage.getData().getCode(), - ethPeer); + LOG.debug("Unsolicited message {} received from, disconnecting: {}", code, ethPeer); ethPeer.disconnect(DisconnectReason.BREACH_OF_PROTOCOL_UNSOLICITED_MESSAGE_RECEIVED); return; } - // This will handle responses - ethPeers.dispatchMessage(ethPeer, ethMessage, getSupportedProtocol()); - - // This will handle requests Optional maybeResponseData = Optional.empty(); try { + final MessageData messageData = AbstractSnapMessageData.create(message); + final EthMessage decodedEthMessage = new EthMessage(ethPeer, messageData); + + // This will handle responses + ethPeers.dispatchMessage(ethPeer, decodedEthMessage, getSupportedProtocol()); + + // This will handle requests final Map.Entry requestIdAndEthMessage = - ethMessage.getData().unwrapMessageData(); + decodedEthMessage.getData().unwrapMessageData(); maybeResponseData = snapMessages .dispatch(new EthMessage(ethPeer, requestIdAndEthMessage.getValue()), cap) .map(responseData -> responseData.wrapMessageData(requestIdAndEthMessage.getKey())); + } catch (final FramingException e) { + LOG.atDebug() + .setMessage("Disconnecting peer {} due to decompression failure for message code {}") + .addArgument(ethPeer::getLoggableId) + .addArgument(code) + .setCause(e) + .log(); + ethPeer.disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); } catch (final RLPException e) { LOG.debug( - "Received malformed message code={} data={} (BREACH_OF_PROTOCOL), disconnecting: {}", - messageData.getCode(), - messageData.getData(), + "Received malformed message code={} (BREACH_OF_PROTOCOL), disconnecting: {}", + code, ethPeer, e); ethPeer.disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/task/AbstractPeerRequestTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/task/AbstractPeerRequestTask.java index d66e62104af..3bb2220f111 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/task/AbstractPeerRequestTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/task/AbstractPeerRequestTask.java @@ -20,6 +20,7 @@ import org.hyperledger.besu.ethereum.eth.manager.PendingPeerRequest; import org.hyperledger.besu.ethereum.eth.manager.RequestManager; import org.hyperledger.besu.ethereum.eth.manager.exceptions.PeerBreachedProtocolException; +import org.hyperledger.besu.ethereum.p2p.rlpx.framing.FramingException; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import org.hyperledger.besu.ethereum.rlp.RLPException; @@ -111,6 +112,16 @@ private void handleMessage( promise.complete(r); peer.recordUsefulResponse(); }); + } catch (final FramingException e) { + // Peer sent us data that failed to decompress - disconnect + LOG.atDebug() + .setMessage("Disconnecting peer {} due to decompression failure for message code {}") + .addArgument(peer::getLoggableId) + .addArgument(message.getCode()) + .setCause(e) + .log(); + peer.disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); + promise.completeExceptionally(new PeerBreachedProtocolException()); } catch (final RLPException e) { // Peer sent us malformed data - disconnect LOG.debug( diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageHandler.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageHandler.java index f2e9378fd4b..a71e8d3008e 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageHandler.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageHandler.java @@ -22,12 +22,19 @@ import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.messages.NewPooledTransactionHashesMessage; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import java.time.Duration; import java.time.Instant; import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + class NewPooledTransactionHashesMessageHandler implements EthMessages.MessageCallback { + private static final Logger LOG = + LoggerFactory.getLogger(NewPooledTransactionHashesMessageHandler.class); private final NewPooledTransactionHashesMessageProcessor transactionsMessageProcessor; private final EthScheduler scheduler; @@ -47,13 +54,30 @@ public NewPooledTransactionHashesMessageHandler( public void exec(final EthMessage message) { if (isEnabled.get()) { final Capability capability = message.getPeer().getConnection().capability(EthProtocol.NAME); - final NewPooledTransactionHashesMessage transactionsMessage = - NewPooledTransactionHashesMessage.readFrom(message.getData(), capability); + final MessageData rawMessage = message.getData(); final Instant startedAt = now(); scheduler.scheduleTxWorkerTask( - () -> - transactionsMessageProcessor.processNewPooledTransactionHashesMessage( - message.getPeer(), transactionsMessage, startedAt, txMsgKeepAlive)); + () -> { + if (message.getPeer().isDisconnected()) { + return; + } + final NewPooledTransactionHashesMessage transactionsMessage; + try { + transactionsMessage = + NewPooledTransactionHashesMessage.readFrom(rawMessage, capability); + } catch (final Exception e) { + LOG.debug( + "Malformed pooled transaction hashes message received (BREACH_OF_PROTOCOL), disconnecting: {}", + message.getPeer(), + e); + message + .getPeer() + .disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); + return; + } + transactionsMessageProcessor.processNewPooledTransactionHashesMessage( + message.getPeer(), transactionsMessage, startedAt, txMsgKeepAlive); + }); } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionsMessageHandler.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionsMessageHandler.java index b6bbf442dab..c1bed5975b2 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionsMessageHandler.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionsMessageHandler.java @@ -20,12 +20,18 @@ import org.hyperledger.besu.ethereum.eth.manager.EthMessages; import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.messages.TransactionsMessage; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import java.time.Duration; import java.time.Instant; import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + class TransactionsMessageHandler implements EthMessages.MessageCallback { + private static final Logger LOG = LoggerFactory.getLogger(TransactionsMessageHandler.class); private final TransactionsMessageProcessor transactionsMessageProcessor; private final EthScheduler scheduler; @@ -44,13 +50,29 @@ public TransactionsMessageHandler( @Override public void exec(final EthMessage message) { if (isEnabled.get()) { - final TransactionsMessage transactionsMessage = - TransactionsMessage.readFrom(message.getData()); + final MessageData rawMessage = message.getData(); final Instant startedAt = now(); scheduler.scheduleTxWorkerTask( - () -> - transactionsMessageProcessor.processTransactionsMessage( - message.getPeer(), transactionsMessage, startedAt, txMsgKeepAlive)); + () -> { + if (message.getPeer().isDisconnected()) { + return; + } + final TransactionsMessage transactionsMessage; + try { + transactionsMessage = TransactionsMessage.readFrom(rawMessage); + } catch (final Exception e) { + LOG.debug( + "Malformed transactions message received (BREACH_OF_PROTOCOL), disconnecting: {}", + message.getPeer(), + e); + message + .getPeer() + .disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); + return; + } + transactionsMessageProcessor.processTransactionsMessage( + message.getPeer(), transactionsMessage, startedAt, txMsgKeepAlive); + }); } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java index 20525bb55f2..fd9890668f9 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java @@ -133,6 +133,28 @@ public static void setup() { assertThat(blockchainSetupUtil.getMaxBlockNumber()).isGreaterThanOrEqualTo(20L); } + @Test + public void disconnectOnDecompressionFailure() { + try (final EthProtocolManager ethManager = + EthProtocolManagerTestBuilder.builder() + .setProtocolSchedule(protocolSchedule) + .setBlockchain(blockchain) + .setEthScheduler(new DeterministicEthScheduler(() -> false)) + .setWorldStateArchive(protocolContext.getWorldStateArchive()) + .setTransactionPool(transactionPool) + .setEthereumWireProtocolConfiguration(EthProtocolConfiguration.DEFAULT) + .build()) { + // Create a RawMessage with invalid compressed data that will throw FramingException + final MessageData messageData = + new RawMessage(EthProtocolMessages.GET_BLOCK_HEADERS, new byte[] {0x01, 0x02, 0x03}); + final MockPeerConnection peer = setupPeer(ethManager, (cap, msg, conn) -> {}); + ethManager.processMessage(EthProtocol.ETH68, new DefaultMessage(peer, messageData)); + assertThat(peer.isDisconnected()).isTrue(); + assertThat(peer.getDisconnectReason()) + .contains(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); + } + } + @Test public void handleMalformedRequestIdMessage() { try (final EthProtocolManager ethManager = diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManagerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManagerTest.java new file mode 100644 index 00000000000..8901ba4fd04 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapProtocolManagerTest.java @@ -0,0 +1,90 @@ +/* + * 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.eth.manager.snap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.core.Synchronizer; +import org.hyperledger.besu.ethereum.eth.SnapProtocol; +import org.hyperledger.besu.ethereum.eth.manager.EthMessages; +import org.hyperledger.besu.ethereum.eth.manager.EthPeer; +import org.hyperledger.besu.ethereum.eth.manager.EthPeers; +import org.hyperledger.besu.ethereum.eth.manager.MockPeerConnection; +import org.hyperledger.besu.ethereum.eth.sync.snapsync.SnapSyncConfiguration; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.DefaultMessage; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; +import org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator; + +import java.util.Collections; +import java.util.HashSet; + +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; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class SnapProtocolManagerTest { + + @Mock private WorldStateStorageCoordinator worldStateStorageCoordinator; + @Mock private SnapSyncConfiguration snapConfig; + @Mock private EthPeers ethPeers; + @Mock private EthMessages snapMessages; + @Mock private ProtocolContext protocolContext; + @Mock private Synchronizer synchronizer; + @Mock private EthPeer ethPeer; + + private SnapProtocolManager snapProtocolManager; + + @BeforeEach + void setUp() { + when(snapConfig.isSnapServerEnabled()).thenReturn(false); + snapProtocolManager = + new SnapProtocolManager( + worldStateStorageCoordinator, + snapConfig, + ethPeers, + snapMessages, + protocolContext, + synchronizer); + } + + @Test + void disconnectsPeerOnDecompressionFailure() { + final MockPeerConnection peerConnection = + new MockPeerConnection( + new HashSet<>(Collections.singletonList(SnapProtocol.SNAP1)), (cap, msg, conn) -> {}); + when(ethPeers.peer(peerConnection)).thenReturn(ethPeer); + when(ethPeer.validateReceivedMessage(any(), any())).thenReturn(true); + + // Create a RawMessage with invalid compressed data that will throw FramingException + final RawMessage badMessage = new RawMessage(0x00, new byte[] {0x01, 0x02, 0x03}); + snapProtocolManager.processMessage( + SnapProtocol.SNAP1, new DefaultMessage(peerConnection, badMessage)); + + assertThat(peerConnection.isDisconnected()).isFalse(); + // ethPeer (mock) receives the disconnect call + org.mockito.Mockito.verify(ethPeer) + .disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageHandlerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageHandlerTest.java new file mode 100644 index 00000000000..ab24cbc1443 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageHandlerTest.java @@ -0,0 +1,65 @@ +/* + * 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.eth.transactions; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.eth.EthProtocol; +import org.hyperledger.besu.ethereum.eth.manager.EthMessage; +import org.hyperledger.besu.ethereum.eth.manager.EthPeer; +import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; +import org.hyperledger.besu.testutil.DeterministicEthScheduler; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class NewPooledTransactionHashesMessageHandlerTest { + + @Mock private NewPooledTransactionHashesMessageProcessor processor; + @Mock private EthPeer peer; + @Mock private PeerConnection peerConnection; + + private NewPooledTransactionHashesMessageHandler handler; + + @BeforeEach + void setUp() { + final DeterministicEthScheduler scheduler = new DeterministicEthScheduler(); + handler = new NewPooledTransactionHashesMessageHandler(scheduler, processor, 300); + handler.setEnabled(); + when(peer.getConnection()).thenReturn(peerConnection); + when(peerConnection.capability(EthProtocol.NAME)).thenReturn(EthProtocol.ETH68); + } + + @Test + void disconnectsPeerOnDecompressionFailure() { + // Create a RawMessage with invalid compressed data that will throw FramingException + final RawMessage badMessage = new RawMessage(0x08, new byte[] {0x01, 0x02, 0x03}); + final EthMessage ethMessage = new EthMessage(peer, badMessage); + + handler.exec(ethMessage); + + verify(peer).disconnect(eq(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED)); + verifyNoInteractions(processor); + } +} diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/ApiHandler.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/ApiHandler.java index d0a689a0521..acedf401a26 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/ApiHandler.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/ApiHandler.java @@ -99,15 +99,17 @@ protected void channelRead0(final ChannelHandlerContext ctx, final MessageData o } return; } - LOG.atTrace() - .addMarker(P2P_MESSAGE_MARKER) - .setMessage("Received {} from {} via protocol {}") - .addArgument(message) - .addArgument(connection.getPeerInfo()) - .addArgument(demultiplexed.getCapability()) - .addKeyValue("rawData", message.getData()) - .addKeyValue("decodedData", message::toStringDecoded) - .log(); + if (LOG.isTraceEnabled()) { + LOG.atTrace() + .addMarker(P2P_MESSAGE_MARKER) + .setMessage("Received {} from {} via protocol {}") + .addArgument(message) + .addArgument(connection.getPeerInfo()) + .addArgument(demultiplexed.getCapability()) + .addKeyValue("rawData", message.getData()) + .addKeyValue("decodedData", message::toStringDecoded) + .log(); + } connectionEventDispatcher.dispatchMessage(demultiplexed.getCapability(), connection, message); } diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramer.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramer.java index 42601f7134d..ab01bef7075 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramer.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramer.java @@ -31,6 +31,7 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.wire.CapabilityMultiplexer; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.PeerInfo; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.HelloMessage; @@ -54,6 +55,7 @@ import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.DecoderException; import io.netty.handler.timeout.IdleStateHandler; +import org.apache.tuweni.bytes.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -227,7 +229,9 @@ protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final L "Message received before HELLO's exchanged (BREACH_OF_PROTOCOL), disconnecting. Peer: {}, Code: {}, Data: {}", expectedPeer.map(Peer::getEnodeURLString).orElse("unknown"), message.getCode(), - message.getData().toString()); + message instanceof RawMessage raw && raw.getCompressedData() != null + ? "snappy compressed data: " + Bytes.wrap(raw.getCompressedData()) + : message.getData().toString()); ctx.writeAndFlush( new OutboundMessage( null, diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/framing/Framer.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/framing/Framer.java index 1302b3765d4..c2ba9571944 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/framing/Framer.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/framing/Framer.java @@ -282,38 +282,35 @@ private MessageData processFrame(final ByteBuf f, final int frameSize) { final int id = idbv.isZero() || idbv.size() == 0 ? 0 : idbv.get(0); // Write message data to ByteBuf, decompressing as necessary - final Bytes data; if (compressionEnabled) { final byte[] compressedMessageData = Arrays.copyOfRange(frameData, 1, frameData.length - pad); final int uncompressedLength = compressor.uncompressedLength(compressedMessageData); if (uncompressedLength >= LENGTH_MAX_MESSAGE_FRAME) { throw error("Message size %s in excess of maximum length.", uncompressedLength); } - Bytes _data; - try { - final byte[] decompressedMessageData = compressor.decompress(compressedMessageData); - _data = Bytes.wrap(decompressedMessageData); - compressionSuccessful = true; - } catch (final FramingException fe) { - if (compressionSuccessful) { - throw fe; - } else { - // OpenEthereum/Parity does not implement EIP-706 - // If failing on the first packet downgrade to uncompressed + + if (!compressionSuccessful) { + // First compressed message: decompress eagerly to validate and handle + // the OpenEthereum/Parity fallback (non-Snappy peer detection via EIP-706) + try { + final byte[] decompressedMessageData = compressor.decompress(compressedMessageData); + compressionSuccessful = true; + return new RawMessage(id, Bytes.wrap(decompressedMessageData)); + } catch (final FramingException fe) { compressionEnabled = false; LOG.debug("Snappy decompression failed: downgrading to uncompressed"); final int messageLength = frameSize - LENGTH_MESSAGE_ID; - _data = Bytes.wrap(frameData, 1, messageLength); + return new RawMessage(id, Bytes.wrap(frameData, 1, messageLength)); } + } else { + // Subsequent messages: store compressed, decompress lazily on getData() + return new RawMessage(id, compressedMessageData); } - data = _data; } else { - // Move data to a ByteBuf final int messageLength = frameSize - LENGTH_MESSAGE_ID; - data = Bytes.wrap(frameData, 1, messageLength); + final Bytes data = Bytes.wrap(frameData, 1, messageLength); + return new RawMessage(id, data); } - - return new RawMessage(id, data); } private void validateMac(final byte[] candidateMac, final byte[] expectedMac) { diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/AbstractMessageData.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/AbstractMessageData.java index c6c613b90b0..04ba4e0eb22 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/AbstractMessageData.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/AbstractMessageData.java @@ -27,7 +27,7 @@ protected AbstractMessageData(final Bytes data) { } @Override - public final int getSize() { + public int getSize() { return data.size(); } diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/RawMessage.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/RawMessage.java index b4b0bc20c59..7d6b3600229 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/RawMessage.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/RawMessage.java @@ -14,19 +14,66 @@ */ package org.hyperledger.besu.ethereum.p2p.rlpx.wire; +import org.hyperledger.besu.ethereum.p2p.rlpx.framing.SnappyCompressor; + import org.apache.tuweni.bytes.Bytes; public final class RawMessage extends AbstractMessageData { + private static final SnappyCompressor compressor = new SnappyCompressor(); + private final int code; + private volatile byte[] compressedData; + private volatile boolean decompressed; + private volatile Bytes decompressedData; + /** Constructor for uncompressed messages. */ public RawMessage(final int code, final Bytes data) { super(data); this.code = code; + this.compressedData = null; + this.decompressed = true; + this.decompressedData = data; + } + + /** Constructor for compressed messages — decompression is deferred until getData() is called. */ + public RawMessage(final int code, final byte[] compressedData) { + super(Bytes.EMPTY); + this.code = code; + this.compressedData = compressedData; + this.decompressed = false; + this.decompressedData = null; } @Override public int getCode() { return code; } + + @Override + public Bytes getData() { + if (!decompressed) { + synchronized (this) { + if (!decompressed) { + decompressedData = Bytes.wrap(compressor.decompress(compressedData)); + decompressed = true; + compressedData = null; + } + } + } + return decompressedData; + } + + public byte[] getCompressedData() { + return compressedData; + } + + @Override + public int getSize() { + final byte[] compressed = compressedData; + if (compressed != null) { + return compressor.uncompressedLength(compressed); + } + return decompressedData.size(); + } } diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/framing/FramerTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/framing/FramerTest.java index 403a0d48cc9..182fabe9a16 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/framing/FramerTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/framing/FramerTest.java @@ -271,6 +271,63 @@ public void compressionWorks() { assertThat(receivingFramer.isCompressionSuccessful()).isTrue(); } + @Test + public void firstMessageDecompressesEagerlySubsequentMessagesDeferred() { + final HandshakeSecrets sendSecrets = + new HandshakeSecrets( + Bytes.fromHexString( + "0x75b3ee95adff0c529a05efd7612aa1dbe5057eb9facdde0dfc837ad143da1d43") + .toArray(), + Bytes.fromHexString( + "0x030dfd1566f4800c4842c177f7d476b64ae2b99a2aa0ab5600aa2f41a8710575") + .toArray(), + Bytes.fromHexString( + "0xc9d3385b1588a5969cba312f8c29bedb4cb9d56ec0cf825436addc1ec644f1d6") + .toArray()); + final HandshakeSecrets recvSecrets = + new HandshakeSecrets( + Bytes.fromHexString( + "0x75b3ee95adff0c529a05efd7612aa1dbe5057eb9facdde0dfc837ad143da1d43") + .toArray(), + Bytes.fromHexString( + "0x030dfd1566f4800c4842c177f7d476b64ae2b99a2aa0ab5600aa2f41a8710575") + .toArray(), + Bytes.fromHexString( + "0xc9d3385b1588a5969cba312f8c29bedb4cb9d56ec0cf825436addc1ec644f1d6") + .toArray()); + final Framer sendingFramer = new Framer(sendSecrets); + final Framer receivingFramer = new Framer(recvSecrets); + sendingFramer.enableCompression(); + receivingFramer.enableCompression(); + + final Bytes payload1 = DisconnectMessage.create(DisconnectReason.TIMEOUT).getData(); + final Bytes payload2 = DisconnectMessage.create(DisconnectReason.BREACH_OF_PROTOCOL).getData(); + + // Frame two messages + final ByteBuf out1 = Unpooled.buffer(); + sendingFramer.frame(new RawMessage(0x01, payload1), out1); + final ByteBuf out2 = Unpooled.buffer(); + sendingFramer.frame(new RawMessage(0x01, payload2), out2); + + // First message: eagerly decompressed (compressionSuccessful was false) + final MessageData msg1 = receivingFramer.deframe(out1); + assertThat(msg1).isNotNull(); + final RawMessage raw1 = (RawMessage) msg1; + assertThat(raw1.getCompressedData()).isNull(); + assertThat(raw1.getData()).isEqualTo(payload1); + assertThat(receivingFramer.isCompressionSuccessful()).isTrue(); + + // Second message: deferred decompression (compressionSuccessful was true) + final MessageData msg2 = receivingFramer.deframe(out2); + assertThat(msg2).isNotNull(); + final RawMessage raw2 = (RawMessage) msg2; + assertThat(raw2.getCompressedData()).isNotNull(); + // getData() triggers lazy decompression + assertThat(raw2.getData()).isEqualTo(payload2); + // Compressed data released after decompression + assertThat(raw2.getCompressedData()).isNull(); + } + private HandshakeSecrets secretsFrom(final JsonNode td, final boolean swap) { final byte[] aes = decodeHexDump(td.get("aes_secret").asText()); final byte[] mac = decodeHexDump(td.get("mac_secret").asText()); diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/RawMessageTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/RawMessageTest.java new file mode 100644 index 00000000000..94c52fcf495 --- /dev/null +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/RawMessageTest.java @@ -0,0 +1,142 @@ +/* + * 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.p2p.rlpx.wire; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.ethereum.p2p.rlpx.framing.SnappyCompressor; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; + +class RawMessageTest { + + private static final SnappyCompressor compressor = new SnappyCompressor(); + private static final int CODE = 0x10; + private static final byte[] ORIGINAL_DATA = "hello world, this is a test message".getBytes(UTF_8); + private static final byte[] COMPRESSED_DATA = compressor.compress(ORIGINAL_DATA); + + @Test + void uncompressedConstructorReturnsDataImmediately() { + final Bytes data = Bytes.wrap(ORIGINAL_DATA); + final RawMessage message = new RawMessage(CODE, data); + + assertThat(message.getCode()).isEqualTo(CODE); + assertThat(message.getData()).isEqualTo(data); + assertThat(message.getCompressedData()).isNull(); + } + + @Test + void uncompressedConstructorGetSizeIsCorrect() { + final Bytes data = Bytes.wrap(ORIGINAL_DATA); + final RawMessage message = new RawMessage(CODE, data); + + assertThat(message.getSize()).isEqualTo(ORIGINAL_DATA.length); + } + + @Test + void compressedConstructorDefersDecompression() { + final RawMessage message = new RawMessage(CODE, COMPRESSED_DATA.clone()); + + assertThat(message.getCompressedData()).isNotNull(); + } + + @Test + void compressedConstructorGetDataDecompressesCorrectly() { + final RawMessage message = new RawMessage(CODE, COMPRESSED_DATA.clone()); + + final Bytes result = message.getData(); + + assertThat(result).isEqualTo(Bytes.wrap(ORIGINAL_DATA)); + } + + @Test + void compressedConstructorGetSizeBeforeDecompression() { + final RawMessage message = new RawMessage(CODE, COMPRESSED_DATA.clone()); + + assertThat(message.getSize()).isEqualTo(ORIGINAL_DATA.length); + // compressed data should still be present (getSize does not trigger decompression) + assertThat(message.getCompressedData()).isNotNull(); + } + + @Test + void compressedConstructorGetSizeAfterDecompression() { + final RawMessage message = new RawMessage(CODE, COMPRESSED_DATA.clone()); + + message.getData(); // trigger decompression + assertThat(message.getSize()).isEqualTo(ORIGINAL_DATA.length); + } + + @Test + void compressedDataIsNulledAfterDecompression() { + final RawMessage message = new RawMessage(CODE, COMPRESSED_DATA.clone()); + + message.getData(); + assertThat(message.getCompressedData()).isNull(); + } + + @Test + void getDataIsIdempotent() { + final RawMessage message = new RawMessage(CODE, COMPRESSED_DATA.clone()); + + final Bytes first = message.getData(); + final Bytes second = message.getData(); + + assertThat(first).isEqualTo(Bytes.wrap(ORIGINAL_DATA)); + assertThat(first).isSameAs(second); + } + + @Test + void concurrentGetDataDecompressesOnlyOnce() throws Exception { + final int threadCount = 16; + final RawMessage message = new RawMessage(CODE, COMPRESSED_DATA.clone()); + final CyclicBarrier barrier = new CyclicBarrier(threadCount); + final ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + try { + final List> futures = new ArrayList<>(); + for (int i = 0; i < threadCount; i++) { + futures.add( + executor.submit( + () -> { + barrier.await(); + return message.getData(); + })); + } + + Bytes firstResult = null; + for (final Future future : futures) { + final Bytes result = future.get(); + assertThat(result).isEqualTo(Bytes.wrap(ORIGINAL_DATA)); + if (firstResult == null) { + firstResult = result; + } else { + // all threads should get the exact same Bytes instance + assertThat(result).isSameAs(firstResult); + } + } + } finally { + executor.shutdown(); + } + } +} From 82827617390b54ab69ba6da0f63e8f11b3d014b7 Mon Sep 17 00:00:00 2001 From: Kanchan Kaur <87459628+kkaur01@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:58:51 +1000 Subject: [PATCH 71/77] return -38015 when tx gas exceeds block gas limit (#10073) * return -38015 when tx gas exceeds block gas limit * added tets for mid-block partial consumption Signed-off-by: kkaur01 --------- Signed-off-by: kkaur01 Co-authored-by: Sally MacFarlane Signed-off-by: Kanchan Kaur <87459628+kkaur01@users.noreply.github.com> --- CHANGELOG.md | 1 + .../ethereum/transaction/BlockSimulator.java | 8 ++ .../exceptions/BlockStateCallError.java | 2 + .../transaction/BlockSimulatorTest.java | 122 ++++++++++++++++++ 4 files changed, 133 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 676f53cfbde..5d982f5465b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Fix QBFT `RLPException` when decoding proposals from pre-26.1.0 nodes that do not include the `blockAccessList` field [#9977](https://github.com/hyperledger/besu/pull/9977) - Fix eth_simulateV1 discrepancy [9960] (https://github.com/besu-eth/besu/issues/9960) eth_simulateV1 now accepts calls where both input and data are provided with different values, using input as per the execution-apis spec instead of returning an error. +- Fix eth_simulateV1 returning wrong error code when transaction gas exceeds block gas limit: now correctly returns -38015 (BLOCK_GAS_LIMIT_EXCEEDED) instead of -38014 or succeeding [#10073](https://github.com/besu-eth/besu/pull/10073) - Wait for peers before starting chain download. Prevents an OutOfMemory (OOM) error when the node has zero peers [#9979](https://github.com/hyperledger/besu/pull/9979) ### Additions and Improvements diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java index d7dc6e0967b..2279a766652 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java @@ -377,6 +377,14 @@ protected BlockStateCallSimulationResult processTransactions( } } + if (callParameter.getGas().isPresent() + && callParameter.getGas().getAsLong() + > blockStateCallSimulationResult.getRemainingGas()) { + throw new BlockStateCallException( + BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED.getMessage(), + BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED); + } + long gasLimit = transactionSimulator.calculateSimulationGasCap( blockHeader, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/exceptions/BlockStateCallError.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/exceptions/BlockStateCallError.java index 1c4de32d86e..1cd6f703f04 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/exceptions/BlockStateCallError.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/exceptions/BlockStateCallError.java @@ -32,6 +32,8 @@ public enum BlockStateCallError { DUPLICATED_PRECOMPILE_TARGET(-38023, "Duplicated move precompile target"), /** The nonce is invalid. */ INVALID_NONCES(-32602, "Invalid nonces"), + /** Block gas limit exceeded by the block's transactions. */ + BLOCK_GAS_LIMIT_EXCEEDED(-38015, "Transaction gas exceeds block gas limit"), /** Upfront cost exceeds balance. */ UPFRONT_COST_EXCEEDS_BALANCE(-38014, "Upfront cost exceeds balance"), /** Gas price too low. */ diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java index ad92b1eecac..0eaba2b13dd 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java @@ -29,6 +29,7 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.StateOverride; import org.hyperledger.besu.datatypes.StateOverrideMap; +import org.hyperledger.besu.datatypes.TransactionType; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.datatypes.parameters.UnsignedLongParameter; import org.hyperledger.besu.ethereum.GasLimitCalculator; @@ -38,12 +39,16 @@ import org.hyperledger.besu.ethereum.core.Difficulty; import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.mainnet.AbstractBlockProcessor; import org.hyperledger.besu.ethereum.mainnet.MiningBeneficiaryCalculator; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.mainnet.blockhash.PreExecutionProcessor; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.ethereum.transaction.exceptions.BlockStateCallError; import org.hyperledger.besu.ethereum.transaction.exceptions.BlockStateCallException; import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams; @@ -57,6 +62,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.OptionalLong; import java.util.Set; import org.apache.tuweni.bytes.Bytes; @@ -145,6 +151,7 @@ public void shouldStopWhenTransactionSimulationIsInvalid() { when(mutableWorldState.updater()).thenReturn(updater); CallParameter callParameter = mock(CallParameter.class); + when(callParameter.getGas()).thenReturn(OptionalLong.empty()); BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); TransactionSimulatorResult transactionSimulatorResult = mock(TransactionSimulatorResult.class); @@ -179,6 +186,7 @@ public void shouldStopWhenTransactionSimulationIsEmpty() { when(mutableWorldState.updater()).thenReturn(updater); CallParameter callParameter = mock(CallParameter.class); + when(callParameter.getGas()).thenReturn(OptionalLong.empty()); BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); when(transactionSimulator.processWithWorldUpdater( @@ -307,6 +315,120 @@ public void shouldAllowDuplicatePrecompileTargetAddresses() { assertThat(validationResult).isEmpty(); } + @Test + public void shouldThrowBlockGasLimitExceededWhenTxGasExceedsBlockLimitWithValidationDisabled() { + when(mutableWorldState.updater()).thenReturn(updater); + + // gasLimitCalculator.nextGasLimit returns 1L (from setUp), so block gas limit = 1 + CallParameter callParameter = mock(CallParameter.class); + when(callParameter.getGas()).thenReturn(OptionalLong.of(1_000_000L)); + BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); + + BlockSimulationParameter parameter = + new BlockSimulationParameter.BlockSimulationParameterBuilder() + .blockStateCalls(List.of(blockStateCall)) + .validation(false) + .build(); + + BlockStateCallException exception = + assertThrows( + BlockStateCallException.class, + () -> blockSimulator.process(blockHeader, parameter, mutableWorldState)); + + assertThat(exception.getError()).isEqualTo(BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED); + assertThat(exception.getError().getCode()).isEqualTo(-38015); + } + + @Test + public void shouldThrowBlockGasLimitExceededWhenTxGasExceedsBlockLimitWithValidationEnabled() { + when(mutableWorldState.updater()).thenReturn(updater); + + // gasLimitCalculator.nextGasLimit returns 1L (from setUp), so block gas limit = 1 + CallParameter callParameter = mock(CallParameter.class); + when(callParameter.getGas()).thenReturn(OptionalLong.of(1_000_000L)); + BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); + + BlockSimulationParameter parameter = + new BlockSimulationParameter.BlockSimulationParameterBuilder() + .blockStateCalls(List.of(blockStateCall)) + .validation(true) + .build(); + + BlockStateCallException exception = + assertThrows( + BlockStateCallException.class, + () -> blockSimulator.process(blockHeader, parameter, mutableWorldState)); + + assertThat(exception.getError()).isEqualTo(BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED); + assertThat(exception.getError().getCode()).isEqualTo(-38015); + } + + @Test + public void + shouldThrowBlockGasLimitExceededWhenSecondTxGasExceedsRemainingAfterFirstTxConsumed() { + // Block gas limit = 30,000. First tx consumes 21,000 (leaving 9,000 remaining). + // Second tx explicitly requests 10,000 gas, which exceeds the 9,000 remaining. + GasLimitCalculator gasLimitCalculator = mock(GasLimitCalculator.class); + when(protocolSpec.getGasLimitCalculator()).thenReturn(gasLimitCalculator); + when(gasLimitCalculator.nextGasLimit(anyLong(), anyLong(), anyLong())).thenReturn(30_000L); + when(gasLimitCalculator.computeExcessBlobGas(anyLong(), anyLong(), anyLong())).thenReturn(0L); + + WorldUpdater transactionUpdater = mock(WorldUpdater.class); + when(mutableWorldState.updater()).thenReturn(updater); + when(updater.updater()).thenReturn(transactionUpdater); + + CallParameter firstCallParam = mock(CallParameter.class); + when(firstCallParam.getGas()).thenReturn(OptionalLong.empty()); + when(firstCallParam.getGasPrice()).thenReturn(Optional.empty()); + when(firstCallParam.getMaxFeePerGas()).thenReturn(Optional.empty()); + when(firstCallParam.getMaxPriorityFeePerGas()).thenReturn(Optional.empty()); + + CallParameter secondCallParam = mock(CallParameter.class); + when(secondCallParam.getGas()).thenReturn(OptionalLong.of(10_000L)); + + BlockStateCall blockStateCall = + new BlockStateCall(List.of(firstCallParam, secondCallParam), null, null); + + Transaction tx = mock(Transaction.class); + when(tx.getType()).thenReturn(TransactionType.FRONTIER); + + TransactionProcessingResult processingResult = mock(TransactionProcessingResult.class); + when(processingResult.getPartialBlockAccessView()).thenReturn(Optional.empty()); + + TransactionSimulatorResult firstTxResult = mock(TransactionSimulatorResult.class); + when(firstTxResult.isInvalid()).thenReturn(false); + when(firstTxResult.getGasEstimate()).thenReturn(21_000L); + when(firstTxResult.transaction()).thenReturn(tx); + when(firstTxResult.result()).thenReturn(processingResult); + + when(transactionSimulator.processWithWorldUpdater( + any(), any(), any(), any(), any(), any(), any(), anyLong(), any(), any(), any(), any(), + any())) + .thenReturn(Optional.of(firstTxResult)); + + AbstractBlockProcessor.TransactionReceiptFactory receiptFactory = + mock(AbstractBlockProcessor.TransactionReceiptFactory.class); + when(protocolSpec.getTransactionReceiptFactory()).thenReturn(receiptFactory); + TransactionReceipt receipt = mock(TransactionReceipt.class); + when(receipt.getLogsList()).thenReturn(List.of()); + when(receiptFactory.create(any(TransactionType.class), any(), any(), anyLong())) + .thenReturn(receipt); + + BlockSimulationParameter parameter = + new BlockSimulationParameter.BlockSimulationParameterBuilder() + .blockStateCalls(List.of(blockStateCall)) + .validation(false) + .build(); + + BlockStateCallException exception = + assertThrows( + BlockStateCallException.class, + () -> blockSimulator.process(blockHeader, parameter, mutableWorldState)); + + assertThat(exception.getError()).isEqualTo(BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED); + assertThat(exception.getError().getCode()).isEqualTo(-38015); + } + private BlockSimulationParameter createSimulationParameter(final BlockStateCall blockStateCall) { return new BlockSimulationParameter.BlockSimulationParameterBuilder() .blockStateCalls(List.of(blockStateCall)) From e26e47ef28029d16baec437c0a5aa844655f02d8 Mon Sep 17 00:00:00 2001 From: Ali Zhagparov Date: Mon, 23 Mar 2026 03:26:48 -0700 Subject: [PATCH 72/77] Reduce memory usage of debug_trace* calls #9584 (#9938) * use latest snapshot if no memory wright op Signed-off-by: Ali Co-authored-by: ahamlat Signed-off-by: Ali Zhagparov --- .../ethereum/vm/DebugOperationTracer.java | 5 + .../ethereum/vm/DebugOperationTracerTest.java | 158 ++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracer.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracer.java index b6a05d4a4bf..3619783564d 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracer.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracer.java @@ -286,6 +286,11 @@ private Optional> captureStorage(final MessageFrame frame) private Optional captureMemory(final MessageFrame frame) { if (!options.traceMemory() || frame.memoryWordSize() == 0) { return Optional.empty(); + } else if (frame.getMaybeUpdatedMemory().isEmpty() + && lastFrame != null + && lastFrame.getDepth() == frame.getDepth() + && lastFrame.getMemory().get().length == frame.memoryWordSize()) { + return lastFrame.getMemory(); } final Bytes[] memoryContents = new Bytes[frame.memoryWordSize()]; for (int i = 0; i < memoryContents.length; i++) { diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracerTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracerTest.java index 0c824a9a0eb..5df6f63c993 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracerTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracerTest.java @@ -38,10 +38,12 @@ import org.hyperledger.besu.evm.tracing.TraceFrame; import org.hyperledger.besu.evm.worldstate.WorldUpdater; +import java.util.List; import java.util.Map; import java.util.OptionalLong; import java.util.TreeMap; +import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; import org.assertj.core.api.Assertions; @@ -55,6 +57,8 @@ class DebugOperationTracerTest { private static final int DEPTH = 4; private static final long INITIAL_GAS = 1000L; + private static final Bytes32 WORD_1 = Bytes32.fromHexString("0x" + "aa".repeat(32)); + private static final Bytes32 WORD_2 = Bytes32.fromHexString("0x" + "bb".repeat(32)); @Mock private WorldUpdater worldUpdater; @@ -67,6 +71,15 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { return new OperationResult(20L, null); } }; + private final Operation MSTORE_OPERATION = + new AbstractOperation(0x52, "MSTORE", 2, 0, null) { + @Override + public OperationResult execute(final MessageFrame frame, final EVM evm) { + // explicitMemoryUpdate=true simulates what MSTORE does in the real EVM + frame.writeMemory(0L, 32, WORD_2, true); + return new OperationResult(3L, null); + } + }; private final CallOperation callOperation = new CallOperation(new CancunGasCalculator()); @@ -284,6 +297,126 @@ void shouldCaptureFrameWhenExceptionalHaltOccurs() { assertThat(traceFrame.getStorage()).contains(updatedStorage); } + @Test + void shouldUseLastSnapshotInNonMemoryWriteOperation() { + final MessageFrame frame = validMessageFrameWithInitiatedMemory(WORD_1); + final DebugOperationTracer tracer = createDebugOperationTracerWithMemory(); + + // Frame 0: non-memory-writing op — captures initial snapshot + traceFrame(frame, tracer, anOperation); + // Frame 1: another non-memory-writing op — should reuse last snapshot, not re-capture + traceFrame(frame, tracer, anOperation); + + final List frames = tracer.getTraceFrames(); + assertThat(frames).hasSize(2); + assertThat(frames.get(0).getMemory()).isPresent(); + assertThat(frames.get(1).getMemory()).isPresent(); + final Bytes[] frame0Memory = frames.get(0).getMemory().get(); + final Bytes[] frame1Memory = frames.get(1).getMemory().get(); + + assertThat(frame1Memory) + .as("For a non-memory-writing opcode, the last snapshot must be reused") + .isSameAs(frame0Memory); + } + + @Test + void shouldTakeNewMemorySnapshotWhenMemorySizeGrowsWithoutExplicitWrite() { + final MessageFrame frame = validMessageFrameWithInitiatedMemory(WORD_1); + final DebugOperationTracer tracer = createDebugOperationTracerWithMemory(); + + traceFrame(frame, tracer, anOperation); + + frame.writeMemory(32L, 32, WORD_2); + traceFrame(frame, tracer, anOperation); + + final List frames = tracer.getTraceFrames(); + assertThat(frames).hasSize(2); + + assertThat(frames.get(0).getMemory()).isPresent(); + assertThat(frames.get(1).getMemory()).isPresent(); + final Bytes[] snapshot0 = frames.get(0).getMemory().get(); + final Bytes[] snapshot1 = frames.get(1).getMemory().get(); + + assertThat(snapshot1) + .as("When memory size grows, a fresh snapshot must be taken even without an explicit write") + .isNotSameAs(snapshot0); + assertThat(snapshot0).hasSize(1); + assertThat(snapshot1).hasSize(2); + assertThat(snapshot1[1]).isEqualTo(WORD_2); + } + + @Test + void shouldTakeNewMemorySnapshotAfterExplicitMemoryWrite() { + final MessageFrame frame = validMessageFrameWithInitiatedMemory(WORD_1); + final DebugOperationTracer tracer = createDebugOperationTracerWithMemory(); + + traceFrame(frame, tracer, anOperation); + traceFrame(frame, tracer, MSTORE_OPERATION); + + final List frames = tracer.getTraceFrames(); + assertThat(frames).hasSize(2); + assertThat(frames.get(0).getMemory()).isPresent(); + assertThat(frames.get(1).getMemory()).isPresent(); + + final Bytes[] before = frames.get(0).getMemory().get(); + final Bytes[] after = frames.get(1).getMemory().get(); + + assertThat(after) + .as("After a memory-writing opcode, a new memory snapshot must be taken") + .isNotSameAs(before); + assertThat(before[0]).isEqualTo(WORD_1); + assertThat(after[0]).isEqualTo(WORD_2); + } + + @Test + void shouldCaptureMemoryDirectlyWhenLastFrameIsNull() { + final MessageFrame frame = validMessageFrameWithInitiatedMemory(WORD_1); + final DebugOperationTracer tracer = createDebugOperationTracerWithMemory(); + traceFrame(frame, tracer, anOperation); + + final List frames = tracer.getTraceFrames(); + assertThat(frames).hasSize(1); + assertThat(frames.getFirst().getMemory()).isPresent(); + assertThat(frames.getFirst().getMemory().get()).containsExactly(WORD_1); + } + + @Test + void shouldHandleCallScenarioAndDepthChange() { + final DebugOperationTracer tracer = createDebugOperationTracerWithMemory(); + final MessageFrame parentFrame = validMessageFrameWithInitiatedMemory(WORD_1); + + traceFrame(parentFrame, tracer, callOperation); + + // Child frame enters at depth 1 + final MessageFrame childFrame = validMessageFrameWithInitiatedMemory(WORD_2); + childFrame.getMessageFrameStack().add(childFrame); + + traceFrame(childFrame, tracer, anOperation); + traceFrame(parentFrame, tracer, anOperation); + + final List frames = tracer.getTraceFrames(); + assertThat(frames).hasSize(3); + assertThat(frames.get(0).getMemory()).isPresent(); + assertThat(frames.get(1).getMemory()).isPresent(); + assertThat(frames.get(2).getMemory()).isPresent(); + + final Bytes[] parentSnapshot = frames.get(0).getMemory().get(); + final Bytes[] childSnapshot = frames.get(1).getMemory().get(); + final Bytes[] parentAfterReturn = frames.get(2).getMemory().get(); + + assertThat(childSnapshot) + .as("On CALL entry, child must get a fresh snapshot — not reuse the parent's") + .isNotSameAs(parentSnapshot); + assertThat(childSnapshot[0]).isEqualTo(WORD_2); + + assertThat(parentAfterReturn) + .as("After RETURN, parent memory must be freshly captured — not the child's snapshot") + .isNotSameAs(childSnapshot); + assertThat(parentAfterReturn[0]) + .as("After RETURN, parent memory must reflect the parent frame's actual content") + .isEqualTo(WORD_1); + } + private TraceFrame traceFrame(final MessageFrame frame) { return traceFrame( frame, @@ -306,6 +439,14 @@ private TraceFrame traceFrame( return getOnlyTraceFrame(tracer); } + private void traceFrame( + final MessageFrame frame, final DebugOperationTracer tracer, final Operation operation) { + frame.setCurrentOperation(operation); + tracer.tracePreExecution(frame); + OperationResult operationResult = operation.execute(frame, null); + tracer.tracePostExecution(frame, operationResult); + } + private MessageFrame validMessageFrame() { final MessageFrame frame = validMessageFrameBuilder().build(); frame.setCurrentOperation(anOperation); @@ -337,6 +478,13 @@ private MessageFrameTestFixture validMessageFrameBuilder() { .blockchain(blockchain); } + private MessageFrame validMessageFrameWithInitiatedMemory(final Bytes32 initiatedMemory) { + final MessageFrame frame = validMessageFrameBuilder().build(); + frame.writeMemory(0L, 32, initiatedMemory); + + return frame; + } + private Map setupStorageForCapture(final MessageFrame frame) { final MutableAccount account = mock(MutableAccount.class); when(worldUpdater.getAccount(frame.getRecipientAddress())).thenReturn(account); @@ -353,4 +501,14 @@ private Map setupStorageForCapture(final MessageFrame frame) { frame.writeMemory(64, 32, word3); return updatedStorage; } + + private static DebugOperationTracer createDebugOperationTracerWithMemory() { + final OpCodeTracerConfig config = + OpCodeTracerConfigBuilder.createFrom(OpCodeTracerConfig.DEFAULT) + .traceMemory(true) + .traceStack(false) + .traceStorage(false) + .build(); + return new DebugOperationTracer(config, false); + } } From d741455c309be60d1dee685f1fc4b56f7470376e Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Mon, 23 Mar 2026 12:02:07 +0100 Subject: [PATCH 73/77] Update Amsterdam to bal@v5.4.0 spec (#10075) * update devnet test Signed-off-by: daniellehrner * oversized code no longer charges state gas Signed-off-by: daniellehrner * do not force-charge state gas when code deposit state gas is insufficient When charge_state_gas fails (OutOfGasError), the spec modifies nothing: no reservoir drain, no stateGasUsed increment. Remove consumeStateGasForced which was incorrectly inflating stateGasUsed on failure, fixing short_one_gas reference tests. Co-Authored-By: Claude Opus 4.6 Signed-off-by: daniellehrner * Track collision-burned gas in regular_gas_used so 2D block gas accounting correctly reflects gas consumed on EIP-684 address collisions Signed-off-by: daniellehrner * addressed pr comments Signed-off-by: daniellehrner --------- Signed-off-by: daniellehrner Co-authored-by: Claude Opus 4.6 --- .../mainnet/MainnetTransactionProcessor.java | 1 - .../mainnet/TransactionGasAccounting.java | 17 +-- .../mainnet/TransactionGasAccountingTest.java | 3 +- ethereum/referencetests/build.gradle | 2 +- .../besu/evm/frame/MessageFrame.java | 20 --- .../hyperledger/besu/evm/frame/TxValues.java | 7 +- .../operation/AbstractCreateOperation.java | 8 +- .../processor/AbstractMessageProcessor.java | 16 +-- .../processor/ContractCreationProcessor.java | 121 ++++++++---------- .../ContractCreationProcessorTest.java | 24 ++-- gradle/verification-metadata.xml | 6 +- 11 files changed, 79 insertions(+), 146 deletions(-) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java index 666ca545831..bc58ca284c3 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java @@ -536,7 +536,6 @@ public TransactionProcessingResult processTransaction( .stateGasUsed(initialFrame.getStateGasUsed()) .initialFrameStateGasSpill(initialFrameStateGasSpill) .stateGasSpillBurned(initialFrame.getStateGasSpillBurned()) - .regularGasCollisionBurned(initialFrame.getRegularGasCollisionBurned()) .refundedGas(refundedGas) .floorCost(floorCost) .regularGasLimitExceeded(regularGasLimitExceeded) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java index c99be511830..08e4722c6c7 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java @@ -55,9 +55,6 @@ public record GasResult(long effectiveStateGas, long gasUsedByTransaction, long /** Total state gas spilled into gasRemaining from reverted frames. */ public abstract long stateGasSpillBurned(); - /** Gas burned by CREATE children that halted before executing code (address collision). */ - public abstract long regularGasCollisionBurned(); - /** Gas refunded to the sender. */ public abstract long refundedGas(); @@ -95,32 +92,26 @@ public GasResult calculate() { // EIP-8037: Include leftover reservoir in remaining gas for execution gas calculation final long executionGas = txGasLimit() - remainingGas() - stateGasReservoir(); // EIP-8037: Floor applies to regular gas only, not total gas. - // Pre-Amsterdam: stateGasUsed=0, spillBurned=0, collisionBurned=0 — identical. + // Pre-Amsterdam: stateGasUsed=0, spillBurned=0 — identical. // stateGasSpillBurned: state gas that spilled into gasRemaining from reverted frames. // For child frame reverts: not metered as regular or state gas (invisible to block). // For initial frame revert/halt: counts as state gas for block accounting, because the // transaction's state gas consumption is final regardless of execution outcome. - // regularGasCollisionBurned: gas burned by CREATE children that halted before executing - // code (address collision); excluded from block regular gas but still charged as fees. final long stateGas = stateGasUsed() + initialFrameStateGasSpill(); // initialFrameStateGasSpill is already included in spillBurned AND stateGas, // so subtract it from spillBurned to avoid double-counting. final long regularGas = - executionGas - - stateGas - - (stateGasSpillBurned() - initialFrameStateGasSpill()) - - regularGasCollisionBurned(); + executionGas - stateGas - (stateGasSpillBurned() - initialFrameStateGasSpill()); if (regularGas < 0) { // This should not happen under normal circumstances. A negative regularGas indicates a // bug in gas accounting — log at error level to ensure visibility. LOG.error( - "Negative regularGas={} (executionGas={}, stateGas={}, spillBurned={}, initialSpill={}, collisionBurned={})", + "Negative regularGas={} (executionGas={}, stateGas={}, spillBurned={}, initialSpill={})", regularGas, executionGas, stateGas, stateGasSpillBurned(), - initialFrameStateGasSpill(), - regularGasCollisionBurned()); + initialFrameStateGasSpill()); } final long gasUsedByTransaction = Math.max(regularGas, floorCost()) + stateGas; final long usedGas = txGasLimit() - refundedGas(); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccountingTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccountingTest.java index ff021908f53..99082dd2408 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccountingTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccountingTest.java @@ -31,7 +31,6 @@ private static ImmutableTransactionGasAccounting.Builder baseBuilder() { .stateGasUsed(0L) .initialFrameStateGasSpill(0L) .stateGasSpillBurned(0L) - .regularGasCollisionBurned(0L) .refundedGas(0L) .floorCost(0L) .regularGasLimitExceeded(false); @@ -143,7 +142,7 @@ public void stateGasSpill_doubleCountingAvoided() { @Test public void zeroStateGas_preAmsterdamEquivalent() { - // Pre-Amsterdam: stateGasUsed=0, spillBurned=0, collisionBurned=0 + // Pre-Amsterdam: stateGasUsed=0, spillBurned=0 // Should behave identically to pre-8037 gas accounting final var result = baseBuilder() diff --git a/ethereum/referencetests/build.gradle b/ethereum/referencetests/build.gradle index 655f4ba774f..bd92b0b625e 100644 --- a/ethereum/referencetests/build.gradle +++ b/ethereum/referencetests/build.gradle @@ -314,7 +314,7 @@ dependencies { ) devnetTarConfig( group: 'ethereum', name: 'execution-spec-tests', - version: 'bal@v5.3.0', classifier: 'fixtures_bal', ext: 'tar.gz' + version: 'bal@v5.4.0', classifier: 'fixtures_bal', ext: 'tar.gz' ) referenceTestImplementation 'com.fasterxml.jackson.core:jackson-databind' referenceTestImplementation 'com.google.guava:guava' diff --git a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java index 29f2c597a22..bdce7e34c13 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java @@ -919,26 +919,6 @@ public long getStateGasSpillBurned() { return txValues.stateGasSpillBurned()[0]; } - /** - * Accumulates regular gas burned by a CREATE child frame that halted before executing code - * (address collision). NOT undone on revert — excluded from block gas accounting but still - * charged for fee purposes. - * - * @param amount the collision gas amount to accumulate - */ - public void accumulateRegularGasCollisionBurned(final long amount) { - txValues.regularGasCollisionBurned()[0] += amount; - } - - /** - * Returns accumulated regular gas burned by pre-execution CREATE collision halts. - * - * @return accumulated collision gas burned - */ - public long getRegularGasCollisionBurned() { - return txValues.regularGasCollisionBurned()[0]; - } - /** * Add recipient to the self-destruct set if not already present. * diff --git a/evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java b/evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java index 5fbe6b878fd..679402f2315 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java @@ -56,9 +56,6 @@ * 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 regularGasCollisionBurned EIP-8037 accumulated regular gas burned by CREATE child frames - * that halted before executing any code (address collision); NOT undone on revert. Excluded - * from block regular gas accounting but still counts toward fee deduction. */ public record TxValues( BlockHashLookup blockHashLookup, @@ -78,8 +75,7 @@ public record TxValues( UndoScalar gasRefunds, UndoScalar stateGasUsed, UndoScalar stateGasReservoir, - long[] stateGasSpillBurned, - long[] regularGasCollisionBurned) { + long[] stateGasSpillBurned) { /** * Creates a new TxValues for the initial (depth-0) frame of a transaction. EIP-8037 gas tracking @@ -124,7 +120,6 @@ public static TxValues forTransaction( new UndoScalar<>(0L), new UndoScalar<>(0L), new UndoScalar<>(0L), - new long[] {0L}, new long[] {0L}); } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java index 8e7791c9e47..b3a4d50e9a3 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java @@ -89,9 +89,7 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { frame.clearReturnData(); - // EIP-8037: Charge state gas for CREATE operation (new account creation: 112 * cpsb) - // Charged before balance/depth/initcode-size checks — state gas is consumed even on failure. - // For oversized initcode, the spill mechanism tracks the burned state gas via spillBurned. + // EIP-8037: Charge state gas for CREATE operation. if (!gasCalculator().stateGasCostCalculator().chargeCreateStateGas(frame)) { return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); } @@ -103,7 +101,9 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); } - Code code = codeSupplier.get(); + // Resolve initcode after state gas charge to avoid unnecessary work (e.g. memory read, + // Code object creation) on paths where state gas is insufficient. + final Code code = codeSupplier.get(); if (code != null && code.getSize() > evm.getMaxInitcodeSize()) { frame.popStackItems(getStackItemsConsumed()); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java b/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java index 5e6677c6a34..b56dad19c1f 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java @@ -167,21 +167,9 @@ private void handleStateGasSpill(final MessageFrame frame) { * Gets called when the message frame encounters an exceptional halt. * * @param frame The message frame - * @param preExecutionHalt true if the halt occurred before any code was executed (e.g. address - * collision in CONTRACT_CREATION detected in start()) */ - private void exceptionalHalt(final MessageFrame frame, final boolean preExecutionHalt) { + private void exceptionalHalt(final MessageFrame frame) { handleStateGasSpill(frame); - - // EIP-8037: Gas burned by a CREATE child that halted before executing any code (address - // collision) is excluded from block regular gas accounting. It still counts toward fees. - if (preExecutionHalt && frame.getType() == MessageFrame.Type.CONTRACT_CREATION) { - final long collisionGas = frame.getRemainingGas(); - if (collisionGas > 0) { - frame.accumulateRegularGasCollisionBurned(collisionGas); - } - } - frame.clearGasRemaining(); frame.clearOutputData(); frame.setState(MessageFrame.State.COMPLETED_FAILED); @@ -262,7 +250,7 @@ public void process(final MessageFrame frame, final OperationTracer operationTra } if (frame.getState() == MessageFrame.State.EXCEPTIONAL_HALT) { - exceptionalHalt(frame, !wasCodeExecuting); + exceptionalHalt(frame); } if (frame.getState() == MessageFrame.State.REVERT) { diff --git a/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java b/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java index d264c91e9d6..cb67c029fb4 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java @@ -169,8 +169,26 @@ public void codeSuccess(final MessageFrame frame, final OperationTracer operatio final Bytes contractCode = frame.getCreatedCode() == null ? frame.getOutputData() : frame.getCreatedCode().getBytes(); - final long depositFee = evm.getGasCalculator().codeDepositGasCost(contractCode.size()); + // Oversized contracts must fail without charging code deposit gas or state gas. + // We must check this first. + final Optional firstValidationFailure = + contractValidationRules.stream() + .map(rule -> rule.validate(contractCode, frame, evm)) + .flatMap(Optional::stream) + .findFirst(); + if (firstValidationFailure.isPresent()) { + if (frame.getDepth() == 0) { + failCodeDepositWithoutRollback(frame, operationTracer, firstValidationFailure); + } else { + frame.setExceptionalHaltReason(firstValidationFailure); + frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); + operationTracer.traceAccountCreationResult(frame, firstValidationFailure); + } + return; + } + // Check and charge code deposit gas (regular gas) before state gas + final long depositFee = evm.getGasCalculator().codeDepositGasCost(contractCode.size()); if (frame.getRemainingGas() < depositFee) { LOG.trace( "Not enough gas to pay the code deposit fee for {}: " @@ -187,80 +205,41 @@ public void codeSuccess(final MessageFrame frame, final OperationTracer operatio } else { frame.setState(MessageFrame.State.COMPLETED_SUCCESS); } - } else { - final var invalidReason = - contractValidationRules.stream() - .map(rule -> rule.validate(contractCode, frame, evm)) - .filter(Optional::isPresent) - .findFirst(); - if (invalidReason.isEmpty()) { - frame.decrementRemainingGas(depositFee); - - // EIP-8037: Charge state gas for code deposit (cpsb * codeSize) - if (!evm.getGasCalculator() - .stateGasCostCalculator() - .chargeCodeDepositStateGas(frame, contractCode.size())) { - LOG.trace("Contract creation error: insufficient state gas for code deposit"); - // EIP-8037: For depth-0 (initial tx) frames, use forced charge + no-rollback failure so - // that stateGasUsed is preserved for block gas accounting (e.g. short_one_gas test). - if (frame.getDepth() == 0) { - final long stateGasAmount = - evm.getGasCalculator() - .stateGasCostCalculator() - .codeDepositStateGas(contractCode.size(), frame.getBlockValues().getGasLimit()); - if (stateGasAmount > 0) { - frame.consumeStateGasForced(stateGasAmount); - } - failCodeDepositWithoutRollback( - frame, operationTracer, Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); - } else { - frame.setExceptionalHaltReason(Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); - frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); - operationTracer.traceAccountCreationResult( - frame, Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); - } - return; - } + return; + } + frame.decrementRemainingGas(depositFee); - // Finalize contract creation, setting the contract code. - final MutableAccount contract = - frame.getWorldUpdater().getOrCreate(frame.getContractAddress()); - contract.setCode(contractCode); - LOG.trace( - "Successful creation of contract {} with code of size {} (Gas remaining: {})", - frame.getContractAddress(), - contractCode.size(), - frame.getRemainingGas()); - frame.setState(MessageFrame.State.COMPLETED_SUCCESS); - if (operationTracer.isExtendedTracing()) { - operationTracer.traceAccountCreationResult(frame, Optional.empty()); - } + // Only now charge state gas for code deposit (cpsb * codeSize). + if (!evm.getGasCalculator() + .stateGasCostCalculator() + .chargeCodeDepositStateGas(frame, contractCode.size())) { + LOG.trace("Contract creation error: insufficient state gas for code deposit"); + if (frame.getDepth() == 0) { + // Do NOT force-charge state gas here. The spec's charge_state_gas raises + // OutOfGasError without modifying anything (no reservoir drain, no stateGasUsed + // increment). failCodeDepositWithoutRollback clears remaining gas, matching + // the spec's exception handler behavior of burning gas_left. + failCodeDepositWithoutRollback( + frame, operationTracer, Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); } else { - // EIP-8037: For depth-0 (initial tx) frames with code that fails validation (e.g. - // CODE_TOO_LARGE), charge the code deposit state gas before failing so the charge is - // preserved in stateGasUsed for block gas accounting (e.g. over_max test). - // Use consumeStateGas (not forced) — it returns false without modifying on failure, so - // we can safely fall through to EXCEPTIONAL_HALT if gas is insufficient. - if (frame.getDepth() == 0) { - final long stateGasAmount = - evm.getGasCalculator() - .stateGasCostCalculator() - .codeDepositStateGas(contractCode.size(), frame.getBlockValues().getGasLimit()); - if (stateGasAmount > 0 && frame.consumeStateGas(stateGasAmount)) { - // Sufficient state gas: charge succeeded, fail without rollback so stateGasUsed - // is preserved for block accounting. - final Optional haltReason = invalidReason.get(); - failCodeDepositWithoutRollback(frame, operationTracer, haltReason); - return; - } - // Insufficient state gas or no state gas (non-EIP-8037): fall through to - // EXCEPTIONAL_HALT. - } - final Optional exceptionalHaltReason = invalidReason.get(); - frame.setExceptionalHaltReason(exceptionalHaltReason); + frame.setExceptionalHaltReason(Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); - operationTracer.traceAccountCreationResult(frame, exceptionalHaltReason); + operationTracer.traceAccountCreationResult( + frame, Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS)); } + return; + } + + final MutableAccount contract = frame.getWorldUpdater().getOrCreate(frame.getContractAddress()); + contract.setCode(contractCode); + LOG.trace( + "Successful creation of contract {} with code of size {} (Gas remaining: {})", + frame.getContractAddress(), + contractCode.size(), + frame.getRemainingGas()); + frame.setState(MessageFrame.State.COMPLETED_SUCCESS); + if (operationTracer.isExtendedTracing()) { + operationTracer.traceAccountCreationResult(frame, Optional.empty()); } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/processor/ContractCreationProcessorTest.java b/evm/src/test/java/org/hyperledger/besu/evm/processor/ContractCreationProcessorTest.java index e173146b489..4ac2f704866 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/processor/ContractCreationProcessorTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/processor/ContractCreationProcessorTest.java @@ -15,15 +15,14 @@ package org.hyperledger.besu.evm.processor; import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.evm.frame.MessageFrame.State.COMPLETED_FAILED; import static org.hyperledger.besu.evm.frame.MessageFrame.State.COMPLETED_SUCCESS; -import static org.hyperledger.besu.evm.frame.MessageFrame.State.EXCEPTIONAL_HALT; import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.EvmSpecVersion; import org.hyperledger.besu.evm.MainnetEVMs; import org.hyperledger.besu.evm.contractvalidation.MaxCodeSizeRule; import org.hyperledger.besu.evm.contractvalidation.PrefixCodeRule; -import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.internal.EvmConfiguration; import org.hyperledger.besu.evm.testutils.TestMessageFrameBuilder; @@ -56,9 +55,10 @@ void shouldThrowAnExceptionWhenCodeContractFormatInvalidPreEOF() { messageFrame.setGasRemaining(10600L); processor.codeSuccess(messageFrame, OperationTracer.NO_TRACING); - assertThat(messageFrame.getState()).isEqualTo(EXCEPTIONAL_HALT); - assertThat(messageFrame.getExceptionalHaltReason()) - .contains(ExceptionalHaltReason.INVALID_CODE); + // At depth 0, validation failures use COMPLETED_FAILED to preserve state gas reservoir + assertThat(messageFrame.getState()).isEqualTo(COMPLETED_FAILED); + // Gas should be cleared — failCodeDepositWithoutRollback burns all remaining gas + assertThat(messageFrame.getRemainingGas()).isEqualTo(0L); } @Test @@ -102,9 +102,10 @@ void shouldThrowAnExceptionWhenCodeContractTooLarge() { messageFrame.setGasRemaining(10_000_000L); processor.codeSuccess(messageFrame, OperationTracer.NO_TRACING); - assertThat(messageFrame.getState()).isEqualTo(EXCEPTIONAL_HALT); - assertThat(messageFrame.getExceptionalHaltReason()) - .contains(ExceptionalHaltReason.CODE_TOO_LARGE); + // At depth 0, validation failures use COMPLETED_FAILED to preserve state gas reservoir + assertThat(messageFrame.getState()).isEqualTo(COMPLETED_FAILED); + // Gas should be cleared — failCodeDepositWithoutRollback burns all remaining gas + assertThat(messageFrame.getRemainingGas()).isEqualTo(0L); } @Test @@ -142,9 +143,10 @@ void shouldRejectDeployedCodeAboveAmsterdamLimit() { messageFrame.setGasRemaining(10_000_000L); processor.codeSuccess(messageFrame, OperationTracer.NO_TRACING); - assertThat(messageFrame.getState()).isEqualTo(EXCEPTIONAL_HALT); - assertThat(messageFrame.getExceptionalHaltReason()) - .contains(ExceptionalHaltReason.CODE_TOO_LARGE); + // At depth 0, validation failures use COMPLETED_FAILED to preserve state gas reservoir + assertThat(messageFrame.getState()).isEqualTo(COMPLETED_FAILED); + // Gas should be cleared — failCodeDepositWithoutRollback burns all remaining gas + assertThat(messageFrame.getRemainingGas()).isEqualTo(0L); } @Test diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e3c72034b1e..eea257e5128 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1721,9 +1721,9 @@ - - - + + + From e12463010b356bda0110bbb63924e82c99189991 Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Wed, 18 Mar 2026 10:06:14 +0100 Subject: [PATCH 74/77] oversized code no longer charges state gas Signed-off-by: daniellehrner --- .../besu/evm/processor/ContractCreationProcessor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java b/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java index cb67c029fb4..dd5b144b201 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java @@ -169,6 +169,7 @@ public void codeSuccess(final MessageFrame frame, final OperationTracer operatio final Bytes contractCode = frame.getCreatedCode() == null ? frame.getOutputData() : frame.getCreatedCode().getBytes(); + // Oversized contracts must fail without charging code deposit gas or state gas. // We must check this first. final Optional firstValidationFailure = From 9e311959850d3dc2a4675ced993b8061df76d3a1 Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Thu, 19 Mar 2026 14:19:29 +0100 Subject: [PATCH 75/77] charge SSTORE regular gas before state gas per EIP-8037 ordering requirement EIP-8037 specifies that regular gas must be deducted before state gas so the reservoir/gasRemaining split is correct when state gas overflows. Also updates devnet reference tests to bal@v5.5.1. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: daniellehrner --- ethereum/referencetests/build.gradle | 10 +++++----- .../besu/evm/operation/SStoreOperation.java | 8 ++++++++ gradle/verification-metadata.xml | 6 +++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ethereum/referencetests/build.gradle b/ethereum/referencetests/build.gradle index bd92b0b625e..3aba4c6cbee 100644 --- a/ethereum/referencetests/build.gradle +++ b/ethereum/referencetests/build.gradle @@ -314,7 +314,7 @@ dependencies { ) devnetTarConfig( group: 'ethereum', name: 'execution-spec-tests', - version: 'bal@v5.4.0', classifier: 'fixtures_bal', ext: 'tar.gz' + version: 'bal@v5.5.1', classifier: 'fixtures_bal', ext: 'tar.gz' ) referenceTestImplementation 'com.fasterxml.jackson.core:jackson-databind' referenceTestImplementation 'com.google.guava:guava' @@ -367,14 +367,14 @@ tasks.register('validateReferenceTestSubmodule') { if (!result.toString().contains(expectedHash)) { throw new GradleException("""For the Ethereum Reference Tests the git commit did not match what was expected. - -If this is a deliberate change where you are updating the reference tests -then update "expectedHash" in `ethereum/referencetests/build.gradle` as the + +If this is a deliberate change where you are updating the reference tests +then update "expectedHash" in `ethereum/referencetests/build.gradle` as the commit hash for this task. Expected hash : ${expectedHash} Full git output : ${result} -If this is accidental you can correct the reference test versions with the +If this is accidental you can correct the reference test versions with the following commands: pushd ${submodulePath} git fetch diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/SStoreOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/SStoreOperation.java index 92a1de7b975..79b5843b8fc 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/SStoreOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/SStoreOperation.java @@ -96,6 +96,11 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); } + // EIP-8037: Deduct regular gas before charging state gas (ordering requirement). + // State gas draws from the reservoir first, then from gasRemaining; deducting regular + // gas first ensures the reservoir/gasRemaining split is correct. + frame.decrementRemainingGas(cost); + // Increment the refund counter. frame.incrementGasRefund( gasCalculator() @@ -113,6 +118,9 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); } + // Add regular gas back — the EVM loop will deduct it via the OperationResult. + frame.incrementRemainingGas(cost); + account.setStorageValue(key, newValue); frame.storageWasUpdated(key, newValue); frame.getEip7928AccessList().ifPresent(t -> t.addSlotAccessForAccount(address, key)); diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index eea257e5128..883b9790810 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1721,9 +1721,9 @@ - - - + + + From 35d95e6251a2ec454863be6996472a20fd593f79 Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Thu, 19 Mar 2026 18:48:10 +0100 Subject: [PATCH 76/77] charge SELFDESTRUCT and CREATE regular gas before state gas per EIP-8037 ordering requirement Same fix as the prior SSTORE commit: deduct regular gas before charging state gas so the reservoir/gasRemaining split is correct. Fixes the remaining 6 failing amsterdam devnet reference tests (bal@v5.5.1). Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: daniellehrner --- .../besu/evm/operation/AbstractCreateOperation.java | 11 +++++------ .../besu/evm/operation/SelfDestructOperation.java | 6 ++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java index b3a4d50e9a3..99f775168e0 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java @@ -89,17 +89,16 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { frame.clearReturnData(); + // EIP-8037: Deduct regular gas before charging state gas (ordering requirement). + frame.decrementRemainingGas(cost); + // EIP-8037: Charge state gas for CREATE operation. if (!gasCalculator().stateGasCostCalculator().chargeCreateStateGas(frame)) { return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); } - // Re-check after state gas charge: when the reservoir is empty, consumeStateGas spills - // overflow into gasRemaining. The subsequent decrementRemainingGas(cost) would underflow - // if gasRemaining was reduced below cost by the spill. - if (frame.getRemainingGas() < cost) { - return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); - } + // Add regular gas back — the EVM loop will deduct it via the OperationResult. + frame.incrementRemainingGas(cost); // Resolve initcode after state gas charge to avoid unnecessary work (e.g. memory read, // Code object creation) on paths where state gas is insufficient. diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/SelfDestructOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/SelfDestructOperation.java index c7dd3d46ccc..92cb470aeaa 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/SelfDestructOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/SelfDestructOperation.java @@ -103,6 +103,9 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); } + // EIP-8037: Deduct regular gas before charging state gas (ordering requirement). + frame.decrementRemainingGas(cost); + // EIP-8037: Charge state gas for new account creation in SELFDESTRUCT if (!gasCalculator() .stateGasCostCalculator() @@ -110,6 +113,9 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); } + // Add regular gas back — the EVM loop will deduct it via the OperationResult. + frame.incrementRemainingGas(cost); + // We passed preliminary checks, get mutable accounts. final MutableAccount beneficiaryAccount = getOrCreateAccount(beneficiaryAddress, frame); From d1293d8edd2c82e7c92d51eca7591a97aebc2ba3 Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Mon, 23 Mar 2026 13:00:23 +0100 Subject: [PATCH 77/77] spotless Signed-off-by: daniellehrner --- .../besu/evm/processor/ContractCreationProcessor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java b/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java index dd5b144b201..cb67c029fb4 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/processor/ContractCreationProcessor.java @@ -169,7 +169,6 @@ public void codeSuccess(final MessageFrame frame, final OperationTracer operatio final Bytes contractCode = frame.getCreatedCode() == null ? frame.getOutputData() : frame.getCreatedCode().getBytes(); - // Oversized contracts must fail without charging code deposit gas or state gas. // We must check this first. final Optional firstValidationFailure =