diff --git a/CHANGELOG.md b/CHANGELOG.md index cc3386e1cd3..8d5b6694d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,7 @@ are provided with different values, using input as per the execution-apis spec i - Add ability to pass a custom tracer to block simulation [#9708](https://github.com/hyperledger/besu/pull/9708) - Add support for `4byteTracer` in `debug_trace*` methods to collect function selectors from internal calls via PR [#9642](https://github.com/hyperledger/besu/pull/9642). Thanks to [@JukLee0ira](https://github.com/JukLee0ira). - Update assertj to v3.27.7 [#9710](https://github.com/hyperledger/besu/pull/9710) +- Update rocksdbjni lib version from 9.7.3 to 10.6.2 [#9767](https://github.com/hyperledger/besu/pull/9767) - Update vertx to 4.5.24 [#9645](https://github.com/hyperledger/besu/pull/9645) - Add byte-level metrics for P2P message exchange [#9666](https://github.com/hyperledger/besu/pull/9666) - 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/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 201333f5988..7ff46565ef4 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 @@ -62,7 +62,6 @@ import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; @@ -210,11 +209,4 @@ protected void waitForFile(final Path path) { } }); } - - @Test - public void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/jsonrpc/AbstractJsonRpcTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/jsonrpc/AbstractJsonRpcTest.java index 12907491763..d88b1ed8b55 100644 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/jsonrpc/AbstractJsonRpcTest.java +++ b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/jsonrpc/AbstractJsonRpcTest.java @@ -40,7 +40,6 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -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; @@ -148,11 +147,4 @@ public static Stream testCasesFromPath(final String testCasesPath) return Arrays.stream(testCasesList).sorted().map(File::toURI).map(Arguments::of); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/acceptance-tests/tests/src/acceptanceTest/resources/jsonrpc/engine/paris/test-cases/08_unknown_payload.json b/acceptance-tests/tests/src/acceptanceTest/resources/jsonrpc/engine/paris/test-cases/08_unknown_payload.json index 21ccd28c154..de7eb865dea 100644 --- a/acceptance-tests/tests/src/acceptanceTest/resources/jsonrpc/engine/paris/test-cases/08_unknown_payload.json +++ b/acceptance-tests/tests/src/acceptanceTest/resources/jsonrpc/engine/paris/test-cases/08_unknown_payload.json @@ -11,8 +11,8 @@ "jsonrpc": "2.0", "id": 67, "error": { - "code": -32001, - "message": "Payload does not exist / is not available" + "code": -38001, + "message": "Unknown payload" } }, "statusCode": 200 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 5b2bfd0b364..2767db2ebfa 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -1917,7 +1917,10 @@ private void configure() throws Exception { jsonRpcConfiguration = jsonRpcHttpOptions.jsonRpcConfiguration( - hostsAllowlist, p2PDiscoveryConfig.p2pHost(), unstableRPCOptions.getHttpTimeoutSec()); + hostsAllowlist, + p2PDiscoveryConfig.p2pHost(), + unstableRPCOptions.getHttpTimeoutSec(), + unstableRPCOptions.getHttpStreamingTimeoutSec()); logger.info("RPC HTTP JSON-RPC config: {}", jsonRpcConfiguration); if (isEngineApiEnabled()) { engineJsonRpcConfiguration = createEngineJsonRpcConfiguration(); @@ -2086,7 +2089,8 @@ private JsonRpcConfiguration createEngineJsonRpcConfiguration() { jsonRpcHttpOptions.jsonRpcConfiguration( engineRPCConfig.engineHostsAllowlist(), p2PDiscoveryConfig.p2pHost(), - unstableRPCOptions.getHttpTimeoutSec()); + unstableRPCOptions.getHttpTimeoutSec(), + unstableRPCOptions.getHttpStreamingTimeoutSec()); engineConfig.setPort(engineRPCConfig.engineRpcPort()); engineConfig.setRpcApis(Arrays.asList("ENGINE", "ETH")); engineConfig.setEnabled(isEngineApiEnabled()); diff --git a/app/src/main/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptions.java b/app/src/main/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptions.java index d77719f6677..3e5e6c6ad7c 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptions.java +++ b/app/src/main/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptions.java @@ -302,10 +302,14 @@ public JsonRpcConfiguration jsonRpcConfiguration() { * @param hostsAllowlist List of hosts allowed * @param defaultHostAddress Default host address * @param timeoutSec timeout in seconds + * @param streamingTimeoutSec timeout in seconds for streaming methods * @return A JsonRpcConfiguration instance */ public JsonRpcConfiguration jsonRpcConfiguration( - final List hostsAllowlist, final String defaultHostAddress, final Long timeoutSec) { + final List hostsAllowlist, + final String defaultHostAddress, + final Long timeoutSec, + final Long streamingTimeoutSec) { final JsonRpcConfiguration jsonRpcConfiguration = this.jsonRpcConfiguration(); @@ -313,6 +317,7 @@ public JsonRpcConfiguration jsonRpcConfiguration( Strings.isNullOrEmpty(rpcHttpHost) ? defaultHostAddress : rpcHttpHost); jsonRpcConfiguration.setHostsAllowlist(hostsAllowlist); jsonRpcConfiguration.setHttpTimeoutSec(timeoutSec); + jsonRpcConfiguration.setHttpStreamingTimeoutSec(streamingTimeoutSec); return jsonRpcConfiguration; } diff --git a/app/src/main/java/org/hyperledger/besu/cli/options/RPCOptions.java b/app/src/main/java/org/hyperledger/besu/cli/options/RPCOptions.java index 2ab264cbf1b..e833ce6dff4 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/options/RPCOptions.java +++ b/app/src/main/java/org/hyperledger/besu/cli/options/RPCOptions.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.cli.options; import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions; +import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; import picocli.CommandLine; @@ -27,6 +28,14 @@ public class RPCOptions { description = "HTTP timeout in seconds (default: ${DEFAULT-VALUE})") private final Long httpTimeoutSec = TimeoutOptions.defaultOptions().getTimeoutSeconds(); + @CommandLine.Option( + hidden = true, + names = {"--Xhttp-streaming-timeout-seconds"}, + description = + "HTTP timeout in seconds for streaming methods like debug_traceBlock (default: ${DEFAULT-VALUE})") + private final Long httpStreamingTimeoutSec = + JsonRpcConfiguration.DEFAULT_HTTP_STREAMING_TIMEOUT_SEC; + @CommandLine.Option( hidden = true, names = {"--Xws-timeout-seconds"}, @@ -54,6 +63,15 @@ public Long getHttpTimeoutSec() { return httpTimeoutSec; } + /** + * Gets http streaming timeout sec. + * + * @return the http streaming timeout sec + */ + public Long getHttpStreamingTimeoutSec() { + return httpStreamingTimeoutSec; + } + /** * Gets WebSocket timeout sec. * diff --git a/app/src/test/java/org/hyperledger/besu/ForkIdsNetworkConfigTest.java b/app/src/test/java/org/hyperledger/besu/ForkIdsNetworkConfigTest.java index a8196f07469..6a2799178d4 100644 --- a/app/src/test/java/org/hyperledger/besu/ForkIdsNetworkConfigTest.java +++ b/app/src/test/java/org/hyperledger/besu/ForkIdsNetworkConfigTest.java @@ -49,7 +49,6 @@ import com.google.common.collect.Streams; import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -207,11 +206,4 @@ public Stream streamMilestoneBlocks() { MilestoneStreamingProtocolSchedule::streamMilestoneBlocks); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/app/src/test/java/org/hyperledger/besu/chainimport/JsonBlockImporterTest.java b/app/src/test/java/org/hyperledger/besu/chainimport/JsonBlockImporterTest.java index 5de21513ee0..2d0a3539121 100644 --- a/app/src/test/java/org/hyperledger/besu/chainimport/JsonBlockImporterTest.java +++ b/app/src/test/java/org/hyperledger/besu/chainimport/JsonBlockImporterTest.java @@ -64,7 +64,6 @@ import dagger.Component; import dagger.Module; import dagger.Provides; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -348,13 +347,6 @@ public void importChain_specialFields(final String consensusEngine) throws IOExc .hasMessage( "Some fields (coinbase, extraData) are no longer supported for block import since PoW consensus has been removed"); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } protected Block getBlockAt(final Blockchain blockchain, final long blockNumber) { diff --git a/app/src/test/java/org/hyperledger/besu/cli/NetworkDeprecationMessageTest.java b/app/src/test/java/org/hyperledger/besu/cli/NetworkDeprecationMessageTest.java index df3e9801952..3f71d38b36f 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/NetworkDeprecationMessageTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/NetworkDeprecationMessageTest.java @@ -19,7 +19,6 @@ import org.hyperledger.besu.config.NetworkDefinition; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -42,11 +41,4 @@ void shouldThrowErrorForNonDeprecatedNetworks(final NetworkDefinition network) { assertThatThrownBy(() -> NetworkDeprecationMessage.generate(network)) .isInstanceOf(AssertionError.class); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/build.gradle b/build.gradle index 3a2833632f4..a226ed7e2c8 100644 --- a/build.gradle +++ b/build.gradle @@ -261,7 +261,7 @@ configure(allprojects - project(':platform')) { // Below this line are currently only license header tasks format 'ShellScripts', { target '**/*.sh' - targetExclude '**/src/reference-test/**', '**/src/main/generated/**', '**/src/test/generated/**', '**/src/jmh/generated/**' + targetExclude '**/src/reference-test/**', '**/src/main/generated/**', '**/src/test/generated/**', '**/src/jmh/generated/**', '**/bin/default/**' trimTrailingWhitespace() endWithNewline() diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/headervalidationrules/VoteValidationRuleTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/headervalidationrules/VoteValidationRuleTest.java index 0ba0e5f3c8b..252064ccab6 100644 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/headervalidationrules/VoteValidationRuleTest.java +++ b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/headervalidationrules/VoteValidationRuleTest.java @@ -22,7 +22,6 @@ import java.util.stream.Stream; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -53,11 +52,4 @@ public void test(final long input, final boolean expectedResult) { assertThat(uut.validate(header, null)).isEqualTo(expectedResult); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/consensus/merge/src/test/java/org/hyperledger/besu/consensus/merge/MergeProtocolScheduleTest.java b/consensus/merge/src/test/java/org/hyperledger/besu/consensus/merge/MergeProtocolScheduleTest.java index 867d5d723f3..a3000eda52f 100644 --- a/consensus/merge/src/test/java/org/hyperledger/besu/consensus/merge/MergeProtocolScheduleTest.java +++ b/consensus/merge/src/test/java/org/hyperledger/besu/consensus/merge/MergeProtocolScheduleTest.java @@ -18,6 +18,7 @@ import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.AMSTERDAM; import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.CANCUN; import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.PARIS; +import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.PRAGUE; import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.SHANGHAI; import org.hyperledger.besu.config.GenesisConfig; @@ -31,6 +32,7 @@ import org.hyperledger.besu.ethereum.mainnet.MainnetBlockProcessor; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.blockhash.PraguePreExecutionProcessor; import org.hyperledger.besu.evm.internal.EvmConfiguration; import org.hyperledger.besu.evm.operation.InvalidOperation; import org.hyperledger.besu.evm.operation.PrevRanDaoOperation; @@ -269,4 +271,76 @@ public void amsterdamHasBlockAccessListFactoryWithForkActivated() { .withFailMessage("BlockAccessListFactory should be present for Amsterdam, but it was empty") .isPresent(); } + + /** + * Verifies that a Clique-to-PoS network (with TTD set) uses PraguePreExecutionProcessor for + * post-merge Prague blocks, not FrontierPreExecutionProcessor. This is a regression test for a + * bug where isPoAConsensus() returned true for Clique genesis configs even when TTD was set, + * causing EIP-2935 and EIP-4788 system calls to be skipped for post-merge blocks. + */ + @Test + public void cliqueToPoSNetworkUsesPraguePreExecutionProcessorAfterMerge() { + final String jsonInput = + "{\"config\": " + + "{\"chainId\": 59139,\n" + + "\"homesteadBlock\": 0,\n" + + "\"eip150Block\": 0,\n" + + "\"eip155Block\": 0,\n" + + "\"eip158Block\": 0,\n" + + "\"byzantiumBlock\": 0,\n" + + "\"constantinopleBlock\": 0,\n" + + "\"petersburgBlock\": 0,\n" + + "\"istanbulBlock\": 0,\n" + + "\"berlinBlock\": 0,\n" + + "\"londonBlock\": 0,\n" + + "\"terminalTotalDifficulty\": 17628883,\n" + + "\"shanghaiTime\": 1755165600,\n" + + "\"pragueTime\": 1755770400,\n" + + "\"clique\": {\n" + + " \"blockperiodseconds\": 1,\n" + + " \"epochlength\": 30000,\n" + + " \"createemptyblocks\": true\n" + + "},\n" + + "\"depositContractAddress\": \"0x45152B0bD93Dc1e4c84d70e24edA3CEb12b1a1D3\",\n" + + "\"withdrawalRequestContractAddress\": \"0xF7B4391C85B1ad1eAF38cb4B45a235cDF9295a7D\",\n" + + "\"consolidationRequestContractAddress\": \"0x0bbfd3E844Cc1D63D4D86498Ca91FF6de417Ed73\"\n" + + "}}"; + + final GenesisConfigOptions config = GenesisConfig.fromConfig(jsonInput).getConfigOptions(); + + // Verify that the genesis is detected as Clique (this is expected) + assertThat(config.isClique()).isTrue(); + // Verify TTD is set (this is a Clique-to-PoS network) + assertThat(config.getTerminalTotalDifficulty()).isPresent(); + + final ProtocolSchedule protocolSchedule = + MergeProtocolSchedule.create( + config, + false, + MiningConfiguration.MINING_DISABLED, + new BadBlockManager(), + false, + BalConfiguration.DEFAULT, + new NoOpMetricsSystem(), + EvmConfiguration.DEFAULT); + + // Get Prague spec at post-merge timestamp + final ProtocolSpec pragueSpec = + protocolSchedule.getByBlockHeader( + new BlockHeaderTestFixture().number(18000000).timestamp(1755770400).buildHeader()); + + assertThat(pragueSpec.getHardforkId()).isEqualTo(PRAGUE); + assertProofOfStakeConfigIsEnabled(pragueSpec); + + // The critical assertion: Prague blocks on a Clique-to-PoS network must use + // PraguePreExecutionProcessor (for EIP-2935 and EIP-4788 system calls), + // NOT FrontierPreExecutionProcessor + assertThat(pragueSpec.getPreExecutionProcessor()) + .withFailMessage( + "Prague blocks on a Clique-to-PoS network should use PraguePreExecutionProcessor, " + + "but got %s. This means EIP-2935 blockhash storage and EIP-4788 beacon root " + + "system calls are being skipped, causing stateroot mismatches.", + pragueSpec.getPreExecutionProcessor().getClass().getSimpleName()) + .isInstanceOf(PraguePreExecutionProcessor.class); + } } diff --git a/ethereum/api/build.gradle b/ethereum/api/build.gradle index c013f6147ee..1ba3ad68f1d 100644 --- a/ethereum/api/build.gradle +++ b/ethereum/api/build.gradle @@ -34,6 +34,8 @@ dependencies { api 'org.slf4j:slf4j-api' api 'org.apache.logging.log4j:log4j-api' + annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess' + compileOnly 'org.jspecify:jspecify' implementation project(':config') @@ -83,6 +85,7 @@ dependencies { implementation 'com.github.ben-manes.caffeine:caffeine' annotationProcessor "org.immutables:value" + annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess' implementation "org.immutables:value-annotations" runtimeOnly 'org.bouncycastle:bcpkix-jdk18on' diff --git a/ethereum/api/src/jmh/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/CompactHexBenchmark.java b/ethereum/api/src/jmh/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/CompactHexBenchmark.java new file mode 100644 index 00000000000..bb12597690b --- /dev/null +++ b/ethereum/api/src/jmh/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/CompactHexBenchmark.java @@ -0,0 +1,94 @@ +/* + * 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 java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +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; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Benchmarks {@link HexWriter#encodeTo}. Each invocation encodes {@link #N} values to simulate a + * realistic per-opcode workload (stack entries). + * + *

Run with: {@code ./gradlew :ethereum:api:jmh -Pincludes=CompactHexBenchmark} + */ +@State(Scope.Thread) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +@Fork(3) +public class CompactHexBenchmark { + + private static final int N = 16; + private static final int BUF_SIZE = 32 * 1024; + + public enum ValueSize { + BYTES_1(1), + BYTES_8(8), + BYTES_20(20), + BYTES_32(32); + + final int size; + + ValueSize(final int size) { + this.size = size; + } + } + + @Param({"BYTES_1", "BYTES_8", "BYTES_20", "BYTES_32"}) + private ValueSize valueSize; + + private byte[][] values; + private byte[] writeBuf; + + @Setup + public void setup() { + final Random rng = new Random(42); + values = new byte[N][]; + for (int i = 0; i < N; i++) { + values[i] = new byte[valueSize.size]; + rng.nextBytes(values[i]); + // ~25% of values have leading zero bytes (realistic for stack values < 256 bits) + if (i % 4 == 0 && values[i].length > 1) { + values[i][0] = 0; + } + } + writeBuf = new byte[BUF_SIZE]; + } + + @Benchmark + @OperationsPerInvocation(N) + public void encodeTo(final Blackhole bh) { + int pos = 0; + for (final byte[] bytes : values) { + pos = HexWriter.encodeTo(bytes, bytes.length, writeBuf, pos, true); + } + bh.consume(pos); + } +} diff --git a/ethereum/api/src/jmh/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityLongToHexBenchmark.java b/ethereum/api/src/jmh/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityLongToHexBenchmark.java new file mode 100644 index 00000000000..acbacc920ae --- /dev/null +++ b/ethereum/api/src/jmh/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityLongToHexBenchmark.java @@ -0,0 +1,126 @@ +/* + * 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 java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.apache.tuweni.units.bigints.UInt256; +import org.apache.tuweni.units.bigints.UInt256Value; +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.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(value = TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class QuantityLongToHexBenchmark { + private static final String HEX_PREFIX = "0x"; + public static final String HEX_ZERO = "0x0"; + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + + @Param({"0", "255", "65535", "1000000", "9007199254740991", "9223372036854775807"}) + public long value; + + @Benchmark + public void current(final Blackhole blackhole) { + blackhole.consume(uint256ToHex(UInt256.fromHexString(Long.toHexString(value)))); + } + + @Benchmark + public void simpleHexString(final Blackhole blackhole) { + blackhole.consume(simpleHexString(value)); + } + + @Benchmark + public void directCharArray(final Blackhole blackhole) { + blackhole.consume(directCharArray(value)); + } + + @Benchmark + public void unrolled(final Blackhole blackhole) { + blackhole.consume(unrolled(value)); + } + + private static String simpleHexString(final long value) { + return HEX_PREFIX + Long.toHexString(value); + } + + private static String unrolled(final long value) { + if (value == 0L) { + return HEX_ZERO; + } + final int leadingZeroNibbles = Long.numberOfLeadingZeros(value) >>> 2; + final char[] buf = new char[18]; + buf[2] = HEX_DIGITS[(int) ((value >>> 60) & 0xF)]; + buf[3] = HEX_DIGITS[(int) ((value >>> 56) & 0xF)]; + buf[4] = HEX_DIGITS[(int) ((value >>> 52) & 0xF)]; + buf[5] = HEX_DIGITS[(int) ((value >>> 48) & 0xF)]; + buf[6] = HEX_DIGITS[(int) ((value >>> 44) & 0xF)]; + buf[7] = HEX_DIGITS[(int) ((value >>> 40) & 0xF)]; + buf[8] = HEX_DIGITS[(int) ((value >>> 36) & 0xF)]; + buf[9] = HEX_DIGITS[(int) ((value >>> 32) & 0xF)]; + buf[10] = HEX_DIGITS[(int) ((value >>> 28) & 0xF)]; + buf[11] = HEX_DIGITS[(int) ((value >>> 24) & 0xF)]; + buf[12] = HEX_DIGITS[(int) ((value >>> 20) & 0xF)]; + buf[13] = HEX_DIGITS[(int) ((value >>> 16) & 0xF)]; + buf[14] = HEX_DIGITS[(int) ((value >>> 12) & 0xF)]; + buf[15] = HEX_DIGITS[(int) ((value >>> 8) & 0xF)]; + buf[16] = HEX_DIGITS[(int) ((value >>> 4) & 0xF)]; + buf[17] = HEX_DIGITS[(int) (value & 0xF)]; + buf[leadingZeroNibbles] = '0'; + buf[leadingZeroNibbles + 1] = 'x'; + return new String(buf, leadingZeroNibbles, 18 - leadingZeroNibbles); + } + + private static String directCharArray(final long value) { + if (value == 0L) { + return HEX_ZERO; + } + int nibbles = 16; + while (nibbles > 1 && ((value >>> ((nibbles - 1) * 4)) & 0xFL) == 0L) { + nibbles--; + } + final char[] buf = new char[2 + nibbles]; + buf[0] = '0'; + buf[1] = 'x'; + for (int i = 0; i < nibbles; i++) { + buf[2 + i] = HEX_DIGITS[(int) ((value >>> ((nibbles - 1 - i) * 4)) & 0xFL)]; + } + return new String(buf); + } + + private static String uint256ToHex(final UInt256Value value) { + return value == null ? null : formatMinimalValue(value.toMinimalBytes().toShortHexString()); + } + + private static String formatMinimalValue(final String hexValue) { + final String prefixedHexString = prefixHexNotation(hexValue); + return Objects.equals(prefixedHexString, HEX_PREFIX) ? HEX_ZERO : prefixedHexString; + } + + private static String prefixHexNotation(final String hexValue) { + return hexValue.startsWith(HEX_PREFIX) ? hexValue : HEX_PREFIX + hexValue; + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/AbstractJsonRpcExecutor.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/AbstractJsonRpcExecutor.java index f73d162fa14..62d2c920313 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/AbstractJsonRpcExecutor.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/AbstractJsonRpcExecutor.java @@ -43,7 +43,7 @@ public abstract class AbstractJsonRpcExecutor { private static final Logger LOG = LoggerFactory.getLogger(AbstractJsonRpcExecutor.class); - private static final String SPAN_CONTEXT = "span_context"; + protected static final String SPAN_CONTEXT = "span_context"; final JsonRpcExecutor jsonRpcExecutor; final Tracer tracer; final RoutingContext ctx; @@ -97,9 +97,16 @@ protected static void handleJsonRpcError( final RoutingContext routingContext, final Object id, final RpcErrorType error) { final HttpServerResponse response = routingContext.response(); if (!response.closed()) { - response - .setStatusCode(statusCodeFromError(error).code()) - .end(Json.encode(new JsonRpcErrorResponse(id, error))); + if (response.headWritten()) { + // Streaming already started — cannot change status code or headers. + // Reset the connection so the client sees a transport error rather than + // silently receiving truncated JSON. + response.reset(); + } else { + response + .setStatusCode(statusCodeFromError(error).code()) + .end(Json.encode(new JsonRpcErrorResponse(id, error))); + } } } @@ -107,6 +114,7 @@ private static HttpResponseStatus statusCodeFromError(final RpcErrorType error) return switch (error) { case INVALID_REQUEST, PARSE_ERROR -> HttpResponseStatus.BAD_REQUEST; case TIMEOUT_ERROR -> HttpResponseStatus.REQUEST_TIMEOUT; + case UNAUTHORIZED -> HttpResponseStatus.UNAUTHORIZED; default -> HttpResponseStatus.OK; }; } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/HandlerFactory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/HandlerFactory.java index 03465f70f21..2d4b5abe1c3 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/HandlerFactory.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/HandlerFactory.java @@ -22,7 +22,6 @@ import java.util.Collection; import java.util.Map; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Collectors; import io.opentelemetry.api.trace.Tracer; @@ -34,10 +33,15 @@ public class HandlerFactory { public static Handler timeout( final TimeoutOptions globalOptions, final Map methods) { assert methods != null && globalOptions != null; + // Only explicitly registered non-streaming methods get a + // timeout from this handler. Streaming methods are excluded because they can + // run much longer than the default 30s. Their timeout is managed by JsonRpcExecutorHandler + // instead. return TimeoutHandler.handler( - Optional.of(globalOptions), - methods.keySet().stream() - .collect(Collectors.toMap(Function.identity(), ignored -> globalOptions))); + Optional.empty(), + methods.entrySet().stream() + .filter(e -> !e.getValue().isStreaming()) + .collect(Collectors.toMap(Map.Entry::getKey, ignored -> globalOptions))); } public static Handler authentication( diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcExecutorHandler.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcExecutorHandler.java index 84093bd5177..b0ae06904fa 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcExecutorHandler.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcExecutorHandler.java @@ -26,6 +26,7 @@ import io.opentelemetry.api.trace.Tracer; import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +41,7 @@ public static Handler handler( final Tracer tracer, final JsonRpcConfiguration jsonRpcConfiguration) { return ctx -> { - long timeoutMillis = jsonRpcConfiguration.getHttpTimeoutSec() * 1000; + final long timeoutMillis = resolveTimeoutMillis(ctx, jsonRpcExecutor, jsonRpcConfiguration); final long timerId = ctx.vertx() .setTimer( @@ -147,4 +148,17 @@ private static boolean isJsonObjectRequest(final RoutingContext ctx) { private static boolean isJsonArrayRequest(final RoutingContext ctx) { return ctx.data().containsKey(ContextKey.REQUEST_BODY_AS_JSON_ARRAY.name()); } + + private static long resolveTimeoutMillis( + final RoutingContext ctx, + final JsonRpcExecutor jsonRpcExecutor, + final JsonRpcConfiguration config) { + if (isJsonObjectRequest(ctx)) { + final JsonObject req = ctx.get(ContextKey.REQUEST_BODY_AS_JSON_OBJECT.name()); + if (req != null && jsonRpcExecutor.isStreamingMethod(req.getString("method"))) { + return config.getHttpStreamingTimeoutSec() * 1000; + } + } + return config.getHttpTimeoutSec() * 1000; + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcObjectExecutor.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcObjectExecutor.java index 70c78e9ab3e..3afec845791 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcObjectExecutor.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcObjectExecutor.java @@ -14,25 +14,28 @@ */ package org.hyperledger.besu.ethereum.api.handlers; -import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_JSON; - import org.hyperledger.besu.ethereum.api.jsonrpc.JsonResponseStreamer; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.context.ContextKey; import org.hyperledger.besu.ethereum.api.jsonrpc.execution.JsonRpcExecutor; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcRequestException; 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.RpcErrorType; import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; import java.io.IOException; +import java.util.Optional; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectWriter; import io.netty.handler.codec.http.HttpResponseStatus; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; import io.vertx.ext.web.RoutingContext; public class JsonRpcObjectExecutor extends AbstractJsonRpcExecutor { @@ -48,16 +51,70 @@ public JsonRpcObjectExecutor( @Override void execute() throws IOException { - HttpServerResponse response = ctx.response(); - response = response.putHeader("Content-Type", APPLICATION_JSON); - + final HttpServerResponse response = prepareHttpResponse(ctx); final JsonObject jsonRequest = ctx.get(ContextKey.REQUEST_BODY_AS_JSON_OBJECT.name()); + + if (jsonRpcExecutor.isStreamingMethod(jsonRequest.getString("method"))) { + executeStreamingMethod(response, jsonRequest); + return; + } + lazyTraceLogger(jsonRequest::toString); final JsonRpcResponse jsonRpcResponse = executeRequest(jsonRpcExecutor, tracer, jsonRequest, ctx); handleJsonObjectResponse(response, jsonRpcResponse, ctx); } + private void executeStreamingMethod( + final HttpServerResponse response, final JsonObject jsonRequest) throws IOException { + // Do NOT set the status code eagerly — let JsonResponseStreamer flush headers + // on first write. This keeps the response uncommitted so that pre-stream + // errors (bad params, auth failures, missing blocks) can still produce a + // proper HTTP error with the correct status code. + final JsonResponseStreamer streamer = + new JsonResponseStreamer(response, ctx.request().remoteAddress()); + try { + final Optional user = ContextKey.AUTHENTICATED_USER.extractFrom(ctx, Optional::empty); + final Context spanContext = ctx.get(SPAN_CONTEXT); + final Optional preStreamError = + jsonRpcExecutor.executeStreaming( + user, + tracer, + spanContext, + () -> !ctx.response().closed(), + jsonRequest, + req -> req.mapTo(JsonRpcRequest.class), + streamer, + getJsonObjectMapper()); + if (preStreamError.isPresent()) { + // Validation failed before any data was written to the stream. + // The streamer's close() is a no-op (chunked never set), so we can + // send a proper error response with the correct HTTP status code. + handleJsonObjectResponse(response, preStreamError.get(), ctx); + return; + } + // Streaming completed — end the chunked response. + streamer.close(); + } catch (final Exception e) { + if (!response.headWritten()) { + // Headers not flushed yet — send a proper HTTP error response. + final Object id = jsonRequest.getValue("id"); + final RpcErrorType errorType = + e instanceof InvalidJsonRpcRequestException ijrp + ? ijrp.getRpcErrorType() + : RpcErrorType.INTERNAL_ERROR; + handleJsonRpcError(ctx, id, errorType); + } else if (!response.ended()) { + // Streaming started but failed mid-stream — reset the connection so the + // client sees a transport error rather than truncated JSON. + response.reset(); + } + if (e instanceof IOException ioe) { + throw ioe; + } + } + } + @Override String getRpcMethodName(final RoutingContext ctx) { final JsonObject jsonObject = ctx.get(ContextKey.REQUEST_BODY_AS_JSON_OBJECT.name()); @@ -72,13 +129,13 @@ private void handleJsonObjectResponse( response.setStatusCode(status(jsonRpcResponse).code()); if (jsonRpcResponse.getType() == RpcResponseType.NONE) { response.end(); - } else { - try (final JsonResponseStreamer streamer = - new JsonResponseStreamer(response, ctx.request().remoteAddress())) { - // underlying output stream lifecycle is managed by the json object writer - lazyTraceLogger(() -> getJsonObjectMapper().writeValueAsString(jsonRpcResponse)); - jsonObjectWriter.writeValue(streamer, jsonRpcResponse); - } + return; + } + + try (final JsonResponseStreamer streamer = + new JsonResponseStreamer(response, ctx.request().remoteAddress())) { + lazyTraceLogger(() -> getJsonObjectMapper().writeValueAsString(jsonRpcResponse)); + jsonObjectWriter.writeValue(streamer, jsonRpcResponse); } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/TimeoutHandler.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/TimeoutHandler.java index 69093c08d0f..5981956f1a5 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/TimeoutHandler.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/TimeoutHandler.java @@ -53,7 +53,19 @@ private static void processHandler( .setTimer( timeoutOptions.getTimeoutMillis(), t -> { - ctx.fail(timeoutOptions.getErrorCode()); + final var response = ctx.response(); + if (response.ended() || response.closed()) { + // Response already finished — nothing to do. + return; + } + if (response.headWritten()) { + // Streaming in progress — headers already flushed so we + // cannot change the status code. Reset the connection + // (RST_STREAM for HTTP/2, TCP close for HTTP/1.1). + response.reset(); + } else { + ctx.fail(timeoutOptions.getErrorCode()); + } ctx.request().connection().close(); }); ctx.addBodyEndHandler(v -> ctx.vertx().cancelTimer(tid)); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonResponseStreamer.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonResponseStreamer.java index b3b4f9af3f3..10f71887a69 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonResponseStreamer.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonResponseStreamer.java @@ -61,6 +61,8 @@ public void write(final byte[] bbuf, final int off, final int len) throws IOExce chunked = true; } + StreamBackpressure.awaitDrain(response); + Buffer buf = Buffer.buffer(len); buf.appendBytes(bbuf, off, len); response.write(buf).onFailure(this::handleFailure); @@ -69,8 +71,13 @@ public void write(final byte[] bbuf, final int off, final int len) throws IOExce @Override public void close() throws IOException { if (!closed) { - response.end(); closed = true; + if (chunked) { + // Only end the response if data was actually written. When nothing + // was written the headers have not been flushed, leaving the caller + // free to send a proper error response with the correct status code. + response.end(); + } } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java index f2e39f40d3c..3bdc1ff2cb8 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java @@ -39,6 +39,7 @@ public class JsonRpcConfiguration { public static final int DEFAULT_MAX_BATCH_SIZE = 1024; public static final long DEFAULT_HTTP_TIMEOUT_SEC = TimeoutOptions.defaultOptions().getTimeoutSeconds(); + public static final long DEFAULT_HTTP_STREAMING_TIMEOUT_SEC = 600; // 10 minutes public static final long DEFAULT_MAX_REQUEST_CONTENT_LENGTH = 128 * 1024 * 1024; // 128MB public static final boolean DEFAULT_PRETTY_JSON_ENABLED = false; @@ -55,6 +56,7 @@ public class JsonRpcConfiguration { private File authenticationPublicKeyFile; private Optional tlsConfiguration = Optional.empty(); private long httpTimeoutSec = DEFAULT_HTTP_TIMEOUT_SEC; + private long httpStreamingTimeoutSec = DEFAULT_HTTP_STREAMING_TIMEOUT_SEC; private int maxActiveConnections; private int maxBatchSize; private long maxRequestContentLength; @@ -201,6 +203,14 @@ public void setHttpTimeoutSec(final long httpTimeoutSec) { this.httpTimeoutSec = httpTimeoutSec; } + public long getHttpStreamingTimeoutSec() { + return httpStreamingTimeoutSec; + } + + public void setHttpStreamingTimeoutSec(final long httpStreamingTimeoutSec) { + this.httpStreamingTimeoutSec = httpStreamingTimeoutSec; + } + public boolean isPrettyJsonEnabled() { return prettyJsonEnabled; } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/StreamBackpressure.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/StreamBackpressure.java new file mode 100644 index 00000000000..36b5b4b5728 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/StreamBackpressure.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.api.jsonrpc; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.streams.WriteStream; + +/** + * Blocks the calling thread until a Vertx {@link WriteStream} write queue is no longer full. This + * prevents unbounded accumulation of Netty direct-memory buffers when the producer is faster than + * the network can drain. + * + *

Safe to call from {@code executeBlocking} worker threads only — must never be called from the + * Vertx event loop. + */ +public final class StreamBackpressure { + + private static final long DRAIN_TIMEOUT_SECONDS = 60; + + private StreamBackpressure() {} + + /** + * If the write queue is full, blocks until it drains below the low watermark or the timeout + * expires. + * + * @param stream the Vertx WriteStream to check + * @throws IOException if the timeout expires or the thread is interrupted + */ + public static void awaitDrain(final WriteStream stream) throws IOException { + if (stream.writeQueueFull()) { + final CountDownLatch latch = new CountDownLatch(1); + stream.drainHandler(v -> latch.countDown()); + // Re-check after setting handler to avoid race where queue drained + // between the full-check and the handler registration + if (stream.writeQueueFull()) { + try { + if (!latch.await(DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + throw new IOException("Timed out waiting for write queue to drain"); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted waiting for write queue to drain", e); + } + } + } + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/AuthenticatedJsonRpcProcessor.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/AuthenticatedJsonRpcProcessor.java index 1d731fe2a08..adc981764a4 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/AuthenticatedJsonRpcProcessor.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/AuthenticatedJsonRpcProcessor.java @@ -17,13 +17,17 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.AuthenticationService; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestId; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcRequestException; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcUnauthorizedResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import java.io.IOException; +import java.io.OutputStream; import java.util.Collection; +import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.api.trace.Span; public class AuthenticatedJsonRpcProcessor implements JsonRpcProcessor { @@ -52,4 +56,20 @@ public JsonRpcResponse process( } return new JsonRpcUnauthorizedResponse(id, RpcErrorType.UNAUTHORIZED); } + + @Override + public void streamProcess( + final JsonRpcRequestId id, + final JsonRpcMethod method, + final Span metricSpan, + final JsonRpcRequestContext request, + final OutputStream out, + final ObjectMapper mapper) + throws IOException { + if (authenticationService.isPermitted(request.getUser(), method, noAuthRpcApis)) { + rpcProcessor.streamProcess(id, method, metricSpan, request, out, mapper); + } else { + throw new InvalidJsonRpcRequestException("Unauthorized", RpcErrorType.UNAUTHORIZED); + } + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/BaseJsonRpcProcessor.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/BaseJsonRpcProcessor.java index 220e860f9b7..2274de046ba 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/BaseJsonRpcProcessor.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/BaseJsonRpcProcessor.java @@ -18,12 +18,16 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestId; 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.methods.StreamingJsonRpcMethod; 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.RpcErrorType; +import java.io.IOException; +import java.io.OutputStream; import java.util.Arrays; +import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.api.trace.Span; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -56,4 +60,18 @@ public JsonRpcResponse process( return new JsonRpcErrorResponse(id, RpcErrorType.INTERNAL_ERROR); } } + + @Override + public void streamProcess( + final JsonRpcRequestId id, + final JsonRpcMethod method, + final Span metricSpan, + final JsonRpcRequestContext request, + final OutputStream out, + final ObjectMapper mapper) + throws IOException { + // Let exceptions propagate — the caller decides how to handle them based on + // whether the response headers have already been flushed. + ((StreamingJsonRpcMethod) method).streamResponse(request, out, mapper); + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/JsonRpcExecutor.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/JsonRpcExecutor.java index b8786eb7a5d..788bc2daf0e 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/JsonRpcExecutor.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/JsonRpcExecutor.java @@ -26,11 +26,14 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import java.io.IOException; +import java.io.OutputStream; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; +import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.StatusCode; @@ -49,6 +52,9 @@ public class JsonRpcExecutor { private final JsonRpcProcessor rpcProcessor; private final Map rpcMethods; + private record PreparedRequest( + JsonRpcRequestId id, JsonRpcMethod method, Span span, JsonRpcRequestContext context) {} + public JsonRpcExecutor( final JsonRpcProcessor rpcProcessor, final Map rpcMethods) { this.rpcProcessor = rpcProcessor; @@ -62,25 +68,82 @@ public JsonRpcResponse execute( final Supplier alive, final JsonObject jsonRpcRequest, final Function requestBodyProvider) { + final Object prepared = + prepareExecution( + optionalUser, tracer, spanContext, alive, jsonRpcRequest, requestBodyProvider); + if (prepared == null) { + return new JsonRpcNoResponse(); + } + if (prepared instanceof JsonRpcResponse response) { + return response; + } + final PreparedRequest req = (PreparedRequest) prepared; + return rpcProcessor.process(req.id(), req.method(), req.span(), req.context()); + } + + public boolean isStreamingMethod(final String methodName) { + final JsonRpcMethod method = rpcMethods.get(methodName); + return method != null && method.isStreaming(); + } + + /** + * Executes a streaming JSON-RPC method. If the request fails validation (bad params, missing + * method, notification, etc.) the error response is returned without writing to {@code out}, + * allowing the caller to send a proper HTTP error before headers are flushed. + * + * @return an error/no-op response if streaming did not start, or empty on success + */ + public Optional executeStreaming( + final Optional optionalUser, + final Tracer tracer, + final Context spanContext, + final Supplier alive, + final JsonObject jsonRpcRequest, + final Function requestBodyProvider, + final OutputStream out, + final ObjectMapper mapper) + throws IOException { + final Object prepared = + prepareExecution( + optionalUser, tracer, spanContext, alive, jsonRpcRequest, requestBodyProvider); + if (prepared == null) { + return Optional.of(new JsonRpcNoResponse()); + } + if (prepared instanceof JsonRpcResponse response) { + return Optional.of(response); + } + final PreparedRequest req = (PreparedRequest) prepared; + rpcProcessor.streamProcess(req.id(), req.method(), req.span(), req.context(), out, mapper); + return Optional.empty(); + } + + /** + * Shared preamble for execute/executeStreaming: parses the request, creates the span, and + * validates method availability. Returns a {@link PreparedRequest} if ready to process, a {@link + * JsonRpcResponse} if there was a validation error, or null for notifications. + */ + private Object prepareExecution( + final Optional optionalUser, + final Tracer tracer, + final Context spanContext, + final Supplier alive, + final JsonObject jsonRpcRequest, + final Function requestBodyProvider) { try { final JsonRpcRequest requestBody = requestBodyProvider.apply(jsonRpcRequest); final JsonRpcRequestId id = new JsonRpcRequestId(requestBody.getId()); - // Handle notifications if (requestBody.isNotification()) { - // Notifications aren't handled so create empty result for now. - return new JsonRpcNoResponse(); - } - final Span span; - if (tracer != null) { - span = - tracer - .spanBuilder(requestBody.getMethod()) - .setSpanKind(SpanKind.INTERNAL) - .setParent(spanContext) - .startSpan(); - } else { - span = Span.getInvalid(); + return null; } + final Span span = + tracer != null + ? tracer + .spanBuilder(requestBody.getMethod()) + .setSpanKind(SpanKind.INTERNAL) + .setParent(spanContext) + .startSpan() + : Span.getInvalid(); + final Optional unavailableMethod = validateMethodAvailability(requestBody); if (unavailableMethod.isPresent()) { span.setStatus(StatusCode.ERROR, "method unavailable"); @@ -88,8 +151,7 @@ public JsonRpcResponse execute( } final JsonRpcMethod method = rpcMethods.get(requestBody.getMethod()); - - return rpcProcessor.process( + return new PreparedRequest( id, method, span, new JsonRpcRequestContext(requestBody, optionalUser, alive)); } catch (final IllegalArgumentException e) { try { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/JsonRpcProcessor.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/JsonRpcProcessor.java index e9168577e31..272ccb2988a 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/JsonRpcProcessor.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/JsonRpcProcessor.java @@ -19,6 +19,10 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import java.io.IOException; +import java.io.OutputStream; + +import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.api.trace.Span; public interface JsonRpcProcessor { @@ -27,4 +31,15 @@ JsonRpcResponse process( final JsonRpcMethod method, final Span metricSpan, final JsonRpcRequestContext request); + + default void streamProcess( + final JsonRpcRequestId id, + final JsonRpcMethod method, + final Span metricSpan, + final JsonRpcRequestContext request, + final OutputStream out, + final ObjectMapper mapper) + throws IOException { + throw new UnsupportedOperationException(); + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/TimedJsonRpcProcessor.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/TimedJsonRpcProcessor.java index cdc35fc64df..585f8f194fe 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/TimedJsonRpcProcessor.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/TimedJsonRpcProcessor.java @@ -21,6 +21,10 @@ import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; +import java.io.IOException; +import java.io.OutputStream; + +import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.api.trace.Span; public class TimedJsonRpcProcessor implements JsonRpcProcessor { @@ -45,4 +49,19 @@ public JsonRpcResponse process( return rpcProcessor.process(id, method, metricSpan, request); } } + + @Override + public void streamProcess( + final JsonRpcRequestId id, + final JsonRpcMethod method, + final Span metricSpan, + final JsonRpcRequestContext request, + final OutputStream out, + final ObjectMapper mapper) + throws IOException { + try (final OperationTimer.TimingContext ignored = + requestTimer.labels(request.getRequest().getMethod()).startTimer()) { + rpcProcessor.streamProcess(id, method, metricSpan, request, out, mapper); + } + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/TracedJsonRpcProcessor.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/TracedJsonRpcProcessor.java index 2d49cc0f69a..71c67f26337 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/TracedJsonRpcProcessor.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/execution/TracedJsonRpcProcessor.java @@ -16,6 +16,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestId; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcRequestException; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; @@ -25,6 +26,10 @@ import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; +import java.io.IOException; +import java.io.OutputStream; + +import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; @@ -137,4 +142,28 @@ public JsonRpcResponse process( metricSpan.end(); return jsonRpcResponse; } + + @Override + public void streamProcess( + final JsonRpcRequestId id, + final JsonRpcMethod method, + final Span metricSpan, + final JsonRpcRequestContext request, + final OutputStream out, + final ObjectMapper mapper) + throws IOException { + try { + rpcProcessor.streamProcess(id, method, metricSpan, request, out, mapper); + } catch (final IOException | RuntimeException e) { + final String errorLabel = + e instanceof InvalidJsonRpcRequestException ijrp + ? ijrp.getRpcErrorType().name() + : "INTERNAL_ERROR"; + rpcErrorsCounter.labels(method.getName(), errorLabel).inc(); + metricSpan.setStatus(StatusCode.ERROR, "Error processing JSON-RPC requestBody"); + throw e; + } finally { + metricSpan.end(); + } + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractDebugTraceBlock.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractDebugTraceBlock.java index eade86613d0..43e3cf34393 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractDebugTraceBlock.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractDebugTraceBlock.java @@ -14,62 +14,36 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; -import static org.hyperledger.besu.services.pipeline.PipelineBuilder.createPipelineFrom; - 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.parameters.JsonRpcParameter; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.TransactionTraceParams; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.Tracer; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace; +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.jsonrpc.internal.results.DebugTraceTransactionResult; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.debug.TraceOptions; -import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; -import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; -import org.hyperledger.besu.ethereum.vm.DebugOperationTracer; -import org.hyperledger.besu.metrics.BesuMetricCategory; -import org.hyperledger.besu.metrics.ObservableMetricsSystem; -import org.hyperledger.besu.plugin.services.metrics.Counter; -import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; -import org.hyperledger.besu.services.pipeline.Pipeline; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.Optional; -import java.util.concurrent.ExecutionException; import java.util.function.Supplier; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Suppliers; -public abstract class AbstractDebugTraceBlock implements JsonRpcMethod { +public abstract class AbstractDebugTraceBlock implements StreamingJsonRpcMethod { private final ProtocolSchedule protocolSchedule; - private final LabelledMetric outputCounter; private final Supplier blockchainQueriesSupplier; - private final EthScheduler ethScheduler; public AbstractDebugTraceBlock( - final ProtocolSchedule protocolSchedule, - final BlockchainQueries blockchainQueries, - final ObservableMetricsSystem metricsSystem, - final EthScheduler ethScheduler) { + final ProtocolSchedule protocolSchedule, final BlockchainQueries blockchainQueries) { this.blockchainQueriesSupplier = Suppliers.ofInstance(blockchainQueries); this.protocolSchedule = protocolSchedule; - this.outputCounter = - metricsSystem.createLabelledCounter( - BesuMetricCategory.BLOCKCHAIN, - "transactions_debugTraceblock_pipeline_processed_total", - "Number of transactions processed for each block", - "step", - "action"); - this.ethScheduler = ethScheduler; } protected BlockchainQueries getBlockchainQueries() { @@ -97,61 +71,56 @@ protected TraceOptions getTraceOptions(final JsonRpcRequestContext requestContex return traceOptions; } - protected Collection getTraces( - final JsonRpcRequestContext requestContext, - final TraceOptions traceOptions, - final Optional maybeBlock) { - return maybeBlock - .flatMap( - block -> - Tracer.processTracing( - getBlockchainQueries(), - Optional.of(block.getHeader()), - traceableState -> { - List tracesList = - Collections.synchronizedList(new ArrayList<>()); - final ProtocolSpec protocolSpec = - protocolSchedule.getByBlockHeader(block.getHeader()); - final MainnetTransactionProcessor transactionProcessor = - protocolSpec.getTransactionProcessor(); - final TraceBlock.ChainUpdater chainUpdater = - new TraceBlock.ChainUpdater(traceableState); - - TransactionSource transactionSource = new TransactionSource(block); - DebugOperationTracer debugOperationTracer = - new DebugOperationTracer(traceOptions.opCodeTracerConfig(), true); - ExecuteTransactionStep executeTransactionStep = - new ExecuteTransactionStep( - chainUpdater, - transactionProcessor, - getBlockchainQueries().getBlockchain(), - debugOperationTracer, - protocolSpec, - block); + /** + * Finds the block to be traced from the request parameters. + * + * @param request the RPC request context + * @return the block to trace, or empty if it cannot be found (not found, invalid params, etc.) + */ + protected abstract Optional findBlock(JsonRpcRequestContext request); - Pipeline traceBlockPipeline = - createPipelineFrom( - "getTransactions", - transactionSource, - 4, - outputCounter, - false, - "debug_trace_block") - .thenProcess("executeTransaction", executeTransactionStep) - .thenProcessAsyncOrdered( - "debugTraceTransactionStep", - DebugTraceTransactionStepFactory.createAsync( - traceOptions, protocolSpec), - 4) - .andFinishWith("collect_results", tracesList::add); + /** + * Synchronous response path used by batch JSON-RPC requests. Accumulates all transaction traces + * in memory rather than streaming, so that the result can be returned as a single object. + */ + @Override + public JsonRpcResponse response(final JsonRpcRequestContext request) { + final Optional maybeBlock = findBlock(request); + if (maybeBlock.isEmpty()) { + return new JsonRpcSuccessResponse(request.getRequest().getId(), null); + } + final TraceOptions traceOptions = getTraceOptions(request); + final DebugTraceBlockStreamer streamer = createStreamer(traceOptions, maybeBlock); + return new JsonRpcSuccessResponse(request.getRequest().getId(), streamer.accumulateAll()); + } - try { - ethScheduler.startPipeline(traceBlockPipeline).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - return Optional.of(tracesList); - })) + protected DebugTraceBlockStreamer createStreamer( + final TraceOptions traceOptions, final Optional maybeBlock) { + return maybeBlock + .map( + block -> + new DebugTraceBlockStreamer( + block, traceOptions, protocolSchedule, getBlockchainQueries())) .orElse(null); } + + protected static void writeStreamingResponse( + final Object id, + final DebugTraceBlockStreamer streamer, + final OutputStream out, + final ObjectMapper mapper) + throws IOException { + if (streamer == null) { + out.write( + ("{\"jsonrpc\":\"2.0\",\"id\":" + mapper.writeValueAsString(id) + ",\"result\":null}") + .getBytes(StandardCharsets.UTF_8)); + return; + } + final byte[] prefix = + ("{\"jsonrpc\":\"2.0\",\"id\":" + mapper.writeValueAsString(id) + ",\"result\":") + .getBytes(StandardCharsets.UTF_8); + out.write(prefix); + streamer.streamTo(out, mapper); + out.write("}".getBytes(StandardCharsets.UTF_8)); + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlock.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlock.java index 9111d3c6ebe..7f992c445bd 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlock.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlock.java @@ -19,24 +19,21 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; 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.jsonrpc.internal.results.DebugTraceTransactionResult; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockHeaderFunctions; import org.hyperledger.besu.ethereum.debug.TraceOptions; -import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ScheduleBasedBlockHeaderFunctions; import org.hyperledger.besu.ethereum.rlp.RLP; import org.hyperledger.besu.ethereum.rlp.RLPException; -import org.hyperledger.besu.metrics.ObservableMetricsSystem; -import java.util.Collection; +import java.io.IOException; +import java.io.OutputStream; import java.util.Optional; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.tuweni.bytes.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,11 +44,8 @@ public class DebugTraceBlock extends AbstractDebugTraceBlock { private final BlockHeaderFunctions blockHeaderFunctions; public DebugTraceBlock( - final ProtocolSchedule protocolSchedule, - final BlockchainQueries blockchainQueries, - final ObservableMetricsSystem metricsSystem, - final EthScheduler ethScheduler) { - super(protocolSchedule, blockchainQueries, metricsSystem, ethScheduler); + final ProtocolSchedule protocolSchedule, final BlockchainQueries blockchainQueries) { + super(protocolSchedule, blockchainQueries); this.blockHeaderFunctions = ScheduleBasedBlockHeaderFunctions.create(protocolSchedule); } @@ -61,15 +55,39 @@ public String getName() { } @Override - public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + protected Optional findBlock(final JsonRpcRequestContext request) { + try { + final String input = request.getRequiredParameter(0, String.class); + final Block block = + Block.readFrom(RLP.input(Bytes.fromHexString(input)), this.blockHeaderFunctions); + if (getBlockchainQueries() + .getBlockchain() + .getBlockByHash(block.getHeader().getParentHash()) + .isEmpty()) { + return Optional.empty(); + } + return Optional.of(block); + } catch (final RLPException | IllegalArgumentException | JsonRpcParameterException e) { + LOG.debug("Failed to resolve block for batch trace request", e); + return Optional.empty(); + } + } + + @Override + public void streamResponse( + final JsonRpcRequestContext requestContext, final OutputStream out, final ObjectMapper mapper) + throws IOException { final Block block; try { final String input = requestContext.getRequiredParameter(0, String.class); block = Block.readFrom(RLP.input(Bytes.fromHexString(input)), this.blockHeaderFunctions); } catch (final RLPException | IllegalArgumentException e) { LOG.debug("Failed to parse block RLP (index 0)", e); - return new JsonRpcErrorResponse( - requestContext.getRequest().getId(), RpcErrorType.INVALID_BLOCK_PARAMS); + mapper.writeValue( + out, + new JsonRpcErrorResponse( + requestContext.getRequest().getId(), RpcErrorType.INVALID_BLOCK_PARAMS)); + return; } catch (JsonRpcParameterException e) { throw new InvalidJsonRpcParameters( "Invalid block params (index 0)", RpcErrorType.INVALID_BLOCK_PARAMS, e); @@ -80,12 +98,14 @@ public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { .getBlockchain() .getBlockByHash(block.getHeader().getParentHash()) .isPresent()) { - final Collection results = - getTraces(requestContext, traceOptions, Optional.ofNullable(block)); - return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), results); + final DebugTraceBlockStreamer streamer = + createStreamer(traceOptions, Optional.ofNullable(block)); + writeStreamingResponse(requestContext.getRequest().getId(), streamer, out, mapper); } else { - return new JsonRpcErrorResponse( - requestContext.getRequest().getId(), RpcErrorType.PARENT_BLOCK_NOT_FOUND); + mapper.writeValue( + out, + new JsonRpcErrorResponse( + requestContext.getRequest().getId(), RpcErrorType.PARENT_BLOCK_NOT_FOUND)); } } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByHash.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByHash.java index 0a1cb902aaf..34df500feb4 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByHash.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByHash.java @@ -19,28 +19,23 @@ 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.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; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.DebugTraceTransactionResult; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.debug.TraceOptions; -import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.metrics.ObservableMetricsSystem; -import java.util.Collection; +import java.io.IOException; +import java.io.OutputStream; import java.util.Optional; +import com.fasterxml.jackson.databind.ObjectMapper; + public class DebugTraceBlockByHash extends AbstractDebugTraceBlock { public DebugTraceBlockByHash( - final ProtocolSchedule protocolSchedule, - final BlockchainQueries blockchainQueries, - final ObservableMetricsSystem metricsSystem, - final EthScheduler ethScheduler) { - super(protocolSchedule, blockchainQueries, metricsSystem, ethScheduler); + final ProtocolSchedule protocolSchedule, final BlockchainQueries blockchainQueries) { + super(protocolSchedule, blockchainQueries); } @Override @@ -49,7 +44,19 @@ public String getName() { } @Override - public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + protected Optional findBlock(final JsonRpcRequestContext request) { + try { + final Hash blockHash = request.getRequiredParameter(0, Hash.class); + return getBlockchainQueries().getBlockchain().getBlockByHash(blockHash); + } catch (JsonRpcParameterException e) { + return Optional.empty(); + } + } + + @Override + public void streamResponse( + final JsonRpcRequestContext requestContext, final OutputStream out, final ObjectMapper mapper) + throws IOException { final Hash blockHash; try { blockHash = requestContext.getRequiredParameter(0, Hash.class); @@ -58,11 +65,11 @@ public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { "Invalid block hash parameter (index 0)", RpcErrorType.INVALID_BLOCK_HASH_PARAMS, e); } - TraceOptions traceOptions = getTraceOptions(requestContext); - Optional maybeBlock = getBlockchainQueries().getBlockchain().getBlockByHash(blockHash); + final TraceOptions traceOptions = getTraceOptions(requestContext); + final Optional maybeBlock = + getBlockchainQueries().getBlockchain().getBlockByHash(blockHash); - final Collection results = - getTraces(requestContext, traceOptions, maybeBlock); - return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), results); + final DebugTraceBlockStreamer streamer = createStreamer(traceOptions, maybeBlock); + writeStreamingResponse(requestContext.getRequest().getId(), streamer, out, mapper); } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByNumber.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByNumber.java index b5a12c71db9..ab4b1012a88 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByNumber.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByNumber.java @@ -14,60 +14,38 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; -import static org.hyperledger.besu.services.pipeline.PipelineBuilder.createPipelineFrom; - 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.parameters.BlockParameter; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.TransactionTraceParams; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.Tracer; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace; 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.jsonrpc.internal.results.DebugTraceTransactionResult; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.debug.TraceOptions; -import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; -import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; -import org.hyperledger.besu.ethereum.vm.DebugOperationTracer; -import org.hyperledger.besu.metrics.BesuMetricCategory; -import org.hyperledger.besu.metrics.ObservableMetricsSystem; -import org.hyperledger.besu.plugin.services.metrics.Counter; -import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; -import org.hyperledger.besu.services.pipeline.Pipeline; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; + +import java.io.IOException; +import java.io.OutputStream; import java.util.Optional; -import java.util.concurrent.ExecutionException; -public class DebugTraceBlockByNumber extends AbstractBlockParameterMethod { +import com.fasterxml.jackson.databind.ObjectMapper; + +public class DebugTraceBlockByNumber extends AbstractBlockParameterMethod + implements StreamingJsonRpcMethod { protected final ProtocolSchedule protocolSchedule; - private final LabelledMetric outputCounter; - private final EthScheduler ethScheduler; + private final BlockchainQueries blockchainQueriesRef; public DebugTraceBlockByNumber( - final ProtocolSchedule protocolSchedule, - final BlockchainQueries blockchainQueries, - final ObservableMetricsSystem metricsSystem, - final EthScheduler ethScheduler) { + final ProtocolSchedule protocolSchedule, final BlockchainQueries blockchainQueries) { super(blockchainQueries); this.protocolSchedule = protocolSchedule; - this.outputCounter = - metricsSystem.createLabelledCounter( - BesuMetricCategory.BLOCKCHAIN, - "transactions_debugtraceblock_pipeline_processed_total", - "Number of transactions processed for each block", - "step", - "action"); - this.ethScheduler = ethScheduler; + this.blockchainQueriesRef = blockchainQueries; } @Override @@ -88,12 +66,57 @@ protected BlockParameter blockParameter(final JsonRpcRequestContext request) { @Override protected Object resultByBlockNumber( final JsonRpcRequestContext request, final long blockNumber) { + final TraceOptions traceOptions = getTraceOptions(request); + final Optional maybeBlock = + getBlockchainQueries().getBlockchain().getBlockByNumber(blockNumber); if (blockNumber == 0L) { return new JsonRpcErrorResponse( request.getRequest().getId(), RpcErrorType.GENESIS_BLOCK_NOT_TRACEABLE); } + return maybeBlock + .map( + block -> + new DebugTraceBlockStreamer( + block, traceOptions, protocolSchedule, blockchainQueriesRef)) + .orElse(null); + } + + @Override + public void streamResponse( + final JsonRpcRequestContext requestContext, final OutputStream out, final ObjectMapper mapper) + throws IOException { + final Object result = findResultByParamType(requestContext); + + if (result instanceof JsonRpcErrorResponse errorResponse) { + mapper.writeValue(out, errorResponse); + return; + } + + final DebugTraceBlockStreamer streamer = result instanceof DebugTraceBlockStreamer s ? s : null; + AbstractDebugTraceBlock.writeStreamingResponse( + requestContext.getRequest().getId(), streamer, out, mapper); + } + + /** + * Synchronous response path used by batch JSON-RPC requests. Accumulates all transaction traces + * in memory rather than streaming. + */ + @Override + public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + final Object result = findResultByParamType(requestContext); + if (result instanceof JsonRpcErrorResponse errorResponse) { + return errorResponse; + } + if (!(result instanceof DebugTraceBlockStreamer streamer)) { + return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), null); + } + return new JsonRpcSuccessResponse( + requestContext.getRequest().getId(), streamer.accumulateAll()); + } + + private TraceOptions getTraceOptions(final JsonRpcRequestContext request) { final TraceOptions traceOptions; try { traceOptions = @@ -107,64 +130,9 @@ protected Object resultByBlockNumber( RpcErrorType.INVALID_TRANSACTION_TRACE_PARAMS, e); } catch (IllegalArgumentException e) { - // Handle invalid tracer type from TracerType.fromString() throw new InvalidJsonRpcParameters( e.getMessage(), RpcErrorType.INVALID_TRANSACTION_TRACE_PARAMS, e); } - Optional maybeBlock = - getBlockchainQueries().getBlockchain().getBlockByNumber(blockNumber); - - return maybeBlock - .flatMap( - block -> - Tracer.processTracing( - getBlockchainQueries(), - Optional.of(block.getHeader()), - traceableState -> { - List tracesList = - Collections.synchronizedList(new ArrayList<>()); - final ProtocolSpec protocolSpec = - protocolSchedule.getByBlockHeader(block.getHeader()); - final MainnetTransactionProcessor transactionProcessor = - protocolSpec.getTransactionProcessor(); - final TraceBlock.ChainUpdater chainUpdater = - new TraceBlock.ChainUpdater(traceableState); - - TransactionSource transactionSource = new TransactionSource(block); - DebugOperationTracer debugOperationTracer = - new DebugOperationTracer(traceOptions.opCodeTracerConfig(), true); - ExecuteTransactionStep executeTransactionStep = - new ExecuteTransactionStep( - chainUpdater, - transactionProcessor, - getBlockchainQueries().getBlockchain(), - debugOperationTracer, - protocolSpec, - block); - - Pipeline traceBlockPipeline = - createPipelineFrom( - "getTransactions", - transactionSource, - 4, - outputCounter, - false, - "debug_trace_block_by_number") - .thenProcess("executeTransaction", executeTransactionStep) - .thenProcessAsyncOrdered( - "debugTraceTransactionStep", - DebugTraceTransactionStepFactory.createAsync( - traceOptions, protocolSpec), - 4) - .andFinishWith("collect_results", tracesList::add); - - try { - ethScheduler.startPipeline(traceBlockPipeline).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - return Optional.of(tracesList); - })) - .orElse(null); + return traceOptions; } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockStreamer.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockStreamer.java new file mode 100644 index 00000000000..874d2acaabd --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockStreamer.java @@ -0,0 +1,531 @@ +/* + * 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.hyperledger.besu.ethereum.mainnet.feemarket.ExcessBlobGasCalculator.calculateExcessBlobGasForParent; + +import org.hyperledger.besu.datatypes.BlobGas; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.Tracer; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.DebugTraceTransactionResult; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.debug.TraceOptions; +import org.hyperledger.besu.ethereum.debug.TracerType; +import org.hyperledger.besu.ethereum.mainnet.ImmutableTransactionValidationParams; +import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.AccessLocationTracker; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.ethereum.vm.DebugOperationTracer; +import org.hyperledger.besu.ethereum.vm.StreamingDebugOperationTracer; +import org.hyperledger.besu.evm.ModificationNotAllowedException; +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 java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; + +public class DebugTraceBlockStreamer { + + // Sized to stay below Netty's default high watermark (64 KB). This ensures each + // flush does not overshoot the backpressure threshold in JsonResponseStreamer, allowing + // the drain/resume cycle to regulate direct-memory usage smoothly. + private static final int BUF_SIZE = 32 * 1024; + + private static final byte[] TX_OPEN = "{\"txHash\":\"".getBytes(StandardCharsets.US_ASCII); + private static final byte[] TX_RESULT_STRUCT_OPEN = + "\",\"result\":{\"structLogs\":[".getBytes(StandardCharsets.US_ASCII); + private static final byte[] TX_GAS_PREFIX = "],\"gas\":".getBytes(StandardCharsets.US_ASCII); + private static final byte[] TX_FAILED_TRUE_RV = + ",\"failed\":true,\"returnValue\":\"".getBytes(StandardCharsets.US_ASCII); + private static final byte[] TX_FAILED_FALSE_RV = + ",\"failed\":false,\"returnValue\":\"".getBytes(StandardCharsets.US_ASCII); + private static final byte[] TX_CLOSE = "\"}}".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] SL_PC = "{\"pc\":".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_OP = ",\"op\":\"".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_GAS = "\",\"gas\":".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_GAS_COST = ",\"gasCost\":".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_DEPTH = ",\"depth\":".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_REFUND = ",\"refund\":".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_STACK = ",\"stack\":[".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_MEMORY = ",\"memory\":[".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_STORAGE = ",\"storage\":{".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_REASON = ",\"reason\":\"".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SL_ERROR = ",\"error\":\"".getBytes(StandardCharsets.US_ASCII); + + private static final byte COMMA = ','; + private static final byte QUOTE = '"'; + private static final byte COLON = ':'; + private static final byte OBJ_CLOSE = '}'; + private static final byte ARR_CLOSE = ']'; + + private final Block block; + private final TraceOptions traceOptions; + private final ProtocolSchedule protocolSchedule; + private final BlockchainQueries blockchainQueries; + + private final byte[] numBuf = new byte[20]; + private final byte[] writeBuf = new byte[BUF_SIZE]; + private final TreeMap sortedStorage = new TreeMap<>(); + private final HashMap opcodeCache = new HashMap<>(256); + + private OutputStream rawOut; + private int writePos; + private boolean firstStructLog; + private boolean firstTx; + + public DebugTraceBlockStreamer( + final Block block, + final TraceOptions traceOptions, + final ProtocolSchedule protocolSchedule, + final BlockchainQueries blockchainQueries) { + this.block = block; + this.traceOptions = traceOptions; + this.protocolSchedule = protocolSchedule; + this.blockchainQueries = blockchainQueries; + } + + // ── unsynchronized buffer management ────────────────────────────── + + private void flushBuf() throws IOException { + if (writePos > 0) { + rawOut.write(writeBuf, 0, writePos); + writePos = 0; + } + } + + private void writeByte(final int b) throws IOException { + if (writePos >= BUF_SIZE) flushBuf(); + writeBuf[writePos++] = (byte) b; + } + + private void writeBytes(final byte[] src) throws IOException { + writeBytes(src, 0, src.length); + } + + private void writeBytes(final byte[] src, final int off, final int len) throws IOException { + if (len >= BUF_SIZE) { + flushBuf(); + rawOut.write(src, off, len); + return; + } + if (writePos + len > BUF_SIZE) flushBuf(); + System.arraycopy(src, off, writeBuf, writePos, len); + writePos += len; + } + + // ── public API ──────────────────────────────────────────────────── + + public void streamTo(final OutputStream out, final ObjectMapper mapper) throws IOException { + this.rawOut = out; + this.writePos = 0; + this.firstTx = true; + + try { + writeByte('['); + + Tracer.processTracing( + blockchainQueries, + Optional.of(block.getHeader()), + traceableState -> { + final ProtocolSpec protocolSpec = protocolSchedule.getByBlockHeader(block.getHeader()); + final MainnetTransactionProcessor transactionProcessor = + protocolSpec.getTransactionProcessor(); + final TraceBlock.ChainUpdater chainUpdater = + new TraceBlock.ChainUpdater(traceableState); + + final BlockHeader header = block.getHeader(); + final Optional maybeParentHeader = + blockchainQueries.getBlockchain().getBlockHeader(header.getParentHash()); + final Wei blobGasPrice = + protocolSpec + .getFeeMarket() + .blobGasPricePerGas( + maybeParentHeader + .map(parent -> calculateExcessBlobGasForParent(protocolSpec, parent)) + .orElse(BlobGas.ZERO)); + final BlockHashLookup blockHashLookup = + protocolSpec + .getPreExecutionProcessor() + .createBlockHashLookup(blockchainQueries.getBlockchain(), header); + + final boolean isOpcodeTracer = traceOptions.tracerType() == TracerType.OPCODE_TRACER; + + for (final Transaction transaction : block.getBody().getTransactions()) { + if (isOpcodeTracer) { + streamOpcodeTransaction( + transaction, + chainUpdater, + transactionProcessor, + header, + blobGasPrice, + blockHashLookup); + } else { + try { + final byte[] json = + mapper.writeValueAsBytes( + buildTransactionResult( + transaction, + chainUpdater, + transactionProcessor, + protocolSpec, + header, + blobGasPrice, + blockHashLookup)); + if (!firstTx) writeByte(COMMA); + firstTx = false; + writeBytes(json); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + return Optional.of(Boolean.TRUE); + }); + + writeByte(']'); + flushBuf(); + } finally { + this.rawOut = null; + } + } + + public List accumulateAll() { + final List results = new ArrayList<>(); + Tracer.processTracing( + blockchainQueries, + Optional.of(block.getHeader()), + traceableState -> { + final ProtocolSpec protocolSpec = protocolSchedule.getByBlockHeader(block.getHeader()); + final MainnetTransactionProcessor transactionProcessor = + protocolSpec.getTransactionProcessor(); + final TraceBlock.ChainUpdater chainUpdater = new TraceBlock.ChainUpdater(traceableState); + + final BlockHeader header = block.getHeader(); + final Optional maybeParentHeader = + blockchainQueries.getBlockchain().getBlockHeader(header.getParentHash()); + final Wei blobGasPrice = + protocolSpec + .getFeeMarket() + .blobGasPricePerGas( + maybeParentHeader + .map(parent -> calculateExcessBlobGasForParent(protocolSpec, parent)) + .orElse(BlobGas.ZERO)); + final BlockHashLookup blockHashLookup = + protocolSpec + .getPreExecutionProcessor() + .createBlockHashLookup(blockchainQueries.getBlockchain(), header); + + for (final Transaction transaction : block.getBody().getTransactions()) { + results.add( + buildTransactionResult( + transaction, + chainUpdater, + transactionProcessor, + protocolSpec, + header, + blobGasPrice, + blockHashLookup)); + } + + return Optional.of(Boolean.TRUE); + }); + return results; + } + + // ── streaming opcode transaction ────────────────────────────────── + + private void streamOpcodeTransaction( + final Transaction transaction, + final TraceBlock.ChainUpdater chainUpdater, + final MainnetTransactionProcessor transactionProcessor, + final BlockHeader header, + final Wei blobGasPrice, + final BlockHashLookup blockHashLookup) { + + final StreamingDebugOperationTracer tracer = + new StreamingDebugOperationTracer( + traceOptions.opCodeTracerConfig(), + true, + (pc, opcode, gasRemaining, gasCost, depth, stack, frame, halt, revert) -> + writeStructLog( + pc, opcode, gasRemaining, gasCost, depth, stack, frame, halt, revert)); + + try { + if (!firstTx) writeByte(COMMA); + firstTx = false; + + writeBytes(TX_OPEN); + writeAscii(transaction.getHash().toHexString()); + writeBytes(TX_RESULT_STRUCT_OPEN); + + firstStructLog = true; + + final TransactionProcessingResult result = + transactionProcessor.processTransaction( + chainUpdater.getNextUpdater(), + header, + transaction, + header.getCoinbase(), + tracer, + blockHashLookup, + ImmutableTransactionValidationParams.builder().build(), + blobGasPrice, + Optional.empty()); + + writeBytes(TX_GAS_PREFIX); + writeLong(transaction.getGasLimit() - result.getGasRemaining()); + writeBytes(result.isSuccessful() ? TX_FAILED_FALSE_RV : TX_FAILED_TRUE_RV); + final Bytes output = result.getOutput(); + if (!output.isEmpty()) { + writeAscii(output.toHexString()); + } + writeBytes(TX_CLOSE); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + // ── non-streaming fallback (flat tracer, etc.) ──────────────────── + + private DebugTraceTransactionResult buildTransactionResult( + final Transaction transaction, + final TraceBlock.ChainUpdater chainUpdater, + final MainnetTransactionProcessor transactionProcessor, + final ProtocolSpec protocolSpec, + final BlockHeader header, + final Wei blobGasPrice, + final BlockHashLookup blockHashLookup) { + final DebugOperationTracer tracer = + new DebugOperationTracer(traceOptions.opCodeTracerConfig(), true); + + final AccessLocationTracker accessListTracker = + BlockAccessList.BlockAccessListBuilder.createTransactionAccessLocationTracker(0); + + final TransactionProcessingResult result = + transactionProcessor.processTransaction( + chainUpdater.getNextUpdater(), + header, + transaction, + header.getCoinbase(), + tracer, + blockHashLookup, + ImmutableTransactionValidationParams.builder().build(), + blobGasPrice, + Optional.of(accessListTracker)); + + final TransactionTrace transactionTrace = + new TransactionTrace( + transaction, + result, + tracer.copyTraceFrames(), + Optional.empty(), + accessListTracker.getTouchedAccounts()); + tracer.reset(); + + return DebugTraceTransactionStepFactory.create(traceOptions, protocolSpec) + .apply(transactionTrace); + } + + // ── struct log writer (hot path) ────────────────────────────────── + + private void writeStructLog( + final int pc, + final String opcode, + final long gasRemaining, + final long gasCost, + final int depth, + final Bytes[] stack, + final MessageFrame frame, + final ExceptionalHaltReason haltReason, + final Bytes revertReason) { + try { + if (!firstStructLog) writeByte(COMMA); + firstStructLog = false; + + writeBytes(SL_PC); + writeInt(pc); + + writeBytes(SL_OP); + writeBytes(opcodeToBytes(opcode)); + + writeBytes(SL_GAS); + writeLong(gasRemaining); + + writeBytes(SL_GAS_COST); + writeLong(gasCost); + + writeBytes(SL_DEPTH); + writeInt(depth + 1); + + final long refund = frame.getGasRefund(); + if (refund > 0) { + writeBytes(SL_REFUND); + writeLong(refund); + } + + if (stack != null) { + writeBytes(SL_STACK); + for (int s = 0; s < stack.length; s++) { + if (s > 0) writeByte(COMMA); + writeByte(QUOTE); + writeHex(stack[s].toArrayUnsafe(), true); + writeByte(QUOTE); + } + writeByte(ARR_CLOSE); + } + + if (traceOptions.opCodeTracerConfig().traceMemory() && frame.memoryWordSize() > 0) { + writeBytes(SL_MEMORY); + final int wordCount = frame.memoryWordSize(); + for (int i = 0; i < wordCount; i++) { + if (i > 0) writeByte(COMMA); + writeByte(QUOTE); + writeHex(frame.readMutableMemory(i * 32L, 32).toArrayUnsafe(), false); + writeByte(QUOTE); + } + writeByte(ARR_CLOSE); + } + + if (traceOptions.opCodeTracerConfig().traceStorage()) { + try { + final MutableAccount account = + frame.getWorldUpdater().getAccount(frame.getRecipientAddress()); + if (account != null) { + final Map updatedStorage = account.getUpdatedStorage(); + if (!updatedStorage.isEmpty()) { + writeBytes(SL_STORAGE); + sortedStorage.clear(); + sortedStorage.putAll(updatedStorage); + boolean firstEntry = true; + for (final Map.Entry entry : sortedStorage.entrySet()) { + if (!firstEntry) writeByte(COMMA); + firstEntry = false; + writeByte(QUOTE); + writeHex(entry.getKey().toArrayUnsafe(), false); + writeByte(QUOTE); + writeByte(COLON); + writeByte(QUOTE); + writeHex(entry.getValue().toArrayUnsafe(), false); + writeByte(QUOTE); + } + writeByte(OBJ_CLOSE); + } + } + } catch (final ModificationNotAllowedException e) { + // no storage to write + } + } + + if (revertReason != null) { + writeBytes(SL_REASON); + writeHex(revertReason.toArrayUnsafe(), true); + writeByte(QUOTE); + } + + if (haltReason != null) { + writeBytes(SL_ERROR); + writeBytes(haltReason.name().getBytes(StandardCharsets.US_ASCII)); + writeByte(QUOTE); + } + + writeByte(OBJ_CLOSE); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + // ── primitives ──────────────────────────────────────────────────── + + /** + * Encodes {@code bytes} as {@code 0x}-prefixed hex directly into {@link #writeBuf}, flushing + * first if needed to guarantee room. For values whose hex exceeds the buffer size, writes in + * chunks. + */ + private void writeHex(final byte[] bytes, final boolean stripLeading) throws IOException { + // encodeTo always writes the "0x" prefix (2 bytes) plus at least one hex digit, + // even when the input is empty (producing "0x0" = 3 bytes). + final int maxLen = Math.max(3, 2 + bytes.length * 2); + if (maxLen <= BUF_SIZE) { + if (writePos + maxLen > BUF_SIZE) flushBuf(); + writePos = HexWriter.encodeTo(bytes, bytes.length, writeBuf, writePos, stripLeading); + } else { + // Value too large for writeBuf — write prefix + hex in chunks via rawOut + flushBuf(); + rawOut.write('0'); + rawOut.write('x'); + for (final byte b : bytes) { + rawOut.write(HexWriter.HEX_PAIR[(b & 0xFF) << 1]); + rawOut.write(HexWriter.HEX_PAIR[((b & 0xFF) << 1) + 1]); + } + } + } + + private byte[] opcodeToBytes(final String opcode) { + return opcodeCache.computeIfAbsent(opcode, s -> s.getBytes(StandardCharsets.US_ASCII)); + } + + private void writeLong(final long value) throws IOException { + if (value >= 0 && value <= 9) { + writeByte('0' + (int) value); + return; + } + long remaining = value; + boolean neg = false; + if (remaining < 0) { + neg = true; + remaining = -remaining; + } + int pos = numBuf.length; + while (remaining > 0) { + numBuf[--pos] = (byte) ('0' + (int) (remaining % 10)); + remaining /= 10; + } + if (neg) { + numBuf[--pos] = '-'; + } + writeBytes(numBuf, pos, numBuf.length - pos); + } + + private void writeInt(final int value) throws IOException { + writeLong(value); + } + + private void writeAscii(final String s) throws IOException { + writeBytes(s.getBytes(StandardCharsets.US_ASCII)); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/HexWriter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/HexWriter.java new file mode 100644 index 00000000000..6c41f514a7a --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/HexWriter.java @@ -0,0 +1,111 @@ +/* + * 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 java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; + +/** + * Zero-allocation hex encoding of byte arrays directly into an output buffer using a short-wide + * store via {@link VarHandle}, writing two hex ASCII chars per input byte in a single store + * instruction. + * + *

The caller must ensure {@code dest} has enough room: at most {@code max(3, 2 + len * 2)} + * bytes. When {@code len == 0}, three bytes are written ({@code 0x0}). + */ +public final class HexWriter { + + /** + * byte value to two adjacent hex ASCII bytes: {@code HEX_PAIR[(b & 0xFF) << 1]} is high nibble. + */ + static final byte[] HEX_PAIR = new byte[512]; + + /** byte value to two hex ASCII bytes packed as a native-endian {@code short}. */ + private static final short[] HEX_SHORT = new short[256]; + + private static final VarHandle SHORT_HANDLE = + MethodHandles.byteArrayViewVarHandle(short[].class, ByteOrder.nativeOrder()); + + static { + final byte[] hex = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + for (int i = 0; i < 256; i++) { + final byte hi = hex[(i >> 4) & 0xF]; + final byte lo = hex[i & 0xF]; + HEX_PAIR[i << 1] = hi; + HEX_PAIR[(i << 1) + 1] = lo; + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + HEX_SHORT[i] = (short) ((lo << 8) | (hi & 0xFF)); + } else { + HEX_SHORT[i] = (short) ((hi << 8) | (lo & 0xFF)); + } + } + } + + private HexWriter() {} + + /** + * Encodes {@code bytes[0..len)} directly into {@code dest} at {@code destPos} as {@code + * 0x}-prefixed hex. + * + * @param stripLeading if true, leading zero bytes and the high zero nibble of the first non-zero + * byte are omitted (e.g. {@code 0x1}); if false, every byte produces two hex chars (e.g. + * {@code 0x0001}). + * @return the new write position in {@code dest} + */ + public static int encodeTo( + final byte[] bytes, + final int len, + final byte[] dest, + final int destPos, + final boolean stripLeading) { + int wp = destPos; + dest[wp++] = '0'; + dest[wp++] = 'x'; + + if (len == 0) { + dest[wp++] = '0'; + return wp; + } + + int start = 0; + if (stripLeading) { + while (start < len && bytes[start] == 0) start++; + + if (start == len) { + dest[wp++] = '0'; + return wp; + } + + // First non-zero byte — emit nibbles individually to strip a leading zero nibble + final int idx = (bytes[start] & 0xFF) << 1; + if (HEX_PAIR[idx] != '0') { + dest[wp++] = HEX_PAIR[idx]; + } + dest[wp++] = HEX_PAIR[idx + 1]; + start++; + } + + // Remaining bytes — one short-wide store (2 hex chars) per input byte + for (int i = start; i < len; i++) { + SHORT_HANDLE.set(dest, wp, HEX_SHORT[bytes[i] & 0xFF]); + wp += 2; + } + return wp; + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/JsonRpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/JsonRpcMethod.java index c0b1783ce79..6492acd1cf9 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/JsonRpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/JsonRpcMethod.java @@ -37,6 +37,10 @@ public interface JsonRpcMethod { */ JsonRpcResponse response(JsonRpcRequestContext request); + default boolean isStreaming() { + return false; + } + /** * The list of Permissions that correspond to this JSON-RPC method. * diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/StreamingJsonRpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/StreamingJsonRpcMethod.java new file mode 100644 index 00000000000..142c0f44418 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/StreamingJsonRpcMethod.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.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestId; +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.RpcErrorType; + +import java.io.IOException; +import java.io.OutputStream; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public interface StreamingJsonRpcMethod extends JsonRpcMethod { + + void streamResponse(JsonRpcRequestContext request, OutputStream out, ObjectMapper mapper) + throws IOException; + + @Override + default boolean isStreaming() { + return true; + } + + /** + * Default synchronous response path for batch JSON-RPC requests. Returns an error since streaming + * cannot be used within a batch response. Must use the non-streaming JsonRp instead + */ + @Override + default JsonRpcResponse response(final JsonRpcRequestContext request) { + return new JsonRpcErrorResponse( + new JsonRpcRequestId(request.getRequest().getId()), RpcErrorType.INVALID_REQUEST); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/RpcErrorType.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/RpcErrorType.java index d2ddb483da8..58ba18e336a 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/RpcErrorType.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/RpcErrorType.java @@ -186,7 +186,7 @@ public enum RpcErrorType implements RpcMethodError { EXECUTION_HALTED(-32000, "Transaction processing could not be completed due to an exception"), // Execution engine failures - UNKNOWN_PAYLOAD(-32001, "Payload does not exist / is not available"), + UNKNOWN_PAYLOAD(-38001, "Unknown payload"), INVALID_TERMINAL_BLOCK(-32002, "Terminal block doesn't satisfy terminal block conditions"), INVALID_FORKCHOICE_STATE(-38002, "Invalid forkchoice state"), INVALID_PAYLOAD_ATTRIBUTES(-38003, "Invalid payload attributes"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/Quantity.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/Quantity.java index c9d06e509f5..c47e0b385b1 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/Quantity.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/Quantity.java @@ -44,29 +44,29 @@ public static String create(final UInt64Value value) { } public static String create(final int value) { - return uint256ToHex(UInt256.valueOf(value)); + return HEX_PREFIX + Integer.toHexString(value); } public static String create(final long value) { - return uint256ToHex(UInt256.fromHexString(Long.toHexString(value))); + return HEX_PREFIX + Long.toHexString(value); + } + + public static String create(final byte value) { + return HEX_PREFIX + Integer.toHexString(value); } public static String create(final Bytes value) { - return create(value.toArrayUnsafe()); + return uint256ToHex(UInt256.fromBytes(Bytes32.leftPad(value))); } public static String create(final byte[] value) { - return uint256ToHex(UInt256.fromBytes(Bytes32.leftPad(Bytes.wrap(value)))); + return create(Bytes.wrap(value)); } public static String create(final BigInteger value) { return uint256ToHex(UInt256.valueOf(value)); } - public static String create(final byte value) { - return formatMinimalValue(Integer.toHexString(value)); - } - /** * Fixed-length bytes sequences and should be returned as hex strings zero-padded to the expected * length. diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/StructLog.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/StructLog.java index c3a0dcb1c21..5bf18523ca5 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/StructLog.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/StructLog.java @@ -206,4 +206,83 @@ public static String toCompactHex(final Bytes abytes, final boolean prefix) { return result.toString(); } + + /** + * Writes compact hex directly into a char[] buffer. Returns the number of chars written. No + * StringBuilder, no String allocation. + */ + public static int toCompactHex(final Bytes abytes, final boolean prefix, final char[] buf) { + final byte[] bytes = abytes.toArrayUnsafe(); + final int size = bytes.length; + if (size == 0) { + if (prefix) { + buf[0] = '0'; + buf[1] = 'x'; + buf[2] = '0'; + return 3; + } else { + buf[0] = '0'; + return 1; + } + } + int pos = 0; + if (prefix) { + buf[pos++] = '0'; + buf[pos++] = 'x'; + } + boolean leadingZero = true; + for (int i = 0; i < size; i++) { + final byte b = bytes[i]; + final int hi = (b >> 4) & 0xF; + if (!leadingZero || hi != 0) { + buf[pos++] = hexChars[hi]; + leadingZero = false; + } + final int lo = b & 0xF; + if (!leadingZero || lo != 0 || i == size - 1) { + buf[pos++] = hexChars[lo]; + leadingZero = false; + } + } + return pos; + } + + /** + * Appends compact hex representation to an existing StringBuilder, avoiding allocation. The + * StringBuilder is cleared before use but retains its internal buffer. + */ + public static void toCompactHex( + final Bytes abytes, final boolean prefix, final StringBuilder buf) { + buf.setLength(0); + + if (abytes.isEmpty()) { + buf.append(prefix ? "0x0" : "0"); + return; + } + + final byte[] bytes = abytes.toArrayUnsafe(); + final int size = bytes.length; + + if (prefix) { + buf.append("0x"); + } + + boolean leadingZero = true; + + for (int i = 0; i < size; i++) { + final byte b = bytes[i]; + + final int highNibble = (b >> 4) & 0xF; + if (!leadingZero || highNibble != 0) { + buf.append(hexChars[highNibble]); + leadingZero = false; + } + + final int lowNibble = b & 0xF; + if (!leadingZero || lowNibble != 0 || i == size - 1) { + buf.append(hexChars[lowNibble]); + leadingZero = false; + } + } + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/DebugJsonRpcMethods.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/DebugJsonRpcMethods.java index 272ccedf3b3..a1f49c226e5 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/DebugJsonRpcMethods.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/DebugJsonRpcMethods.java @@ -43,7 +43,6 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlockResultFactory; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.core.Synchronizer; -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.transaction.TransactionSimulator; @@ -64,7 +63,6 @@ public class DebugJsonRpcMethods extends ApiGroupJsonRpcMethods { private final Synchronizer synchronizer; private final Path dataDir; private final TransactionSimulator transactionSimulator; - private final EthScheduler ethScheduler; DebugJsonRpcMethods( final BlockchainQueries blockchainQueries, @@ -74,8 +72,7 @@ public class DebugJsonRpcMethods extends ApiGroupJsonRpcMethods { final TransactionPool transactionPool, final Synchronizer synchronizer, final Path dataDir, - final TransactionSimulator transactionSimulator, - final EthScheduler ethScheduler) { + final TransactionSimulator transactionSimulator) { this.blockchainQueries = blockchainQueries; this.protocolContext = protocolContext; this.protocolSchedule = protocolSchedule; @@ -84,7 +81,6 @@ public class DebugJsonRpcMethods extends ApiGroupJsonRpcMethods { this.synchronizer = synchronizer; this.dataDir = dataDir; this.transactionSimulator = transactionSimulator; - this.ethScheduler = ethScheduler; } @Override @@ -104,12 +100,11 @@ blockchainQueries, new TransactionTracer(blockReplay), protocolSchedule), new DebugStorageRangeAt(blockchainQueries, blockReplay), new DebugMetrics(metricsSystem), new DebugResyncWorldstate(protocolContext, synchronizer), - new DebugTraceBlock(protocolSchedule, blockchainQueries, metricsSystem, ethScheduler), + new DebugTraceBlock(protocolSchedule, blockchainQueries), new DebugSetHead(blockchainQueries, protocolContext), new DebugReplayBlock(blockchainQueries, protocolContext, protocolSchedule), - new DebugTraceBlockByNumber( - protocolSchedule, blockchainQueries, metricsSystem, ethScheduler), - new DebugTraceBlockByHash(protocolSchedule, blockchainQueries, metricsSystem, ethScheduler), + new DebugTraceBlockByNumber(protocolSchedule, blockchainQueries), + new DebugTraceBlockByHash(protocolSchedule, blockchainQueries), new DebugBatchSendRawTransaction(transactionPool), new DebugGetBadBlocks(protocolContext, blockResult), new DebugStandardTraceBlockToFile( diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/JsonRpcMethodsFactory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/JsonRpcMethodsFactory.java index 76791c261a6..d1796fe7c5c 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/JsonRpcMethodsFactory.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/JsonRpcMethodsFactory.java @@ -113,8 +113,7 @@ public Map methods( transactionPool, synchronizer, dataDir, - transactionSimulator, - ethScheduler), + transactionSimulator), new ExecutionEngineJsonRpcMethods( miningCoordinator, protocolSchedule, diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/JsonResponseStreamer.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/JsonResponseStreamer.java index dbec85a8f7e..af7c4f4f895 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/JsonResponseStreamer.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/JsonResponseStreamer.java @@ -14,6 +14,8 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.websocket; +import org.hyperledger.besu.ethereum.api.jsonrpc.StreamBackpressure; + import java.io.IOException; import java.io.OutputStream; import java.util.concurrent.atomic.AtomicReference; @@ -56,6 +58,7 @@ public void write(final byte[] bbuf, final int off, final int len) throws IOExce stopOnFailureOrClosed(); if (buffer != EMPTY_BUFFER) { + StreamBackpressure.awaitDrain(response); writeFrame(buffer, false); } Buffer buf = Buffer.buffer(len); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketMessageHandler.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketMessageHandler.java index da7baa5084c..e5850b51c06 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketMessageHandler.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketMessageHandler.java @@ -82,6 +82,44 @@ public void handle( } else { try { final JsonObject jsonRpcRequest = buffer.toJsonObject(); + + if (jsonRpcExecutor.isStreamingMethod(jsonRpcRequest.getString("method"))) { + vertx + .executeBlocking( + promise -> { + try (JsonResponseStreamer streamer = new JsonResponseStreamer(websocket)) { + jsonRpcExecutor.executeStreaming( + user, + null, + null, + new IsAliveHandler(ethScheduler, timeoutSec), + jsonRpcRequest, + req -> { + final WebSocketRpcRequest websocketRequest = + req.mapTo(WebSocketRpcRequest.class); + websocketRequest.setConnectionId(websocket.textHandlerID()); + return websocketRequest; + }, + streamer, + jsonObjectMapper); + promise.complete(); + } catch (IOException e) { + promise.fail(e); + } + }) + .onFailure( + throwable -> { + LOG.error("Error streaming JSON-RPC response", throwable); + try { + final Integer id = jsonRpcRequest.getInteger("id", null); + replyToClient(websocket, errorResponse(id, RpcErrorType.INTERNAL_ERROR)); + } catch (ClassCastException idNotIntegerException) { + replyToClient(websocket, errorResponse(null, RpcErrorType.INTERNAL_ERROR)); + } + }); + return; + } + vertx .executeBlocking( promise -> { diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/EthGraphQLHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/EthGraphQLHttpBySpecTest.java index c54687f6de1..8359849db7a 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/EthGraphQLHttpBySpecTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/EthGraphQLHttpBySpecTest.java @@ -15,7 +15,6 @@ package org.hyperledger.besu.ethereum.api.graphql; import static com.google.common.base.Preconditions.checkState; -import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; import java.net.URISyntaxException; @@ -32,7 +31,6 @@ import okhttp3.RequestBody; import okhttp3.Response; import org.assertj.core.api.Assertions; -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; @@ -86,11 +84,4 @@ private void graphQLCall(final String name) throws IOException { Assertions.assertThat(resp.code()).isEqualTo(expectedStatusCode); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcExecutorHandlerTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcExecutorHandlerTest.java index 416cd774ecb..5b07a6104e0 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcExecutorHandlerTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcExecutorHandlerTest.java @@ -15,11 +15,13 @@ package org.hyperledger.besu.ethereum.api.handlers; 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.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -27,12 +29,24 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.context.ContextKey; import org.hyperledger.besu.ethereum.api.jsonrpc.execution.JsonRpcExecutor; +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.RpcErrorType; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import io.netty.handler.codec.http.HttpResponseStatus; import io.opentelemetry.api.trace.Tracer; import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.impl.future.SucceededFuture; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.SocketAddress; import io.vertx.ext.web.RoutingContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -110,4 +124,128 @@ void testCancelTimerOnSuccessfulExecution() { verify(mockVertx).setTimer(anyLong(), any()); verify(mockVertx).cancelTimer(1L); } + + // --- Streaming error handling tests --- + + /** + * Set up the mock context so the handler creates a JsonRpcObjectExecutor for a streaming method. + */ + private void setUpStreamingContext() { + final JsonObject jsonRequest = + new JsonObject() + .put("jsonrpc", "2.0") + .put("id", 1) + .put("method", "debug_traceBlockByNumber"); + + // isJsonObjectRequest(ctx) checks ctx.data().containsKey(...) + final Map contextData = new HashMap<>(); + contextData.put(ContextKey.REQUEST_BODY_AS_JSON_OBJECT.name(), jsonRequest); + when(mockContext.data()).thenReturn(contextData); + when(mockContext.get(ContextKey.REQUEST_BODY_AS_JSON_OBJECT.name())).thenReturn(jsonRequest); + + // Make the method recognized as streaming + when(mockExecutor.isStreamingMethod("debug_traceBlockByNumber")).thenReturn(true); + + // Mock HttpServerRequest (needed by JsonResponseStreamer constructor) + final HttpServerRequest mockRequest = mock(HttpServerRequest.class); + when(mockContext.request()).thenReturn(mockRequest); + when(mockRequest.remoteAddress()).thenReturn(SocketAddress.domainSocketAddress("test")); + + // Mock response methods used in the streaming path + when(mockResponse.putHeader(any(CharSequence.class), any(CharSequence.class))) + .thenReturn(mockResponse); + when(mockResponse.setChunked(anyBoolean())).thenReturn(mockResponse); + when(mockResponse.exceptionHandler(any())).thenReturn(mockResponse); + when(mockResponse.write(any(Buffer.class))).thenReturn(new SucceededFuture<>(null, null)); + when(mockResponse.headWritten()).thenReturn(false); + when(mockResponse.closed()).thenReturn(false); + + // Timer setup (not the focus of these tests, but required by the handler) + when(mockVertx.setTimer(anyLong(), any())).thenReturn(1L); + when(mockContext.get("timerId")).thenReturn(1L); + } + + @Test + void streamingPreStreamValidationError_sendsProperHttpErrorResponse() throws Exception { + setUpStreamingContext(); + + // executeStreaming returns a validation error (e.g., bad request) before any data is written + when(mockExecutor.executeStreaming(any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(Optional.of(new JsonRpcErrorResponse(1, RpcErrorType.INVALID_REQUEST))); + + Handler handler = + JsonRpcExecutorHandler.handler(mockExecutor, mockTracer, mockConfig); + handler.handle(mockContext); + + // Should send 400 BAD_REQUEST (INVALID_REQUEST maps to BAD_REQUEST) + verify(mockResponse).setStatusCode(eq(HttpResponseStatus.BAD_REQUEST.code())); + // The response should be written (error body) and ended, NOT reset + verify(mockResponse, never()).reset(); + } + + @Test + void streamingInvalidParamsBeforeHeaders_sendsProperHttpError() throws Exception { + setUpStreamingContext(); + + // Processor chain throws InvalidJsonRpcParameters before any data is written + when(mockExecutor.executeStreaming(any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow( + new InvalidJsonRpcParameters( + "Invalid block number", RpcErrorType.INVALID_BLOCK_NUMBER_PARAMS)); + + Handler handler = + JsonRpcExecutorHandler.handler(mockExecutor, mockTracer, mockConfig); + handler.handle(mockContext); + + // Headers not sent yet → proper error response via handleJsonRpcError + // INVALID_BLOCK_NUMBER_PARAMS → default HTTP 200 in statusCodeFromError, with error body + verify(mockResponse).setStatusCode(eq(HttpResponseStatus.OK.code())); + verify(mockResponse).end(contains("Invalid block number params")); + verify(mockResponse, never()).reset(); + } + + @Test + void streamingExceptionAfterHeaders_resetsConnection() throws Exception { + setUpStreamingContext(); + when(mockResponse.headWritten()).thenReturn(true); + + // Exception thrown mid-stream — headers are already flushed + when(mockExecutor.executeStreaming(any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(new RuntimeException("trace execution failed")); + + Handler handler = + JsonRpcExecutorHandler.handler(mockExecutor, mockTracer, mockConfig); + handler.handle(mockContext); + + // Headers already sent → cannot change status code, must reset the connection + verify(mockResponse).reset(); + // setStatusCode is only called once by prepareHttpResponse for Content-Type setup, not for + // error + verify(mockResponse, never()).setStatusCode(anyInt()); + } + + @Test + void streamingTimeoutAfterHeaders_resetsInsteadOfSettingStatus() { + // This tests the timer handler behavior when streaming is already in progress. + // Capture the timer handler, then invoke it with headWritten=true. + setUpStreamingContext(); + + @SuppressWarnings("unchecked") + ArgumentCaptor> timerHandlerCaptor = ArgumentCaptor.forClass(Handler.class); + when(mockVertx.setTimer(anyLong(), timerHandlerCaptor.capture())).thenReturn(1L); + + // Streaming completed or is in progress — headers already written + when(mockResponse.headWritten()).thenReturn(true); + + Handler handler = + JsonRpcExecutorHandler.handler(mockExecutor, mockTracer, mockConfig); + handler.handle(mockContext); + + // Simulate timeout firing while streaming is in progress + timerHandlerCaptor.getValue().handle(1L); + + // Should reset (not try to setStatusCode which would throw IllegalStateException) + verify(mockResponse).reset(); + verify(mockResponse, never()).setStatusCode(anyInt()); + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonResponseStreamerTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonResponseStreamerTest.java index 04863cbd63f..47870f7f621 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonResponseStreamerTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonResponseStreamerTest.java @@ -98,6 +98,16 @@ public void writeStringAndClose() throws IOException { verify(httpResponse).end(); } + @Test + public void closeWithoutWriteDoesNotEndResponse() throws IOException { + try (JsonResponseStreamer streamer = new JsonResponseStreamer(httpResponse, testAddress)) { + // no writes + } + + verify(httpResponse, never()).write(any(Buffer.class)); + verify(httpResponse, never()).end(); + } + @Test public void stopOnError() throws IOException { try (JsonResponseStreamer streamer = new JsonResponseStreamer(failedResponse, testAddress)) { diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceParameterizedTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceParameterizedTest.java index a561079e7cf..a93575aa8f5 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceParameterizedTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceParameterizedTest.java @@ -27,7 +27,6 @@ import okhttp3.Response; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -61,11 +60,4 @@ public void invalidJsonShouldReturnParseError(final String requestJson) throws E json, null, expectedError.getCode(), expectedError.getMessage()); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcErrorTypeConverterTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcErrorTypeConverterTest.java index 0ae5d01f611..443857e792e 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcErrorTypeConverterTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcErrorTypeConverterTest.java @@ -22,7 +22,6 @@ import java.util.Arrays; import java.util.Collection; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -80,11 +79,4 @@ public void expectedTransactionValidationToJsonRpcErrorConversion( assertThat(JsonRpcErrorConverter.convertTransactionInvalidReason(txInvalidReason)) .isEqualTo(expectedJsonRpcError); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 20d6975518a..d34cc877da2 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 @@ -14,14 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.bonsai; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.Test; - public class DebugGethTraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override protected void doSetup() throws Exception { @@ -49,11 +45,4 @@ public static Object[][] specs() { "debug-geth/specs/4byte-tracer" }); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 8c38f0975be..ae260a0a7fb 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 @@ -14,12 +14,8 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.bonsai; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; -import org.junit.jupiter.api.Test; - public class DebugJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -34,11 +30,4 @@ public static Object[][] specs() { "debug/account-at", "debug/batch-send-raw-transaction", "debug/trace-transaction" }); // storageRange and accountRange are not working with bonsai trie } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 82cdcde5a59..1525b0c379d 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 @@ -14,14 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.bonsai; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.Test; - public class DebugTraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -40,11 +36,4 @@ public static Object[][] specs() { return AbstractJsonRpcHttpBySpecTest.findSpecFiles( new String[] {"debug/trace-call", "debug/trace-block"}); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 508a724d87c..bed6f5831e2 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 @@ -14,14 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.bonsai; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.Test; - public class EthByzantiumJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -39,11 +35,4 @@ protected BlockchainSetupUtil getBlockchainSetupUtil(final DataStorageFormat dat public static Object[][] specs() { return AbstractJsonRpcHttpBySpecTest.findSpecFiles(new String[] {"eth_latest"}); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 b034159f642..f028c842234 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 @@ -14,14 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.bonsai; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.Test; - public class EthConfigBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -47,11 +43,4 @@ protected BlockchainSetupUtil getBlockchainSetupUtil(final DataStorageFormat sto public static Object[][] specs() { return findSpecFiles(new String[] {"eth/config"}); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 9e16861fdbc..db1a142ad79 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 @@ -14,12 +14,8 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.bonsai; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; -import org.junit.jupiter.api.Test; - public class EthJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -31,11 +27,4 @@ protected void doSetup() throws Exception { public static Object[][] specs() { return findSpecFiles(new String[] {"eth"}); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 7cc7899c194..7d54f431591 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 @@ -14,7 +14,6 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.bonsai; -import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.ethereum.api.ApiConfiguration.DEFAULT_GAS_CAP; import org.hyperledger.besu.ethereum.api.ApiConfiguration; @@ -24,8 +23,6 @@ import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.Test; - /** * This class is a test runner for the Bonsai test suite. It runs the tests defined in the Bonsai * test suite against the Besu implementation of the JSON-RPC API. @@ -60,11 +57,4 @@ protected ApiConfiguration createApiConfiguration() { public static Object[][] specs() { return findSpecFiles(new String[] {"eth/simulateV1/specs"}); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 fc323a18c38..18281c6965a 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 @@ -14,7 +14,6 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.bonsai; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; @@ -39,7 +38,6 @@ import java.util.Optional; import org.apache.tuweni.bytes.Bytes32; -import org.junit.jupiter.api.Test; public class TestingBuildBlockJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @@ -106,11 +104,4 @@ protected BlockchainSetupUtil getBlockchainSetupUtil(final DataStorageFormat sto public static Object[][] specs() { return findSpecFiles(new String[] {"testing_buildBlockV1"}); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 487fc734de8..66db8dafcf3 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 @@ -14,14 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.bonsai; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.Test; - public class TraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -53,11 +49,4 @@ public static Object[][] specs() { "trace/specs/trace-raw-transaction" }); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/context/ContextKeyTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/context/ContextKeyTest.java index ed2574170ed..a31bcb96d8e 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/context/ContextKeyTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/context/ContextKeyTest.java @@ -26,7 +26,6 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -67,11 +66,4 @@ private static RoutingContext ctx(final T value) { private static Supplier supplier(final T value) { return () -> value; } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 fb99d320a23..1ac7a9e46bd 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 @@ -14,12 +14,8 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.forest; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; -import org.junit.jupiter.api.Test; - public class DebugJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -38,11 +34,4 @@ public static Object[][] specs() { "debug/storage-range" }); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 058a7d5f654..684a0906617 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 @@ -14,14 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.forest; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.Test; - public class DebugTraceJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -40,11 +36,4 @@ public static Object[][] specs() { return AbstractJsonRpcHttpBySpecTest.findSpecFiles( new String[] {"debug/trace-call", "debug/trace-block"}); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 9ce37ea7d47..4f3e92e8680 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 @@ -14,14 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.forest; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; -import org.junit.jupiter.api.Test; - public class EthByzantiumJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -39,11 +35,4 @@ protected BlockchainSetupUtil getBlockchainSetupUtil(final DataStorageFormat sto public static Object[][] specs() { return AbstractJsonRpcHttpBySpecTest.findSpecFiles(new String[] {"eth_latest"}); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 c64d19b6c5d..c77385717de 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 @@ -14,12 +14,8 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.forest; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; -import org.junit.jupiter.api.Test; - public class EthJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { @Override @@ -31,11 +27,4 @@ protected void doSetup() throws Exception { public static Object[][] specs() { return findSpecFiles(new String[] {"eth"}); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 9388db1ee21..d3c9898f4dc 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 @@ -14,14 +14,11 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.forest; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; 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 { @@ -55,11 +52,4 @@ public static Object[][] specs() { "trace/specs/trace-raw-transaction" }); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByHashTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByHashTest.java index 2011bff7d7f..d5b55414b10 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByHashTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByHashTest.java @@ -14,59 +14,117 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; +import org.hyperledger.besu.config.GenesisConfig; +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.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; 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.processor.Tracer; 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.OpCodeLoggerTracerResult; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; -import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.Block; -import org.hyperledger.besu.ethereum.core.BlockDataGenerator; -import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; -import org.hyperledger.besu.metrics.ObservableMetricsSystem; -import org.hyperledger.besu.testutil.DeterministicEthScheduler; - -import java.util.Arrays; -import java.util.Collection; +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.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.function.Function; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +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.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) + public class DebugTraceBlockByHashTest { - @Mock private ProtocolSchedule protocolSchedule; - @Mock private BlockchainQueries blockchainQueries; - @Mock private ObservableMetricsSystem metricsSystem; - @Mock private Blockchain blockchain; + + private static final String GENESIS_RESOURCE = + "/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data/genesis-osaka.json"; + private static final KeyPair KEY_PAIR = + SignatureAlgorithmFactory.getInstance() + .createKeyPair( + SECPPrivateKey.create( + Bytes32.fromHexString( + "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"), + SignatureAlgorithm.ALGORITHM)); + private static final Address CONTRACT_ADDRESS = + Address.fromHexString("0x0030000000000000000000000000000000000000"); + private DebugTraceBlockByHash debugTraceBlockByHash; + private Block testBlock; + private Transaction testTransaction; + private final ObjectMapper mapper = new ObjectMapper().registerModule(new Jdk8Module()); @BeforeEach public void setUp() { + final GenesisConfig genesisConfig = GenesisConfig.fromResource(GENESIS_RESOURCE); + final ExecutionContextTestFixture fixture = + ExecutionContextTestFixture.builder(genesisConfig) + .dataStorageFormat(DataStorageFormat.BONSAI) + .build(); + + final BlockchainQueries blockchainQueries = + new BlockchainQueries( + fixture.getProtocolSchedule(), + fixture.getBlockchain(), + fixture.getStateArchive(), + MiningConfiguration.MINING_DISABLED); + debugTraceBlockByHash = - new DebugTraceBlockByHash( - protocolSchedule, blockchainQueries, metricsSystem, new DeterministicEthScheduler()); + new DebugTraceBlockByHash(fixture.getProtocolSchedule(), blockchainQueries); + + // Build a signed EIP-1559 transaction calling the increment contract with input=5 + testTransaction = + Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(0) + .maxPriorityFeePerGas(Wei.of(5)) + .maxFeePerGas(Wei.of(7)) + .gasLimit(100_000L) + .to(CONTRACT_ADDRESS) + .value(Wei.ZERO) + .payload(Bytes32.leftPad(Bytes.of(5))) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(KEY_PAIR); + + // Build a block whose parent is the genesis block and store it in the blockchain + final BlockHeader genesis = fixture.getBlockchain().getChainHeadHeader(); + final BlockHeader blockHeader = + new BlockHeaderTestFixture() + .number(genesis.getNumber() + 1L) + .parentHash(genesis.getHash()) + .gasLimit(30_000_000L) + .baseFeePerGas(Wei.of(7)) + .buildHeader(); + final BlockBody blockBody = + new BlockBody(List.of(testTransaction), Collections.emptyList(), Optional.empty()); + testBlock = new Block(blockHeader, blockBody); + + // Store block in blockchain so getBlockByHash can find it + final TransactionReceipt receipt = + new TransactionReceipt(testTransaction.getType(), 1, 21_000L, List.of(), Optional.empty()); + fixture.getBlockchain().appendBlock(testBlock, List.of(receipt)); } @Test @@ -74,51 +132,79 @@ public void nameShouldBeDebugTraceBlockByHash() { assertThat(debugTraceBlockByHash.getName()).isEqualTo("debug_traceBlockByHash"); } - @SuppressWarnings("unchecked") @Test - public void shouldReturnCorrectResponse() { - final Block block = - new BlockDataGenerator() - .block( - BlockDataGenerator.BlockOptions.create() - .setBlockHeaderFunctions(new MainnetBlockHeaderFunctions())); - - final Object[] params = new Object[] {block.getHash()}; + public void shouldReturnCorrectResponse() throws IOException { + final Object[] params = new Object[] {testBlock.getHash().toHexString()}; final JsonRpcRequestContext request = new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlockByHash", params)); - when(blockchainQueries.getBlockchain()).thenReturn(blockchain); - when(blockchain.getBlockByHash(block.getHash())).thenReturn(Optional.of(block)); - - OpCodeLoggerTracerResult result1 = mock(OpCodeLoggerTracerResult.class); - OpCodeLoggerTracerResult result2 = mock(OpCodeLoggerTracerResult.class); - - List resultList = Arrays.asList(result1, result2); - - try (MockedStatic mockedTracer = mockStatic(Tracer.class)) { - mockedTracer - .when( - () -> - Tracer.processTracing( - eq(blockchainQueries), - eq(Optional.of(block.getHeader())), - any(Function.class))) - .thenReturn(Optional.of(resultList)); - - final JsonRpcResponse jsonRpcResponse = debugTraceBlockByHash.response(request); - assertThat(jsonRpcResponse).isInstanceOf(JsonRpcSuccessResponse.class); - JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) jsonRpcResponse; - - final Collection traceResult = getResult(response); - assertThat(traceResult).isNotEmpty(); - assertThat(traceResult).isInstanceOf(Collection.class).hasSize(2); - assertThat(traceResult).containsExactly(result1, result2); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + debugTraceBlockByHash.streamResponse(request, out, mapper); + final String json = out.toString(UTF_8); + assertThat(json).startsWith("{\"jsonrpc\":\"2.0\""); + assertThat(json).contains("\"result\":"); + + final JsonNode response = mapper.readTree(json); + assertThat(response.has("result")).isTrue(); + final JsonNode result = response.get("result"); + assertThat(result.isArray()).isTrue(); + assertThat(result.size()).isEqualTo(1); + + final JsonNode txTrace = result.get(0); + assertThat(txTrace.has("txHash")).isTrue(); + assertThat(txTrace.get("txHash").asText()).isEqualTo(testTransaction.getHash().toHexString()); + + final JsonNode traceResult = txTrace.get("result"); + assertThat(traceResult.get("failed").asBoolean()).isFalse(); + assertThat(traceResult.get("gas").asLong()).isGreaterThan(0); + // Contract increments input (5) by 1, returns 6 as 32-byte value + assertThat(traceResult.get("returnValue").asText()) + .isEqualTo("0x0000000000000000000000000000000000000000000000000000000000000006"); + + // Verify structLogs contains the expected opcode sequence + final JsonNode structLogs = traceResult.get("structLogs"); + assertThat(structLogs.isArray()).isTrue(); + assertThat(structLogs.size()).isEqualTo(9); + + // First opcode: PUSH1 0x00 + assertThat(structLogs.get(0).get("pc").asInt()).isEqualTo(0); + assertThat(structLogs.get(0).get("op").asText()).isEqualTo("PUSH1"); + assertThat(structLogs.get(0).get("depth").asInt()).isEqualTo(1); + assertThat(structLogs.get(0).get("gas").asLong()).isGreaterThan(0); + assertThat(structLogs.get(0).get("gasCost").asLong()).isGreaterThan(0); + + // Verify the full opcode sequence + final String[] expectedOps = { + "PUSH1", "CALLDATALOAD", "PUSH1", "ADD", "PUSH1", "MSTORE", "PUSH1", "PUSH1", "RETURN" + }; + for (int i = 0; i < expectedOps.length; i++) { + assertThat(structLogs.get(i).get("op").asText()).isEqualTo(expectedOps[i]); } } - @SuppressWarnings("unchecked") - private Collection getResult(final JsonRpcSuccessResponse response) { - return (Collection) response.getResult(); + @Test + public void batchResponseShouldMatchStreamingOutput() throws IOException { + final Object[] params = new Object[] {testBlock.getHash().toHexString()}; + + // batch (accumulating) path + final JsonRpcRequestContext batchRequest = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlockByHash", params)); + final JsonRpcResponse response = debugTraceBlockByHash.response(batchRequest); + assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); + final Object batchResult = ((JsonRpcSuccessResponse) response).getResult(); + assertThat(batchResult).isNotNull(); + final JsonNode batchJson = mapper.readTree(mapper.writeValueAsBytes(batchResult)); + + // streaming path + final JsonRpcRequestContext streamRequest = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlockByHash", params)); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + debugTraceBlockByHash.streamResponse(streamRequest, out, mapper); + final JsonNode streamJson = mapper.readTree(out.toByteArray()).get("result"); + + assertThat(batchJson) + .as("batch (accumulating) and streaming paths must produce identical result JSON") + .isEqualTo(streamJson); } @Test @@ -128,7 +214,9 @@ public void shouldHandleInvalidParametersGracefully() { new JsonRpcRequestContext( new JsonRpcRequest("2.0", "debug_traceBlockByHash", invalidParams)); - assertThatThrownBy(() -> debugTraceBlockByHash.response(request)) + assertThatThrownBy( + () -> + debugTraceBlockByHash.streamResponse(request, new ByteArrayOutputStream(), mapper)) .isInstanceOf(InvalidJsonRpcParameters.class) .hasMessageContaining("Invalid block hash parameter"); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByNumberTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByNumberTest.java index c4568ac4860..6287dde170b 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByNumberTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockByNumberTest.java @@ -14,63 +14,118 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; +import org.hyperledger.besu.config.GenesisConfig; +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.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; 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.processor.Tracer; 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.jsonrpc.internal.results.OpCodeLoggerTracerResult; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; -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.mainnet.ProtocolSchedule; -import org.hyperledger.besu.metrics.ObservableMetricsSystem; -import org.hyperledger.besu.testutil.DeterministicEthScheduler; - -import java.util.Arrays; -import java.util.Collection; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.core.ExecutionContextTestFixture; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.function.Function; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +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.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) + public class DebugTraceBlockByNumberTest { - @Mock private BlockchainQueries blockchainQueries; - @Mock private Blockchain blockchain; - @Mock private Block block; - @Mock private BlockHeader blockHeader; - @Mock private ProtocolSchedule protocolSchedule; - @Mock private ObservableMetricsSystem metricsSystem; + private static final String GENESIS_RESOURCE = + "/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data/genesis-osaka.json"; + private static final KeyPair KEY_PAIR = + SignatureAlgorithmFactory.getInstance() + .createKeyPair( + SECPPrivateKey.create( + Bytes32.fromHexString( + "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"), + SignatureAlgorithm.ALGORITHM)); + private static final Address CONTRACT_ADDRESS = + Address.fromHexString("0x0030000000000000000000000000000000000000"); + private DebugTraceBlockByNumber debugTraceBlockByNumber; + private Transaction testTransaction; + private final ObjectMapper mapper = new ObjectMapper().registerModule(new Jdk8Module()); @BeforeEach public void setUp() { + final GenesisConfig genesisConfig = GenesisConfig.fromResource(GENESIS_RESOURCE); + final ExecutionContextTestFixture fixture = + ExecutionContextTestFixture.builder(genesisConfig) + .dataStorageFormat(DataStorageFormat.BONSAI) + .build(); + + final BlockchainQueries blockchainQueries = + new BlockchainQueries( + fixture.getProtocolSchedule(), + fixture.getBlockchain(), + fixture.getStateArchive(), + MiningConfiguration.MINING_DISABLED); + debugTraceBlockByNumber = - new DebugTraceBlockByNumber( - protocolSchedule, blockchainQueries, metricsSystem, new DeterministicEthScheduler()); + new DebugTraceBlockByNumber(fixture.getProtocolSchedule(), blockchainQueries); + + // Build a signed EIP-1559 transaction calling the increment contract with input=5 + testTransaction = + Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(0) + .maxPriorityFeePerGas(Wei.of(5)) + .maxFeePerGas(Wei.of(7)) + .gasLimit(100_000L) + .to(CONTRACT_ADDRESS) + .value(Wei.ZERO) + .payload(Bytes32.leftPad(Bytes.of(5))) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(KEY_PAIR); + + // Build a block at number 1 whose parent is the genesis block and store it + final BlockHeader genesis = fixture.getBlockchain().getChainHeadHeader(); + final BlockHeader blockHeader = + new BlockHeaderTestFixture() + .number(genesis.getNumber() + 1L) + .parentHash(genesis.getHash()) + .gasLimit(30_000_000L) + .baseFeePerGas(Wei.of(7)) + .buildHeader(); + final BlockBody blockBody = + new BlockBody(List.of(testTransaction), Collections.emptyList(), Optional.empty()); + final Block testBlock = new Block(blockHeader, blockBody); + + // Store block in blockchain so getBlockByNumber can find it + final TransactionReceipt receipt = + new TransactionReceipt(testTransaction.getType(), 1, 21_000L, List.of(), Optional.empty()); + fixture.getBlockchain().appendBlock(testBlock, List.of(receipt)); } @Test @@ -78,45 +133,80 @@ public void nameShouldBeDebugTraceBlockByNumber() { assertThat(debugTraceBlockByNumber.getName()).isEqualTo("debug_traceBlockByNumber"); } - @SuppressWarnings("unchecked") @Test - public void shouldReturnCorrectResponse() { - - final long blockNumber = 1L; - final Object[] params = new Object[] {Long.toHexString(blockNumber)}; + public void shouldReturnCorrectResponse() throws IOException { + // Block number 1 in hex + final Object[] params = new Object[] {"0x1"}; final JsonRpcRequestContext request = new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlockByNumber", params)); - when(blockchainQueries.getBlockchain()).thenReturn(blockchain); - when(blockchain.getBlockByNumber(blockNumber)).thenReturn(Optional.of(block)); - when(block.getHeader()).thenReturn(blockHeader); - - OpCodeLoggerTracerResult result1 = mock(OpCodeLoggerTracerResult.class); - OpCodeLoggerTracerResult result2 = mock(OpCodeLoggerTracerResult.class); - - List resultList = Arrays.asList(result1, result2); - - try (MockedStatic mockedTracer = mockStatic(Tracer.class)) { - mockedTracer - .when( - () -> - Tracer.processTracing( - eq(blockchainQueries), eq(Optional.of(blockHeader)), any(Function.class))) - .thenReturn(Optional.of(resultList)); - - final JsonRpcResponse jsonRpcResponse = debugTraceBlockByNumber.response(request); - assertThat(jsonRpcResponse).isInstanceOf(JsonRpcSuccessResponse.class); - JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) jsonRpcResponse; - - final Collection traceResult = getResult(response); - assertThat(traceResult).isNotEmpty(); - assertThat(traceResult).isInstanceOf(Collection.class).hasSize(2); - assertThat(traceResult).containsExactly(result1, result2); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + debugTraceBlockByNumber.streamResponse(request, out, mapper); + final String json = out.toString(UTF_8); + assertThat(json).startsWith("{\"jsonrpc\":\"2.0\""); + assertThat(json).contains("\"result\":"); + + final JsonNode response = mapper.readTree(json); + assertThat(response.has("result")).isTrue(); + final JsonNode result = response.get("result"); + assertThat(result.isArray()).isTrue(); + assertThat(result.size()).isEqualTo(1); + + final JsonNode txTrace = result.get(0); + assertThat(txTrace.has("txHash")).isTrue(); + assertThat(txTrace.get("txHash").asText()).isEqualTo(testTransaction.getHash().toHexString()); + + final JsonNode traceResult = txTrace.get("result"); + assertThat(traceResult.get("failed").asBoolean()).isFalse(); + assertThat(traceResult.get("gas").asLong()).isGreaterThan(0); + // Contract increments input (5) by 1, returns 6 as 32-byte value + assertThat(traceResult.get("returnValue").asText()) + .isEqualTo("0x0000000000000000000000000000000000000000000000000000000000000006"); + + // Verify structLogs contains the expected opcode sequence + final JsonNode structLogs = traceResult.get("structLogs"); + assertThat(structLogs.isArray()).isTrue(); + assertThat(structLogs.size()).isEqualTo(9); + + // First opcode: PUSH1 0x00 + assertThat(structLogs.get(0).get("pc").asInt()).isEqualTo(0); + assertThat(structLogs.get(0).get("op").asText()).isEqualTo("PUSH1"); + assertThat(structLogs.get(0).get("depth").asInt()).isEqualTo(1); + assertThat(structLogs.get(0).get("gas").asLong()).isGreaterThan(0); + assertThat(structLogs.get(0).get("gasCost").asLong()).isGreaterThan(0); + + // Verify the full opcode sequence + final String[] expectedOps = { + "PUSH1", "CALLDATALOAD", "PUSH1", "ADD", "PUSH1", "MSTORE", "PUSH1", "PUSH1", "RETURN" + }; + for (int i = 0; i < expectedOps.length; i++) { + assertThat(structLogs.get(i).get("op").asText()).isEqualTo(expectedOps[i]); } } - @SuppressWarnings("unchecked") - private Collection getResult(final JsonRpcSuccessResponse response) { - return (Collection) response.getResult(); + @Test + public void batchResponseShouldMatchStreamingOutput() throws IOException { + final Object[] params = new Object[] {"0x1"}; + + // batch (accumulating) path + final JsonRpcRequestContext batchRequest = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlockByNumber", params)); + final JsonRpcResponse response = debugTraceBlockByNumber.response(batchRequest); + assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); + final Object batchResult = ((JsonRpcSuccessResponse) response).getResult(); + assertThat(batchResult).isNotNull(); + final JsonNode batchJson = mapper.readTree(mapper.writeValueAsBytes(batchResult)); + + // streaming path + final JsonRpcRequestContext streamRequest = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlockByNumber", params)); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + debugTraceBlockByNumber.streamResponse(streamRequest, out, mapper); + final JsonNode streamJson = mapper.readTree(out.toByteArray()).get("result"); + + assertThat(batchJson) + .as("batch (accumulating) and streaming paths must produce identical result JSON") + .isEqualTo(streamJson); } @Test @@ -139,7 +229,10 @@ public void shouldHandleInvalidParametersGracefully() { new JsonRpcRequestContext( new JsonRpcRequest("2.0", "debug_traceBlockByNumber", invalidParams)); - assertThatThrownBy(() -> debugTraceBlockByNumber.response(request)) + assertThatThrownBy( + () -> + debugTraceBlockByNumber.streamResponse( + request, new ByteArrayOutputStream(), mapper)) .isInstanceOf(InvalidJsonRpcParameters.class) .hasMessageContaining("Invalid block parameter"); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockStreamerHexEncodingTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockStreamerHexEncodingTest.java new file mode 100644 index 00000000000..b737e89770a --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockStreamerHexEncodingTest.java @@ -0,0 +1,431 @@ +/* + * 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.assertj.core.api.Assertions.assertThatCode; + +import org.hyperledger.besu.config.GenesisConfig; +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.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +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.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.debug.TraceOptions; +import org.hyperledger.besu.ethereum.debug.TracerType; +import org.hyperledger.besu.evm.tracing.OpCodeTracerConfigBuilder; +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class DebugTraceBlockStreamerHexEncodingTest { + + private static final String GENESIS_RESOURCE = + "/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data/genesis-osaka.json"; + private static final KeyPair KEY_PAIR = + SignatureAlgorithmFactory.getInstance() + .createKeyPair( + SECPPrivateKey.create( + Bytes32.fromHexString( + "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"), + SignatureAlgorithm.ALGORITHM)); + // Contract that copies calldata to memory then REVERTs with it + private static final Address REVERT_CONTRACT = + Address.fromHexString("0x0032000000000000000000000000000000000000"); + + // ABI encoding of Error("") — 68 bytes; first byte is 0x08 (has a leading-zero high nibble) + private static final Bytes ABI_ERROR_EMPTY = + Bytes.fromHexString( + "0x08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000000"); + + // ABI encoding of Error("revert") — 4+32+32+32 = 100 bytes + private static final Bytes ABI_ERROR_WITH_MESSAGE = + Bytes.fromHexString( + "0x08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000006" + + "7265766572740000000000000000000000000000000000000000000000000000"); + + // Init code that loops PUSH0/POP until gas runs out: + // JUMPDEST PUSH0 POP PUSH1 0x00 JUMP + // Each iteration pushes Bytes.EMPTY (0x0) onto the stack, generating thousands of struct logs. + private static final Bytes PUSH0_INIT_CODE = Bytes.fromHexString("0x5b5f506000560000"); + + private ExecutionContextTestFixture fixture; + private BlockchainQueries blockchainQueries; + private final ObjectMapper mapper = new ObjectMapper(); + + @BeforeEach + public void setUp() { + final GenesisConfig genesisConfig = GenesisConfig.fromResource(GENESIS_RESOURCE); + fixture = + ExecutionContextTestFixture.builder(genesisConfig) + .dataStorageFormat(DataStorageFormat.BONSAI) + .build(); + blockchainQueries = + new BlockchainQueries( + fixture.getProtocolSchedule(), + fixture.getBlockchain(), + fixture.getStateArchive(), + MiningConfiguration.MINING_DISABLED); + } + + // ── revert reason tests ─────────────────────────────────────────── + + /** + * A 68-byte revert reason requires 138 chars (2 for "0x" + 136 hex digits). The old hexBuf was + * only 130 bytes — this test verifies no ArrayIndexOutOfBoundsException is thrown. + */ + @Test + public void hexBufDoesNotOverflowFor68ByteRevertReason() { + final Block block = buildBlockWithCalldata(ABI_ERROR_EMPTY, 0); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertThatCode(() -> streamer.streamTo(out, mapper)).doesNotThrowAnyException(); + } + + /** + * The first byte of an ABI Error selector is 0x08. Leading zeros are stripped (compact hex + * format), matching the accumulating path's StructLog.toCompactHex behaviour. + */ + @Test + public void revertReasonUsesCompactHex() throws Exception { + final Block block = buildBlockWithCalldata(ABI_ERROR_EMPTY, 0); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamer.streamTo(out, mapper); + + final JsonNode structLogs = getStructLogs(out); + final String reason = findRevertReason(structLogs); + assertThat(reason).isNotNull(); + // 0x08 → high nibble stripped → "0x8c379a0..." + assertThat(reason).startsWith("0x8c379a0"); + } + + /** Verifies the full hex encoding length for a 100-byte revert reason in compact format. */ + @Test + public void revertReasonEncodedCorrectlyFor100BytePayload() throws Exception { + final Block block = buildBlockWithCalldata(ABI_ERROR_WITH_MESSAGE, 0); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamer.streamTo(out, mapper); + + final JsonNode structLogs = getStructLogs(out); + final String reason = findRevertReason(structLogs); + assertThat(reason).isNotNull(); + assertThat(reason).startsWith("0x8c379a0"); + // compact hex: "0x" + 1 (stripped high nibble of 0x08) + 99*2 = 201 chars + assertThat(reason).hasSize(201); + } + + /** + * Streaming and accumulating paths must produce structurally identical JSON for a transaction + * with a revert reason. + */ + @Test + public void streamingAndAccumulatingPathsMatchForRevertReason() throws Exception { + final Block block = buildBlockWithCalldata(ABI_ERROR_WITH_MESSAGE, 0); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + // streaming path + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamer.streamTo(out, mapper); + final JsonNode streamedRoot = mapper.readTree(out.toByteArray()); + + // accumulating path + final List accumulated = streamer.accumulateAll(); + final JsonNode accRoot = mapper.readTree(mapper.writeValueAsBytes(accumulated)); + + assertThat(streamedRoot) + .as("streaming and accumulating paths must produce identical JSON") + .isEqualTo(accRoot); + } + + // ── zero-stack / writeHex buffer boundary tests ─────────────────── + + /** + * Directly tests writeHex at the buffer boundary that triggers the off-by-one bug. + * + *

Uses reflection to position writePos at BUF_SIZE - 2, then calls writeHex with an empty + * byte[]. Without the fix (maxLen = 2), writeHex does not flush and encodeTo writes 3 bytes past + * the 2-byte headroom → ArrayIndexOutOfBoundsException. With the fix (maxLen = max(3, ...)), + * writeHex flushes first, leaving room for all 3 bytes. + */ + @Test + public void writeHexAtBufferBoundaryWithEmptyBytes() throws Exception { + final Block block = buildBlockWithPush0(); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + // Use reflection to access private internals + final Field rawOutField = DebugTraceBlockStreamer.class.getDeclaredField("rawOut"); + rawOutField.setAccessible(true); + + final Field writePosField = DebugTraceBlockStreamer.class.getDeclaredField("writePos"); + writePosField.setAccessible(true); + + final Field bufSizeField = DebugTraceBlockStreamer.class.getDeclaredField("BUF_SIZE"); + bufSizeField.setAccessible(true); + final int bufSize = (int) bufSizeField.get(null); + + final Method writeHexMethod = + DebugTraceBlockStreamer.class.getDeclaredMethod("writeHex", byte[].class, boolean.class); + writeHexMethod.setAccessible(true); + + // Set up the streamer with a real output stream and position at the boundary + rawOutField.set(streamer, new ByteArrayOutputStream()); + writePosField.set(streamer, bufSize - 2); // exactly 2 bytes of headroom + + // This call exercises the exact condition: empty byte[] with maxLen=2 vs 3 + assertThatCode(() -> writeHexMethod.invoke(streamer, new byte[0], true)) + .as("writeHex with empty byte[] at BUF_SIZE-2 must not throw") + .doesNotThrowAnyException(); + } + + /** + * Streaming a block trace with stack tracing enabled must produce valid JSON even when the stack + * contains zero values (empty Bytes). + */ + @Test + public void streamingWithZeroStackValuesProducesValidJson() { + final Block block = buildBlockWithPush0(); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertThatCode(() -> streamer.streamTo(out, mapper)).doesNotThrowAnyException(); + + // Must be valid JSON + assertThatCode(() -> mapper.readTree(out.toByteArray())).doesNotThrowAnyException(); + } + + /** + * Streaming and accumulating paths must produce structurally identical JSON — not just the same + * struct log count but the same fields and values for every entry. + */ + @Test + public void streamingAndAccumulatingPathsMatchForZeroStackValues() throws Exception { + final Block block = buildBlockWithPush0(); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + // streaming path + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamer.streamTo(out, mapper); + final JsonNode streamedRoot = mapper.readTree(out.toByteArray()); + + // accumulating path + final List accumulated = streamer.accumulateAll(); + final JsonNode accRoot = mapper.readTree(mapper.writeValueAsBytes(accumulated)); + + assertThat(streamedRoot) + .as("streaming and accumulating paths must produce identical JSON") + .isEqualTo(accRoot); + } + + /** Stack arrays for PUSH0 operations must contain "0x0" entries. */ + @Test + public void zeroStackEntriesAreEncodedAsHexZero() throws Exception { + final Block block = buildBlockWithPush0(); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamer.streamTo(out, mapper); + final JsonNode root = mapper.readTree(out.toByteArray()); + final JsonNode structLogs = root.get(0).get("result").get("structLogs"); + + // Find a struct log whose stack contains "0x0" (from PUSH0) + boolean foundZero = false; + for (final JsonNode log : structLogs) { + if (log.has("stack")) { + for (final JsonNode item : log.get("stack")) { + if ("0x0".equals(item.asText())) { + foundZero = true; + break; + } + } + } + if (foundZero) break; + } + assertThat(foundZero).as("Expected at least one 0x0 stack entry from PUSH0").isTrue(); + } + + /** + * Memory words must be 32-byte zero-padded hex (66 chars each), not compact. Streaming and + * accumulating paths must agree on the format when memory tracing is enabled. + */ + @Test + public void streamingAndAccumulatingPathsMatchWithMemoryEnabled() throws Exception { + final TraceOptions withMemory = + new TraceOptions( + TracerType.OPCODE_TRACER, + OpCodeTracerConfigBuilder.createFrom(TraceOptions.DEFAULT.opCodeTracerConfig()) + .traceMemory(true) + .build(), + java.util.Map.of()); + // Call the increment contract (uses MSTORE) with input=5 + final Address incrementContract = + Address.fromHexString("0x0030000000000000000000000000000000000000"); + final Transaction tx = + Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(0) + .maxPriorityFeePerGas(Wei.of(5)) + .maxFeePerGas(Wei.of(7)) + .gasLimit(100_000L) + .to(incrementContract) + .value(Wei.ZERO) + .payload(Bytes32.leftPad(Bytes.of(5))) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(KEY_PAIR); + final Block block = buildBlock(tx); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, withMemory, fixture.getProtocolSchedule(), blockchainQueries); + + // streaming path + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamer.streamTo(out, mapper); + final JsonNode streamedRoot = mapper.readTree(out.toByteArray()); + + // accumulating path + final List accumulated = streamer.accumulateAll(); + final JsonNode accRoot = mapper.readTree(mapper.writeValueAsBytes(accumulated)); + + // Verify memory entries are actually present (MSTORE creates memory) + final JsonNode structLogs = streamedRoot.get(0).get("result").get("structLogs"); + assertThat(structLogs.size()).as("trace must have struct logs").isGreaterThan(0); + boolean hasMemory = false; + for (final JsonNode log : structLogs) { + if (log.has("memory")) { + hasMemory = true; + break; + } + } + assertThat(hasMemory) + .as( + "trace with traceMemory=true must contain memory entries, got %d logs: %s", + structLogs.size(), structLogs.get(0)) + .isTrue(); + + assertThat(streamedRoot) + .as("streaming and accumulating paths must produce identical JSON with memory enabled") + .isEqualTo(accRoot); + } + + // ── helpers ────────────────────────────────────────────────────────────── + + private Block buildBlockWithCalldata(final Bytes calldata, final int nonce) { + final Transaction tx = + Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(nonce) + .maxPriorityFeePerGas(Wei.of(5)) + .maxFeePerGas(Wei.of(7)) + .gasLimit(200_000L) + .to(REVERT_CONTRACT) + .value(Wei.ZERO) + .payload(calldata) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(KEY_PAIR); + + return buildBlock(tx); + } + + private Block buildBlockWithPush0() { + // Contract creation tx whose init code uses PUSH0 opcodes + final Transaction tx = + Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(0) + .maxPriorityFeePerGas(Wei.of(5)) + .maxFeePerGas(Wei.of(7)) + .gasLimit(200_000L) + .value(Wei.ZERO) + .payload(PUSH0_INIT_CODE) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(KEY_PAIR); + + return buildBlock(tx); + } + + private Block buildBlock(final Transaction tx) { + final BlockHeader genesis = fixture.getBlockchain().getChainHeadHeader(); + final BlockHeader header = + new BlockHeaderTestFixture() + .number(genesis.getNumber() + 1L) + .parentHash(genesis.getHash()) + .gasLimit(30_000_000L) + .baseFeePerGas(Wei.of(7)) + .buildHeader(); + return new Block(header, new BlockBody(List.of(tx), Collections.emptyList(), Optional.empty())); + } + + private JsonNode getStructLogs(final ByteArrayOutputStream out) throws Exception { + final JsonNode root = mapper.readTree(out.toByteArray()); + return root.get(0).get("result").get("structLogs"); + } + + /** Returns the "reason" field from the first struct log that has one, or null. */ + private String findRevertReason(final JsonNode structLogs) { + for (final JsonNode log : structLogs) { + if (log.has("reason")) { + return log.get("reason").asText(); + } + } + return null; + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockStreamerPrecompileTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockStreamerPrecompileTest.java new file mode 100644 index 00000000000..f62d29b4a53 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockStreamerPrecompileTest.java @@ -0,0 +1,162 @@ +/* + * 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 org.hyperledger.besu.config.GenesisConfig; +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.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +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.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.debug.TraceOptions; +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class DebugTraceBlockStreamerPrecompileTest { + + private static final String GENESIS_RESOURCE = + "/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data/genesis-osaka.json"; + private static final KeyPair KEY_PAIR = + SignatureAlgorithmFactory.getInstance() + .createKeyPair( + SECPPrivateKey.create( + Bytes32.fromHexString( + "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"), + SignatureAlgorithm.ALGORITHM)); + // IDENTITY precompile — returns its input unchanged; no EVM opcodes are executed + private static final Address IDENTITY_PRECOMPILE = + Address.fromHexString("0x0000000000000000000000000000000000000004"); + + private ExecutionContextTestFixture fixture; + private BlockchainQueries blockchainQueries; + private final ObjectMapper mapper = new ObjectMapper(); + + @BeforeEach + public void setUp() { + final GenesisConfig genesisConfig = GenesisConfig.fromResource(GENESIS_RESOURCE); + fixture = + ExecutionContextTestFixture.builder(genesisConfig) + .dataStorageFormat(DataStorageFormat.BONSAI) + .build(); + blockchainQueries = + new BlockchainQueries( + fixture.getProtocolSchedule(), + fixture.getBlockchain(), + fixture.getStateArchive(), + MiningConfiguration.MINING_DISABLED); + } + + /** + * When the transaction target is a precompile, no EVM opcodes run and tracePrecompileCall is the + * only tracer hook fired. The streaming path must emit a synthetic struct log so that the result + * is non-empty (matching the accumulating path). + */ + @Test + public void streamingPathEmitsSyntheticFrameForDirectPrecompileCall() throws Exception { + final Block block = buildPrecompileBlock(0); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamer.streamTo(out, mapper); + + final JsonNode structLogs = getStructLogs(out); + assertThat(structLogs.isArray()).isTrue(); + assertThat(structLogs.size()).isEqualTo(1); + assertThat(structLogs.get(0).get("op").asText()).isEqualTo(""); + } + + /** + * Streaming and accumulating paths must produce structurally identical JSON for a direct + * precompile call — not just the same struct log count but the same fields and values. + */ + @Test + public void streamingAndAccumulatingPathsMatchForPrecompileCall() throws Exception { + final Block block = buildPrecompileBlock(0); + final DebugTraceBlockStreamer streamer = + new DebugTraceBlockStreamer( + block, TraceOptions.DEFAULT, fixture.getProtocolSchedule(), blockchainQueries); + + // streaming path + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamer.streamTo(out, mapper); + final JsonNode streamedRoot = mapper.readTree(out.toByteArray()); + + // accumulating path + final List accumulated = streamer.accumulateAll(); + final JsonNode accRoot = mapper.readTree(mapper.writeValueAsBytes(accumulated)); + + assertThat(streamedRoot) + .as("streaming and accumulating paths must produce identical JSON") + .isEqualTo(accRoot); + } + + // ── helpers ────────────────────────────────────────────────────────────── + + private Block buildPrecompileBlock(final int nonce) { + final Transaction tx = + Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(nonce) + .maxPriorityFeePerGas(Wei.of(5)) + .maxFeePerGas(Wei.of(7)) + .gasLimit(100_000L) + .to(IDENTITY_PRECOMPILE) + .value(Wei.ZERO) + .payload(Bytes.of(1, 2, 3, 4)) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(KEY_PAIR); + + final BlockHeader genesis = fixture.getBlockchain().getChainHeadHeader(); + final BlockHeader header = + new BlockHeaderTestFixture() + .number(genesis.getNumber() + 1L) + .parentHash(genesis.getHash()) + .gasLimit(30_000_000L) + .baseFeePerGas(Wei.of(7)) + .buildHeader(); + return new Block(header, new BlockBody(List.of(tx), Collections.emptyList(), Optional.empty())); + } + + private JsonNode getStructLogs(final ByteArrayOutputStream out) throws Exception { + final JsonNode root = mapper.readTree(out.toByteArray()); + return root.get(0).get("result").get("structLogs"); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockTest.java index c3d25b2f2f7..cac3a7a8428 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceBlockTest.java @@ -14,106 +14,111 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; +import static java.nio.charset.StandardCharsets.UTF_8; 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.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; import org.hyperledger.besu.config.GenesisConfig; +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.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; 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.processor.Tracer; -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.jsonrpc.internal.results.OpCodeLoggerTracerResult; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; -import org.hyperledger.besu.ethereum.chain.BadBlockManager; -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.BlockDataGenerator; +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.MiningConfiguration; -import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; +import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions; -import org.hyperledger.besu.ethereum.mainnet.ProtocolScheduleBuilder; -import org.hyperledger.besu.ethereum.mainnet.ProtocolSpecAdapters; -import org.hyperledger.besu.ethereum.mainnet.TransactionValidatorFactory; -import org.hyperledger.besu.ethereum.mainnet.WithdrawalsProcessor; -import org.hyperledger.besu.evm.internal.EvmConfiguration; -import org.hyperledger.besu.metrics.ObservableMetricsSystem; -import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; -import org.hyperledger.besu.testutil.DeterministicEthScheduler; +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigInteger; -import java.util.Arrays; -import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.function.Function; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +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.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) + public class DebugTraceBlockTest { - @Mock private BlockchainQueries blockchainQueries; - @Mock private Blockchain blockchain; - @Mock private ObservableMetricsSystem metricsSystem; - @Mock private WithdrawalsProcessor withdrawalsProcessor; - @Mock private TransactionValidatorFactory alwaysValidTransactionValidatorFactory; + + private static final String GENESIS_RESOURCE = + "/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data/genesis-osaka.json"; + private static final KeyPair KEY_PAIR = + SignatureAlgorithmFactory.getInstance() + .createKeyPair( + SECPPrivateKey.create( + Bytes32.fromHexString( + "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"), + SignatureAlgorithm.ALGORITHM)); + private static final Address CONTRACT_ADDRESS = + Address.fromHexString("0x0030000000000000000000000000000000000000"); + private DebugTraceBlock debugTraceBlock; + private ExecutionContextTestFixture fixture; + private Block testBlock; + private Transaction testTransaction; + private final ObjectMapper mapper = new ObjectMapper().registerModule(new Jdk8Module()); @BeforeEach public void setUp() { - // As we build the block from RLP in DebugTraceBlock, we need to have non mocked - // protocolSchedule (and ProtocolSpec) - // to be able to get the hash of the block - final var genesisConfig = - GenesisConfig.fromResource( - "/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data/genesis.json"); - final ProtocolSpecAdapters protocolSpecAdapters = - ProtocolSpecAdapters.create( - 0, - specBuilder -> { - specBuilder.isReplayProtectionSupported(true); - specBuilder.withdrawalsProcessor(withdrawalsProcessor); - specBuilder.transactionValidatorFactoryBuilder( - (evm, gasLimitCalculator, feeMarket) -> alwaysValidTransactionValidatorFactory); - return specBuilder; - }); - final ExecutionContextTestFixture executionContextTestFixture = + final GenesisConfig genesisConfig = GenesisConfig.fromResource(GENESIS_RESOURCE); + fixture = ExecutionContextTestFixture.builder(genesisConfig) - .protocolSchedule( - new ProtocolScheduleBuilder( - genesisConfig.getConfigOptions(), - Optional.of(BigInteger.valueOf(42)), - protocolSpecAdapters, - false, - EvmConfiguration.DEFAULT, - MiningConfiguration.MINING_DISABLED, - new BadBlockManager(), - false, - BalConfiguration.DEFAULT, - new NoOpMetricsSystem()) - .createProtocolSchedule()) + .dataStorageFormat(DataStorageFormat.BONSAI) .build(); - debugTraceBlock = - new DebugTraceBlock( - executionContextTestFixture.getProtocolSchedule(), - blockchainQueries, - metricsSystem, - new DeterministicEthScheduler()); + + final BlockchainQueries blockchainQueries = + new BlockchainQueries( + fixture.getProtocolSchedule(), + fixture.getBlockchain(), + fixture.getStateArchive(), + MiningConfiguration.MINING_DISABLED); + + debugTraceBlock = new DebugTraceBlock(fixture.getProtocolSchedule(), blockchainQueries); + + // Build a signed EIP-1559 transaction calling the increment contract with input=5 + testTransaction = + Transaction.builder() + .type(TransactionType.EIP1559) + .nonce(0) + .maxPriorityFeePerGas(Wei.of(5)) + .maxFeePerGas(Wei.of(7)) + .gasLimit(100_000L) + .to(CONTRACT_ADDRESS) + .value(Wei.ZERO) + .payload(Bytes32.leftPad(Bytes.of(5))) + .chainId(BigInteger.valueOf(42)) + .signAndBuild(KEY_PAIR); + + // Build a block whose parent is the genesis block + final BlockHeader genesis = fixture.getBlockchain().getChainHeadHeader(); + final BlockHeader blockHeader = + new BlockHeaderTestFixture() + .number(genesis.getNumber() + 1L) + .parentHash(genesis.getHash()) + .gasLimit(30_000_000L) + .baseFeePerGas(Wei.of(7)) + .buildHeader(); + final BlockBody blockBody = + new BlockBody(List.of(testTransaction), Collections.emptyList(), Optional.empty()); + testBlock = new Block(blockHeader, blockBody); } @Test @@ -121,92 +126,112 @@ public void nameShouldBeDebugTraceBlock() { assertThat(debugTraceBlock.getName()).isEqualTo("debug_traceBlock"); } - @SuppressWarnings("unchecked") @Test - public void shouldReturnCorrectResponse() { - final Block parentBlock = - new BlockDataGenerator() - .block( - BlockDataGenerator.BlockOptions.create() - .setBlockHeaderFunctions(new MainnetBlockHeaderFunctions())); - final Block block = - new BlockDataGenerator() - .block( - BlockDataGenerator.BlockOptions.create() - .setBlockHeaderFunctions(new MainnetBlockHeaderFunctions()) - .setParentHash(parentBlock.getHash())); - - final Object[] params = new Object[] {block.toRlp().toString()}; + public void shouldReturnCorrectResponse() throws IOException { + final Object[] params = new Object[] {testBlock.toRlp().toString()}; final JsonRpcRequestContext request = new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlock", params)); - when(blockchainQueries.getBlockchain()).thenReturn(blockchain); - when(blockchain.getBlockByHash(block.getHeader().getParentHash())) - .thenReturn(Optional.of(parentBlock)); - - OpCodeLoggerTracerResult result1 = mock(OpCodeLoggerTracerResult.class); - OpCodeLoggerTracerResult result2 = mock(OpCodeLoggerTracerResult.class); - - List resultList = Arrays.asList(result1, result2); - - try (MockedStatic mockedTracer = mockStatic(Tracer.class)) { - mockedTracer - .when( - () -> - Tracer.processTracing( - eq(blockchainQueries), - eq(Optional.of(block.getHeader())), - any(Function.class))) - .thenReturn(Optional.of(resultList)); - - final JsonRpcResponse jsonRpcResponse = debugTraceBlock.response(request); - assertThat(jsonRpcResponse).isInstanceOf(JsonRpcSuccessResponse.class); - JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) jsonRpcResponse; - - final Collection traceResult = getResult(response); - assertThat(traceResult).isNotEmpty(); - assertThat(traceResult).isInstanceOf(Collection.class).hasSize(2); - assertThat(traceResult).containsExactly(result1, result2); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + debugTraceBlock.streamResponse(request, out, mapper); + final String json = out.toString(UTF_8); + assertThat(json).startsWith("{\"jsonrpc\":\"2.0\""); + assertThat(json).contains("\"result\":"); + + final JsonNode response = mapper.readTree(json); + assertThat(response.has("result")).isTrue(); + final JsonNode result = response.get("result"); + assertThat(result.isArray()).isTrue(); + assertThat(result.size()).isEqualTo(1); + + final JsonNode txTrace = result.get(0); + assertThat(txTrace.has("txHash")).isTrue(); + assertThat(txTrace.get("txHash").asText()).isEqualTo(testTransaction.getHash().toHexString()); + + final JsonNode traceResult = txTrace.get("result"); + assertThat(traceResult.get("failed").asBoolean()).isFalse(); + assertThat(traceResult.get("gas").asLong()).isGreaterThan(0); + // Contract increments input (5) by 1, returns 6 as 32-byte value + assertThat(traceResult.get("returnValue").asText()) + .isEqualTo("0x0000000000000000000000000000000000000000000000000000000000000006"); + + // Verify structLogs contains the expected opcode sequence + final JsonNode structLogs = traceResult.get("structLogs"); + assertThat(structLogs.isArray()).isTrue(); + assertThat(structLogs.size()).isEqualTo(9); + + // First opcode: PUSH1 0x00 + assertThat(structLogs.get(0).get("pc").asInt()).isEqualTo(0); + assertThat(structLogs.get(0).get("op").asText()).isEqualTo("PUSH1"); + assertThat(structLogs.get(0).get("depth").asInt()).isEqualTo(1); + assertThat(structLogs.get(0).get("gas").asLong()).isGreaterThan(0); + assertThat(structLogs.get(0).get("gasCost").asLong()).isGreaterThan(0); + + // Verify the full opcode sequence + final String[] expectedOps = { + "PUSH1", "CALLDATALOAD", "PUSH1", "ADD", "PUSH1", "MSTORE", "PUSH1", "PUSH1", "RETURN" + }; + for (int i = 0; i < expectedOps.length; i++) { + assertThat(structLogs.get(i).get("op").asText()).isEqualTo(expectedOps[i]); } } - @SuppressWarnings("unchecked") - private Collection getResult(final JsonRpcSuccessResponse response) { - return (Collection) response.getResult(); + @Test + public void batchResponseShouldMatchStreamingOutput() throws IOException { + final Object[] params = new Object[] {testBlock.toRlp().toString()}; + + // batch (accumulating) path + final JsonRpcRequestContext batchRequest = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlock", params)); + final JsonRpcResponse response = debugTraceBlock.response(batchRequest); + assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); + final Object batchResult = ((JsonRpcSuccessResponse) response).getResult(); + assertThat(batchResult).isNotNull(); + final JsonNode batchJson = mapper.readTree(mapper.writeValueAsBytes(batchResult)); + + // streaming path + final JsonRpcRequestContext streamRequest = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlock", params)); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + debugTraceBlock.streamResponse(streamRequest, out, mapper); + final JsonNode streamJson = mapper.readTree(out.toByteArray()).get("result"); + + assertThat(batchJson) + .as("batch (accumulating) and streaming paths must produce identical result JSON") + .isEqualTo(streamJson); } @Test - public void shouldReturnErrorResponseWhenParentBlockMissing() { - final Block block = + public void shouldReturnErrorResponseWhenParentBlockMissing() throws IOException { + // Create a block whose parent doesn't exist in the blockchain + final Block orphanBlock = new BlockDataGenerator() .block( BlockDataGenerator.BlockOptions.create() .setBlockHeaderFunctions(new MainnetBlockHeaderFunctions())); - final Object[] params = new Object[] {block.toRlp().toString()}; + final Object[] params = new Object[] {orphanBlock.toRlp().toString()}; final JsonRpcRequestContext request = new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlock", params)); - when(blockchainQueries.getBlockchain()).thenReturn(blockchain); - when(blockchain.getBlockByHash(block.getHeader().getParentHash())).thenReturn(Optional.empty()); - - final JsonRpcResponse jsonRpcResponse = debugTraceBlock.response(request); - assertThat(jsonRpcResponse).isInstanceOf(JsonRpcErrorResponse.class); - final JsonRpcErrorResponse response = (JsonRpcErrorResponse) debugTraceBlock.response(request); - - assertThat(response.getErrorType()).isEqualByComparingTo(RpcErrorType.PARENT_BLOCK_NOT_FOUND); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + debugTraceBlock.streamResponse(request, out, mapper); + final JsonNode response = mapper.readTree(out.toByteArray()); + assertThat(response.has("error")).isTrue(); + assertThat(response.get("error").get("message").asText()).contains("Parent block not found"); } @Test - public void shouldHandleInvalidParametersGracefully() { + public void shouldHandleInvalidParametersGracefully() throws IOException { final Object[] invalidParams = new Object[] {"invalid RLP"}; final JsonRpcRequestContext request = new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_traceBlock", invalidParams)); - final JsonRpcResponse jsonRpcResponse = debugTraceBlock.response(request); - assertThat(jsonRpcResponse).isInstanceOf(JsonRpcErrorResponse.class); - final JsonRpcErrorResponse response = (JsonRpcErrorResponse) debugTraceBlock.response(request); - - assertThat(response.getError().getMessage()).contains("Invalid block param (block not found)"); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + debugTraceBlock.streamResponse(request, out, mapper); + final JsonNode response = mapper.readTree(out.toByteArray()); + assertThat(response.has("error")).isTrue(); + assertThat(response.get("error").get("message").asText()) + .contains("Invalid block param (block not found)"); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/HexWriterTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/HexWriterTest.java new file mode 100644 index 00000000000..c977e5db251 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/HexWriterTest.java @@ -0,0 +1,352 @@ +/* + * 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.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +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; + +class HexWriterTest { + + private final byte[] dest = new byte[256]; + + private String encode(final byte[] input, final boolean stripLeading) { + final int end = HexWriter.encodeTo(input, input.length, dest, 0, stripLeading); + return new String(dest, 0, end, StandardCharsets.US_ASCII); + } + + // ── stripLeading = true ──────────────────────────────────────────── + + static Stream stripLeadingCases() { + return Stream.of( + // empty input + Arguments.of(new byte[] {}, "0x0"), + // single zero byte + Arguments.of(new byte[] {0x00}, "0x0"), + // single byte, value 1 + Arguments.of(new byte[] {0x01}, "0x1"), + // single byte, high nibble only (0xF0) + Arguments.of(new byte[] {(byte) 0xF0}, "0xf0"), + // single byte, low nibble only (0x0F) — must not lose the 'f' + Arguments.of(new byte[] {0x0F}, "0xf"), + // single byte, max (0xFF) + Arguments.of(new byte[] {(byte) 0xFF}, "0xff"), + // two bytes, leading zero byte + Arguments.of(new byte[] {0x00, 0x01}, "0x1"), + // two bytes, leading zero nibble on first byte + Arguments.of(new byte[] {0x0A, (byte) 0xBC}, "0xabc"), + // two bytes, no leading zeros + Arguments.of(new byte[] {(byte) 0xAB, (byte) 0xCD}, "0xabcd"), + // all zeros, multiple bytes + Arguments.of(new byte[] {0x00, 0x00, 0x00}, "0x0"), + // leading zeros then value in last byte + Arguments.of(new byte[] {0x00, 0x00, 0x01}, "0x1"), + // 32-byte uint256 value = 1 (stack entry) + Arguments.of(uint256(1), "0x1"), + // 32-byte uint256 value = 0xFF + Arguments.of(uint256(0xFF), "0xff"), + // 32-byte uint256 value = 0x100 + Arguments.of(uint256(0x100), "0x100"), + // 32-byte all zeros + Arguments.of(new byte[32], "0x0"), + // 32-byte all 0xFF + Arguments.of(filledBytes(32, 0xFF), "0x" + "ff".repeat(32))); + } + + @ParameterizedTest + @MethodSource("stripLeadingCases") + void shortPackStripLeading(final byte[] input, final String expected) { + assertThat(encode(input, true)).isEqualTo(expected); + } + + // ── stripLeading = false (full-width) ────────────────────────────── + + static Stream fullWidthCases() { + return Stream.of( + // empty input + Arguments.of(new byte[] {}, "0x0"), + // single zero byte → "0x00" not "0x0" + Arguments.of(new byte[] {0x00}, "0x00"), + // single byte, value 1 + Arguments.of(new byte[] {0x01}, "0x01"), + // single byte, max + Arguments.of(new byte[] {(byte) 0xFF}, "0xff"), + // two bytes with leading zero + Arguments.of(new byte[] {0x00, 0x01}, "0x0001"), + // 32-byte uint256 value = 1 (storage key format) + Arguments.of( + uint256(1), "0x0000000000000000000000000000000000000000000000000000000000000001"), + // 32-byte all zeros + Arguments.of( + new byte[32], "0x0000000000000000000000000000000000000000000000000000000000000000"), + // 32-byte all 0xFF + Arguments.of(filledBytes(32, 0xFF), "0x" + "ff".repeat(32))); + } + + @ParameterizedTest + @MethodSource("fullWidthCases") + void shortPackFullWidth(final byte[] input, final String expected) { + assertThat(encode(input, false)).isEqualTo(expected); + } + + // ── destPos offset ───────────────────────────────────────────────── + + @Test + void writesAtNonZeroOffset() { + dest[0] = 'Z'; + final int end = HexWriter.encodeTo(new byte[] {(byte) 0xAB}, 1, dest, 1, true); + assertThat(dest[0]).isEqualTo((byte) 'Z'); // untouched + assertThat(new String(dest, 1, end - 1, StandardCharsets.US_ASCII)).isEqualTo("0xab"); + } + + // ── tight buffer (no room to spare) ──────────────────────────────── + + @Test + void tightBufferStripLeading() { + // 1 byte → worst case "0xff" = 4 chars + final byte[] tight = new byte[4]; + final int end = HexWriter.encodeTo(new byte[] {(byte) 0xFF}, 1, tight, 0, true); + assertThat(end).isEqualTo(4); + assertThat(new String(tight, StandardCharsets.US_ASCII)).isEqualTo("0xff"); + } + + @Test + void tightBufferStripLeadingSingleNibble() { + // 0x01 → "0x1" = 3 chars + final byte[] tight = new byte[3]; + final int end = HexWriter.encodeTo(new byte[] {0x01}, 1, tight, 0, true); + assertThat(end).isEqualTo(3); + assertThat(new String(tight, 0, end, StandardCharsets.US_ASCII)).isEqualTo("0x1"); + } + + @Test + void tightBufferFullWidth() { + // 1 byte → "0x01" = 4 chars + final byte[] tight = new byte[4]; + final int end = HexWriter.encodeTo(new byte[] {0x01}, 1, tight, 0, false); + assertThat(end).isEqualTo(4); + assertThat(new String(tight, StandardCharsets.US_ASCII)).isEqualTo("0x01"); + } + + @Test + void tightBufferEmpty() { + // empty → "0x0" = 3 chars + final byte[] tight = new byte[3]; + final int end = HexWriter.encodeTo(new byte[] {}, 0, tight, 0, true); + assertThat(end).isEqualTo(3); + assertThat(new String(tight, 0, end, StandardCharsets.US_ASCII)).isEqualTo("0x0"); + } + + // ── partial length (len < bytes.length) ──────────────────────────── + + @Test + void partialLength() { + // only encode the first 2 bytes of a 4-byte array + final byte[] input = new byte[] {(byte) 0xAB, (byte) 0xCD, (byte) 0xEF, 0x01}; + final int end = HexWriter.encodeTo(input, 2, dest, 0, true); + assertThat(new String(dest, 0, end, StandardCharsets.US_ASCII)).isEqualTo("0xabcd"); + } + + // ── odd byte counts ──────────────────────────────────────────────── + + @Test + void oddByteCountStripLeading() { + // 3 bytes with leading zero → "0xbcde" + final byte[] input = new byte[] {0x00, (byte) 0xBC, (byte) 0xDE}; + assertThat(encode(input, true)).isEqualTo("0xbcde"); + } + + @Test + void oddByteCountFullWidth() { + // 3 bytes → "0x00bcde" + final byte[] input = new byte[] {0x00, (byte) 0xBC, (byte) 0xDE}; + assertThat(encode(input, false)).isEqualTo("0x00bcde"); + } + + // ── large values (> 32 bytes) ─────────────────────────────────────── + + @Test + void largeValueStripLeading() { + // 64 bytes, first 60 are zero, last 4 are 0xDEADBEEF + final byte[] input = new byte[64]; + input[60] = (byte) 0xDE; + input[61] = (byte) 0xAD; + input[62] = (byte) 0xBE; + input[63] = (byte) 0xEF; + assertThat(encode(input, true)).isEqualTo("0xdeadbeef"); + } + + @Test + void largeValueFullWidth() { + // 64 bytes, first 60 are zero, last 4 are 0xDEADBEEF + final byte[] input = new byte[64]; + input[60] = (byte) 0xDE; + input[61] = (byte) 0xAD; + input[62] = (byte) 0xBE; + input[63] = (byte) 0xEF; + assertThat(encode(input, false)).isEqualTo("0x" + "00".repeat(60) + "deadbeef"); + } + + @Test + void revertReasonSized() { + // Typical Solidity revert: 4-byte selector + 32-byte offset + 32-byte length + message. + // Total could easily be 100+ bytes. + final byte[] input = new byte[100]; + input[0] = 0x08; + input[1] = (byte) 0xC3; + input[2] = 0x79; + input[3] = (byte) 0xA0; + input[99] = 0x21; // '!' character in the message area + final byte[] largeDest = new byte[2 + 100 * 2]; + final int end = HexWriter.encodeTo(input, input.length, largeDest, 0, false); + final String result = new String(largeDest, 0, end, StandardCharsets.US_ASCII); + assertThat(result).startsWith("0x08c379a0"); + assertThat(result).hasSize(2 + 200); // "0x" + 200 hex chars + assertThat(result).endsWith("21"); + } + + @Test + void largeValueTightBuffer() { + // 128 bytes → 258 chars ("0x" + 256 hex chars) + final byte[] input = filledBytes(128, 0xAB); + final byte[] tight = new byte[258]; + final int end = HexWriter.encodeTo(input, input.length, tight, 0, false); + assertThat(end).isEqualTo(258); + assertThat(new String(tight, 0, end, StandardCharsets.US_ASCII)) + .isEqualTo("0x" + "ab".repeat(128)); + } + + @Test + void largeValueAllZerosStripLeading() { + final byte[] input = new byte[128]; + assertThat(encode(input, true)).isEqualTo("0x0"); + } + + // ── regression: buffer overflow with values exceeding dest capacity ── + // + // The original compactHexBytes wrote into a fixed hexBuf without checking + // whether the hex output (2 + len*2 bytes) would fit, causing AIOBE for + // large revert reasons or any value whose hex output exceeded the buffer. + + @Test + void regression_veryLargeValueDoesNotOverflow() { + // 1024 bytes → "0x" + 2048 hex chars = 2050 bytes total + final byte[] input = filledBytes(1024, 0xCD); + final byte[] largeDest = new byte[2 + 1024 * 2]; + final int end = HexWriter.encodeTo(input, input.length, largeDest, 0, false); + assertThat(end).isEqualTo(2050); + assertThat(new String(largeDest, 0, end, StandardCharsets.US_ASCII)) + .isEqualTo("0x" + "cd".repeat(1024)); + } + + @Test + void regression_largeStripLeadingWithOneNonZeroByte() { + // 512 bytes, all zeros except the last byte — strips to "0xff" + final byte[] input = new byte[512]; + input[511] = (byte) 0xFF; + final byte[] largeDest = new byte[2 + 512 * 2]; + final int end = HexWriter.encodeTo(input, input.length, largeDest, 0, true); + assertThat(new String(largeDest, 0, end, StandardCharsets.US_ASCII)).isEqualTo("0xff"); + } + + @Test + void regression_largeStripLeadingWithLeadingZeroNibble() { + // 256 bytes, first 254 are zero, then 0x0A, 0xBC + // Should produce "0xabc" (strip leading zero nibble of 0x0A) + final byte[] input = new byte[256]; + input[254] = 0x0A; + input[255] = (byte) 0xBC; + final byte[] largeDest = new byte[2 + 256 * 2]; + final int end = HexWriter.encodeTo(input, input.length, largeDest, 0, true); + assertThat(new String(largeDest, 0, end, StandardCharsets.US_ASCII)).isEqualTo("0xabc"); + } + + @Test + void regression_destExactlyFitsOutput() { + // Ensure no off-by-one when dest is exactly the right size + final byte[] input = new byte[] {(byte) 0xAB, (byte) 0xCD}; + // "0xabcd" = 6 bytes + final byte[] tight = new byte[6]; + final int end = HexWriter.encodeTo(input, input.length, tight, 0, true); + assertThat(end).isEqualTo(6); + assertThat(new String(tight, StandardCharsets.US_ASCII)).isEqualTo("0xabcd"); + } + + @Test + void regression_destExactlyFitsFullWidth() { + final byte[] input = new byte[] {0x00, 0x01}; + // "0x0001" = 6 bytes + final byte[] tight = new byte[6]; + final int end = HexWriter.encodeTo(input, input.length, tight, 0, false); + assertThat(end).isEqualTo(6); + assertThat(new String(tight, StandardCharsets.US_ASCII)).isEqualTo("0x0001"); + } + + // ── regression: empty input writes 3 bytes, not 2 ────────────────── + // + // writeHex computed maxLen = 2 + bytes.length * 2. For an empty byte[] + // (Bytes.EMPTY.toArrayUnsafe()) that evaluates to 2, but encodeTo always + // emits "0x0" — three bytes. When the write buffer had exactly two bytes + // of headroom the third byte wrote past the array end, producing an + // ArrayIndexOutOfBoundsException that corrupted the streaming JSON output. + // Fix: maxLen = Math.max(3, 2 + bytes.length * 2). + + @Test + void regression_emptyInputNeedsThreeNotTwoBytes() { + // Simulate a buffer with only 2 bytes of space remaining. + // Before the fix, the caller would think 2 bytes were enough. + final byte[] buf = new byte[10]; + final int destPos = buf.length - 2; // only 2 bytes left + + // encodeTo writes 3 bytes ("0x0") — one past the end → AIOOBE + assertThatThrownBy(() -> HexWriter.encodeTo(new byte[] {}, 0, buf, destPos, true)) + .isInstanceOf(ArrayIndexOutOfBoundsException.class); + } + + @Test + void regression_emptyInputSucceedsWithThreeBytesOfRoom() { + // With 3 bytes of space, the same call succeeds. + final byte[] buf = new byte[10]; + final int destPos = buf.length - 3; + final int end = HexWriter.encodeTo(new byte[] {}, 0, buf, destPos, true); + assertThat(end).isEqualTo(buf.length); + assertThat(new String(buf, destPos, end - destPos, StandardCharsets.US_ASCII)).isEqualTo("0x0"); + } + + // ── helpers ──────────────────────────────────────────────────────── + + private static byte[] uint256(final long value) { + final byte[] bytes = new byte[32]; + long v = value; + for (int i = 31; i >= 0 && v != 0; i--) { + bytes[i] = (byte) (v & 0xFF); + v >>>= 8; + } + return bytes; + } + + private static byte[] filledBytes(final int size, final int value) { + final byte[] bytes = new byte[size]; + java.util.Arrays.fill(bytes, (byte) value); + return bytes; + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceFilterTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceFilterTest.java index 3f13e06c3f8..bc17fcda7d7 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceFilterTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceFilterTest.java @@ -32,7 +32,6 @@ import java.util.function.Supplier; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -85,11 +84,4 @@ public void shouldFailIfParamsExceedMaxRange( final JsonRpcErrorResponse errorResponse = (JsonRpcErrorResponse) response; assertThat(errorResponse.getErrorType()).isEqualTo(RpcErrorType.EXCEEDS_RPC_MAX_BLOCK_RANGE); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityTest.java new file mode 100644 index 00000000000..28f6239fc57 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityTest.java @@ -0,0 +1,216 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.apache.tuweni.units.bigints.UInt64; +import org.junit.jupiter.api.Test; + +public class QuantityTest { + + // --- create(int) --- + + @Test + public void createInt_zero() { + assertThat(Quantity.create(0)).isEqualTo("0x0"); + } + + @Test + public void createInt_one() { + assertThat(Quantity.create(1)).isEqualTo("0x1"); + } + + @Test + public void createInt_maxValue() { + assertThat(Quantity.create(Integer.MAX_VALUE)) + .isEqualTo("0x" + Integer.toHexString(Integer.MAX_VALUE)); + } + + // --- create(long) --- + + @Test + public void createLong_zero() { + assertThat(Quantity.create(0L)).isEqualTo("0x0"); + } + + @Test + public void createLong_one() { + assertThat(Quantity.create(1L)).isEqualTo("0x1"); + } + + @Test + public void createLong_maxValue() { + assertThat(Quantity.create(Long.MAX_VALUE)).isEqualTo("0x" + Long.toHexString(Long.MAX_VALUE)); + } + + // --- create(byte) --- + + @Test + public void createByte_zero() { + assertThat(Quantity.create((byte) 0)).isEqualTo("0x0"); + } + + @Test + public void createByte_one() { + assertThat(Quantity.create((byte) 1)).isEqualTo("0x1"); + } + + @Test + public void createByte_maxPositive() { + assertThat(Quantity.create(Byte.MAX_VALUE)).isEqualTo("0x7f"); + } + + // --- create(Bytes) --- + + @Test + public void createBytes_empty() { + assertThat(Quantity.create(Bytes.EMPTY)).isEqualTo("0x0"); + } + + @Test + public void createBytes_zero() { + assertThat(Quantity.create(Bytes.of(0x00))).isEqualTo("0x0"); + } + + @Test + public void createBytes_oneByte() { + assertThat(Quantity.create(Bytes.of(0x01))).isEqualTo("0x1"); + } + + @Test + public void createBytes_leadingZerosStripped() { + assertThat(Quantity.create(Bytes.fromHexString("0x000001"))).isEqualTo("0x1"); + } + + @Test + public void createBytes_multipleBytes() { + assertThat(Quantity.create(Bytes.fromHexString("0x0100"))).isEqualTo("0x100"); + } + + // --- create(byte[]) --- + + @Test + public void createByteArray_zero() { + assertThat(Quantity.create(new byte[] {0x00})).isEqualTo("0x0"); + } + + @Test + public void createByteArray_leadingZerosStripped() { + assertThat(Quantity.create(new byte[] {0x00, 0x00, 0x01})).isEqualTo("0x1"); + } + + @Test + public void createByteArray_multipleBytes() { + assertThat(Quantity.create(new byte[] {0x01, 0x00})).isEqualTo("0x100"); + } + + // --- create(BigInteger) --- + + @Test + public void createBigInteger_zero() { + assertThat(Quantity.create(BigInteger.ZERO)).isEqualTo("0x0"); + } + + @Test + public void createBigInteger_one() { + assertThat(Quantity.create(BigInteger.ONE)).isEqualTo("0x1"); + } + + @Test + public void createBigInteger_large() { + final BigInteger value = BigInteger.TWO.pow(128); + assertThat(Quantity.create(value)).isEqualTo("0x" + value.toString(16)); + } + + // --- create(UInt256Value) --- + + @Test + public void createUInt256_null() { + assertThat(Quantity.create((UInt256) null)).isNull(); + } + + @Test + public void createUInt256_zero() { + assertThat(Quantity.create(UInt256.ZERO)).isEqualTo("0x0"); + } + + @Test + public void createUInt256_one() { + assertThat(Quantity.create(UInt256.ONE)).isEqualTo("0x1"); + } + + @Test + public void createUInt256_maxValue() { + assertThat(Quantity.create(UInt256.MAX_VALUE)) + .startsWith("0x") + .hasSize(66); // 0x + 64 hex chars + } + + // --- create(UInt64Value) --- + + @Test + public void createUInt64_null() { + assertThat(Quantity.create((UInt64) null)).isEqualTo("0x0"); + } + + @Test + public void createUInt64_zero() { + assertThat(Quantity.create(UInt64.ZERO)).isEqualTo("0x0"); + } + + @Test + public void createUInt64_one() { + assertThat(Quantity.create(UInt64.ONE)).startsWith("0x"); + } + + // --- longToPaddedHex --- + + @Test + public void longToPaddedHex_zero() { + assertThat(Quantity.longToPaddedHex(0L, 4)).isEqualTo("0x00000000"); + } + + @Test + public void longToPaddedHex_padded() { + assertThat(Quantity.longToPaddedHex(1L, 4)).isEqualTo("0x00000001"); + } + + @Test + public void longToPaddedHex_full() { + assertThat(Quantity.longToPaddedHex(0xdeadbeefL, 4)).isEqualTo("0xdeadbeef"); + } + + // --- isValid --- + + @Test + public void isValid_withPrefix() { + assertThat(Quantity.isValid("0x1")).isTrue(); + } + + @Test + public void isValid_withoutPrefix() { + assertThat(Quantity.isValid("1")).isFalse(); + } + + @Test + public void isValid_zeroHex() { + assertThat(Quantity.isValid("0x0")).isTrue(); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/transaction/pool/PendingPermissionTransactionFilterTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/transaction/pool/PendingPermissionTransactionFilterTest.java index 234e0ee5b1e..f6bacbbe18b 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/transaction/pool/PendingPermissionTransactionFilterTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/transaction/pool/PendingPermissionTransactionFilterTest.java @@ -39,7 +39,6 @@ import java.util.Optional; import java.util.Set; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -127,11 +126,4 @@ private Set getPendingTransactions() { } return new LinkedHashSet<>(pendingTransactionList); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/timeout/TimeoutHandlerTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/timeout/TimeoutHandlerTest.java index 2fd96d6b98c..a8620f34f7f 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/timeout/TimeoutHandlerTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/timeout/TimeoutHandlerTest.java @@ -14,11 +14,13 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.timeout; -import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod.ETH_BLOCK_NUMBER; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod.ETH_GET_LOGS; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -38,11 +40,15 @@ import com.google.common.collect.ImmutableMap; import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.http.HttpConnection; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; public class TimeoutHandlerTest { @@ -89,10 +95,97 @@ public void test( verify(ctx, times(timerMustBeSet ? 1 : 0)).addBodyEndHandler(any()); } + // --- Timer callback behavior tests --- + + /** + * Creates a RoutingContext with a timer that captures the timer callback, so tests can invoke it + * directly. + */ + @SuppressWarnings("unchecked") + private static TimerSetup setupTimerCapture() { + final RoutingContext ctx = mock(RoutingContext.class); + final Vertx vertx = mock(Vertx.class); + final HttpServerResponse response = mock(HttpServerResponse.class); + final HttpServerRequest request = mock(HttpServerRequest.class); + final HttpConnection connection = mock(HttpConnection.class); + + final JsonObject requestBody = mock(JsonObject.class); + when(requestBody.getString("method")).thenReturn(ETH_GET_LOGS.getMethodName()); + when(ctx.data()).thenReturn(Map.of(ContextKey.REQUEST_BODY_AS_JSON_OBJECT.name(), requestBody)); + when(ctx.get(ContextKey.REQUEST_BODY_AS_JSON_OBJECT.name())).thenReturn(requestBody); + when(ctx.vertx()).thenReturn(vertx); + when(ctx.response()).thenReturn(response); + when(ctx.request()).thenReturn(request); + when(request.connection()).thenReturn(connection); + + final ArgumentCaptor> timerCaptor = ArgumentCaptor.forClass(Handler.class); + when(vertx.setTimer(anyLong(), timerCaptor.capture())).thenReturn(1L); + + final Map options = + ImmutableMap.of( + ETH_GET_LOGS.getMethodName(), + new TimeoutOptions(DEFAULT_OPTS.getTimeoutSeconds(), DEFAULT_OPTS.getErrorCode())); + TimeoutHandler.handler(Optional.empty(), options).handle(ctx); + + return new TimerSetup(ctx, response, connection, timerCaptor.getValue()); + } + + private record TimerSetup( + RoutingContext ctx, + HttpServerResponse response, + HttpConnection connection, + Handler timerCallback) {} + + @Test + void timeoutBeforeHeaders_callsCtxFail() { + final TimerSetup setup = setupTimerCapture(); + when(setup.response().ended()).thenReturn(false); + when(setup.response().closed()).thenReturn(false); + when(setup.response().headWritten()).thenReturn(false); + + setup.timerCallback().handle(1L); + + verify(setup.ctx()).fail(DEFAULT_OPTS.getErrorCode()); + verify(setup.response(), never()).reset(); + verify(setup.connection()).close(); + } + + @Test + void timeoutAfterHeaders_resetsInsteadOfFailing() { + final TimerSetup setup = setupTimerCapture(); + when(setup.response().ended()).thenReturn(false); + when(setup.response().closed()).thenReturn(false); + when(setup.response().headWritten()).thenReturn(true); + + setup.timerCallback().handle(1L); + + verify(setup.response()).reset(); + verify(setup.ctx(), never()).fail(DEFAULT_OPTS.getErrorCode()); + verify(setup.connection()).close(); + } + @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); + void timeoutAfterResponseEnded_doesNothing() { + final TimerSetup setup = setupTimerCapture(); + when(setup.response().ended()).thenReturn(true); + + setup.timerCallback().handle(1L); + + verify(setup.response(), never()).reset(); + verify(setup.ctx(), never()).fail(DEFAULT_OPTS.getErrorCode()); + verify(setup.connection(), never()).close(); + } + + @Test + void timeoutAfterResponseClosed_doesNothing() { + final TimerSetup setup = setupTimerCapture(); + when(setup.response().ended()).thenReturn(false); + when(setup.response().closed()).thenReturn(true); + + setup.timerCallback().handle(1L); + + verify(setup.response(), never()).reset(); + verify(setup.ctx(), never()).fail(DEFAULT_OPTS.getErrorCode()); + verify(setup.connection(), never()).close(); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/BackendQueryTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/BackendQueryTest.java index 4c5c2679eed..20eaf656ed7 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/BackendQueryTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/BackendQueryTest.java @@ -20,7 +20,6 @@ import java.util.function.Supplier; import java.util.stream.Stream; -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; @@ -53,11 +52,4 @@ public void test( assertThat(BackendQuery.runIfAlive(() -> wantReturn, alive)).isEqualTo(wantReturn); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_default.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_default.json index d80b35d76f2..ed2e6d13b72 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_default.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_default.json @@ -14,9 +14,6 @@ { "txHash":"0x4de634fe767d1f6d0512ca0c9c0a054d3a2596f7cdd7c1eea5f93046a740b3c7", "result":{ - "gas":61584, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -155,15 +152,15 @@ "0x0000000000000000000000000000000000000000000000000000000000000002":"0x0000000000000000000000000000000000000000000000000000000000000002" } } - ] + ], + "gas":61584, + "failed":false, + "returnValue":"" } }, { "txHash":"0xf882ec206292910527fd7095e59a1ca027b873296f1eba3886aa1addc4ff0ab9", "result":{ - "gas":31584, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -302,15 +299,15 @@ "0x0000000000000000000000000000000000000000000000000000000000000002":"0x0000000000000000000000000000000000000000000000000000000000000004" } } - ] + ], + "gas":31584, + "failed":false, + "returnValue":"" } }, { "txHash":"0x7ca6bf869e8882216f7443accb8d642df41af5bfa3a0e63bf03be2cfe629a030", "result":{ - "gas":13686, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -449,7 +446,10 @@ "0x0000000000000000000000000000000000000000000000000000000000000001":"0x0000000000000000000000000000000000000000000000000000000000000000" } } - ] + ], + "gas":13686, + "failed":false, + "returnValue":"" } } ] diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableMemory.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableMemory.json index f7b391a32b1..7fade2d2f97 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableMemory.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableMemory.json @@ -17,9 +17,6 @@ { "txHash":"0x4de634fe767d1f6d0512ca0c9c0a054d3a2596f7cdd7c1eea5f93046a740b3c7", "result":{ - "gas":61584, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -158,15 +155,15 @@ "0x0000000000000000000000000000000000000000000000000000000000000002":"0x0000000000000000000000000000000000000000000000000000000000000002" } } - ] + ], + "gas":61584, + "failed":false, + "returnValue":"" } }, { "txHash":"0xf882ec206292910527fd7095e59a1ca027b873296f1eba3886aa1addc4ff0ab9", "result":{ - "gas":31584, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -305,15 +302,15 @@ "0x0000000000000000000000000000000000000000000000000000000000000002":"0x0000000000000000000000000000000000000000000000000000000000000004" } } - ] + ], + "gas":31584, + "failed":false, + "returnValue":"" } }, { "txHash":"0x7ca6bf869e8882216f7443accb8d642df41af5bfa3a0e63bf03be2cfe629a030", "result":{ - "gas":13686, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -452,7 +449,10 @@ "0x0000000000000000000000000000000000000000000000000000000000000001":"0x0000000000000000000000000000000000000000000000000000000000000000" } } - ] + ], + "gas":13686, + "failed":false, + "returnValue":"" } } ] diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableStack.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableStack.json index a2083546162..69137752611 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableStack.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableStack.json @@ -17,9 +17,6 @@ { "txHash":"0x4de634fe767d1f6d0512ca0c9c0a054d3a2596f7cdd7c1eea5f93046a740b3c7", "result":{ - "gas":61584, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -121,15 +118,15 @@ "0x0000000000000000000000000000000000000000000000000000000000000002":"0x0000000000000000000000000000000000000000000000000000000000000002" } } - ] + ], + "gas":61584, + "failed":false, + "returnValue":"" } }, { "txHash":"0xf882ec206292910527fd7095e59a1ca027b873296f1eba3886aa1addc4ff0ab9", "result":{ - "gas":31584, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -231,15 +228,15 @@ "0x0000000000000000000000000000000000000000000000000000000000000002":"0x0000000000000000000000000000000000000000000000000000000000000004" } } - ] + ], + "gas":31584, + "failed":false, + "returnValue":"" } }, { "txHash":"0x7ca6bf869e8882216f7443accb8d642df41af5bfa3a0e63bf03be2cfe629a030", "result":{ - "gas":13686, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -341,7 +338,10 @@ "0x0000000000000000000000000000000000000000000000000000000000000001":"0x0000000000000000000000000000000000000000000000000000000000000000" } } - ] + ], + "gas":13686, + "failed":false, + "returnValue":"" } } ] diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableStorage.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableStorage.json index 52f8184fe0f..7a2697be458 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableStorage.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/trace-block/debug_traceBlock_disableStorage.json @@ -17,9 +17,6 @@ { "txHash":"0x4de634fe767d1f6d0512ca0c9c0a054d3a2596f7cdd7c1eea5f93046a740b3c7", "result":{ - "gas":61584, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -135,15 +132,15 @@ ] } - ] + ], + "gas":61584, + "failed":false, + "returnValue":"" } }, { "txHash":"0xf882ec206292910527fd7095e59a1ca027b873296f1eba3886aa1addc4ff0ab9", "result":{ - "gas":31584, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -259,15 +256,15 @@ ] } - ] + ], + "gas":31584, + "failed":false, + "returnValue":"" } }, { "txHash":"0x7ca6bf869e8882216f7443accb8d642df41af5bfa3a0e63bf03be2cfe629a030", "result":{ - "gas":13686, - "failed":false, - "returnValue":"", "structLogs":[ { "pc":0, @@ -385,7 +382,10 @@ ] } - ] + ], + "gas":13686, + "failed":false, + "returnValue":"" } } ] diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data/genesis-osaka.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data/genesis-osaka.json new file mode 100644 index 00000000000..613d3d49eaf --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data/genesis-osaka.json @@ -0,0 +1,124 @@ +{ + "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, + "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":{ + "627306090abab3a6e1400e9345bc60c78a8bef57": { + "balance": "0x3635C9ADC5DEA00000", + "comment": "funded test account" + }, + "0030000000000000000000000000000000000000": { + "comment": "Reads a 32 byte value from input data, increments it, and returns.", + "code": "0x60003560010160005260206000F3", + "balance": "0x0" + }, + "0032000000000000000000000000000000000000": { + "comment": "Copies calldata to memory then REVERTs with it — used by revert-reason tests.", + "code": "0x366000600037366000fd", + "balance": "0x0" + }, + "0x00A3ca265EBcb825B45F985A16CEFB49958cE017": { + "comment": "Withdrawal Request Smart Contract", + "balance": "0", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460c7573615156028575f545f5260205ff35b36603814156101f05760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f057600182026001905f5b5f821115608057810190830284830290049160010191906065565b9093900434106101f057600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160db575060105b5f5b81811461017f5780604c02838201600302600401805490600101805490600101549160601b83528260140152807fffffffffffffffffffffffffffffffff0000000000000000000000000000000016826034015260401c906044018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160dd565b9101809214610191579060025561019c565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101c957505f5b6001546002828201116101de5750505f6101e4565b01600290035b5f555f600155604c025ff35b5f5ffd", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002": "0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0000000000000000000000000000000000000000000000000000000000000001" + } + }, + "0x4242424242424242424242424242424242424242": { + "comment": "Deposit Request Smart Contract", + "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": "Consolidation Request Smart Contract", + "nonce": "0x01", + "balance": "0x00", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500", + "storage": {} + } + }, + "number":"0x0", + "gasUsed":"0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas":"0x7" +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java new file mode 100644 index 00000000000..31e64b2962f --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java @@ -0,0 +1,120 @@ +/* + * 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.v2; + +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.bytesToUInt256; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomUInt256Value; + +import org.hyperledger.besu.evm.UInt256; + +import java.util.concurrent.ThreadLocalRandom; + +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; + +/** + * Abstract base class for shift operation benchmarks (SHL, SHR, SAR). + * + *

Provides shared test case definitions and setup logic. + */ +public abstract class AbstractShiftOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { + + /** Test cases covering different execution paths for shift operations. */ + public enum Case { + /** Shift by 0 - no shift needed. */ + SHIFT_0, + /** Small shift by 1 bit. */ + SHIFT_1, + /** Medium shift by 128 bits. */ + SHIFT_128, + /** Large shift by 255 bits (max valid). */ + SHIFT_255, + /** Overflow: shift of exactly 256. */ + OVERFLOW_SHIFT_256, + /** Overflow: shift amount > 4 bytes. */ + OVERFLOW_LARGE_SHIFT, + /** Random values (original behavior). */ + FULL_RANDOM + } + + @Param({ + "SHIFT_0", + "SHIFT_1", + "SHIFT_128", + "SHIFT_255", + "OVERFLOW_SHIFT_256", + "OVERFLOW_LARGE_SHIFT", + "FULL_RANDOM" + }) + protected String caseName; + + @Setup(Level.Iteration) + @Override + public void setUp() { + frame = BenchmarkHelperV2.createMessageCallFrame(); + + final Case scenario = Case.valueOf(caseName); + aPool = new UInt256[SAMPLE_SIZE]; // shift amount (pushed second, popped first) + bPool = new UInt256[SAMPLE_SIZE]; // value (pushed first, popped second) + + final ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (int i = 0; i < SAMPLE_SIZE; i++) { + switch (scenario) { + case SHIFT_0: + aPool[i] = UInt256.ZERO; + bPool[i] = randomUInt256Value(random); + break; + + case SHIFT_1: + aPool[i] = UInt256.ONE; + bPool[i] = randomUInt256Value(random); + break; + + case SHIFT_128: + aPool[i] = UInt256.fromInt(128); + bPool[i] = randomUInt256Value(random); + break; + + case SHIFT_255: + aPool[i] = UInt256.fromInt(255); + bPool[i] = randomUInt256Value(random); + break; + + case OVERFLOW_SHIFT_256: + aPool[i] = UInt256.fromInt(256); // 256 + bPool[i] = randomUInt256Value(random); + break; + + case OVERFLOW_LARGE_SHIFT: + aPool[i] = bytesToUInt256(new byte[] {0x01, 0x00, 0x00, 0x00, 0x00, 0x00}); + bPool[i] = randomUInt256Value(random); + break; + + case FULL_RANDOM: + default: + final byte[] shift = new byte[1 + random.nextInt(4)]; + final byte[] value = new byte[1 + random.nextInt(32)]; + random.nextBytes(shift); + random.nextBytes(value); + aPool[i] = bytesToUInt256(shift); + bPool[i] = bytesToUInt256(value); + break; + } + } + index = 0; + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AddOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AddOperationBenchmarkV2.java index c9e16edbd0e..97b7fa76aed 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AddOperationBenchmarkV2.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AddOperationBenchmarkV2.java @@ -16,7 +16,7 @@ import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.operation.Operation; -import org.hyperledger.besu.evm.operation.v2.AddOperationV2; +import org.hyperledger.besu.evm.v2.operation.AddOperationV2; public class AddOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/BenchmarkHelperV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/BenchmarkHelperV2.java index fa062fd5fb0..8afcbb92194 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/BenchmarkHelperV2.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/BenchmarkHelperV2.java @@ -25,6 +25,7 @@ import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import org.apache.tuweni.bytes.Bytes32; @@ -101,4 +102,44 @@ static void pushUInt256(final MessageFrame frame, final org.hyperledger.besu.evm s[dst + 3] = value.u0(); frame.setTopV2(top + 1); } + + /** + * Generates a random UInt256 value. + * + * @param random thread-local random source + * @return random UInt256 value + */ + static org.hyperledger.besu.evm.UInt256 randomUInt256Value(final ThreadLocalRandom random) { + final byte[] value = new byte[32]; + random.nextBytes(value); + return org.hyperledger.besu.evm.UInt256.fromBytesBE(value); + } + + /** + * Generates a random positive signed 256-bit UInt256 value (sign bit cleared). + * + * @param random thread-local random source + * @return random positive UInt256 value + */ + static org.hyperledger.besu.evm.UInt256 randomPositiveUInt256Value( + final ThreadLocalRandom random) { + final byte[] value = new byte[32]; + random.nextBytes(value); + value[0] = (byte) (value[0] & 0x7F); + return org.hyperledger.besu.evm.UInt256.fromBytesBE(value); + } + + /** + * Generates a random negative signed 256-bit UInt256 value (sign bit set). + * + * @param random thread-local random source + * @return random negative UInt256 value + */ + static org.hyperledger.besu.evm.UInt256 randomNegativeUInt256Value( + final ThreadLocalRandom random) { + final byte[] value = new byte[32]; + random.nextBytes(value); + value[0] = (byte) (value[0] | 0x80); + return org.hyperledger.besu.evm.UInt256.fromBytesBE(value); + } } diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java new file mode 100644 index 00000000000..e617026a236 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java @@ -0,0 +1,156 @@ +/* + * 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.v2; + +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.bytesToUInt256; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomNegativeUInt256Value; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomPositiveUInt256Value; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomUInt256Value; + +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.operation.SarOperationV2; + +import java.util.concurrent.ThreadLocalRandom; + +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; + +public class SarOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { + + /** Test cases covering different execution paths for SAR operations. */ + public enum Case { + /** Shift by 0 - early return path. */ + SHIFT_0, + /** Negative number with shift=1 - tests sign extension. */ + NEGATIVE_SHIFT_1, + /** Value with all bits set with shift=1. */ + ALL_BITS_SHIFT_1, + /** Positive number with shift=1 - no sign extension needed. */ + POSITIVE_SHIFT_1, + /** Negative number with medium shift. */ + NEGATIVE_SHIFT_128, + /** Negative number with max shift. */ + NEGATIVE_SHIFT_255, + /** Positive number with medium shift. */ + POSITIVE_SHIFT_128, + /** Positive number with max shift. */ + POSITIVE_SHIFT_255, + /** Overflow: shift >= 256. */ + OVERFLOW_SHIFT_256, + /** Overflow: shift amount > 4 bytes. */ + OVERFLOW_LARGE_SHIFT, + /** Random values. */ + FULL_RANDOM + } + + @Param({ + "SHIFT_0", + "NEGATIVE_SHIFT_1", + "POSITIVE_SHIFT_1", + "ALL_BITS_SHIFT_1", + "NEGATIVE_SHIFT_128", + "NEGATIVE_SHIFT_255", + "POSITIVE_SHIFT_128", + "POSITIVE_SHIFT_255", + "OVERFLOW_SHIFT_256", + "OVERFLOW_LARGE_SHIFT", + "FULL_RANDOM" + }) + protected String caseName; + + @Setup(Level.Iteration) + @Override + public void setUp() { + frame = BenchmarkHelperV2.createMessageCallFrame(); + + final Case scenario = Case.valueOf(caseName); + aPool = new UInt256[SAMPLE_SIZE]; + bPool = new UInt256[SAMPLE_SIZE]; + + final ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (int i = 0; i < SAMPLE_SIZE; i++) { + switch (scenario) { + case SHIFT_0: + aPool[i] = UInt256.fromInt(0); + bPool[i] = randomUInt256Value(random); + break; + + case NEGATIVE_SHIFT_1: + aPool[i] = UInt256.fromInt(1); + bPool[i] = randomNegativeUInt256Value(random); + break; + + case ALL_BITS_SHIFT_1: + aPool[i] = UInt256.fromInt(1); + bPool[i] = UInt256.MAX; + break; + + case POSITIVE_SHIFT_1: + aPool[i] = UInt256.fromInt(1); + bPool[i] = randomPositiveUInt256Value(random); + break; + + case NEGATIVE_SHIFT_128: + aPool[i] = UInt256.fromInt(128); + bPool[i] = randomNegativeUInt256Value(random); + break; + + case NEGATIVE_SHIFT_255: + aPool[i] = UInt256.fromInt(255); + bPool[i] = randomNegativeUInt256Value(random); + break; + + case POSITIVE_SHIFT_128: + aPool[i] = UInt256.fromInt(128); + bPool[i] = randomPositiveUInt256Value(random); + break; + case POSITIVE_SHIFT_255: + aPool[i] = UInt256.fromInt(255); + bPool[i] = randomPositiveUInt256Value(random); + break; + + case OVERFLOW_SHIFT_256: + aPool[i] = UInt256.fromInt(256); + bPool[i] = randomUInt256Value(random); + break; + + case OVERFLOW_LARGE_SHIFT: + aPool[i] = bytesToUInt256(new byte[] {0x01, 0x00, 0x00, 0x00, 0x00, 0x00}); + bPool[i] = randomUInt256Value(random); + break; + + case FULL_RANDOM: + default: + final byte[] shift = new byte[1 + random.nextInt(2)]; + final byte[] value = new byte[1 + random.nextInt(32)]; + random.nextBytes(shift); + random.nextBytes(value); + aPool[i] = bytesToUInt256(shift); + bPool[i] = bytesToUInt256(value); + break; + } + } + index = 0; + } + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return SarOperationV2.staticOperation(frame, frame.stackDataV2()); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java new file mode 100644 index 00000000000..117f8148036 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java @@ -0,0 +1,27 @@ +/* + * 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.v2; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.operation.ShlOperationV2; + +public class ShlOperationBenchmarkV2 extends AbstractShiftOperationBenchmarkV2 { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return ShlOperationV2.staticOperation(frame, frame.stackDataV2()); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java new file mode 100644 index 00000000000..fa3dd952609 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java @@ -0,0 +1,27 @@ +/* + * 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.v2; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.operation.ShrOperationV2; + +public class ShrOperationBenchmarkV2 extends AbstractShiftOperationBenchmarkV2 { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return ShrOperationV2.staticOperation(frame, frame.stackDataV2()); + } +} 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 b09b5a53f5e..87f1a6b7f00 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 @@ -71,6 +71,7 @@ import org.hyperledger.besu.ethereum.mainnet.blockhash.CancunPreExecutionProcessor; import org.hyperledger.besu.ethereum.mainnet.blockhash.FrontierPreExecutionProcessor; import org.hyperledger.besu.ethereum.mainnet.blockhash.PraguePreExecutionProcessor; +import org.hyperledger.besu.ethereum.mainnet.blockhash.PreExecutionProcessor; import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; import org.hyperledger.besu.ethereum.mainnet.parallelization.MainnetParallelBlockProcessor; @@ -875,13 +876,28 @@ static ProtocolSpecBuilder cancunDefinition( evm.getMaxInitcodeSize())) .precompileContractRegistryBuilder(MainnetPrecompiledContractRegistries::cancun) .blockHeaderValidatorBuilder(MainnetBlockHeaderValidator::blobAwareBlockHeaderValidator) - .preExecutionProcessor( - isPoAConsensus(genesisConfigOptions) - ? new FrontierPreExecutionProcessor() - : new CancunPreExecutionProcessor()) + .preExecutionProcessor(getPreExecutionProcessor(genesisConfigOptions)) .hardforkId(CANCUN); } + private static PreExecutionProcessor getPreExecutionProcessor( + final GenesisConfigOptions genesisConfigOptions) { + if (isPoAConsensus(genesisConfigOptions) && !hasSystemContractAddresses(genesisConfigOptions)) { + return new FrontierPreExecutionProcessor(); + } + + return new CancunPreExecutionProcessor(); + } + + private static PreExecutionProcessor getPraguePreExecutionProcessor( + final GenesisConfigOptions genesisConfigOptions) { + if (isPoAConsensus(genesisConfigOptions) && !hasSystemContractAddresses(genesisConfigOptions)) { + return new FrontierPreExecutionProcessor(); + } + + return new PraguePreExecutionProcessor(); + } + static ProtocolSpecBuilder pragueDefinition( final Optional chainId, final boolean enableRevertReason, @@ -959,14 +975,11 @@ static ProtocolSpecBuilder pragueDefinition( new CodeDelegationService())) .build()) // EIP-2935 Blockhash processor - .preExecutionProcessor( - isPoAConsensus(genesisConfigOptions) - ? new FrontierPreExecutionProcessor() - : new PraguePreExecutionProcessor()) + .preExecutionProcessor(getPraguePreExecutionProcessor(genesisConfigOptions)) .hardforkId(PRAGUE); - if (isPoAConsensus(genesisConfigOptions)) { - LOG.debug( - "Skipping system contract request processors for PoA consensus (clique/ibft/qbft)."); + if (isPoAConsensus(genesisConfigOptions) && !hasSystemContractAddresses(genesisConfigOptions)) { + LOG.warn( + "Skipping system contract request processors for PoA consensus (clique/ibft/qbft) without system contract addresses."); pragueSpecBuilder.requestProcessorCoordinator(RequestProcessorCoordinator.noOp()); } else { try { @@ -990,6 +1003,13 @@ private static boolean isPoAConsensus(final GenesisConfigOptions genesisConfigOp || genesisConfigOptions.isQbft(); } + private static boolean hasSystemContractAddresses( + final GenesisConfigOptions genesisConfigOptions) { + return genesisConfigOptions.getDepositContractAddress().isPresent() + && genesisConfigOptions.getWithdrawalRequestContractAddress().isPresent() + && genesisConfigOptions.getConsolidationRequestContractAddress().isPresent(); + } + static ProtocolSpecBuilder osakaDefinition( final Optional chainId, final boolean enableRevertReason, 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 050ee2d25c4..ed2f66eb618 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 @@ -447,20 +447,13 @@ public TransactionProcessingResult processTransaction( 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 + // We also need to include leftover reservoir in remaining gas for correct consumption + // calculation final long totalRemaining = initialFrame.getRemainingGas() + initialFrame.getStateGasReservoir(); final long totalConsumed = transaction.getGasLimit() - totalRemaining; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessListChanges.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessListChanges.java new file mode 100644 index 00000000000..5f22dbfe4a4 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessListChanges.java @@ -0,0 +1,79 @@ +/* + * 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.block.access.list; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.StorageSlotKey; +import org.hyperledger.besu.datatypes.Wei; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; + +public final class BlockAccessListChanges { + + private BlockAccessListChanges() {} + + public static List latestChanges(final BlockAccessList blockAccessList) { + final List accountFinalChanges = new ArrayList<>(); + + for (final BlockAccessList.AccountChanges accountChanges : blockAccessList.accountChanges()) { + if (!accountChanges.hasAnyChange()) { + continue; + } + + final List storageFinalChanges = new ArrayList<>(); + for (final BlockAccessList.SlotChanges slotChanges : accountChanges.storageChanges()) { + final Optional latestStorageChange = + lastOf(slotChanges.changes()); + if (latestStorageChange.isPresent()) { + storageFinalChanges.add( + new StorageFinalChange( + slotChanges.slot(), + latestStorageChange.get().newValue() == null + ? UInt256.ZERO + : latestStorageChange.get().newValue())); + } + } + + accountFinalChanges.add( + new AccountFinalChanges( + accountChanges.address(), + lastOf(accountChanges.balanceChanges()) + .map(BlockAccessList.BalanceChange::postBalance), + lastOf(accountChanges.nonceChanges()).map(BlockAccessList.NonceChange::newNonce), + lastOf(accountChanges.codeChanges()).map(BlockAccessList.CodeChange::newCode), + storageFinalChanges)); + } + + return accountFinalChanges; + } + + public record AccountFinalChanges( + Address address, + Optional balance, + Optional nonce, + Optional code, + List storageChanges) {} + + public record StorageFinalChange(StorageSlotKey slot, UInt256 value) {} + + 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/BalStateRootCalculator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BalStateRootCalculator.java index ddca24f990a..9046345eb61 100644 --- 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 @@ -16,11 +16,9 @@ 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.block.access.list.BlockAccessListChanges; 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; @@ -29,12 +27,9 @@ 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 { @@ -79,29 +74,16 @@ private static BonsaiWorldState openParentWorldState( private static void applyBalChanges( final PathBasedWorldStateUpdateAccumulator accumulator, final BlockAccessList bal) { - for (final AccountChanges changes : bal.accountChanges()) { - if (!changes.hasAnyChange()) { - continue; - } + for (final var changes : BlockAccessListChanges.latestChanges(bal)) { 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())); + changes.balance().ifPresent(account::setBalance); + changes.nonce().ifPresent(account::setNonce); + changes.code().ifPresent(account::setCode); - 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); - })); + for (final var storage : changes.storageChanges()) { + storage.slot().getSlotKey().ifPresent(key -> account.setStorageValue(key, storage.value())); } } accumulator.clearAccountsThatAreEmpty(); @@ -116,8 +98,4 @@ private static BalRootComputation computeRoot(final PathBasedWorldState worldSta 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/vm/AbstractDebugOperationTracer.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/AbstractDebugOperationTracer.java new file mode 100644 index 00000000000..b6106cb2ea6 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/AbstractDebugOperationTracer.java @@ -0,0 +1,143 @@ +/* + * 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; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.AbstractCallOperation; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.Operation.OperationResult; +import org.hyperledger.besu.evm.tracing.OpCodeTracerConfigBuilder.OpCodeTracerConfig; +import org.hyperledger.besu.evm.tracing.OperationTracer; + +import java.util.Locale; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; + +/** + * Base class for opcode-level operation tracers. Manages the per-frame state that must be captured + * in {@link #tracePreExecution} and consumed in {@link #tracePostExecution}, along with the shared + * opcode-filter and stack-capture logic. + * + *

Subclasses implement {@link #tracePostExecution} to decide what to do with the captured state + * (accumulate to a list, or stream directly to a writer). Subclasses that need additional + * pre-execution state (e.g., input data) can override {@link #capturePreExecutionState}. + */ +public abstract class AbstractDebugOperationTracer implements OperationTracer { + + protected final OpCodeTracerConfig options; + + /** + * If {@code true}, gas cost for CALL operations includes gas granted to the child call (Parity + * style, {@code trace_} RPCs). If {@code false}, only the operation cost is reported (Geth style, + * {@code debug_} RPCs). + */ + protected final boolean recordChildCallGas; + + /** Pre-execution stack snapshot, set in {@link #tracePreExecution}. */ + protected Optional preExecutionStack; + + /** Gas remaining before the operation executed, set in {@link #tracePreExecution}. */ + protected long gasRemaining; + + /** Program counter before the operation executed, set in {@link #tracePreExecution}. */ + protected int pc; + + /** Call depth before the operation executed, set in {@link #tracePreExecution}. */ + protected int depth; + + /** + * Whether the current opcode should be traced (respects the {@code traceOpcodes} filter). Set by + * {@link #tracePreExecution}; checked by {@link #tracePostExecution} in subclasses. + */ + protected boolean traceOpcode; + + private Operation previousOpcode = null; + + protected AbstractDebugOperationTracer( + final OpCodeTracerConfig options, final boolean recordChildCallGas) { + this.options = options; + this.recordChildCallGas = recordChildCallGas; + } + + @Override + public void tracePreExecution(final MessageFrame frame) { + final Operation currentOperation = frame.getCurrentOperation(); + if (!(traceOpcode = shouldTraceOpcode(currentOperation))) { + return; + } + preExecutionStack = captureStack(frame); + gasRemaining = frame.getRemainingGas(); + pc = frame.getPC(); + depth = frame.getDepth(); + capturePreExecutionState(frame); + } + + /** + * Hook for subclasses to capture additional pre-execution state. Called only when {@link + * #traceOpcode} is {@code true}, after the common fields have been populated. + */ + protected void capturePreExecutionState(final MessageFrame frame) {} + + /** + * Computes the effective gas cost for the operation, optionally including child-call gas. + * + * @param currentOperation the operation that was executed + * @param operationResult the result including raw gas cost + * @param frame the message frame after execution + * @return the effective gas cost to report + */ + protected long computeGasCost( + final Operation currentOperation, + final OperationResult operationResult, + final MessageFrame frame) { + long gasCost = operationResult.getGasCost(); + if (recordChildCallGas && currentOperation instanceof AbstractCallOperation) { + gasCost += frame.getMessageFrameStack().getFirst().getRemainingGas(); + } + return gasCost; + } + + /** + * Captures the current operand stack as an array ordered from bottom to top, or {@link + * Optional#empty()} when stack tracing is disabled. + */ + protected Optional captureStack(final MessageFrame frame) { + if (!options.traceStack()) { + return Optional.empty(); + } + final Bytes[] stackContents = new Bytes[frame.stackSize()]; + for (int i = 0; i < stackContents.length; i++) { + stackContents[i] = frame.getStackItem(stackContents.length - i - 1); + } + return Optional.of(stackContents); + } + + private boolean shouldTraceOpcode(final Operation currentOpcode) { + if (options.traceOpcodes().isEmpty()) { + return true; + } + final boolean traceCurrentOpcode = + options.traceOpcodes().contains(currentOpcode.getName().toLowerCase(Locale.ROOT)); + final boolean tracePreviousOpcode = + previousOpcode != null + && options.traceOpcodes().contains(previousOpcode.getName().toLowerCase(Locale.ROOT)); + if (!traceCurrentOpcode && !tracePreviousOpcode) { + return false; + } + previousOpcode = currentOpcode; + return true; + } +} 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 3619783564d..04f1952645b 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 @@ -20,17 +20,15 @@ import org.hyperledger.besu.evm.ModificationNotAllowedException; import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; import org.hyperledger.besu.evm.frame.MessageFrame; -import org.hyperledger.besu.evm.operation.AbstractCallOperation; +import org.hyperledger.besu.evm.operation.AbstractCreateOperation; import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.operation.Operation.OperationResult; import org.hyperledger.besu.evm.tracing.OpCodeTracerConfigBuilder.OpCodeTracerConfig; -import org.hyperledger.besu.evm.tracing.OperationTracer; import org.hyperledger.besu.evm.tracing.TraceFrame; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; @@ -39,29 +37,12 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; -public class DebugOperationTracer implements OperationTracer { - - private final OpCodeTracerConfig options; - - /** - * A flag to indicate if call operations should trace just the operation cost (false, Geth style, - * debug_ series RPCs) or the operation cost and all gas granted to the child call (true, Parity - * style, trace_ series RPCs) - */ - private final boolean recordChildCallGas; +public class DebugOperationTracer extends AbstractDebugOperationTracer { private List traceFrames = new ArrayList<>(); private TraceFrame lastFrame; - private Optional preExecutionStack; - private long gasRemaining; private Bytes inputData; - private int pc; - private int depth; - - // Flags used for implementing traceOpcodes functionality - private boolean traceOpcode; - private Operation previousOpcode = null; /** * Creates the operation tracer. @@ -71,53 +52,35 @@ public class DebugOperationTracer implements OperationTracer { * (false) gas amounts for call operations */ public DebugOperationTracer(final OpCodeTracerConfig options, final boolean recordChildCallGas) { - this.options = options; - this.recordChildCallGas = recordChildCallGas; + super(options, recordChildCallGas); } @Override - public void tracePreExecution(final MessageFrame frame) { - final Operation currentOperation = frame.getCurrentOperation(); - if (!(traceOpcode = traceOpcode(currentOperation))) { - return; - } - preExecutionStack = captureStack(frame); - gasRemaining = frame.getRemainingGas(); + protected void capturePreExecutionState(final MessageFrame frame) { if (lastFrame != null && frame.getDepth() > lastFrame.getDepth()) inputData = frame.getInputData().copy(); else inputData = frame.getInputData(); - pc = frame.getPC(); - depth = frame.getDepth(); - } - - private boolean traceOpcode(final Operation currentOpcode) { - if (options.traceOpcodes().isEmpty()) { - return true; - } - final boolean traceCurrentOpcode = - options.traceOpcodes().contains(currentOpcode.getName().toLowerCase(Locale.ROOT)); - final boolean tracePreviousOpcode = - previousOpcode != null - && options.traceOpcodes().contains(previousOpcode.getName().toLowerCase(Locale.ROOT)); - - if (!traceCurrentOpcode && !tracePreviousOpcode) { - return false; - } - previousOpcode = currentOpcode; - return true; } @Override public void tracePostExecution(final MessageFrame frame, final OperationResult operationResult) { - final Operation currentOperation = frame.getCurrentOperation(); - final String opcode = currentOperation.getName(); if (!traceOpcode) { return; } + final Operation currentOperation = frame.getCurrentOperation(); + final String opcode = currentOperation.getName(); final int opcodeNumber = (opcode != null) ? currentOperation.getOpcode() : Integer.MAX_VALUE; final WorldUpdater worldUpdater = frame.getWorldUpdater(); final Bytes outputData = frame.getOutputData(); - final Optional memory = captureMemory(frame); + // Always capture memory for soft-failed CREATE/CREATE2 ops so callTracer can extract init code + final Optional memory = + captureMemory(frame) + .or( + () -> + operationResult.getSoftFailureReason().isPresent() + && currentOperation instanceof AbstractCreateOperation + ? forceCaptureMem(frame) + : Optional.empty()); final Optional stackPostExecution = captureStack(frame); if (!traceFrames.isEmpty()) { @@ -130,10 +93,7 @@ public void tracePostExecution(final MessageFrame frame, final OperationResult o final Optional> storage = captureStorage(frame); final Optional> maybeRefunds = frame.getRefunds().isEmpty() ? Optional.empty() : Optional.of(frame.getRefunds()); - long thisGasCost = operationResult.getGasCost(); - if (recordChildCallGas && currentOperation instanceof AbstractCallOperation) { - thisGasCost += frame.getMessageFrameStack().getFirst().getRemainingGas(); - } + final long thisGasCost = computeGasCost(currentOperation, operationResult, frame); final Optional haltReason = Optional.ofNullable(operationResult.getHaltReason()).or(frame::getExceptionalHaltReason); @@ -177,7 +137,6 @@ public void tracePostExecution(final MessageFrame frame, final OperationResult o @Override public void tracePrecompileCall( final MessageFrame frame, final long gasRequirement, final Bytes output) { - final Address recipient = frame.getRecipientAddress(); final Bytes inputData = frame.getInputData().copy(); @@ -286,30 +245,26 @@ 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++) { - memoryContents[i] = frame.readMemory(i * 32L, 32); + } else if (frame.getMaybeUpdatedMemory().isEmpty() && lastFrame != null) { + final Optional lastMemory = lastFrame.getMemory(); + if (lastFrame.getDepth() == frame.getDepth() + && lastMemory.isPresent() + && lastMemory.get().length == frame.memoryWordSize()) { + return lastMemory; + } } - return Optional.of(memoryContents); + return forceCaptureMem(frame); } - private Optional captureStack(final MessageFrame frame) { - if (!options.traceStack()) { + private Optional forceCaptureMem(final MessageFrame frame) { + if (frame.memoryWordSize() == 0) { return Optional.empty(); } - - final Bytes[] stackContents = new Bytes[frame.stackSize()]; - for (int i = 0; i < stackContents.length; i++) { - // Record stack contents in reverse - stackContents[i] = frame.getStackItem(stackContents.length - i - 1); + final Bytes[] memoryContents = new Bytes[frame.memoryWordSize()]; + for (int i = 0; i < memoryContents.length; i++) { + memoryContents[i] = frame.readMemory(i * 32L, 32); } - return Optional.of(stackContents); + return Optional.of(memoryContents); } @Override diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/StreamingDebugOperationTracer.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/StreamingDebugOperationTracer.java new file mode 100644 index 00000000000..97c0fac8555 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/StreamingDebugOperationTracer.java @@ -0,0 +1,117 @@ +/* + * 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; + +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.Operation.OperationResult; +import org.hyperledger.besu.evm.tracing.OpCodeTracerConfigBuilder.OpCodeTracerConfig; + +import org.apache.tuweni.bytes.Bytes; + +/** + * An {@link AbstractDebugOperationTracer} that streams trace data directly to a {@link FrameWriter} + * callback on each operation, without accumulating frames in memory. Precompile and + * account-creation callbacks are no-ops because already-emitted frames cannot be retroactively + * updated. + */ +public class StreamingDebugOperationTracer extends AbstractDebugOperationTracer { + + /** + * Functional interface for zero-allocation streaming of trace data directly from a MessageFrame. + */ + @FunctionalInterface + public interface FrameWriter { + void writeFrame( + int pc, + String opcode, + long gasRemaining, + long gasCost, + int depth, + Bytes[] preExecutionStack, + MessageFrame frame, + ExceptionalHaltReason haltReason, + Bytes revertReason); + } + + private final FrameWriter frameWriter; + private boolean hasEmittedFrame = false; + + /** + * Creates a streaming operation tracer. + * + * @param options The opcode tracer config (stack/memory/storage options, opcode filter) + * @param recordChildCallGas If true, gas cost for CALL ops includes gas granted to the child call + * (Parity style). If false, only the operation cost is reported (Geth style). + * @param frameWriter Callback invoked once per traced operation with live frame data + */ + public StreamingDebugOperationTracer( + final OpCodeTracerConfig options, + final boolean recordChildCallGas, + final FrameWriter frameWriter) { + super(options, recordChildCallGas); + this.frameWriter = frameWriter; + } + + @Override + public void tracePostExecution(final MessageFrame frame, final OperationResult operationResult) { + if (!traceOpcode) { + return; + } + final Operation currentOperation = frame.getCurrentOperation(); + final String opcode = currentOperation.getName(); + final long thisGasCost = computeGasCost(currentOperation, operationResult, frame); + + final ExceptionalHaltReason haltReason = + operationResult.getHaltReason() != null + ? operationResult.getHaltReason() + : frame.getExceptionalHaltReason().orElse(null); + + final Bytes revertReason = frame.getRevertReason().orElse(null); + + frameWriter.writeFrame( + pc, + opcode, + gasRemaining, + thisGasCost, + depth, + preExecutionStack.orElse(null), + frame, + haltReason, + revertReason); + + hasEmittedFrame = true; + frame.reset(); + } + + @Override + public void tracePrecompileCall( + final MessageFrame frame, final long gasRequirement, final Bytes output) { + if (!hasEmittedFrame) { + frameWriter.writeFrame( + frame.getPC(), + "", + frame.getRemainingGas(), + 0L, + frame.getDepth(), + null, + frame, + null, + frame.getRevertReason().orElse(null)); + hasEmittedFrame = true; + } + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/GenesisStateTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/GenesisStateTest.java index fa5ce1c2106..6f9e92a5e17 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/GenesisStateTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/GenesisStateTest.java @@ -32,7 +32,6 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; import org.bouncycastle.util.encoders.Hex; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -371,11 +370,4 @@ void genesisSlotNumberNotPresentPreAmsterdam( final BlockHeader header = genesisState.getBlock().getHeader(); assertThat(header.getOptionalSlotNumber()).isEmpty(); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/BlobTransactionEncodingTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/BlobTransactionEncodingTest.java index 0e2bd36a91c..5aaad55ad36 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/BlobTransactionEncodingTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/BlobTransactionEncodingTest.java @@ -28,7 +28,6 @@ import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; -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; @@ -120,11 +119,4 @@ public String toString() { : bytes.toString(); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/BaseFeeMarketBaseFeeTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/BaseFeeMarketBaseFeeTest.java index 1a6d027c952..56654f3072a 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/BaseFeeMarketBaseFeeTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/BaseFeeMarketBaseFeeTest.java @@ -30,7 +30,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.Resources; import org.junit.jupiter.api.Disabled; -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; @@ -100,11 +99,4 @@ public void setExpectedBaseFee(final Wei expectedBaseFee) { this.expectedBaseFee = expectedBaseFee; } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/CoinbaseFeePriceCalculatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/CoinbaseFeePriceCalculatorTest.java index 1da9a689cf2..2706444f83f 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/CoinbaseFeePriceCalculatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/CoinbaseFeePriceCalculatorTest.java @@ -21,7 +21,6 @@ import java.util.Optional; import java.util.stream.Stream; -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; @@ -55,11 +54,4 @@ public void assertThatCalculatorWorks( assertThat(coinbaseFeePriceCalculator.price(coinbaseFee, transactionGasPrice, baseFee)) .isEqualByComparingTo(expectedPrice); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/TransactionPriceCalculatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/TransactionPriceCalculatorTest.java index df07fd1f752..c23061ac8f1 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/TransactionPriceCalculatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/feemarket/TransactionPriceCalculatorTest.java @@ -29,7 +29,6 @@ import java.util.Optional; import java.util.stream.Stream; -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; @@ -147,11 +146,4 @@ public void assertThatCalculatorWorks( baseFee)) .isEqualByComparingTo(expectedPrice); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 2c02e566b0b..8c2a199df8e 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 @@ -25,7 +25,6 @@ import java.util.List; import java.util.Optional; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; @@ -170,11 +169,4 @@ void pragueDefaultGasLimit() { assertThat(gasLimitCalculator.currentBlobGasLimit()).isEqualTo(0x120000); // 9 * 131072 assertThat(gasLimitCalculator.transactionBlobGasLimitCap()).isEqualTo(0x120000); // 9 * 131072 } - - @Test - void dryRunDetector() { - Assertions.assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .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/Eip8037GasLimitTest.java similarity index 84% rename from ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/Eip8037RegularGasLimitTest.java rename to ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/Eip8037GasLimitTest.java index f851e271edb..116b136fe27 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/Eip8037RegularGasLimitTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/Eip8037GasLimitTest.java @@ -50,7 +50,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class Eip8037RegularGasLimitTest { +class Eip8037GasLimitTest { private static final int MAX_STACK_SIZE = 1024; @@ -107,19 +107,29 @@ private void setupCommonMocks(final long gasLimit) { } @Test - void regularGasExceedingTxMaxGasLimitRevertsTransaction() { + void exceptionalHaltPreservesStateGasReservoirForRefund() { + // EIP-8037: On exceptional halt of the initial frame, the state_gas_reservoir must be + // preserved for transaction-level refund. This test simulates a child frame having refunded + // state gas to the parent's reservoir before the parent runs out of regular gas. The + // refunded reservoir must not be lost: the total gas used should be + // txGasLimit - preserved_reservoir, not the full txGasLimit. setupCommonMocks(20_000_000L); + // txGasLimit=20M, intrinsic≈21k → gasAvailable≈19,979,000. + // regularBudget = TX_MAX_GAS_LIMIT (16,777,216) - intrinsic ≈ 16,756,216. + // gas_left initial = 16,756,216; reservoir initial = 19,979,000 - 16,756,216 = 3,222,784. + // We simulate: child SSTORE spilled 37,568 into child gas_left, then child halted and the + // spill was restored to the reservoir. The initial frame then runs out of regular gas. + final long childRefund = 37_568L; + 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). + // Simulate a child frame halt having added state gas back to the reservoir. + frame.incrementStateGasReservoir(childRefund); + // Now simulate the initial frame running out of regular gas. 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; }) @@ -138,9 +148,12 @@ void regularGasExceedingTxMaxGasLimitRevertsTransaction() { Wei.ZERO); assertThat(result.isSuccessful()).isFalse(); - assertThat(result.getEstimateGasUsedByTransaction()).isEqualTo(20_000_000L); assertThat(result.getValidationResult().getInvalidReason()) .isEqualTo(TransactionInvalidReason.EXECUTION_HALTED); + // The reservoir (initial budget overflow 3,222,784 + childRefund 37,568) must be preserved + // for refund. Total gas used = 20,000,000 - (3,222,784 + 37,568) = 16,739,648. + assertThat(result.getEstimateGasUsedByTransaction()) + .isEqualTo(20_000_000L - 3_222_784L - childRefund); } @Test diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/IntrinsicGasTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/IntrinsicGasTest.java index 8081d779c15..31433538468 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/IntrinsicGasTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/IntrinsicGasTest.java @@ -14,8 +14,6 @@ */ package org.hyperledger.besu.ethereum.mainnet; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.datatypes.AccessListEntry; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; @@ -32,7 +30,6 @@ 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.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -121,13 +118,6 @@ public void validateGasCost( .isEqualTo(expectedGas); } - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } - long baselineGas(final GasCalculator gasCalculator, final Transaction transaction) { final List accessListEntries = transaction.getAccessList().orElse(List.of()); 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 99c73d07310..63020a7e0c0 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 @@ -24,7 +24,6 @@ import java.util.Optional; import java.util.OptionalInt; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; @@ -254,11 +253,4 @@ void blockBuilderBlobGasLimitClampedToProtocolMax() { assertThat(calculator.blockBuilderBlobGasLimit()) .isEqualTo(osakaGasCalculator.getBlobGasPerBlob() * maxBlobs); } - - @Test - void dryRunDetector() { - Assertions.assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/RefundSstoreGasTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/RefundSstoreGasTest.java index 77edb8a76fe..e98bbc528c1 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/RefundSstoreGasTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/RefundSstoreGasTest.java @@ -16,7 +16,6 @@ import static org.apache.tuweni.units.bigints.UInt256.ONE; import static org.apache.tuweni.units.bigints.UInt256.ZERO; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -30,7 +29,6 @@ import org.apache.tuweni.units.bigints.UInt256; import org.assertj.core.api.Assertions; -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; @@ -136,11 +134,4 @@ public void shouldRefundCorrectGas( newValue, mockSupplierCurrentValue, mockSupplierForOriginalValue)) .isEqualTo(expectedGasRefund); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleFixedBaseFeeMarketTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleFixedBaseFeeMarketTest.java index effa3d4d192..084ab23c88d 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleFixedBaseFeeMarketTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleFixedBaseFeeMarketTest.java @@ -25,7 +25,6 @@ import java.util.Optional; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -73,11 +72,4 @@ public void test( assertThat(uut.validate(header, parent)).isEqualTo(expectedResult); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleTest.java index 12b3d6926ca..30519dd1f86 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleTest.java @@ -24,7 +24,6 @@ import java.util.Optional; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -70,11 +69,4 @@ public void testGasLimitElasticityValidationRule( assertThat(uut.validate(header, parent)).isEqualTo(expectedResult); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleZeroBaseFeeMarketTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleZeroBaseFeeMarketTest.java index 54278b47d2d..237334aadb1 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleZeroBaseFeeMarketTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitElasticityValidationRuleZeroBaseFeeMarketTest.java @@ -24,7 +24,6 @@ import java.util.Optional; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -72,11 +71,4 @@ public void test( assertThat(uut.validate(header, parent)).isEqualTo(expectedResult); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRuleTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRuleTest.java index 665da47354c..635f8e1c190 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRuleTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRuleTest.java @@ -23,7 +23,6 @@ import java.util.Optional; import java.util.stream.Stream; -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; @@ -103,11 +102,4 @@ public void test( assertThat(uut.validate(header, parent)).isEqualTo(expectedResult); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasUsageValidationRuleTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasUsageValidationRuleTest.java index a06fa0c7001..06dff080461 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasUsageValidationRuleTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/headervalidationrules/GasUsageValidationRuleTest.java @@ -21,7 +21,6 @@ import java.util.stream.Stream; -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; @@ -49,11 +48,4 @@ public void test(final long gasUsed, final long gasLimit, final boolean expected assertThat(uut.validate(header, null)).isEqualTo(expectedResult); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 5b4cf38a126..29a686305f5 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 @@ -34,7 +34,6 @@ 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; import org.junit.jupiter.params.provider.MethodSource; @@ -143,11 +142,4 @@ public void searchesDescending(final int commonAncestorHeight) { assertThat(maybeAncestorNumber.getAsInt()) .isEqualTo(Math.toIntExact(chainHeight - commonHeader.getNumber())); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapDownloaderFactory.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapDownloaderFactory.java index cfe8107f72b..823d8896547 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapDownloaderFactory.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapDownloaderFactory.java @@ -92,7 +92,12 @@ public static Optional createSnapDownloader( chainSyncState != null ? new PivotSyncState(chainSyncState.pivotBlockHeader(), false) : PivotSyncState.EMPTY_SYNC_STATE; - final SnapSyncProcessState snapSyncState = new SnapSyncProcessState(pivotSyncState); + final Optional firstPivotHeader = + chainSyncState != null + ? Optional.of(chainSyncState.firstPivotBlockHeader()) + : Optional.empty(); + final SnapSyncProcessState snapSyncState = + new SnapSyncProcessState(pivotSyncState, firstPivotHeader); final InMemoryTasksPriorityQueues snapTaskCollection = createSnapWorldStateDownloaderTaskCollection(); @@ -101,6 +106,7 @@ public static Optional createSnapDownloader( ethContext, snapContext, protocolContext, + protocolSchedule, worldStateStorageCoordinator, snapTaskCollection, syncConfig.getSnapSyncConfiguration(), diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncDownloader.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncDownloader.java index 8d0918deabe..f31313757eb 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncDownloader.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncDownloader.java @@ -14,6 +14,7 @@ */ package org.hyperledger.besu.ethereum.eth.sync.snapsync; +import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.eth.sync.common.PivotSyncActions; import org.hyperledger.besu.ethereum.eth.sync.common.PivotSyncDownloader; import org.hyperledger.besu.ethereum.eth.sync.common.PivotSyncState; @@ -22,6 +23,7 @@ import org.hyperledger.besu.metrics.SyncDurationMetrics; import java.nio.file.Path; +import java.util.Optional; import java.util.concurrent.CompletableFuture; public class SnapSyncDownloader extends PivotSyncDownloader { @@ -50,7 +52,11 @@ protected CompletableFuture start(final PivotSyncState fastSyncS @Override protected PivotSyncState storeState(final PivotSyncState fastSyncState) { + final Optional firstPivotBlockHeader = + initialPivotSyncState instanceof SnapSyncProcessState snapSyncState + ? snapSyncState.getFirstPivotBlockHeader().or(fastSyncState::getPivotBlockHeader) + : fastSyncState.getPivotBlockHeader(); initialPivotSyncState = fastSyncState; - return new SnapSyncProcessState(fastSyncState); + return new SnapSyncProcessState(fastSyncState, firstPivotBlockHeader); } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncProcessState.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncProcessState.java index a98654a1bb8..9aa3f1328bb 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncProcessState.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncProcessState.java @@ -14,10 +14,13 @@ */ package org.hyperledger.besu.ethereum.eth.sync.snapsync; +import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.SealableBlockHeader; import org.hyperledger.besu.ethereum.eth.sync.common.PivotSyncState; import org.hyperledger.besu.ethereum.eth.sync.snapsync.request.SnapDataRequest; +import java.util.Optional; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,16 +31,23 @@ public class SnapSyncProcessState extends PivotSyncState { private static final Logger LOG = LoggerFactory.getLogger(SnapSyncProcessState.class); + private final Optional firstPivotBlockHeader; private boolean isHealTrieInProgress; private boolean isHealFlatDatabaseInProgress; private boolean isWaitingBlockchain; - public SnapSyncProcessState(final PivotSyncState fastSyncState) { + public SnapSyncProcessState( + final PivotSyncState fastSyncState, final Optional firstPivotBlockHeader) { super( fastSyncState.getPivotBlockNumber(), fastSyncState.getPivotBlockHash(), fastSyncState.getPivotBlockHeader(), fastSyncState.isSourceTrusted()); + this.firstPivotBlockHeader = firstPivotBlockHeader; + } + + public Optional getFirstPivotBlockHeader() { + return firstPivotBlockHeader; } public boolean isHealTrieInProgress() { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldDownloadState.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldDownloadState.java index 5ffe2297dbb..ddb7127796b 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldDownloadState.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldDownloadState.java @@ -16,6 +16,7 @@ import static org.hyperledger.besu.ethereum.eth.sync.snapsync.request.SnapDataRequest.createAccountFlatHealingRangeRequest; import static org.hyperledger.besu.ethereum.eth.sync.snapsync.request.SnapDataRequest.createAccountTrieNodeDataRequest; +import static org.hyperledger.besu.ethereum.eth.sync.snapsync.request.SnapDataRequest.createBlockAccessListDataRequest; import static org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator.applyForStrategy; import org.hyperledger.besu.ethereum.chain.BlockAddedObserver; @@ -32,6 +33,7 @@ import org.hyperledger.besu.ethereum.eth.sync.snapsync.request.heal.AccountFlatDatabaseHealingRangeRequest; import org.hyperledger.besu.ethereum.eth.sync.snapsync.request.heal.StorageFlatDatabaseHealingRangeRequest; import org.hyperledger.besu.ethereum.eth.sync.worldstate.WorldDownloadState; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.trie.RangeManager; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.worldstate.FlatDbMode; @@ -51,6 +53,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -90,6 +93,7 @@ public class SnapWorldDownloadState extends WorldDownloadState private final SnapSyncStatePersistenceManager snapContext; private final SnapSyncProcessState snapSyncState; + private final ProtocolSchedule protocolSchedule; // blockchain private final Blockchain blockchain; @@ -101,12 +105,14 @@ public class SnapWorldDownloadState extends WorldDownloadState private final AtomicBoolean trieHealStartedBefore = new AtomicBoolean(false); private final AtomicBoolean worldStateHealFinishedNotified = new AtomicBoolean(false); + private final AtomicBoolean blockAccessListHealEnqueued = new AtomicBoolean(false); public SnapWorldDownloadState( final WorldStateStorageCoordinator worldStateStorageCoordinator, final SnapSyncStatePersistenceManager snapContext, final Blockchain blockchain, final SnapSyncProcessState snapSyncState, + final ProtocolSchedule protocolSchedule, final InMemoryTasksPriorityQueues pendingRequests, final int maxRequestsWithoutProgress, final long minMillisBeforeStalling, @@ -124,6 +130,7 @@ public SnapWorldDownloadState( this.snapContext = snapContext; this.blockchain = blockchain; this.snapSyncState = snapSyncState; + this.protocolSchedule = protocolSchedule; this.metricsManager = metricsManager; this.blockObserverId = blockchain.observeBlockAdded(createBlockchainObserver()); this.ethContext = ethContext; @@ -207,6 +214,9 @@ else if (pivotBlockSelector.isBlockchainBehind()) { if (!snapSyncState.isHealFlatDatabaseInProgress() && (worldStateStorageCoordinator.isMatchingFlatMode(FlatDbMode.FULL) || worldStateStorageCoordinator.isMatchingFlatMode(FlatDbMode.ARCHIVE))) { + if (enqueueBlockAccessListsForPivotRangeIfRequired()) { + return false; + } startFlatDatabaseHeal(header); } // If the flat database healing process is in progress or the flat database mode is not FULL @@ -305,6 +315,50 @@ public synchronized void startFlatDatabaseHeal(final BlockHeader header) { createAccountFlatHealingRangeRequest(header.getStateRoot(), key, value))); } + private boolean enqueueBlockAccessListsForPivotRangeIfRequired() { + if (!blockAccessListHealEnqueued.compareAndSet(false, true)) { + return false; + } + + final Optional maybeFirstPivotHeader = snapSyncState.getFirstPivotBlockHeader(); + final Optional maybeLastPivotHeader = snapSyncState.getPivotBlockHeader(); + + LOG.debug("Starting BAL apply attempt"); + + if (maybeFirstPivotHeader.isEmpty() || maybeLastPivotHeader.isEmpty()) { + LOG.debug( + "Skipping BAL apply - firstPivotHeader={}, lastPivotHeader={}", + maybeFirstPivotHeader, + maybeLastPivotHeader); + return false; + } + + final BlockHeader firstPivotHeader = maybeFirstPivotHeader.get(); + if (!protocolSchedule.getByBlockHeader(firstPivotHeader).isBlockAccessListEnabled()) { + LOG.debug("Skipping BAL apply - BALs not enabled on first pivot {}", firstPivotHeader); + return false; + } + + final long fromBlock = firstPivotHeader.getNumber(); + final long toBlock = maybeLastPivotHeader.get().getNumber(); + if (toBlock < fromBlock) { + LOG.error("Attempted to apply BALs with fromBlock {} > {} toBlock", fromBlock, toBlock); + return false; + } + + LOG.info("Queueing block access list heal from block {} to {}", fromBlock, toBlock); + for (long blockNumber = fromBlock; blockNumber <= toBlock; blockNumber++) { + final Optional maybeBlockHeader = blockchain.getBlockHeader(blockNumber); + if (maybeBlockHeader.isPresent()) { + final BlockHeader blockHeader = maybeBlockHeader.get(); + enqueueRequest(createBlockAccessListDataRequest(blockHeader.getStateRoot(), blockHeader)); + } else { + LOG.warn("Unable to queue block access list heal for missing block {}", blockNumber); + } + } + return !pendingBlockAccessListRequests.isEmpty(); + } + @Override public synchronized void enqueueRequest(final SnapDataRequest request) { if (!internalFuture.isDone()) { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldStateDownloader.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldStateDownloader.java index 92fbcba8c79..c229fd05a18 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldStateDownloader.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldStateDownloader.java @@ -27,6 +27,7 @@ import org.hyperledger.besu.ethereum.eth.sync.snapsync.request.AccountRangeDataRequest; import org.hyperledger.besu.ethereum.eth.sync.snapsync.request.SnapDataRequest; import org.hyperledger.besu.ethereum.eth.sync.worldstate.WorldStateDownloader; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.trie.RangeManager; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator; @@ -65,6 +66,7 @@ public class SnapWorldStateDownloader implements WorldStateDownloader { private final int maxOutstandingRequests; private final int maxNodeRequestsWithoutProgress; private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; private final WorldStateStorageCoordinator worldStateStorageCoordinator; private final AtomicReference downloadState = new AtomicReference<>(); @@ -76,6 +78,7 @@ public SnapWorldStateDownloader( final EthContext ethContext, final SnapSyncStatePersistenceManager snapContext, final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, final WorldStateStorageCoordinator worldStateStorageCoordinator, final InMemoryTasksPriorityQueues snapTaskCollection, final SnapSyncConfiguration snapSyncConfiguration, @@ -87,6 +90,7 @@ public SnapWorldStateDownloader( final SyncDurationMetrics syncDurationMetrics) { this.ethContext = ethContext; this.protocolContext = protocolContext; + this.protocolSchedule = protocolSchedule; this.worldStateStorageCoordinator = worldStateStorageCoordinator; this.snapContext = snapContext; this.snapTaskCollection = snapTaskCollection; @@ -160,6 +164,7 @@ public CompletableFuture run( snapContext, protocolContext.getBlockchain(), snapSyncState, + protocolSchedule, snapTaskCollection, maxNodeRequestsWithoutProgress, minMillisBeforeStalling, diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/request/BlockAccessListDataRequest.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/request/BlockAccessListDataRequest.java index 9db8415f505..74a95268a96 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/request/BlockAccessListDataRequest.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/request/BlockAccessListDataRequest.java @@ -15,18 +15,29 @@ package org.hyperledger.besu.ethereum.eth.sync.snapsync.request; import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.eth.sync.snapsync.RequestType; import org.hyperledger.besu.ethereum.eth.sync.snapsync.SnapSyncConfiguration; import org.hyperledger.besu.ethereum.eth.sync.snapsync.SnapSyncProcessState; import org.hyperledger.besu.ethereum.eth.sync.snapsync.SnapWorldDownloadState; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessListChanges; +import org.hyperledger.besu.ethereum.rlp.RLP; +import org.hyperledger.besu.ethereum.trie.common.PmtStateTrieAccountValue; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.trie.patricia.StoredMerklePatriciaTrie; import org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator; import org.hyperledger.besu.plugin.services.storage.WorldStateKeyValueStorage; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; + public class BlockAccessListDataRequest extends SnapDataRequest { private final BlockHeader blockHeader; @@ -56,10 +67,70 @@ protected int doPersist( final SnapWorldDownloadState downloadState, final SnapSyncProcessState snapSyncState, final SnapSyncConfiguration snapSyncConfiguration) { - // TODO: Collect changes from BALs to the updater and commit at the end. + if (!(updater instanceof BonsaiWorldStateKeyValueStorage.Updater bonsaiUpdater)) { + return 0; + } + + final StoredMerklePatriciaTrie accountTrie = + new StoredMerklePatriciaTrie<>( + worldStateStorageCoordinator::getAccountStateTrieNode, + Bytes32.wrap(getRootHash().getBytes()), + Function.identity(), + Function.identity()); + + blockAccessList.ifPresent( + bal -> { + for (final var accountChanges : BlockAccessListChanges.latestChanges(bal)) { + final Hash accountHash = Hash.hash(accountChanges.address().getBytes()); + + final PmtStateTrieAccountValue trieAccountValue = + accountTrie + .get(accountHash.getBytes()) + .map(RLP::input) + .map(PmtStateTrieAccountValue::readFrom) + .orElse( + new PmtStateTrieAccountValue( + 0, Wei.ZERO, Hash.EMPTY_TRIE_HASH, Hash.EMPTY)); + + final var updatedCode = accountChanges.code(); + final Hash updatedCodeHash = + updatedCode.map(Hash::hash).orElse(trieAccountValue.getCodeHash()); + updatedCode.ifPresent( + code -> bonsaiUpdater.putCode(accountHash, updatedCodeHash, code)); + + final Hash updatedStorageRoot = trieAccountValue.getStorageRoot(); + if (!accountChanges.storageChanges().isEmpty()) { + applyStorageChanges(accountHash, accountChanges, bonsaiUpdater); + } + + final PmtStateTrieAccountValue updatedValue = + new PmtStateTrieAccountValue( + accountChanges.nonce().orElse(trieAccountValue.getNonce()), + accountChanges.balance().orElse(trieAccountValue.getBalance()), + updatedStorageRoot, + updatedCodeHash); + bonsaiUpdater.putAccountInfoState(accountHash, RLP.encode(updatedValue::writeTo)); + } + }); + return 0; } + private void applyStorageChanges( + final Hash accountHash, + final BlockAccessListChanges.AccountFinalChanges accountChanges, + final BonsaiWorldStateKeyValueStorage.Updater bonsaiUpdater) { + for (final var storageChange : accountChanges.storageChanges()) { + final Hash slotHash = storageChange.slot().getSlotHash(); + final UInt256 value = storageChange.value(); + if (value.equals(UInt256.ZERO)) { + bonsaiUpdater.removeStorageValueBySlotHash(accountHash, slotHash); + } else { + bonsaiUpdater.putStorageValueBySlotHash(accountHash, slotHash, value.toBytes()); + } + } + } + @Override public boolean isResponseReceived() { return blockAccessList.isPresent(); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerTest.java index f58690e4a86..c4ecb5ba052 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerTest.java @@ -68,8 +68,6 @@ 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.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -997,11 +995,4 @@ AccountRangeMessage.AccountRangeData getAndVerifyAccountRangeData( assertThat(accountData.accounts().size()).isEqualTo(expectedSize); return accountData; } - - @Test - void dryRunDetector() { - Assertions.assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/ChainHeadTrackerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/ChainHeadTrackerTest.java index e9730197ac7..68dbe13318f 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/ChainHeadTrackerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/ChainHeadTrackerTest.java @@ -14,8 +14,6 @@ */ package org.hyperledger.besu.ethereum.eth.sync; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.config.GenesisConfig; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.chain.BadBlockManager; @@ -41,7 +39,6 @@ import java.util.stream.Stream; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -146,11 +143,4 @@ public void shouldCheckTrialingPeerLimits(final DataStorageFormat storageFormat) private ChainState chainHeadState() { return respondingPeer.getEthPeer().chainState(); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotBlockConfirmerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotBlockConfirmerTest.java index db289d74c0b..170abb8ee21 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotBlockConfirmerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotBlockConfirmerTest.java @@ -45,7 +45,6 @@ import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -217,11 +216,4 @@ public void headerMismatch(final DataStorageFormat storageFormat) { assertThat(future).isCompletedExceptionally(); assertThatThrownBy(future::get).hasRootCauseInstanceOf(ContestedPivotBlockException.class); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotBlockRetrieverTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotBlockRetrieverTest.java index cd8ac4fbbff..685b7113e50 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotBlockRetrieverTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotBlockRetrieverTest.java @@ -43,7 +43,6 @@ import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -449,11 +448,4 @@ private void waitUntilComplete(final CompletableFuture future) { // do nothing } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotSyncActionsTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotSyncActionsTest.java index f42d476955f..3dc728430ca 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotSyncActionsTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotSyncActionsTest.java @@ -60,7 +60,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -476,11 +475,4 @@ private PivotSyncActions createPivotSyncActions( throw new RuntimeException(e); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotSyncDownloaderTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotSyncDownloaderTest.java index 3606aab104f..02f74172bc8 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotSyncDownloaderTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/common/PivotSyncDownloaderTest.java @@ -48,7 +48,6 @@ import java.util.stream.Stream; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -632,11 +631,4 @@ private void assertCompletedExceptionally( return null; }); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 3a0a1226e7e..5581773553f 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 @@ -49,7 +49,6 @@ import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -329,11 +328,4 @@ public void choosesBestPeerAsSyncTarget_byTdAndHeight(final DataStorageFormat st assertThat(syncState.syncTarget()).isPresent(); assertThat(syncState.syncTarget().get().peer()).isEqualTo(peerB.getEthPeer()); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 22312716f67..1606cc2c835 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 @@ -47,7 +47,6 @@ import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -211,11 +210,4 @@ public void syncsFullyAndContinuesWhenTTDNotSpecified(final DataStorageFormat st assertThat(future.isDone()).isFalse(); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncDownloaderTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncDownloaderTest.java index 0867cd98ca3..1c502513941 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncDownloaderTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncDownloaderTest.java @@ -38,7 +38,6 @@ import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -137,11 +136,4 @@ public void shouldNotLimitTrailingPeersWhenInSync(final DataStorageFormat storag assertThat(synchronizer.calculateTrailingPeerRequirements()) .isEqualTo(TrailingPeerRequirements.UNRESTRICTED); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncTargetManagerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncTargetManagerTest.java index b720d26b547..418b27fc909 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncTargetManagerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fullsync/FullSyncTargetManagerTest.java @@ -55,7 +55,6 @@ import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -217,11 +216,4 @@ public void shouldAllowSyncTargetWhenIfWorldStateIsAvailableForCommonAncestor( .isEqualTo(new SyncTarget(bestPeer.getEthPeer(), localBlockchain.getChainHeadHeader())); assertThat(bestPeer.getPeerConnection().isDisconnected()).isFalse(); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldDownloadStateTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldDownloadStateTest.java index 090ce726902..9b12f7c7b80 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldDownloadStateTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapWorldDownloadStateTest.java @@ -40,6 +40,8 @@ import org.hyperledger.besu.ethereum.eth.sync.snapsync.request.BytecodeRequest; import org.hyperledger.besu.ethereum.eth.sync.snapsync.request.SnapDataRequest; import org.hyperledger.besu.ethereum.eth.sync.worldstate.WorldStateDownloadProcess; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.trie.RangeManager; import org.hyperledger.besu.ethereum.trie.forest.storage.ForestWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; @@ -63,7 +65,6 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -95,6 +96,8 @@ public class SnapWorldDownloadStateTest { private final DynamicPivotBlockSelector dynamicPivotBlockManager = mock(DynamicPivotBlockSelector.class); private final EthContext ethContext = mock(EthContext.class); + private final ProtocolSchedule protocolSchedule = mock(ProtocolSchedule.class); + private final ProtocolSpec protocolSpec = mock(ProtocolSpec.class); private final TestClock clock = new TestClock(); private SnapWorldDownloadState downloadState; @@ -114,6 +117,8 @@ public Stream provideArguments(final ExtensionContext conte public void setUp(final DataStorageFormat storageFormat) { when(metricsManager.getMetricsSystem()).thenReturn(new NoOpMetricsSystem()); + when(protocolSchedule.getByBlockHeader(any(BlockHeader.class))).thenReturn(protocolSpec); + when(protocolSpec.getBlockAccessListFactory()).thenReturn(Optional.empty()); if (storageFormat == DataStorageFormat.BONSAI) { worldStateKeyValueStorage = @@ -133,6 +138,7 @@ public void setUp(final DataStorageFormat storageFormat) { snapContext, blockchain, snapSyncState, + protocolSchedule, pendingRequests, MAX_REQUESTS_WITHOUT_PROGRESS, MIN_MILLIS_BEFORE_STALLING, @@ -510,11 +516,4 @@ public void shouldNotCompleteReturnedFutureWhenNoPendingTasksRemainAndFlatDBHeal .isEmpty(); assertThat(downloadState.isDownloading()).isTrue(); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/request/heal/StorageTrieNodeHealingRequestTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/request/heal/StorageTrieNodeHealingRequestTest.java index 7fedb7dacdf..851cd62b5de 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/request/heal/StorageTrieNodeHealingRequestTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/request/heal/StorageTrieNodeHealingRequestTest.java @@ -14,8 +14,6 @@ */ package org.hyperledger.besu.ethereum.eth.sync.snapsync.request.heal; -import static org.assertj.core.api.Assertions.assertThat; - import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; @@ -38,7 +36,6 @@ import org.apache.tuweni.bytes.Bytes; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; @@ -118,11 +115,4 @@ void shouldDetectMissingData(final DataStorageFormat storageFormat) { Assertions.assertThat(request.getExistingData(worldStateStorageCoordinator)).isEmpty(); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 0947a8fe7e1..a9e9fac9974 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 @@ -59,7 +59,6 @@ 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; import org.junit.jupiter.params.provider.MethodSource; @@ -228,11 +227,4 @@ public void searchesAgainstNetwork(final int headerRequestSize, final int common assertThat(actualResult.get().getHash()) .isEqualTo(MainnetBlockHeaderFunctions.createHash(commonHeader)); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/PersistBlockTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/PersistBlockTaskTest.java index a39d3658bfe..208e6663cb8 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/PersistBlockTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/PersistBlockTaskTest.java @@ -40,7 +40,6 @@ import java.util.stream.Stream; import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -425,11 +424,4 @@ public void cancelAfterRunning(final DataStorageFormat storageFormat) { assertThat(result.isCancelled()).isTrue(); assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolReplacementHandlerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolReplacementHandlerTest.java index aca1f202c26..f57d6fac3e7 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolReplacementHandlerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolReplacementHandlerTest.java @@ -30,7 +30,6 @@ import java.util.Optional; import java.util.stream.Collectors; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -89,11 +88,4 @@ private static PendingTransaction mockTransactionInfo() { when(pendingTransaction.getTransaction()).thenReturn(transaction); return pendingTransaction; } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRuleTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRuleTest.java index eb22184a2b2..527b26e2512 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRuleTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRuleTest.java @@ -24,7 +24,6 @@ import java.util.Collection; import java.util.Optional; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -108,11 +107,4 @@ public void shouldReplace( .shouldReplace(oldTx, newTx, baseFee)) .isEqualTo(expected); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRuleTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRuleTest.java index 651666fdd55..d4f851e1907 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRuleTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRuleTest.java @@ -24,7 +24,6 @@ import java.util.Collection; import java.util.Optional; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -85,11 +84,4 @@ public void shouldReplace( .shouldReplace(oldTx, newTx, baseFee)) .isEqualTo(expected); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementRulesTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementRulesTest.java index da7b465583d..a6a5524eedf 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementRulesTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementRulesTest.java @@ -27,7 +27,6 @@ import java.util.Collection; import java.util.Optional; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -107,11 +106,4 @@ public void shouldReplace( .shouldReplace(oldTx, newTx, mockHeader)) .isEqualTo(expected); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 d926d057486..1b48609acf0 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 @@ -76,7 +76,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -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; @@ -2235,11 +2234,4 @@ static List toCodeDelegations(final AuthorityAndNonce[] authorit return Arrays.stream(authorityAndNonces).map(AuthorityAndNonce::toCodeDelegation).toList(); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/evmtool/src/test/java/org/hyperledger/besu/evmtool/EvmToolSpecTests.java b/ethereum/evmtool/src/test/java/org/hyperledger/besu/evmtool/EvmToolSpecTests.java index 03401054ef2..504d1f04677 100644 --- a/ethereum/evmtool/src/test/java/org/hyperledger/besu/evmtool/EvmToolSpecTests.java +++ b/ethereum/evmtool/src/test/java/org/hyperledger/besu/evmtool/EvmToolSpecTests.java @@ -45,7 +45,6 @@ import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeType; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -169,11 +168,4 @@ void testBySpec( assertThat(actualNode).isEqualTo(stdoutNode); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/internal/PeerRequirementCombineTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/internal/PeerRequirementCombineTest.java index 7c6b14148cc..681fda1ed03 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/internal/PeerRequirementCombineTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/discv4/internal/PeerRequirementCombineTest.java @@ -23,7 +23,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -77,11 +76,4 @@ public void combine_withOn() { PeerRequirement combined = PeerRequirement.combine(Collections.emptyList()); assertThat(combined.hasSufficientPeers()).isTrue(); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 99c4eb8014b..4384c5e3320 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/EVM.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/EVM.java @@ -86,8 +86,11 @@ import org.hyperledger.besu.evm.operation.VirtualOperation; import org.hyperledger.besu.evm.operation.XorOperation; import org.hyperledger.besu.evm.operation.XorOperationOptimized; -import org.hyperledger.besu.evm.operation.v2.AddOperationV2; import org.hyperledger.besu.evm.tracing.OperationTracer; +import org.hyperledger.besu.evm.v2.operation.AddOperationV2; +import org.hyperledger.besu.evm.v2.operation.SarOperationV2; +import org.hyperledger.besu.evm.v2.operation.ShlOperationV2; +import org.hyperledger.besu.evm.v2.operation.ShrOperationV2; import java.util.Optional; import java.util.function.Function; @@ -485,6 +488,18 @@ private void runToHaltV2(final MessageFrame frame, final OperationTracer tracing result = switch (opcode) { case 0x01 -> AddOperationV2.staticOperation(frame, frame.stackDataV2()); + case 0x1b -> + enableConstantinople + ? ShlOperationV2.staticOperation(frame, frame.stackDataV2()) + : InvalidOperation.invalidOperationResult(opcode); + case 0x1c -> + enableConstantinople + ? ShrOperationV2.staticOperation(frame, frame.stackDataV2()) + : InvalidOperation.invalidOperationResult(opcode); + case 0x1d -> + enableConstantinople + ? SarOperationV2.staticOperation(frame, frame.stackDataV2()) + : InvalidOperation.invalidOperationResult(opcode); // TODO: implement remaining opcodes in v2; until then fall through to v1 default -> { frame.setCurrentOperation(currentOperation); 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 b56dad19c1f..d18f62610ba 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 @@ -141,24 +141,48 @@ private void clearAccumulatedStateBesidesGasAndOutput(final MessageFrame frame) * parent re-use; for the initial frame it is tracked in stateGasSpillBurned for transaction-level * gas accounting. * + *

For the initial (top-level) frame, the reservoir must be preserved for transaction-level + * refund. If child frames had restored state gas to the reservoir during the initial frame's + * execution (via their own revert/halt), that refund must not be lost when the initial frame + * subsequently reverts or halts. We therefore restore the reservoir to the higher of the + * pre-rollback value (which may include child refunds) and the post-rollback value (which + * reflects any reservoir drain that rollback undid). We also compute the spill contribution only + * from the positive part of reservoirRestored so that child-refunded gas is never counted as + * burned spill. + * * @param frame The message frame */ private void handleStateGasSpill(final MessageFrame frame) { final long stateGasUsedBefore = frame.getStateGasUsed(); final long reservoirBefore = frame.getStateGasReservoir(); + final boolean isInitialFrame = frame.getMessageFrameStack().size() == 1; 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) { + + if (isInitialFrame) { + // EIP-8037: Preserve the reservoir for top-level refund. Use the max of the pre-rollback + // value (which may include child frame refunds that must not be lost) and the post-rollback + // value (which reflects any reservoir drain rollback has already restored). + final long reservoirPostRollback = frame.getStateGasReservoir(); + final long preservedReservoir = Math.max(reservoirPostRollback, reservoirBefore); + if (preservedReservoir != reservoirPostRollback) { + frame.setStateGasReservoir(preservedReservoir); + } + // Only burn the portion of state gas that actually spilled into gasRemaining (not the + // portion that was drawn from the reservoir and has already been restored, and not the + // portion that child frames had refunded to the reservoir). + final long spill = Math.max(0L, stateGasRestored - Math.max(0L, reservoirRestored)); + if (spill > 0) { + frame.accumulateStateGasSpillBurned(spill); + } + } else { + final long spill = Math.max(0L, stateGasRestored - reservoirRestored); + if (spill > 0) { // 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); } } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java new file mode 100644 index 00000000000..6a9f808f3dc --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java @@ -0,0 +1,324 @@ +/* + * 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.v2; + +/** + * Static utility operating directly on the flat {@code long[]} operand stack. Each slot occupies 4 + * consecutive longs in big-endian limb order: {@code [u3, u2, u1, u0]} where u3 is the most + * significant limb. + * + *

All methods take {@code (long[] s, int top)} and return the new {@code top}. The caller + * (operation) is responsible for underflow/overflow checks before calling. + */ +public class StackArithmetic { + + /** Utility class — not instantiable. */ + private StackArithmetic() {} + + // region SHL (Shift Left) + // --------------------------------------------------------------------------- + + /** + * Performs EVM SHL (shift left) on the two top stack items. + * + *

Reads the shift amount (unsigned) and the value from the top two stack slots, writes {@code + * value << shift} back into the value slot and decrements the top. Shifts >= 256 or a zero value + * produce 0. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @return the new stack-top after consuming one item + */ + public static int shl(final long[] stack, final int top) { + final int shiftOffset = (top - 1) << 2; + final int valueOffset = (top - 2) << 2; + // If shift amount > 255 or value is zero, result is zero + if (stack[shiftOffset] != 0 + || stack[shiftOffset + 1] != 0 + || stack[shiftOffset + 2] != 0 + || Long.compareUnsigned(stack[shiftOffset + 3], 256) >= 0 + || (stack[valueOffset] == 0 + && stack[valueOffset + 1] == 0 + && stack[valueOffset + 2] == 0 + && stack[valueOffset + 3] == 0)) { + stack[valueOffset] = 0; + stack[valueOffset + 1] = 0; + stack[valueOffset + 2] = 0; + stack[valueOffset + 3] = 0; + return top - 1; + } + int shift = (int) stack[shiftOffset + 3]; + shiftLeftInPlace(stack, valueOffset, shift); + return top - 1; + } + + /** + * Left-shifts a 256-bit value in place by 1..255 bits, zero-filling from the right. + * + * @param stack the flat limb array + * @param valueOffset index of the value's most-significant limb + * @param shift number of bits to shift (must be in [1, 255]) + */ + private static void shiftLeftInPlace(final long[] stack, final int valueOffset, final int shift) { + if (shift == 0) return; + long w0 = stack[valueOffset], + w1 = stack[valueOffset + 1], + w2 = stack[valueOffset + 2], + w3 = stack[valueOffset + 3]; + // 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 = shiftLeftWord(w0, w1, bitShift); + w1 = shiftLeftWord(w1, w2, bitShift); + w2 = shiftLeftWord(w2, w3, bitShift); + w3 = shiftLeftWord(w3, 0, bitShift); + break; + case 1: + w0 = shiftLeftWord(w1, w2, bitShift); + w1 = shiftLeftWord(w2, w3, bitShift); + w2 = shiftLeftWord(w3, 0, bitShift); + w3 = 0; + break; + case 2: + w0 = shiftLeftWord(w2, w3, bitShift); + w1 = shiftLeftWord(w3, 0, bitShift); + w2 = 0; + w3 = 0; + break; + case 3: + w0 = shiftLeftWord(w3, 0, bitShift); + w1 = 0; + w2 = 0; + w3 = 0; + break; + } + stack[valueOffset] = w0; + stack[valueOffset + 1] = w1; + stack[valueOffset + 2] = w2; + stack[valueOffset + 3] = w3; + } + + /** + * Shifts a 64-bit word left and carries in bits from the next less-significant word. + * + * @param value the current word + * @param nextValue the next less-significant word (bits carry in from its top) + * @param bitShift the intra-word shift amount in [0, 63]; 0 returns {@code value} unchanged to + * avoid Java's mod-64 shift semantics on {@code nextValue >>> 64} + * @return the shifted word + */ + private static long shiftLeftWord(final long value, final long nextValue, final int bitShift) { + if (bitShift == 0) return value; + return (value << bitShift) | (nextValue >>> (64 - bitShift)); + } + + // endregion + + // region SHR (Shift Right) + // --------------------------------------------------------------------------- + + /** + * Performs EVM SHR (logical shift right) on the two top stack items. + * + *

Reads the shift amount (unsigned) and the value from the top two stack slots, writes {@code + * value >>> shift} back into the value slot and decrements the top. Shifts >= 256 or a zero value + * produce 0. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @return the new stack-top after consuming one item + */ + public static int shr(final long[] stack, final int top) { + final int shiftOffset = (top - 1) << 2; + final int valueOffset = (top - 2) << 2; + if (stack[shiftOffset] != 0 + || stack[shiftOffset + 1] != 0 + || stack[shiftOffset + 2] != 0 + || Long.compareUnsigned(stack[shiftOffset + 3], 256) >= 0 + || (stack[valueOffset] == 0 + && stack[valueOffset + 1] == 0 + && stack[valueOffset + 2] == 0 + && stack[valueOffset + 3] == 0)) { + stack[valueOffset] = 0; + stack[valueOffset + 1] = 0; + stack[valueOffset + 2] = 0; + stack[valueOffset + 3] = 0; + return top - 1; + } + int shift = (int) stack[shiftOffset + 3]; + shiftRightInPlace(stack, valueOffset, shift); + return top - 1; + } + + /** + * Logically right-shifts a 256-bit value in place by 1..255 bits, zero-filling from the left. + * + * @param stack the flat limb array + * @param valueOffset index of the value's most-significant limb + * @param shift number of bits to shift (must be in [1, 255]) + */ + private static void shiftRightInPlace( + final long[] stack, final int valueOffset, final int shift) { + if (shift == 0) return; + long w0 = stack[valueOffset], + w1 = stack[valueOffset + 1], + w2 = stack[valueOffset + 2], + w3 = stack[valueOffset + 3]; + // 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 = shiftRightWord(w3, w2, bitShift); + w2 = shiftRightWord(w2, w1, bitShift); + w1 = shiftRightWord(w1, w0, bitShift); + w0 = shiftRightWord(w0, 0, bitShift); + break; + case 1: + w3 = shiftRightWord(w2, w1, bitShift); + w2 = shiftRightWord(w1, w0, bitShift); + w1 = shiftRightWord(w0, 0, bitShift); + w0 = 0; + break; + case 2: + w3 = shiftRightWord(w1, w0, bitShift); + w2 = shiftRightWord(w0, 0, bitShift); + w1 = 0; + w0 = 0; + break; + case 3: + w3 = shiftRightWord(w0, 0, bitShift); + w2 = 0; + w1 = 0; + w0 = 0; + break; + } + stack[valueOffset] = w0; + stack[valueOffset + 1] = w1; + stack[valueOffset + 2] = w2; + stack[valueOffset + 3] = w3; + } + + // endregion + + // region SAR (Shift Arithmetic Right) + // --------------------------------------------------------------------------- + + /** + * Performs EVM SAR (arithmetic shift right) on the two top stack items. + * + *

Reads the shift amount (unsigned) and the value (signed) from the top two stack slots, + * writes {@code value >> shift} back into the value slot and decrements the top. Shifts >= 256 + * produce 0 for positive values and -1 for negative values. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @return the new stack-top after consuming one item + */ + public static int sar(final long[] stack, final int top) { + final int shiftOffset = (top - 1) << 2; + final int valueOffset = (top - 2) << 2; + boolean negative = stack[valueOffset] < 0; + if (stack[shiftOffset] != 0 + || stack[shiftOffset + 1] != 0 + || stack[shiftOffset + 2] != 0 + || Long.compareUnsigned(stack[shiftOffset + 3], 256) >= 0) { + long fill = negative ? -1L : 0L; + stack[valueOffset] = fill; + stack[valueOffset + 1] = fill; + stack[valueOffset + 2] = fill; + stack[valueOffset + 3] = fill; + return top - 1; + } + int shift = (int) stack[shiftOffset + 3]; + sarInPlace(stack, valueOffset, shift, negative); + return top - 1; + } + + /** + * Arithmetic right-shifts a 256-bit value in place by 0..255 bits, sign-extending with {@code + * fill}. + * + * @param stack the flat limb array + * @param valueOffset index of the value's most-significant limb + * @param shift number of bits to shift (must be in [0, 255]) + * @param negative true if the original value is negative (fill = -1) + */ + private static void sarInPlace( + final long[] stack, final int valueOffset, final int shift, final boolean negative) { + if (shift == 0) return; + long w0 = stack[valueOffset], + w1 = stack[valueOffset + 1], + w2 = stack[valueOffset + 2], + w3 = stack[valueOffset + 3]; + 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 = shiftRightWord(w3, w2, bitShift); + w2 = shiftRightWord(w2, w1, bitShift); + w1 = shiftRightWord(w1, w0, bitShift); + w0 = shiftRightWord(w0, fill, bitShift); + break; + case 1: + w3 = shiftRightWord(w2, w1, bitShift); + w2 = shiftRightWord(w1, w0, bitShift); + w1 = shiftRightWord(w0, fill, bitShift); + w0 = fill; + break; + case 2: + w3 = shiftRightWord(w1, w0, bitShift); + w2 = shiftRightWord(w0, fill, bitShift); + w1 = fill; + w0 = fill; + break; + case 3: + w3 = shiftRightWord(w0, fill, bitShift); + w2 = fill; + w1 = fill; + w0 = fill; + break; + } + stack[valueOffset] = w0; + stack[valueOffset + 1] = w1; + stack[valueOffset + 2] = w2; + stack[valueOffset + 3] = w3; + } + + /** + * 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 + */ + private static long shiftRightWord(final long value, final long prevValue, final int bitShift) { + if (bitShift == 0) return value; + return (value >>> bitShift) | (prevValue << (64 - bitShift)); + } + + // endregion +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AbstractFixedCostOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java similarity index 98% rename from evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AbstractFixedCostOperationV2.java rename to evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java index 95e059d9605..0480e77b067 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AbstractFixedCostOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.evm.operation.v2; +package org.hyperledger.besu.evm.v2.operation; import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AddOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java similarity index 97% rename from evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AddOperationV2.java rename to evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java index 4c866bcf61f..b4109d29ad1 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AddOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.evm.operation.v2; +package org.hyperledger.besu.evm.v2.operation; import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.frame.MessageFrame; diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java new file mode 100644 index 00000000000..d488886c5df --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.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.evm.v2.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.StackArithmetic; + +/** The Sar operation. */ +public class SarOperationV2 extends AbstractFixedCostOperationV2 { + + /** The Sar operation success result. */ + static final OperationResult sarSuccess = new OperationResult(3, null); + + /** + * Instantiates a new Sar operation. + * + * @param gasCalculator the gas calculator + */ + public SarOperationV2(final GasCalculator gasCalculator) { + super(0x1d, "SAR", 2, 1, gasCalculator, gasCalculator.getVeryLowTierGasCost()); + } + + @Override + public Operation.OperationResult executeFixedCostOperation( + final MessageFrame frame, final EVM evm) { + return staticOperation(frame, frame.stackDataV2()); + } + + /** + * Performs sar operation. + * + * @param frame the frame + * @param stack the v2 operand stack ({@code long[]} in big-endian limb order) + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame, final long[] stack) { + if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; + frame.setTopV2(StackArithmetic.sar(stack, frame.stackTopV2())); + return sarSuccess; + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java new file mode 100644 index 00000000000..2f765e94269 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.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.evm.v2.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.StackArithmetic; + +/** The Shl (Shift Left) operation. */ +public class ShlOperationV2 extends AbstractFixedCostOperationV2 { + + /** The Shl operation success result. */ + static final OperationResult shlSuccess = new OperationResult(3, null); + + /** + * Instantiates a new Shl operation. + * + * @param gasCalculator the gas calculator + */ + public ShlOperationV2(final GasCalculator gasCalculator) { + super(0x1b, "SHL", 2, 1, gasCalculator, gasCalculator.getVeryLowTierGasCost()); + } + + @Override + public Operation.OperationResult executeFixedCostOperation( + final MessageFrame frame, final EVM evm) { + return staticOperation(frame, frame.stackDataV2()); + } + + /** + * Performs Shift Left operation. + * + * @param frame the frame + * @param stack the v2 operand stack ({@code long[]} in big-endian limb order) + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame, final long[] stack) { + if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; + frame.setTopV2(StackArithmetic.shl(stack, frame.stackTopV2())); + return shlSuccess; + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java new file mode 100644 index 00000000000..71c412a04ab --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.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.evm.v2.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.StackArithmetic; + +/** The Shr (Shift Right) operation. */ +public class ShrOperationV2 extends AbstractFixedCostOperationV2 { + + /** The Shr operation success result. */ + static final OperationResult shrSuccess = new OperationResult(3, null); + + /** + * Instantiates a new Shr operation. + * + * @param gasCalculator the gas calculator + */ + public ShrOperationV2(final GasCalculator gasCalculator) { + super(0x1c, "SHR", 2, 1, gasCalculator, gasCalculator.getVeryLowTierGasCost()); + } + + @Override + public Operation.OperationResult executeFixedCostOperation( + final MessageFrame frame, final EVM evm) { + return staticOperation(frame, frame.stackDataV2()); + } + + /** + * Performs SHR operation. + * + * @param frame the frame + * @param stack the v2 operand stack ({@code long[]} in big-endian limb order) + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame, final long[] stack) { + if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; + frame.setTopV2(StackArithmetic.shr(stack, frame.stackTopV2())); + return shrSuccess; + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/internal/WordsTest.java b/evm/src/test/java/org/hyperledger/besu/evm/internal/WordsTest.java index 871f6ed0b1a..30e651436d9 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/internal/WordsTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/internal/WordsTest.java @@ -22,7 +22,6 @@ import java.util.Collection; import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.params.ParameterizedTest; @@ -104,11 +103,4 @@ Collection clampedToLongTestVector() { void clampedToIntTest(final Bytes theBytes, final int theExpectedInt) { assertThat(clampedToInt(theBytes)).isEqualTo(theExpectedInt); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/ChainIdOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/ChainIdOperationTest.java index acae6249c4d..84ce89d2472 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/ChainIdOperationTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/ChainIdOperationTest.java @@ -26,7 +26,6 @@ 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; @@ -67,11 +66,4 @@ void shouldCalculateGasPrice(final String chainIdString, final int expectedGas) final OperationResult result = operation.execute(messageFrame, null); assertThat(result.getGasCost()).isEqualTo(expectedGas); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/ConstantinopleSStoreOperationGasCostTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/ConstantinopleSStoreOperationGasCostTest.java index b8a254bbe40..d6251e2fd15 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/ConstantinopleSStoreOperationGasCostTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/ConstantinopleSStoreOperationGasCostTest.java @@ -23,7 +23,6 @@ import org.hyperledger.besu.evm.testutils.TestCodeExecutor; import org.apache.tuweni.units.bigints.UInt256; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -74,11 +73,4 @@ void shouldCalculateGasAccordingToEip1283( assertThat(frame.getRemainingGas()).isEqualTo(gasLimit - expectedGasUsed); assertThat(frame.getGasRefund()).isEqualTo(expectedGasRefund); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/LondonSStoreOperationGasCostTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/LondonSStoreOperationGasCostTest.java index b4865df5d23..f95b91742f1 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/LondonSStoreOperationGasCostTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/LondonSStoreOperationGasCostTest.java @@ -24,7 +24,6 @@ import org.apache.tuweni.units.bigints.UInt256; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -77,11 +76,4 @@ void shouldCalculateGasAccordingToEip3529( assertThat(frame.getRemainingGas()).isEqualTo(gasLimit - (expectedGasUsed + 2100)); assertThat(frame.getGasRefund()).isEqualTo(expectedGasRefund); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/MCopyOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/MCopyOperationTest.java index 6bf5a8afd21..fe2321cac11 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/MCopyOperationTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/MCopyOperationTest.java @@ -25,7 +25,6 @@ import java.util.Collection; import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -133,11 +132,4 @@ void testMCopy( assertThat(frame.readMemory(0, expected.size())).isEqualTo(expected); assertThat(result.getGasCost()).isEqualTo(gasCost); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } 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 cced7ff3d7b..e5853efcff7 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 @@ -328,11 +328,4 @@ public long getGasLimit() { final long expectedRemainingGas = gasBeforeSstore - expectedSpill; assertThat(frame.getRemainingGas()).isEqualTo(expectedRemainingGas); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/SarOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/SarOperationTest.java index c7d7cad00dc..2aec3c8ccd3 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/SarOperationTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/SarOperationTest.java @@ -14,7 +14,6 @@ */ package org.hyperledger.besu.evm.operation; -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; @@ -27,7 +26,6 @@ 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; @@ -177,11 +175,4 @@ void shiftOperation(final String number, final String shift, final String expect operation.execute(frame, null); verify(frame).pushStackItem(Bytes.fromHexString(expectedResult)); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/SelfDestructOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/SelfDestructOperationTest.java index c03be40f00b..3f42825c884 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/SelfDestructOperationTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/SelfDestructOperationTest.java @@ -34,7 +34,6 @@ import org.hyperledger.besu.evm.worldstate.WorldUpdater; import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -175,11 +174,4 @@ void checkContractDeletionEIP6780( assertThat(messageFrame.getCreates()).doesNotContain(orignatorAddress); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/ShlOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/ShlOperationTest.java index a4d0b8f3104..7de58f8ad34 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/ShlOperationTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/ShlOperationTest.java @@ -14,7 +14,6 @@ */ package org.hyperledger.besu.evm.operation; -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; @@ -28,7 +27,6 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; -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; @@ -114,11 +112,4 @@ void shiftOperation(final String number, final String shift, final String expect operation.execute(frame, null); verify(frame).pushStackItem(Bytes.fromHexString(expectedResult)); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/ShrOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/ShrOperationTest.java index 5acc7466a2e..b78a03272e6 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/operation/ShrOperationTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/ShrOperationTest.java @@ -14,7 +14,6 @@ */ package org.hyperledger.besu.evm.operation; -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; @@ -28,7 +27,6 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; -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; @@ -125,11 +123,4 @@ void shiftOperation(final String number, final String shift, final String expect operation.execute(frame, null); verify(frame).pushStackItem(Bytes.fromHexString(expectedResult)); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLAKE2BFPrecompileContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLAKE2BFPrecompileContractTest.java index 0984c111268..0f5b20ca3f2 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLAKE2BFPrecompileContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLAKE2BFPrecompileContractTest.java @@ -24,7 +24,6 @@ import org.hyperledger.besu.evm.frame.MessageFrame; import org.apache.tuweni.bytes.Bytes; -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; @@ -97,11 +96,4 @@ private void testFCompression( .isEqualTo(expectedComputation); assertThat(contract.gasRequirement(input)).isEqualTo(expectedGasUsed); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1AddPrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1AddPrecompiledContractTest.java index 2c52e09cdd0..4148d96d912 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1AddPrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1AddPrecompiledContractTest.java @@ -27,7 +27,6 @@ import com.google.common.io.CharStreams; import org.apache.tuweni.bytes.Bytes; -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; @@ -80,11 +79,4 @@ void shouldCalculate( assertThat(contract.gasRequirement(input)).isEqualTo(Long.parseLong(expectedGasUsed)); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1MulPrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1MulPrecompiledContractTest.java index 253c4949537..df577673c3c 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1MulPrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1MulPrecompiledContractTest.java @@ -28,7 +28,6 @@ import com.google.common.io.CharStreams; import org.apache.tuweni.bytes.Bytes; -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; @@ -84,11 +83,4 @@ void shouldCalculate( assertThat(contract.gasRequirement(input)).isEqualTo(Long.parseLong(expectedGasUsed)); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1MultiExpPrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1MultiExpPrecompiledContractTest.java index 90bbc926771..18a5379262d 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1MultiExpPrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G1MultiExpPrecompiledContractTest.java @@ -28,7 +28,6 @@ import com.google.common.io.CharStreams; import org.apache.tuweni.bytes.Bytes; -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; @@ -81,11 +80,4 @@ void shouldCalculate( assertThat(contract.gasRequirement(input)).isEqualTo(Long.parseLong(expectedGasUsed)); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2AddPrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2AddPrecompiledContractTest.java index 7d620098a2e..be4c4d8af84 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2AddPrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2AddPrecompiledContractTest.java @@ -28,7 +28,6 @@ import com.google.common.io.CharStreams; import org.apache.tuweni.bytes.Bytes; -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; @@ -80,11 +79,4 @@ void shouldCalculate( assertThat(contract.gasRequirement(input)).isEqualTo(Long.parseLong(expectedGasUsed)); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2MulPrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2MulPrecompiledContractTest.java index 0c044cfc13e..a82814ffd0d 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2MulPrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2MulPrecompiledContractTest.java @@ -28,7 +28,6 @@ import com.google.common.io.CharStreams; import org.apache.tuweni.bytes.Bytes; -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; @@ -84,11 +83,4 @@ void shouldCalculate( assertThat(contract.gasRequirement(input)).isEqualTo(Long.parseLong(expectedGasUsed)); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2MultiExpPrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2MultiExpPrecompiledContractTest.java index 89914997865..c9c842660c1 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2MultiExpPrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12G2MultiExpPrecompiledContractTest.java @@ -27,7 +27,6 @@ import com.google.common.io.CharStreams; import org.apache.tuweni.bytes.Bytes; -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; @@ -80,11 +79,4 @@ void shouldCalculate( assertThat(contract.gasRequirement(input)).isEqualTo(Long.parseLong(expectedGasUsed)); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12MapFp2ToG2PrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12MapFp2ToG2PrecompiledContractTest.java index fa034d7a117..8b87d846b21 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12MapFp2ToG2PrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12MapFp2ToG2PrecompiledContractTest.java @@ -28,7 +28,6 @@ import com.google.common.io.CharStreams; import org.apache.tuweni.bytes.Bytes; -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; @@ -81,11 +80,4 @@ void shouldCalculate( assertThat(contract.gasRequirement(input)).isEqualTo(Long.parseLong(expectedGasUsed)); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12MapFpToG1PrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12MapFpToG1PrecompiledContractTest.java index 1cf9b995b15..4e1fd6535e2 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12MapFpToG1PrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12MapFpToG1PrecompiledContractTest.java @@ -28,7 +28,6 @@ import com.google.common.io.CharStreams; import org.apache.tuweni.bytes.Bytes; -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; @@ -81,11 +80,4 @@ void shouldCalculate( assertThat(contract.gasRequirement(input)).isEqualTo(Long.parseLong(expectedGasUsed)); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12PairingPrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12PairingPrecompiledContractTest.java index bb939a40be8..3fa89213ec9 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12PairingPrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/BLS12PairingPrecompiledContractTest.java @@ -29,7 +29,6 @@ import com.google.common.collect.Streams; import com.google.common.io.CharStreams; import org.apache.tuweni.bytes.Bytes; -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; @@ -90,11 +89,4 @@ void shouldCalculate( assertThat(contract.gasRequirement(input)).isEqualTo(Long.parseLong(expectedGasUsed)); } } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/ECRECPrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/ECRECPrecompiledContractTest.java index 105d44e2894..863ecf08342 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/ECRECPrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/ECRECPrecompiledContractTest.java @@ -382,11 +382,4 @@ void r1CurveShouldUseR1EcRecover() { assertThat(r1Contract.computePrecompile(input, messageFrame).output()).isEqualTo(Bytes.EMPTY); verify(r1Spy).recoverPublicKeyFromSignature(any(), any()); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/KZGPointEvalPrecompileContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/KZGPointEvalPrecompileContractTest.java index 8ac335b1f6c..b8a83ed139d 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/KZGPointEvalPrecompileContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/KZGPointEvalPrecompileContractTest.java @@ -35,7 +35,6 @@ import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -92,11 +91,4 @@ public static List getPointEvaluationPrecompileTestVec record PrecompileTestParameters( Bytes input, boolean valid, Bytes returnValue, VersionedHash versionedHash) {} - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/precompile/MODEXPPrecompiledContractTest.java b/evm/src/test/java/org/hyperledger/besu/evm/precompile/MODEXPPrecompiledContractTest.java index 16f295f4933..ab8337917e7 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/precompile/MODEXPPrecompiledContractTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/precompile/MODEXPPrecompiledContractTest.java @@ -23,7 +23,6 @@ import org.hyperledger.besu.evm.frame.MessageFrame; import org.apache.tuweni.bytes.Bytes; -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; @@ -197,11 +196,4 @@ void testGasPrice( assertThat(byzantiumContract.gasRequirement(input)).isEqualTo(eip198Gas); assertThat(berlinContract.gasRequirement(input)).isEqualTo(eip2565Gas); } - - @Test - void dryRunDetector() { - assertThat(true) - .withFailMessage("This test is here so gradle --dry-run executes this class") - .isTrue(); - } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2Test.java new file mode 100644 index 00000000000..f48f58c794f --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2Test.java @@ -0,0 +1,188 @@ +/* + * 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.v2.operation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.gascalculator.SpuriousDragonGasCalculator; +import org.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2; + +import java.util.List; + +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class SarOperationV2Test { + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final SarOperationV2 operation = new SarOperationV2(gasCalculator); + + static Iterable data() { + return List.of( + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x00", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000002"), + Arguments.of( + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000007"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000004"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0xc000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0xff", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x100", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x101", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x0", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xff", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x100", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x4000000000000000000000000000000000000000000000000000000000000000", + "0xfe", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xf8", + "0x000000000000000000000000000000000000000000000000000000000000007f"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xfe", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xff", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x100", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", "0x8000", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000000000000000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x80", + "0xffffffffffffffffffffffffffffffff80000000000000000000000000000000"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x8000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x80000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x80000000000000000000000000000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); + } + + @ParameterizedTest(name = "{index}: {0}, {1}, {2}") + @MethodSource("data") + void shiftOperation(final String number, final String shift, final String expectedResult) { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexStringLenient(number)) + .pushStackItem(Bytes32.fromHexStringLenient(shift)) + .build(); + operation.execute(frame, null); + UInt256 expected; + if (expectedResult.equals("0x") || expectedResult.equals("0x0")) { + expected = UInt256.ZERO; + } else { + expected = UInt256.fromBytesBE(Bytes32.fromHexStringLenient(expectedResult).toArrayUnsafe()); + } + assertThat(getV2StackItem(frame, 0)).isEqualTo(expected); + } + + private static UInt256 getV2StackItem(final MessageFrame frame, final int offset) { + final long[] s = frame.stackDataV2(); + final int idx = (frame.stackTopV2() - 1 - offset) << 2; + return new UInt256(s[idx], s[idx + 1], s[idx + 2], s[idx + 3]); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java new file mode 100644 index 00000000000..e9d43c145ed --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java @@ -0,0 +1,586 @@ +/* + * 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.v2.operation; + +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.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.SarOperation; +import org.hyperledger.besu.evm.operation.ShlOperation; +import org.hyperledger.besu.evm.operation.ShrOperation; +import org.hyperledger.besu.evm.v2.StackArithmetic; + +import java.util.ArrayDeque; +import java.util.Deque; + +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.Provide; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** + * Property-based tests comparing v2 StackArithmetic shift operations against the original + * non-optimized Bytes-based implementations. + */ +public class ShiftOperationsV2PropertyBasedTest { + + // region Arbitrary Providers + + @Provide + Arbitrary values1to32() { + return Arbitraries.bytes().array(byte[].class).ofMinSize(1).ofMaxSize(32); + } + + @Provide + Arbitrary shiftAmounts() { + return Arbitraries.bytes().array(byte[].class).ofMinSize(0).ofMaxSize(32); + } + + @Provide + Arbitrary smallShifts() { + return Arbitraries.integers().between(0, 255); + } + + @Provide + Arbitrary overflowShifts() { + return Arbitraries.integers().between(256, 1024); + } + + @Provide + Arbitrary negativeValues() { + return Arbitraries.bytes() + .array(byte[].class) + .ofSize(32) + .map( + bytes -> { + bytes[0] = (byte) (bytes[0] | 0x80); + return bytes; + }); + } + + @Provide + Arbitrary positiveValues() { + return Arbitraries.bytes() + .array(byte[].class) + .ofMinSize(1) + .ofMaxSize(32) + .map( + bytes -> { + bytes[0] = (byte) (bytes[0] & 0x7F); + return bytes; + }); + } + + // endregion + + // region SHL Property Tests + + @Property(tries = 10000) + void property_shlV2_matchesOriginal_randomInputs( + @ForAll("values1to32") final byte[] valueBytes, + @ForAll("shiftAmounts") final byte[] shiftBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.wrap(shiftBytes); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShl(shift, value)); + final Bytes32 v2Result = runV2Shl(shift, value); + + assertThat(v2Result) + .as( + "SHL v2 mismatch for shift=%s, value=%s", + shift.toHexString(), Bytes32.leftPad(value).toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 5000) + void property_shlV2_matchesOriginal_smallShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShl(shiftBytes, value)); + final Bytes32 v2Result = runV2Shl(shiftBytes, value); + + assertThat(v2Result).as("SHL v2 mismatch for shift=%d", shift).isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_shlV2_matchesOriginal_overflowShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShl(shiftBytes, value)); + final Bytes32 v2Result = runV2Shl(shiftBytes, value); + + assertThat(v2Result) + .as("SHL v2 overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region SHR Property Tests + + @Property(tries = 10000) + void property_shrV2_matchesOriginal_randomInputs( + @ForAll("values1to32") final byte[] valueBytes, + @ForAll("shiftAmounts") final byte[] shiftBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.wrap(shiftBytes); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShr(shift, value)); + final Bytes32 v2Result = runV2Shr(shift, value); + + assertThat(v2Result) + .as( + "SHR v2 mismatch for shift=%s, value=%s", + shift.toHexString(), Bytes32.leftPad(value).toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 5000) + void property_shrV2_matchesOriginal_smallShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShr(shiftBytes, value)); + final Bytes32 v2Result = runV2Shr(shiftBytes, value); + + assertThat(v2Result).as("SHR v2 mismatch for shift=%d", shift).isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_shrV2_matchesOriginal_overflowShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShr(shiftBytes, value)); + final Bytes32 v2Result = runV2Shr(shiftBytes, value); + + assertThat(v2Result) + .as("SHR v2 overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region SAR Property Tests - Random Inputs + + @Property(tries = 10000) + void property_sarV2_matchesOriginal_randomInputs( + @ForAll("values1to32") final byte[] valueBytes, + @ForAll("shiftAmounts") final byte[] shiftBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.wrap(shiftBytes); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shift, value)); + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result) + .as( + "SAR v2 mismatch for shift=%s, value=%s", + shift.toHexString(), Bytes32.leftPad(value).toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 5000) + void property_sarV2_matchesOriginal_smallShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result).as("SAR v2 mismatch for shift=%d", shift).isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_sarV2_matchesOriginal_overflowShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region SAR Property Tests - Negative Values (Sign Extension) + + @Property(tries = 5000) + void property_sarV2_matchesOriginal_negativeValues_smallShifts( + @ForAll("negativeValues") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 negative mismatch for shift=%d, value=%s", shift, value.toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_sarV2_matchesOriginal_negativeValues_overflowShifts( + @ForAll("negativeValues") final byte[] valueBytes, + @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 negative overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region SAR Property Tests - Negative/Positive Values at Shift 255 + + @Property(tries = 3000) + void property_sarV2_matchesOriginal_negativeValues_shift255( + @ForAll("negativeValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(255); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shift, value)); + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result) + .as("SAR v2 negative shift=255 mismatch for value=%s", value.toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 3000) + void property_sarV2_matchesOriginal_positiveValues_shift255( + @ForAll("positiveValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(255); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shift, value)); + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result) + .as("SAR v2 positive shift=255 mismatch for value=%s", value.toHexString()) + .isEqualTo(originalResult); + } + + // endregion + + // region SAR Property Tests - Positive Values + + @Property(tries = 5000) + void property_sarV2_matchesOriginal_positiveValues_smallShifts( + @ForAll("positiveValues") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 positive mismatch for shift=%d, value=%s", shift, value.toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_sarV2_matchesOriginal_positiveValues_overflowShifts( + @ForAll("positiveValues") final byte[] valueBytes, + @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 positive overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region Edge Case Tests - SHL / SHR + + @Property(tries = 1000) + void property_shlV2_shiftByZero_returnsOriginalValue( + @ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(0); + + final Bytes32 v2Result = runV2Shl(shift, value); + + assertThat(v2Result).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 1000) + void property_shrV2_shiftByZero_returnsOriginalValue( + @ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(0); + + final Bytes32 v2Result = runV2Shr(shift, value); + + assertThat(v2Result).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 500) + void property_shlV2_largeShift_returnsZero(@ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes32 v2Result = runV2Shl(largeShift, value); + + assertThat(v2Result).isEqualTo(Bytes32.ZERO); + } + + @Property(tries = 500) + void property_shrV2_largeShift_returnsZero(@ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes32 v2Result = runV2Shr(largeShift, value); + + assertThat(v2Result).isEqualTo(Bytes32.ZERO); + } + + // endregion + + // region Edge Case Tests - SAR + + @Property(tries = 1000) + void property_sarV2_shiftByZero_returnsOriginalValue( + @ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(0); + + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 500) + void property_sarV2_negativeValue_largeShift_returnsAllOnes( + @ForAll("negativeValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes32 v2Result = runV2Sar(largeShift, value); + + final Bytes32 allOnes = + Bytes32.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + assertThat(v2Result).isEqualTo(allOnes); + } + + @Property(tries = 500) + void property_sarV2_positiveValue_largeShift_returnsZero( + @ForAll("positiveValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes32 v2Result = runV2Sar(largeShift, value); + + assertThat(v2Result).isEqualTo(Bytes32.ZERO); + } + + @Property(tries = 500) + void property_sarV2_allOnes_anyShift_returnsAllOnes(@ForAll("smallShifts") final int shift) { + + final Bytes value = + Bytes.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 500) + void property_sarV2_minValue_shift255_returnsAllOnes() { + + final Bytes value = + Bytes.fromHexString("0x8000000000000000000000000000000000000000000000000000000000000000"); + final Bytes shift = Bytes.of(255); + + final Bytes32 v2Result = runV2Sar(shift, value); + + final Bytes32 allOnes = + Bytes32.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + assertThat(v2Result).isEqualTo(allOnes); + } + + @Property(tries = 500) + void property_sarV2_maxPositive_shift255_returnsZero() { + + final Bytes value = + Bytes.fromHexString("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + final Bytes shift = Bytes.of(255); + + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result).isEqualTo(Bytes32.ZERO); + } + + // endregion + + // region V1 Oracle Helpers (mock-based, Bytes stack) + + private Bytes runOriginalShl(final Bytes shift, final Bytes value) { + return runOriginalOperation(shift, value, ShlOperation::staticOperation); + } + + private Bytes runOriginalShr(final Bytes shift, final Bytes value) { + return runOriginalOperation(shift, value, ShrOperation::staticOperation); + } + + private Bytes runOriginalSar(final Bytes shift, final Bytes value) { + return runOriginalOperation(shift, value, SarOperation::staticOperation); + } + + @FunctionalInterface + interface OriginalOperationExecutor { + Operation.OperationResult execute(MessageFrame frame); + } + + private Bytes runOriginalOperation( + final Bytes shift, final Bytes value, final OriginalOperationExecutor executor) { + final MessageFrame frame = mock(MessageFrame.class); + final Deque stack = new ArrayDeque<>(); + stack.push(value); + stack.push(shift); + + when(frame.popStackItem()).thenAnswer(invocation -> stack.pop()); + + final Bytes[] result = new Bytes[1]; + doAnswer( + invocation -> { + result[0] = invocation.getArgument(0); + return null; + }) + .when(frame) + .pushStackItem(any(Bytes.class)); + + executor.execute(frame); + return result[0]; + } + + // endregion + + // region V2 Helpers (long[] stack) + + private Bytes32 runV2Shl(final Bytes shift, final Bytes value) { + return runV2Operation(shift, value, StackArithmetic::shl); + } + + private Bytes32 runV2Shr(final Bytes shift, final Bytes value) { + return runV2Operation(shift, value, StackArithmetic::shr); + } + + private Bytes32 runV2Sar(final Bytes shift, final Bytes value) { + return runV2Operation(shift, value, StackArithmetic::sar); + } + + @FunctionalInterface + interface V2OperationExecutor { + int execute(long[] stack, int top); + } + + private Bytes32 runV2Operation( + final Bytes shift, final Bytes value, final V2OperationExecutor executor) { + final UInt256 shiftVal = UInt256.fromBytesBE(Bytes32.leftPad(shift).toArrayUnsafe()); + final UInt256 valueVal = UInt256.fromBytesBE(Bytes32.leftPad(value).toArrayUnsafe()); + + final long[] s = new long[8]; + writeLimbs(s, 0, valueVal); + writeLimbs(s, 4, shiftVal); + + executor.execute(s, 2); + + final UInt256 result = new UInt256(s[0], s[1], s[2], s[3]); + return Bytes32.wrap(result.toBytesBE()); + } + + private static void writeLimbs(final long[] s, final int offset, final UInt256 val) { + s[offset] = val.u3(); + s[offset + 1] = val.u2(); + s[offset + 2] = val.u1(); + s[offset + 3] = val.u0(); + } + + // endregion + + // region Utility + + private Bytes intToMinimalBytes(final int value) { + if (value == 0) { + return Bytes.EMPTY; + } + if (value <= 0xFF) { + return Bytes.of(value); + } + if (value <= 0xFFFF) { + return Bytes.of(value >> 8, value & 0xFF); + } + if (value <= 0xFFFFFF) { + return Bytes.of(value >> 16, (value >> 8) & 0xFF, value & 0xFF); + } + return Bytes.of(value >> 24, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); + } + + // endregion +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2Test.java new file mode 100644 index 00000000000..d60910e1a3c --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2Test.java @@ -0,0 +1,124 @@ +/* + * 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.v2.operation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.gascalculator.SpuriousDragonGasCalculator; +import org.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2; + +import java.util.Arrays; + +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ShlOperationV2Test { + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final ShlOperationV2 operation = new ShlOperationV2(gasCalculator); + + static Iterable data() { + return Arrays.asList( + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x00", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000002"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000004"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000008"), + Arguments.of( + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x01", + "0x000000000000000000000000000000000000000000000000000000000000001e"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000010"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", "0x100", "0x"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80", + "0x0000000000000000000000000000040000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", "0x8000", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000000000000000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x")); + } + + @ParameterizedTest + @MethodSource("data") + void shiftOperation(final String number, final String shift, final String expectedResult) { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexStringLenient(number)) + .pushStackItem(Bytes32.fromHexStringLenient(shift)) + .build(); + operation.execute(frame, null); + UInt256 expected; + if (expectedResult.equals("0x") || expectedResult.equals("0x0")) { + expected = UInt256.ZERO; + } else { + expected = UInt256.fromBytesBE(Bytes32.fromHexStringLenient(expectedResult).toArrayUnsafe()); + } + assertThat(getV2StackItem(frame, 0)).isEqualTo(expected); + } + + private static UInt256 getV2StackItem(final MessageFrame frame, final int offset) { + final long[] s = frame.stackDataV2(); + final int idx = (frame.stackTopV2() - 1 - offset) << 2; + return new UInt256(s[idx], s[idx + 1], s[idx + 2], s[idx + 3]); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2Test.java new file mode 100644 index 00000000000..398acafb3b9 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2Test.java @@ -0,0 +1,136 @@ +/* + * 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.v2.operation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.gascalculator.SpuriousDragonGasCalculator; +import org.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2; + +import java.util.Arrays; + +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ShrOperationV2Test { + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final ShrOperationV2 operation = new ShrOperationV2(gasCalculator); + + static Iterable data() { + return Arrays.asList( + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x00", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000002"), + Arguments.of( + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000007"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000004"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0xff", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", "0x100", "0x"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", "0x101", "0x"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x0", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xff", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x100", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", "0x8000", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000000000000000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x")); + } + + @ParameterizedTest + @MethodSource("data") + void shiftOperation(final String number, final String shift, final String expectedResult) { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexStringLenient(number)) + .pushStackItem(Bytes32.fromHexStringLenient(shift)) + .build(); + operation.execute(frame, null); + UInt256 expected; + if (expectedResult.equals("0x") || expectedResult.equals("0x0")) { + expected = UInt256.ZERO; + } else { + expected = UInt256.fromBytesBE(Bytes32.fromHexStringLenient(expectedResult).toArrayUnsafe()); + } + assertThat(getV2StackItem(frame, 0)).isEqualTo(expected); + } + + private static UInt256 getV2StackItem(final MessageFrame frame, final int offset) { + final long[] s = frame.stackDataV2(); + final int idx = (frame.stackTopV2() - 1 - offset) << 2; + return new UInt256(s[idx], s[idx + 1], s[idx + 2], s[idx + 3]); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/testutils/TestMessageFrameBuilderV2.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/testutils/TestMessageFrameBuilderV2.java new file mode 100644 index 00000000000..ebf852e768a --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/testutils/TestMessageFrameBuilderV2.java @@ -0,0 +1,190 @@ +/* + * 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.v2.testutils; + +import static org.hyperledger.besu.evm.frame.MessageFrame.DEFAULT_MAX_STACK_SIZE; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.Code; +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.blockhash.BlockHashLookup; +import org.hyperledger.besu.evm.frame.BlockValues; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.internal.Words; +import org.hyperledger.besu.evm.testutils.FakeBlockValues; +import org.hyperledger.besu.evm.toy.ToyWorld; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; + +public class TestMessageFrameBuilderV2 { + + public static final Address DEFAULT_ADDRESS = Address.fromHexString("0xe8f1b89"); + private static final int maxStackSize = DEFAULT_MAX_STACK_SIZE; + + private Optional blockValues = Optional.empty(); + private Optional worldUpdater = Optional.empty(); + private long initialGas = Long.MAX_VALUE; + private Address address = DEFAULT_ADDRESS; + private Address sender = DEFAULT_ADDRESS; + private Address originator = DEFAULT_ADDRESS; + private Address contract = DEFAULT_ADDRESS; + private Wei gasPrice = Wei.ZERO; + private Wei blobGasPrice = Wei.ZERO; + private Wei value = Wei.ZERO; + private Bytes inputData = Bytes.EMPTY; + private Code code = Code.EMPTY_CODE; + private int pc = 0; + private final List stackItems = new ArrayList<>(); + private Optional blockHashLookup = Optional.empty(); + private Bytes memory = Bytes.EMPTY; + private boolean isStatic = false; + + public TestMessageFrameBuilderV2 worldUpdater(final WorldUpdater worldUpdater) { + this.worldUpdater = Optional.of(worldUpdater); + return this; + } + + public TestMessageFrameBuilderV2 initialGas(final long initialGas) { + this.initialGas = initialGas; + return this; + } + + public TestMessageFrameBuilderV2 sender(final Address sender) { + this.sender = sender; + return this; + } + + public TestMessageFrameBuilderV2 address(final Address address) { + this.address = address; + return this; + } + + TestMessageFrameBuilderV2 originator(final Address originator) { + this.originator = originator; + return this; + } + + public TestMessageFrameBuilderV2 contract(final Address contract) { + this.contract = contract; + return this; + } + + public TestMessageFrameBuilderV2 gasPrice(final Wei gasPrice) { + this.gasPrice = gasPrice; + return this; + } + + public TestMessageFrameBuilderV2 blobGasPrice(final Wei blobGasPrice) { + this.blobGasPrice = blobGasPrice; + return this; + } + + public TestMessageFrameBuilderV2 value(final Wei value) { + this.value = value; + return this; + } + + public TestMessageFrameBuilderV2 inputData(final Bytes inputData) { + this.inputData = inputData; + return this; + } + + public TestMessageFrameBuilderV2 code(final Code code) { + this.code = code; + return this; + } + + public TestMessageFrameBuilderV2 pc(final int pc) { + this.pc = pc; + return this; + } + + public TestMessageFrameBuilderV2 blockValues(final BlockValues blockValues) { + this.blockValues = Optional.of(blockValues); + return this; + } + + public TestMessageFrameBuilderV2 pushStackItem(final Bytes item) { + stackItems.add(item); + return this; + } + + public TestMessageFrameBuilderV2 blockHashLookup(final BlockHashLookup blockHashLookup) { + this.blockHashLookup = Optional.of(blockHashLookup); + return this; + } + + public TestMessageFrameBuilderV2 memory(final Bytes memory) { + this.memory = memory; + return this; + } + + public TestMessageFrameBuilderV2 isStatic(final boolean isStatic) { + this.isStatic = isStatic; + return this; + } + + public MessageFrame build() { + final MessageFrame frame = + MessageFrame.builder() + .type(MessageFrame.Type.MESSAGE_CALL) + .worldUpdater(worldUpdater.orElseGet(this::createDefaultWorldUpdater)) + .initialGas(initialGas) + .address(address) + .originator(originator) + .gasPrice(gasPrice) + .blobGasPrice(blobGasPrice) + .inputData(inputData) + .sender(sender) + .value(value) + .apparentValue(value) + .contract(contract) + .code(code) + .blockValues(blockValues.orElseGet(() -> new FakeBlockValues(1337))) + .completer(c -> {}) + .miningBeneficiary(Address.ZERO) + .blockHashLookup( + blockHashLookup.orElse((__, number) -> Hash.hash(Words.longBytes(number)))) + .maxStackSize(maxStackSize) + .isStatic(isStatic) + .enableEvmV2(true) + .build(); + frame.setPC(pc); + stackItems.forEach( + item -> { + final UInt256 val = UInt256.fromBytesBE(item.toArrayUnsafe()); + final long[] s = frame.stackDataV2(); + final int dst = frame.stackTopV2() << 2; + s[dst] = val.u3(); + s[dst + 1] = val.u2(); + s[dst + 2] = val.u1(); + s[dst + 3] = val.u0(); + frame.setTopV2(frame.stackTopV2() + 1); + }); + frame.writeMemory(0, memory.size(), memory); + return frame; + } + + private WorldUpdater createDefaultWorldUpdater() { + return new ToyWorld(); + } +} diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 1901b77866a..97d10a6be85 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -8879,15 +8879,23 @@ - - - + + + - - + + - - + + + + + + + + + + diff --git a/platform/build.gradle b/platform/build.gradle index 88543986745..e986d283724 100644 --- a/platform/build.gradle +++ b/platform/build.gradle @@ -173,7 +173,7 @@ dependencies { api 'org.owasp.encoder:encoder:1.4.0' - api 'org.rocksdb:rocksdbjni:9.7.3' + api 'org.rocksdb:rocksdbjni:10.6.2' api 'org.springframework.security:spring-security-crypto:6.4.9' diff --git a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBColumnarKeyValueStorage.java b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBColumnarKeyValueStorage.java index 89d215c74a6..add8e445c84 100644 --- a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBColumnarKeyValueStorage.java +++ b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBColumnarKeyValueStorage.java @@ -308,6 +308,10 @@ private void setGlobalOptions(final RocksDBConfiguration configuration, final St .setCreateIfMissing(true) .setMaxOpenFiles(configuration.getMaxOpenFiles()) .setStatistics(stats) + // Disable RocksDB's periodic DumpStats() to LOG (default is often 600s). Besu already + // exposes Statistics via Prometheus; the native dump path has been a source of JNI SIGSEGVs + // on some versions/platforms under load. + .setStatsDumpPeriodSec(0) .setCreateMissingColumnFamilies(true) .setLogFileTimeToRoll(TIME_TO_ROLL_LOG_FILE) .setKeepLogFileNum(NUMBER_OF_LOG_FILES_TO_KEEP)