diff --git a/CHANGELOG.md b/CHANGELOG.md index 286d3ecb9..1ee6a8d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,11 @@ - Proof of Work consensus - Fast Sync ### Additions and Improvements +- Add TLS/mTLS options and configure the GraphQL HTTP service[#7910](https://github.com/hyperledger/besu/pull/7910) +- Allow plugins to propose transactions during block creation [#8268](https://github.com/hyperledger/besu/pull/8268) ### Bug fixes +- Upgrade Netty to version 4.1.118 to fix CVE-2025-24970 [#8275](https://github.com/hyperledger/besu/pull/8275) +- Add missing RPC method `debug_accountRange` to `RpcMethod.java` and implemented its handler. [#8153](https://github.com/hyperledger/besu/issues/8153) ## 25.2.0 @@ -37,6 +41,7 @@ - Add a tx selector to skip txs from the same sender after the first not selected [#8216](https://github.com/hyperledger/besu/pull/8216) - `rpc-gas-cap` default value has changed from 0 (unlimited) to 50M [#8251](https://github.com/hyperledger/besu/issues/8251) + #### Prague - Add timestamps to enable Prague hardfork on Sepolia and Holesky test networks [#8163](https://github.com/hyperledger/besu/pull/8163) - Update system call addresses to match [devnet-6](https://github.com/ethereum/execution-spec-tests/releases/) values [#8209](https://github.com/hyperledger/besu/issues/8209) @@ -74,7 +79,7 @@ - Retrieve all transaction receipts for a block in one request [#6646](https://github.com/hyperledger/besu/pull/6646) - Implement EIP-7840: Add blob schedule to config files [#8042](https://github.com/hyperledger/besu/pull/8042) - Allow gasPrice (legacy) and 1559 gasPrice params to be specified simultaneously for `eth_call`, `eth_createAccessList`, and `eth_estimateGas` [#8059](https://github.com/hyperledger/besu/pull/8059) -- Improve debug_traceBlock calls performance and reduce output size [#8076](https://github.com/hyperledger/besu/pull/8076) +- Improve debug_traceBlock calls performance and reduce output size [#8103](https://github.com/hyperledger/besu/pull/8103) - Add support for EIP-7702 transaction in the txpool [#8018](https://github.com/hyperledger/besu/pull/8018) [#7984](https://github.com/hyperledger/besu/pull/7984) - Add support for `movePrecompileToAddress` in `StateOverrides` (`eth_call`)[8115](https://github.com/hyperledger/besu/pull/8115) - Default target-gas-limit to 36M for holesky [#8125](https://github.com/hyperledger/besu/pull/8125) @@ -82,7 +87,7 @@ - Add nonce to transaction call object [#8139](https://github.com/hyperledger/besu/pull/8139) ### Bug fixes -- Fix serialization of state overrides when `movePrecompileToAddress` is present [#8204](https://github.com/hyperledger/besu/pull/8024) +- Fix serialization of state overrides when `movePrecompileToAddress` is present [#8024](https://github.com/hyperledger/besu/pull/8024) - Revise the approach for setting level_compaction_dynamic_level_bytes RocksDB configuration option [#8037](https://github.com/hyperledger/besu/pull/8037) - Fix possible incomplete txpool restore from dump file [#7991](https://github.com/hyperledger/besu/pull/7991) @@ -93,14 +98,14 @@ This is an optional hotfix to address serialization of state overrides parameter There is no need to upgrade from 24.12.0 (or 24.12.1) to this release if you are not yet using this functionality. ### Bug fixes -- Fix serialization of state overrides when `movePrecompileToAddress` is present [#8204](https://github.com/hyperledger/besu/pull/8024) +- Fix serialization of state overrides when `movePrecompileToAddress` is present [#8024](https://github.com/hyperledger/besu/pull/8024) ## 24.12.1 Hotfix This is a hotfix to address publishing besu maven artifacts. There are no issues with 24.12.0 other than incomplete artifact publishing, and there is no functional difference between 24.12.0 and 24.12.1 release binaries. ### Bug fixes -- Fix BOM pom publication to Artifactory [#8201](https://github.com/hyperledger/besu/pull/8021) +- Fix BOM pom publication to Artifactory [#8021](https://github.com/hyperledger/besu/pull/8021) ## 24.12.0 diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/GraphQlOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/GraphQlOptions.java index 643ca22e0..e72e6c23d 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/GraphQlOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/GraphQlOptions.java @@ -57,6 +57,36 @@ public class GraphQlOptions { private final CorsAllowedOriginsProperty graphQLHttpCorsAllowedOrigins = new CorsAllowedOriginsProperty(); + @CommandLine.Option( + names = {"--graphql-tls-enabled"}, + description = "Enable TLS for GraphQL HTTP service") + private Boolean graphqlTlsEnabled = false; + + @CommandLine.Option( + names = {"--graphql-tls-keystore-file"}, + description = "Path to the TLS keystore file for GraphQL HTTP service") + private String graphqlTlsKeystoreFile; + + @CommandLine.Option( + names = {"--graphql-tls-keystore-password-file"}, + description = "Path to the file containing the password for the TLS keystore") + private String graphqlTlsKeystorePasswordFile; + + @CommandLine.Option( + names = {"--graphql-mtls-enabled"}, + description = "Enable mTLS for GraphQL HTTP service") + private Boolean graphqlMtlsEnabled = false; + + @CommandLine.Option( + names = {"--graphql-tls-truststore-file"}, + description = "Path to the TLS truststore file for GraphQL HTTP service") + private String graphqlTlsTruststoreFile; + + @CommandLine.Option( + names = {"--graphql-tls-truststore-password-file"}, + description = "Path to the file containing the password for the TLS truststore") + private String graphqlTlsTruststorePasswordFile; + /** Default constructor */ public GraphQlOptions() {} @@ -72,7 +102,28 @@ public class GraphQlOptions { commandLine, "--graphql-http-enabled", !isGraphQLHttpEnabled, - asList("--graphql-http-cors-origins", "--graphql-http-host", "--graphql-http-port")); + asList( + "--graphql-http-cors-origins", + "--graphql-http-host", + "--graphql-http-port", + "--graphql-tls-enabled")); + + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--graphql-tls-enabled", + !graphqlTlsEnabled, + asList( + "--graphql-tls-keystore-file", + "--graphql-tls-keystore-password-file", + "--graphql-mtls-enabled")); + + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--graphql-mtls-enabled", + !graphqlMtlsEnabled, + asList("--graphql-tls-truststore-file", "--graphql-tls-truststore-password-file")); } /** @@ -93,6 +144,13 @@ public class GraphQlOptions { graphQLConfiguration.setHostsAllowlist(hostsAllowlist); graphQLConfiguration.setCorsAllowedDomains(graphQLHttpCorsAllowedOrigins); graphQLConfiguration.setHttpTimeoutSec(timoutSec); + graphQLConfiguration.setTlsEnabled(graphqlTlsEnabled); + graphQLConfiguration.setTlsKeyStorePath(graphqlTlsKeystoreFile); + graphQLConfiguration.setTlsKeyStorePasswordFile(graphqlTlsKeystorePasswordFile); + graphQLConfiguration.setMtlsEnabled(graphqlMtlsEnabled); + graphQLConfiguration.setTlsTrustStorePath(graphqlTlsTruststoreFile); + graphQLConfiguration.setTlsTrustStorePasswordFile(graphqlTlsTruststorePasswordFile); + return graphQLConfiguration; } diff --git a/besu/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java b/besu/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java index 1ea8db7ea..588599665 100644 --- a/besu/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java +++ b/besu/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java @@ -14,11 +14,14 @@ */ package org.hyperledger.besu.services; +import static com.google.common.base.Preconditions.checkState; + +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.services.TransactionSelectionService; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; - -import java.util.Optional; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; /** The Transaction Selection service implementation. */ public class TransactionSelectionServiceImpl implements TransactionSelectionService { @@ -26,18 +29,27 @@ public class TransactionSelectionServiceImpl implements TransactionSelectionServ /** Default Constructor. */ public TransactionSelectionServiceImpl() {} - private Optional factory = Optional.empty(); + private PluginTransactionSelectorFactory factory = PluginTransactionSelectorFactory.NO_OP_FACTORY; @Override - public PluginTransactionSelector createPluginTransactionSelector() { - return factory - .map(PluginTransactionSelectorFactory::create) - .orElse(PluginTransactionSelector.ACCEPT_ALL); + public PluginTransactionSelector createPluginTransactionSelector( + final SelectorsStateManager selectorsStateManager) { + return factory.create(selectorsStateManager); + } + + @Override + public void selectPendingTransactions( + final BlockTransactionSelectionService selectionService, + final ProcessableBlockHeader pendingBlockHeader) { + factory.selectPendingTransactions(selectionService, pendingBlockHeader); } @Override public void registerPluginTransactionSelectorFactory( final PluginTransactionSelectorFactory pluginTransactionSelectorFactory) { - factory = Optional.ofNullable(pluginTransactionSelectorFactory); + checkState( + factory == PluginTransactionSelectorFactory.NO_OP_FACTORY, + "PluginTransactionSelectorFactory was already registered"); + factory = pluginTransactionSelectorFactory; } } diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index 0e944e7f0..4f191d435 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -109,6 +109,12 @@ graphql-http-enabled=false graphql-http-host="6.7.8.9" graphql-http-port=6789 graphql-http-cors-origins=["none"] +graphql-tls-enabled=false +graphql-tls-keystore-file="none.pfx" +graphql-tls-keystore-password-file="none.passwd" +graphql-mtls-enabled=false +graphql-tls-truststore-file="none.pfx" +graphql-tls-truststore-password-file="none.passwd" # WebSockets API rpc-ws-enabled=false diff --git a/datatypes/src/main/java/org/hyperledger/besu/datatypes/HardforkId.java b/datatypes/src/main/java/org/hyperledger/besu/datatypes/HardforkId.java index 04b2145cc..bf36611d2 100644 --- a/datatypes/src/main/java/org/hyperledger/besu/datatypes/HardforkId.java +++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/HardforkId.java @@ -80,7 +80,7 @@ public interface HardforkId { /** Cancun + EOF fork. */ CANCUN_EOF(false, "Cancun + EOF"), /** Prague fork. */ - PRAGUE(false, "Prague"), + PRAGUE(true, "Prague"), /** Osaka fork. */ OSAKA(false, "Osaka"), /** Amsterdam fork. */ diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLConfiguration.java index a2829edb0..76d9f372e 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLConfiguration.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLConfiguration.java @@ -18,6 +18,9 @@ import static com.google.common.base.Preconditions.checkNotNull; import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -44,6 +47,13 @@ public class GraphQLConfiguration { private List hostsAllowlist = Arrays.asList("localhost", DEFAULT_GRAPHQL_HTTP_HOST); private long httpTimeoutSec = TimeoutOptions.defaultOptions().getTimeoutSeconds(); + private String tlsKeyStorePath; + private String tlsKeyStorePasswordFile; + private String tlsTrustStorePath; + private String tlsTrustStorePasswordFile; + private boolean tlsEnabled; + private boolean mtlsEnabled; + /** * Creates a default configuration for GraphQL. * @@ -174,6 +184,120 @@ public class GraphQLConfiguration { this.httpTimeoutSec = httpTimeoutSec; } + /** + * Retrieves the TLS key store path. + * + * @return the TLS key store path + */ + public String getTlsKeyStorePath() { + return tlsKeyStorePath; + } + + /** + * Sets the TLS key store path. + * + * @param tlsKeyStorePath the path to the TLS key store + */ + public void setTlsKeyStorePath(final String tlsKeyStorePath) { + this.tlsKeyStorePath = tlsKeyStorePath; + } + + /** + * Retrieves the TLS key store password. + * + * @return the TLS key store password + * @throws Exception if an error occurs while reading the password file + */ + public String getTlsKeyStorePassword() throws Exception { + return new String( + Files.readAllBytes(Paths.get(tlsKeyStorePasswordFile)), Charset.defaultCharset()) + .trim(); + } + + /** + * Sets the TLS key store password file. + * + * @param tlsKeyStorePasswordFile the path to the TLS key store password file + */ + public void setTlsKeyStorePasswordFile(final String tlsKeyStorePasswordFile) { + this.tlsKeyStorePasswordFile = tlsKeyStorePasswordFile; + } + + /** + * Retrieves the TLS trust store path. + * + * @return the TLS trust store path + */ + public String getTlsTrustStorePath() { + return tlsTrustStorePath; + } + + /** + * Sets the TLS trust store path. + * + * @param tlsTrustStorePath the path to the TLS trust store + */ + public void setTlsTrustStorePath(final String tlsTrustStorePath) { + this.tlsTrustStorePath = tlsTrustStorePath; + } + + /** + * Retrieves the TLS trust store password. + * + * @return the TLS trust store password + * @throws Exception if an error occurs while reading the password file + */ + public String getTlsTrustStorePassword() throws Exception { + return new String( + Files.readAllBytes(Paths.get(tlsTrustStorePasswordFile)), Charset.defaultCharset()) + .trim(); + } + + /** + * Sets the TLS trust store password file. + * + * @param tlsTrustStorePasswordFile the path to the TLS trust store password file + */ + public void setTlsTrustStorePasswordFile(final String tlsTrustStorePasswordFile) { + this.tlsTrustStorePasswordFile = tlsTrustStorePasswordFile; + } + + /** + * Retrieves the TLS enabled status. + * + * @return true if TLS is enabled, false otherwise + */ + public boolean isTlsEnabled() { + return tlsEnabled; + } + + /** + * Sets the TLS enabled status. + * + * @param tlsEnabled the status to set. true to enable TLS, false to disable it + */ + public void setTlsEnabled(final boolean tlsEnabled) { + this.tlsEnabled = tlsEnabled; + } + + /** + * Retrieves the mTLS enabled status. + * + * @return true if mTLS is enabled, false otherwise + */ + public boolean isMtlsEnabled() { + return mtlsEnabled; + } + + /** + * Sets the mTLS enabled status. + * + * @param mtlsEnabled the status to set. true to enable mTLS, false to disable it + */ + public void setMtlsEnabled(final boolean mtlsEnabled) { + this.mtlsEnabled = mtlsEnabled; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpService.java index fc763a1e3..f494cfc54 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpService.java @@ -51,6 +51,7 @@ import graphql.GraphQLError; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.http.ClientAuth; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; @@ -60,6 +61,7 @@ import io.vertx.core.json.DecodeException; import io.vertx.core.json.Json; import io.vertx.core.json.jackson.JacksonCodec; import io.vertx.core.net.HostAndPort; +import io.vertx.core.net.JksOptions; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; @@ -147,13 +149,43 @@ public class GraphQLHttpService { public CompletableFuture start() { LOG.info("Starting GraphQL HTTP service on {}:{}", config.getHost(), config.getPort()); // Create the HTTP server and a router object. - httpServer = - vertx.createHttpServer( - new HttpServerOptions() - .setHost(config.getHost()) - .setPort(config.getPort()) - .setHandle100ContinueAutomatically(true) - .setCompressionSupported(true)); + HttpServerOptions options = + new HttpServerOptions() + .setHost(config.getHost()) + .setPort(config.getPort()) + .setHandle100ContinueAutomatically(true) + .setCompressionSupported(true); + + if (config.isTlsEnabled()) { + try { + options + .setSsl(true) + .setKeyCertOptions( + new JksOptions() + .setPath(config.getTlsKeyStorePath()) + .setPassword(config.getTlsKeyStorePassword())); + } catch (Exception e) { + LOG.error("Failed to get TLS keystore password", e); + return CompletableFuture.failedFuture(e); + } + + if (config.isMtlsEnabled()) { + try { + options + .setTrustOptions( + new JksOptions() + .setPath(config.getTlsTrustStorePath()) + .setPassword(config.getTlsTrustStorePassword())) + .setClientAuth(ClientAuth.REQUIRED); + } catch (Exception e) { + LOG.error("Failed to get TLS truststore password", e); + return CompletableFuture.failedFuture(e); + } + } + } + + LOG.info("Options {}", options); + httpServer = vertx.createHttpServer(options); // Handle graphql http requests final Router router = Router.router(vertx); @@ -303,7 +335,8 @@ public class GraphQLHttpService { if (httpServer == null) { return ""; } - return NetworkUtility.urlForSocketAddress("http", socketAddress()); + String scheme = config.isTlsEnabled() ? "https" : "http"; + return NetworkUtility.urlForSocketAddress(scheme, socketAddress()); } // Empty Get/Post requests to / will be redirected to /graphql using 308 Permanent Redirect diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java index f8bc597ff..4d0b088e8 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java @@ -33,6 +33,7 @@ public enum RpcMethod { CLIQUE_PROPOSE("clique_propose"), CLIQUE_GET_SIGNER_METRICS("clique_getSignerMetrics"), DEBUG_ACCOUNT_AT("debug_accountAt"), + DEBUG_ACCOUNT_RANGE("debug_accountRange"), DEBUG_METRICS("debug_metrics"), DEBUG_RESYNC_WORLDSTATE("debug_resyncWorldState"), DEBUG_SET_HEAD("debug_setHead"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountRange.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountRange.java index 852972f4d..290c0f7a4 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountRange.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountRange.java @@ -16,6 +16,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameterOrBlockHash; @@ -52,9 +53,7 @@ public class DebugAccountRange implements JsonRpcMethod { @Override public String getName() { - // TODO(shemnon) 5229b899 is the last stable commit of retesteth, after this they rename the - // method to just "debug_accountRange". Once the tool is stable we will support the new name. - return "debug_accountRange"; + return RpcMethod.DEBUG_ACCOUNT_RANGE.getMethodName(); } @Override diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpsServiceTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpsServiceTest.java new file mode 100644 index 000000000..0b61af03b --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpsServiceTest.java @@ -0,0 +1,490 @@ +/* + * 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.graphql; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.api.query.TransactionWithMetadata; +import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.Synchronizer; +import org.hyperledger.besu.ethereum.eth.EthProtocol; +import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; +import org.hyperledger.besu.testutil.BlockTestUtil; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.Writer; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import graphql.GraphQL; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.apache.tuweni.bytes.Bytes; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +public class GraphQLHttpsServiceTest { + + // this tempDir is deliberately static + @TempDir private static Path folder; + + private static final Vertx vertx = Vertx.vertx(); + + private static GraphQLHttpService service; + private static OkHttpClient client; + private static String baseUrl; + protected static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + protected static final MediaType GRAPHQL = MediaType.parse("application/graphql; charset=utf-8"); + private static BlockchainQueries blockchainQueries; + private static GraphQL graphQL; + private static Map graphQlContextMap; + private static PoWMiningCoordinator miningCoordinatorMock; + + private final GraphQLTestHelper testHelper = new GraphQLTestHelper(); + // Generate a self-signed certificate + private static SelfSignedCertificate ssc; + private static SelfSignedCertificate clientSsc; + + @BeforeAll + public static void initServerAndClient() throws Exception { + blockchainQueries = Mockito.mock(BlockchainQueries.class); + final Synchronizer synchronizer = Mockito.mock(Synchronizer.class); + graphQL = Mockito.mock(GraphQL.class); + ssc = new SelfSignedCertificate(); + clientSsc = new SelfSignedCertificate(); + + miningCoordinatorMock = Mockito.mock(PoWMiningCoordinator.class); + graphQlContextMap = + Map.of( + GraphQLContextType.BLOCKCHAIN_QUERIES, + blockchainQueries, + GraphQLContextType.TRANSACTION_POOL, + Mockito.mock(TransactionPool.class), + GraphQLContextType.MINING_COORDINATOR, + miningCoordinatorMock, + GraphQLContextType.SYNCHRONIZER, + synchronizer); + + final Set supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(EthProtocol.ETH62); + supportedCapabilities.add(EthProtocol.ETH63); + final GraphQLDataFetchers dataFetchers = new GraphQLDataFetchers(supportedCapabilities); + graphQL = GraphQLProvider.buildGraphQL(dataFetchers); + service = createGraphQLHttpService(); + service.start().join(); + // Build an OkHttp client. + client = createHttpClientforMtls(); + baseUrl = service.url() + "/graphql/"; + } + + public static OkHttpClient createHttpClientforMtls() throws Exception { + + // Create a temporary truststore file + File truststoreFile = File.createTempFile("truststore", ".jks"); + truststoreFile.deleteOnExit(); + + // Create a PKCS12 truststore and load the server's certificate + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(null, null); + trustStore.setCertificateEntry("alias", ssc.cert()); + + // Save the truststore to the temporary file + try (FileOutputStream fos = new FileOutputStream(truststoreFile)) { + trustStore.store(fos, "password".toCharArray()); + } + + // Create TrustManagerFactory + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + // Get TrustManagers + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + + // Create a temporary keystore file + File keystoreFile = File.createTempFile("keystore", ".jks"); + keystoreFile.deleteOnExit(); + + // Create a PKCS12 keystore and load the client's certificate + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, "password".toCharArray()); + keyStore.setKeyEntry( + "alias", + clientSsc.key(), + "password".toCharArray(), + new java.security.cert.Certificate[] {clientSsc.cert()}); + + // Save the keystore to the temporary file + try (FileOutputStream fos = new FileOutputStream(keystoreFile)) { + keyStore.store(fos, "password".toCharArray()); + } + + // Create KeyManagerFactory + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, "password".toCharArray()); + + // Get KeyManagers + KeyManager[] keyManagers = keyManagerFactory.getKeyManagers(); + + // Initialize SSLContext + SSLContext sslContext = SSLContext.getInstance("TLS"); + // Obtain a SecureRandom instance + SecureRandom secureRandom = SecureRandom.getInstanceStrong(); + + // Initialize SSLContext + sslContext.init(keyManagers, trustManagers, secureRandom); + + if (!(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException( + "Unexpected default trust managers: " + Arrays.toString(trustManagers)); + } + + // Create OkHttpClient with custom SSLSocketFactory and TrustManager + return new OkHttpClient.Builder() + .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagers[0]) + .hostnameVerifier((hostname, session) -> "localhost".equals(hostname)) + .followRedirects(false) + .build(); + } + + private static GraphQLHttpService createGraphQLHttpService(final GraphQLConfiguration config) + throws Exception { + return new GraphQLHttpService( + vertx, folder, config, graphQL, graphQlContextMap, Mockito.mock(EthScheduler.class)); + } + + private static GraphQLHttpService createGraphQLHttpService() throws Exception { + return new GraphQLHttpService( + vertx, + folder, + createGraphQLConfig(), + graphQL, + graphQlContextMap, + Mockito.mock(EthScheduler.class)); + } + + private static GraphQLConfiguration createGraphQLConfig() throws Exception { + final GraphQLConfiguration config = GraphQLConfiguration.createDefault(); + + // Create a temporary keystore file + File keystoreFile = File.createTempFile("keystore", ".jks"); + keystoreFile.deleteOnExit(); + + // Create a PKCS12 keystore and load the self-signed certificate + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, "password".toCharArray()); + keyStore.setKeyEntry( + "alias", + ssc.key(), + "password".toCharArray(), + new java.security.cert.Certificate[] {ssc.cert()}); + + // Save the keystore to the temporary file + FileOutputStream fos = new FileOutputStream(keystoreFile); + keyStore.store(fos, "password".toCharArray()); + + // Create a temporary password file + File keystorePasswordFile = File.createTempFile("keystorePassword", ".txt"); + keystorePasswordFile.deleteOnExit(); + try (Writer writer = + Files.newBufferedWriter(keystorePasswordFile.toPath(), Charset.defaultCharset())) { + writer.write("password"); + } + + // Create a temporary truststore file + File truststoreFile = File.createTempFile("truststore", ".jks"); + truststoreFile.deleteOnExit(); + + // Create a JKS truststore and load the client's certificate + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(null, "password".toCharArray()); + trustStore.setCertificateEntry("clientAlias", clientSsc.cert()); + + // Save the truststore to the temporary file + try (FileOutputStream fos2 = new FileOutputStream(truststoreFile)) { + trustStore.store(fos2, "password".toCharArray()); + } + + config.setPort(0); + config.setTlsEnabled(true); + config.setTlsKeyStorePath(keystoreFile.getAbsolutePath()); + config.setTlsKeyStorePasswordFile(keystorePasswordFile.getAbsolutePath()); + config.setMtlsEnabled(true); + config.setTlsTrustStorePath(truststoreFile.getAbsolutePath()); + config.setTlsTrustStorePasswordFile(keystorePasswordFile.getAbsolutePath()); + return config; + } + + @BeforeAll + public static void setupConstants() { + final URL blocksUrl = BlockTestUtil.getTestBlockchainUrl(); + + final URL genesisJsonUrl = BlockTestUtil.getTestGenesisUrl(); + + Assertions.assertThat(blocksUrl).isNotNull(); + Assertions.assertThat(genesisJsonUrl).isNotNull(); + } + + /** Tears down the HTTP server. */ + @AfterAll + public static void shutdownServer() { + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + service.stop().join(); + vertx.close(); + } + + @Test + public void invalidCallToStart() { + service + .start() + .whenComplete( + (unused, exception) -> assertThat(exception).isInstanceOf(IllegalStateException.class)); + } + + @Test + public void http404() throws Exception { + try (final Response resp = client.newCall(buildGetRequest("/foo")).execute()) { + Assertions.assertThat(resp.code()).isEqualTo(404); + } + } + + @Test + public void handleEmptyRequestAndRedirect_post() throws Exception { + final RequestBody body = RequestBody.create("", null); + try (final Response resp = + client.newCall(new Request.Builder().post(body).url(service.url()).build()).execute()) { + Assertions.assertThat(resp.code()).isEqualTo(HttpResponseStatus.PERMANENT_REDIRECT.code()); + final String location = resp.header("Location"); + Assertions.assertThat(location).isNotEmpty().isNotNull(); + final HttpUrl redirectUrl = resp.request().url().resolve(location); + Assertions.assertThat(redirectUrl).isNotNull(); + final Request.Builder redirectBuilder = resp.request().newBuilder(); + redirectBuilder.post(resp.request().body()); + resp.body().close(); + try (final Response redirectResp = + client.newCall(redirectBuilder.url(redirectUrl).build()).execute()) { + Assertions.assertThat(redirectResp.code()).isEqualTo(HttpResponseStatus.BAD_REQUEST.code()); + } + } + } + + @Test + public void handleEmptyRequestAndRedirect_get() throws Exception { + String url = service.url(); + Request req = new Request.Builder().get().url(url).build(); + try (final Response resp = client.newCall(req).execute()) { + Assertions.assertThat(resp.code()).isEqualTo(HttpResponseStatus.PERMANENT_REDIRECT.code()); + final String location = resp.header("Location"); + Assertions.assertThat(location).isNotEmpty().isNotNull(); + final HttpUrl redirectUrl = resp.request().url().resolve(location); + Assertions.assertThat(redirectUrl).isNotNull(); + final Request.Builder redirectBuilder = resp.request().newBuilder(); + redirectBuilder.get(); + resp.body().close(); + try (final Response redirectResp = + client.newCall(redirectBuilder.url(redirectUrl).build()).execute()) { + Assertions.assertThat(redirectResp.code()).isEqualTo(HttpResponseStatus.BAD_REQUEST.code()); + } + } + } + + @Test + public void handleInvalidQuerySchema() throws Exception { + final RequestBody body = RequestBody.create("{gasPrice1}", GRAPHQL); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidGraphQLError(json); + Assertions.assertThat(resp.code()).isEqualTo(400); + } + } + + @Test + public void query_get() throws Exception { + final Wei price = Wei.of(16); + Mockito.when(blockchainQueries.gasPrice()).thenReturn(price); + Mockito.when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(price); + + try (final Response resp = client.newCall(buildGetRequest("?query={gasPrice}")).execute()) { + Assertions.assertThat(resp.code()).isEqualTo(200); + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidGraphQLResult(json); + final String result = json.getJsonObject("data").getString("gasPrice"); + Assertions.assertThat(result).isEqualTo("0x10"); + } + } + + @Test + public void query_jsonPost() throws Exception { + final RequestBody body = RequestBody.create("{\"query\":\"{gasPrice}\"}", JSON); + final Wei price = Wei.of(16); + Mockito.when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(price); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + Assertions.assertThat(resp.code()).isEqualTo(200); // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidGraphQLResult(json); + final String result = json.getJsonObject("data").getString("gasPrice"); + Assertions.assertThat(result).isEqualTo("0x10"); + } + } + + @Test + public void query_graphqlPost() throws Exception { + final RequestBody body = RequestBody.create("{gasPrice}", GRAPHQL); + final Wei price = Wei.of(16); + Mockito.when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(price); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + Assertions.assertThat(resp.code()).isEqualTo(200); // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidGraphQLResult(json); + final String result = json.getJsonObject("data").getString("gasPrice"); + Assertions.assertThat(result).isEqualTo("0x10"); + } + } + + @Test + public void query_untypedPost() throws Exception { + final RequestBody body = RequestBody.create("{gasPrice}", null); + final Wei price = Wei.of(16); + Mockito.when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(price); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + Assertions.assertThat(resp.code()).isEqualTo(200); // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidGraphQLResult(json); + final String result = json.getJsonObject("data").getString("gasPrice"); + Assertions.assertThat(result).isEqualTo("0x10"); + } + } + + @Test + public void getSocketAddressWhenActive() { + final InetSocketAddress socketAddress = service.socketAddress(); + Assertions.assertThat(socketAddress.getAddress().getHostAddress()).isEqualTo("127.0.0.1"); + Assertions.assertThat(socketAddress.getPort()).isPositive(); + } + + @Test + public void getSocketAddressWhenStoppedIsEmpty() throws Exception { + final GraphQLHttpService service = createGraphQLHttpService(); + + final InetSocketAddress socketAddress = service.socketAddress(); + Assertions.assertThat(socketAddress.getAddress().getHostAddress()).isEqualTo("0.0.0.0"); + Assertions.assertThat(socketAddress.getPort()).isZero(); + Assertions.assertThat(service.url()).isEmpty(); + } + + @Test + public void getSocketAddressWhenBindingToAllInterfaces() throws Exception { + final GraphQLConfiguration config = createGraphQLConfig(); + config.setHost("0.0.0.0"); + final GraphQLHttpService service = createGraphQLHttpService(config); + service.start().join(); + + try { + final InetSocketAddress socketAddress = service.socketAddress(); + Assertions.assertThat(socketAddress.getAddress().getHostAddress()).isEqualTo("0.0.0.0"); + Assertions.assertThat(socketAddress.getPort()).isPositive(); + Assertions.assertThat(!service.url().contains("0.0.0.0")).isTrue(); + } finally { + service.stop().join(); + } + } + + @Test + public void responseContainsJsonContentTypeHeader() throws Exception { + + final RequestBody body = RequestBody.create("{gasPrice}", GRAPHQL); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + Assertions.assertThat(resp.header("Content-Type")).isEqualTo(JSON.toString()); + } + } + + @Test + public void ethGetBlockNumberByBlockHash() throws Exception { + final long blockNumber = 12345L; + final Hash blockHash = Hash.hash(Bytes.of(1)); + @SuppressWarnings("unchecked") + final BlockWithMetadata block = + Mockito.mock(BlockWithMetadata.class); + @SuppressWarnings("unchecked") + final BlockHeader blockHeader = Mockito.mock(BlockHeader.class); + + Mockito.when(blockchainQueries.blockByHash(blockHash)).thenReturn(Optional.of(block)); + Mockito.when(block.getHeader()).thenReturn(blockHeader); + Mockito.when(blockHeader.getNumber()).thenReturn(blockNumber); + + final String query = "{block(hash:\"" + blockHash + "\") {number}}"; + + final RequestBody body = RequestBody.create(query, GRAPHQL); + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + Assertions.assertThat(resp.code()).isEqualTo(200); + final String jsonStr = resp.body().string(); + final JsonObject json = new JsonObject(jsonStr); + testHelper.assertValidGraphQLResult(json); + final String result = json.getJsonObject("data").getJsonObject("block").getString("number"); + Assertions.assertThat(Integer.parseInt(result.substring(2), 16)).isEqualTo(blockNumber); + } + } + + private Request buildPostRequest(final RequestBody body) { + return new Request.Builder().post(body).url(baseUrl).build(); + } + + private Request buildGetRequest(final String path) { + return new Request.Builder().get().url(baseUrl + path).build(); + } +} diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java index 30a81f28a..9837f2f7c 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java @@ -55,8 +55,8 @@ import org.hyperledger.besu.evm.account.MutableAccount; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import org.hyperledger.besu.plugin.services.exception.StorageException; import org.hyperledger.besu.plugin.services.securitymodule.SecurityModuleException; -import org.hyperledger.besu.plugin.services.tracer.BlockAwareOperationTracer; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import java.util.List; import java.util.Optional; @@ -213,11 +213,15 @@ public abstract class AbstractBlockCreator implements AsyncBlockCreator { throwIfStopped(); - final PluginTransactionSelector pluginTransactionSelector = - miningConfiguration.getTransactionSelectionService().createPluginTransactionSelector(); - - final BlockAwareOperationTracer operationTracer = - pluginTransactionSelector.getOperationTracer(); + final var selectorsStateManager = new SelectorsStateManager(); + final var pluginTransactionSelector = + miningConfiguration + .getTransactionSelectionService() + .createPluginTransactionSelector(selectorsStateManager); + final var operationTracer = pluginTransactionSelector.getOperationTracer(); + pluginTransactionSelector + .getOperationTracer() + .traceStartBlock(processableBlockHeader, miningBeneficiary); operationTracer.traceStartBlock(processableBlockHeader, miningBeneficiary); BlockProcessingContext blockProcessingContext = @@ -240,6 +244,7 @@ public abstract class AbstractBlockCreator implements AsyncBlockCreator { miningBeneficiary, newProtocolSpec, pluginTransactionSelector, + selectorsStateManager, parentHeader); transactionResults.logSelectionStats(); timings.register("txsSelection"); @@ -362,6 +367,7 @@ public abstract class AbstractBlockCreator implements AsyncBlockCreator { final Address miningBeneficiary, final ProtocolSpec protocolSpec, final PluginTransactionSelector pluginTransactionSelector, + final SelectorsStateManager selectorsStateManager, final BlockHeader parentHeader) throws RuntimeException { final MainnetTransactionProcessor transactionProcessor = protocolSpec.getTransactionProcessor(); @@ -391,7 +397,8 @@ public abstract class AbstractBlockCreator implements AsyncBlockCreator { protocolSpec.getGasLimitCalculator(), protocolSpec.getBlockHashProcessor(), pluginTransactionSelector, - ethScheduler); + ethScheduler, + selectorsStateManager); if (transactions.isPresent()) { return selector.evaluateTransactions(transactions.get()); diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java index 5873acb8b..86259dad2 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java @@ -50,10 +50,14 @@ import org.hyperledger.besu.evm.blockhash.BlockHashLookup; import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.TransactionSelectionService; import org.hyperledger.besu.plugin.services.tracer.BlockAwareOperationTracer; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -88,7 +92,7 @@ import org.slf4j.LoggerFactory; * Once "used" this class must be discarded and another created. This class contains state which is * not cleared between executions of buildTransactionListForBlock(). */ -public class BlockTransactionSelector { +public class BlockTransactionSelector implements BlockTransactionSelectionService { private static final Logger LOG = LoggerFactory.getLogger(BlockTransactionSelector.class); private final Supplier isCancelled; private final MainnetTransactionProcessor transactionProcessor; @@ -99,13 +103,17 @@ public class BlockTransactionSelector { private final TransactionSelectionResults transactionSelectionResults = new TransactionSelectionResults(); private final List transactionSelectors; + private final SelectorsStateManager selectorsStateManager; + private final TransactionSelectionService transactionSelectionService; private final PluginTransactionSelector pluginTransactionSelector; private final BlockAwareOperationTracer operationTracer; private final EthScheduler ethScheduler; private final AtomicBoolean isTimeout = new AtomicBoolean(false); private final long blockTxsSelectionMaxTime; private WorldUpdater blockWorldStateUpdater; + private WorldUpdater txWorldStateUpdater; private volatile TransactionEvaluationContext currTxEvaluationContext; + private final List selectedTxPendingActions = new ArrayList<>(1); public BlockTransactionSelector( final MiningConfiguration miningConfiguration, @@ -123,7 +131,8 @@ public class BlockTransactionSelector { final GasLimitCalculator gasLimitCalculator, final BlockHashProcessor blockHashProcessor, final PluginTransactionSelector pluginTransactionSelector, - final EthScheduler ethScheduler) { + final EthScheduler ethScheduler, + final SelectorsStateManager selectorsStateManager) { this.transactionProcessor = transactionProcessor; this.blockchain = blockchain; this.worldState = worldState; @@ -141,20 +150,24 @@ public class BlockTransactionSelector { blobGasPrice, miningBeneficiary, transactionPool); - transactionSelectors = createTransactionSelectors(blockSelectionContext); + this.selectorsStateManager = selectorsStateManager; + this.transactionSelectionService = miningConfiguration.getTransactionSelectionService(); + this.transactionSelectors = + createTransactionSelectors(blockSelectionContext, selectorsStateManager); this.pluginTransactionSelector = pluginTransactionSelector; this.operationTracer = new InterruptibleOperationTracer(pluginTransactionSelector.getOperationTracer()); blockWorldStateUpdater = worldState.updater(); + txWorldStateUpdater = blockWorldStateUpdater.updater(); blockTxsSelectionMaxTime = miningConfiguration.getBlockTxsSelectionMaxTime(); } private List createTransactionSelectors( - final BlockSelectionContext context) { + final BlockSelectionContext context, final SelectorsStateManager selectorsStateManager) { return List.of( new SkipSenderTransactionSelector(context), - new BlockSizeTransactionSelector(context), - new BlobSizeTransactionSelector(context), + new BlockSizeTransactionSelector(context, selectorsStateManager), + new BlobSizeTransactionSelector(context, selectorsStateManager), new PriceTransactionSelector(context), new BlobPriceTransactionSelector(context), new MinPriorityFeePerGasTransactionSelector(context), @@ -171,10 +184,6 @@ public class BlockTransactionSelector { * evaluation. */ public TransactionSelectionResults buildTransactionListForBlock() { - LOG.atDebug() - .setMessage("Transaction pool stats {}") - .addArgument(blockSelectionContext.transactionPool()::logStats) - .log(); timeLimitedSelection(); LOG.atTrace() .setMessage("Transaction selection result {}") @@ -186,10 +195,19 @@ public class BlockTransactionSelector { private void timeLimitedSelection() { final var txSelectionTask = new FutureTask( - () -> - blockSelectionContext - .transactionPool() - .selectTransactions(this::evaluateTransaction), + () -> { + selectorsStateManager.blockSelectionStarted(); + + LOG.debug("Starting plugin transaction selection"); + transactionSelectionService.selectPendingTransactions( + this, blockSelectionContext.pendingBlockHeader()); + + LOG.atDebug() + .setMessage("Starting internal pool transaction selection, stats {}") + .addArgument(blockSelectionContext.transactionPool()::logStats) + .log(); + blockSelectionContext.transactionPool().selectTransactions(this::evaluateTransaction); + }, null); ethScheduler.scheduleBlockCreationTask(txSelectionTask); @@ -208,11 +226,18 @@ public class BlockTransactionSelector { cancelEvaluatingTxWithGraceTime(txSelectionTask); - LOG.warn( - "Interrupting the selection of transactions for block inclusion as it exceeds the maximum configured duration of " - + blockTxsSelectionMaxTime - + "ms", - e); + final var logBuilder = + LOG.atWarn() + .setMessage( + "Interrupting the selection of transactions for block inclusion as it exceeds" + + " the maximum configured duration of {}ms") + .addArgument(blockTxsSelectionMaxTime); + + if (LOG.isTraceEnabled()) { + logBuilder.setCause(e).log(); + } else { + logBuilder.log(); + } } } @@ -260,11 +285,25 @@ public class BlockTransactionSelector { * evaluations. */ public TransactionSelectionResults evaluateTransactions(final List transactions) { + selectorsStateManager.blockSelectionStarted(); + transactions.forEach( transaction -> evaluateTransaction(new PendingTransaction.Local.Priority(transaction))); return transactionSelectionResults; } + private TransactionSelectionResult evaluateTransaction( + final PendingTransaction pendingTransaction) { + final var evaluationResult = evaluatePendingTransaction(pendingTransaction); + + if (evaluationResult.selected()) { + return commit() ? evaluationResult : BLOCK_SELECTION_TIMEOUT; + } else { + rollback(); + return evaluationResult; + } + } + /** * Passed into the PendingTransactions, and is called on each transaction until sufficient * transactions are found which fill a block worth of gas. This function will continue to be @@ -275,10 +314,14 @@ public class BlockTransactionSelector { * @return The result of the transaction evaluation process. * @throws CancellationException if the transaction selection process is cancelled. */ - private TransactionSelectionResult evaluateTransaction( - final PendingTransaction pendingTransaction) { + @Override + public TransactionSelectionResult evaluatePendingTransaction( + final org.hyperledger.besu.datatypes.PendingTransaction pendingTransaction) { + checkCancellation(); + LOG.atTrace().setMessage("Starting evaluation of {}").addArgument(pendingTransaction).log(); + final TransactionEvaluationContext evaluationContext = createTransactionEvaluationContext(pendingTransaction); currTxEvaluationContext = evaluationContext; @@ -288,27 +331,56 @@ public class BlockTransactionSelector { return handleTransactionNotSelected(evaluationContext, selectionResult); } - final WorldUpdater txWorldStateUpdater = blockWorldStateUpdater.updater(); final TransactionProcessingResult processingResult = - processTransaction(evaluationContext.getTransaction(), txWorldStateUpdater); + processTransaction(evaluationContext.getTransaction()); + + txWorldStateUpdater.markTransactionBoundary(); var postProcessingSelectionResult = evaluatePostProcessing(evaluationContext, processingResult); - if (postProcessingSelectionResult.selected()) { - return handleTransactionSelected(evaluationContext, processingResult, txWorldStateUpdater); + return postProcessingSelectionResult.selected() + ? handleTransactionSelected(evaluationContext, processingResult) + : handleTransactionNotSelected(evaluationContext, postProcessingSelectionResult); + } + + @Override + public boolean commit() { + // only add this tx to the selected set if it is not too late, + // this need to be done synchronously to avoid that a concurrent timeout + // could start packing a block while we are updating the state here + synchronized (isTimeout) { + if (!isTimeout.get()) { + selectorsStateManager.commit(); + txWorldStateUpdater.commit(); + blockWorldStateUpdater.commit(); + for (final var pendingAction : selectedTxPendingActions) { + pendingAction.run(); + } + selectedTxPendingActions.clear(); + return true; + } } - return handleTransactionNotSelected( - evaluationContext, postProcessingSelectionResult, txWorldStateUpdater); + selectedTxPendingActions.clear(); + blockWorldStateUpdater = worldState.updater(); + txWorldStateUpdater = blockWorldStateUpdater.updater(); + return false; + } + + @Override + public void rollback() { + selectedTxPendingActions.clear(); + selectorsStateManager.rollback(); + txWorldStateUpdater = blockWorldStateUpdater.updater(); } private TransactionEvaluationContext createTransactionEvaluationContext( - final PendingTransaction pendingTransaction) { + final org.hyperledger.besu.datatypes.PendingTransaction pendingTransaction) { final Wei transactionGasPriceInBlock = blockSelectionContext .feeMarket() .getTransactionPriceCalculator() .price( - pendingTransaction.getTransaction(), + (Transaction) pendingTransaction.getTransaction(), blockSelectionContext.pendingBlockHeader().getBaseFee()); return new TransactionEvaluationContext( @@ -333,7 +405,7 @@ public class BlockTransactionSelector { for (var selector : transactionSelectors) { TransactionSelectionResult result = - selector.evaluateTransactionPreProcessing(evaluationContext, transactionSelectionResults); + selector.evaluateTransactionPreProcessing(evaluationContext); if (!result.equals(SELECTED)) { return result; } @@ -357,8 +429,7 @@ public class BlockTransactionSelector { for (var selector : transactionSelectors) { TransactionSelectionResult result = - selector.evaluateTransactionPostProcessing( - evaluationContext, transactionSelectionResults, processingResult); + selector.evaluateTransactionPostProcessing(evaluationContext, processingResult); if (!result.equals(SELECTED)) { return result; } @@ -371,17 +442,15 @@ public class BlockTransactionSelector { * Processes a transaction * * @param transaction The transaction to be processed. - * @param worldStateUpdater The world state updater. * @return The result of the transaction processing. */ - private TransactionProcessingResult processTransaction( - final Transaction transaction, final WorldUpdater worldStateUpdater) { + private TransactionProcessingResult processTransaction(final Transaction transaction) { final BlockHashLookup blockHashLookup = blockSelectionContext .blockHashProcessor() .createBlockHashLookup(blockchain, blockSelectionContext.pendingBlockHeader()); return transactionProcessor.processTransaction( - worldStateUpdater, + txWorldStateUpdater, blockSelectionContext.pendingBlockHeader(), transaction, blockSelectionContext.miningBeneficiary(), @@ -399,56 +468,48 @@ public class BlockTransactionSelector { * * @param evaluationContext The current selection session data. * @param processingResult The result of the transaction processing. - * @param txWorldStateUpdater The world state updater. * @return The result of the transaction selection process. */ private TransactionSelectionResult handleTransactionSelected( final TransactionEvaluationContext evaluationContext, - final TransactionProcessingResult processingResult, - final WorldUpdater txWorldStateUpdater) { + final TransactionProcessingResult processingResult) { final Transaction transaction = evaluationContext.getTransaction(); final long gasUsedByTransaction = transaction.getGasLimit() - processingResult.getGasRemaining(); - final long cumulativeGasUsed = - transactionSelectionResults.getCumulativeGasUsed() + gasUsedByTransaction; - final long blobGasUsed = - blockSelectionContext.gasCalculator().blobGasCost(transaction.getBlobCount()); - final boolean tooLate; + // queue the creation of the receipt and the update of the final results + // there actions will be performed on commit if the pending tx is definitely selected + selectedTxPendingActions.add( + () -> { + final long cumulativeGasUsed = + transactionSelectionResults.getCumulativeGasUsed() + gasUsedByTransaction; - // only add this tx to the selected set if it is not too late, - // this need to be done synchronously to avoid that a concurrent timeout - // could start packing a block while we are updating the state here - synchronized (isTimeout) { - tooLate = isTimeout.get(); - if (!tooLate) { - txWorldStateUpdater.commit(); - blockWorldStateUpdater.commit(); - final TransactionReceipt receipt = - transactionReceiptFactory.create( - transaction.getType(), processingResult, worldState, cumulativeGasUsed); + final TransactionReceipt receipt = + transactionReceiptFactory.create( + transaction.getType(), processingResult, worldState, cumulativeGasUsed); - transactionSelectionResults.updateSelected( - transaction, receipt, gasUsedByTransaction, blobGasUsed); - } - } + transactionSelectionResults.updateSelected(transaction, receipt, gasUsedByTransaction); - if (tooLate) { + notifySelected(evaluationContext, processingResult); + LOG.atTrace() + .setMessage("Selected and commited {} for block creation") + .addArgument(transaction::toTraceLog) + .log(); + }); + + if (isTimeout.get()) { // even if this tx passed all the checks, it is too late to include it in this block, // so we need to treat it as not selected // do not rely on the presence of this result, since by the time it is added, the code // reading it could have been already executed by another thread - return handleTransactionNotSelected( - evaluationContext, BLOCK_SELECTION_TIMEOUT, txWorldStateUpdater); + return handleTransactionNotSelected(evaluationContext, BLOCK_SELECTION_TIMEOUT); } - notifySelected(evaluationContext, processingResult); - - blockWorldStateUpdater = worldState.updater(); LOG.atTrace() - .setMessage("Selected {} for block creation, evaluated in {}") + .setMessage( + "Potentially selected {} for block creation, evaluated in {}, waiting for commit") .addArgument(transaction::toTraceLog) .addArgument(evaluationContext.getEvaluationTimer()) .log(); @@ -479,12 +540,16 @@ public class BlockTransactionSelector { transactionSelectionResults.updateNotSelected(evaluationContext.getTransaction(), actualResult); notifyNotSelected(evaluationContext, actualResult); + LOG.atTrace() - .setMessage( - "Not selected {} for block creation with result {} (original result {}), evaluated in {}") + .setMessage("Not selected {} for block creation with result {}{}, evaluated in {}") .addArgument(pendingTransaction::toTraceLog) .addArgument(actualResult) - .addArgument(selectionResult) + .addArgument( + () -> + selectionResult.equals(actualResult) + ? "" + : " (original result " + selectionResult + ")") .addArgument(evaluationContext.getEvaluationTimer()) .log(); @@ -545,14 +610,6 @@ public class BlockTransactionSelector { return false; } - private TransactionSelectionResult handleTransactionNotSelected( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResult selectionResult, - final WorldUpdater txWorldStateUpdater) { - txWorldStateUpdater.revert(); - return handleTransactionNotSelected(evaluationContext, selectionResult); - } - private void notifySelected( final TransactionEvaluationContext evaluationContext, final TransactionProcessingResult processingResult) { diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java index 5977b2f7b..f9d55849f 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java @@ -48,27 +48,20 @@ public class TransactionSelectionResults { new ConcurrentHashMap<>(); private long cumulativeGasUsed = 0; - private long cumulativeBlobGasUsed = 0; void updateSelected( - final Transaction transaction, - final TransactionReceipt receipt, - final long gasUsed, - final long blobGasUsed) { + final Transaction transaction, final TransactionReceipt receipt, final long gasUsed) { selectedTransactions.add(transaction); transactionsByType .computeIfAbsent(transaction.getType(), type -> new ArrayList<>()) .add(transaction); receipts.add(receipt); cumulativeGasUsed += gasUsed; - cumulativeBlobGasUsed += blobGasUsed; LOG.atTrace() - .setMessage( - "New selected transaction {}, total transactions {}, cumulative gas used {}, cumulative blob gas used {}") + .setMessage("New selected transaction {}, total transactions {}, cumulative gas used {}") .addArgument(transaction::toTraceLog) .addArgument(selectedTransactions::size) .addArgument(cumulativeGasUsed) - .addArgument(cumulativeBlobGasUsed) .log(); } @@ -93,10 +86,6 @@ public class TransactionSelectionResults { return cumulativeGasUsed; } - public long getCumulativeBlobGasUsed() { - return cumulativeBlobGasUsed; - } - public Map getNotSelectedTransactions() { return Map.copyOf(notSelectedTransactions); } @@ -135,7 +124,6 @@ public class TransactionSelectionResults { } TransactionSelectionResults that = (TransactionSelectionResults) o; return cumulativeGasUsed == that.cumulativeGasUsed - && cumulativeBlobGasUsed == that.cumulativeBlobGasUsed && selectedTransactions.equals(that.selectedTransactions) && notSelectedTransactions.equals(that.notSelectedTransactions) && receipts.equals(that.receipts); @@ -143,19 +131,12 @@ public class TransactionSelectionResults { @Override public int hashCode() { - return Objects.hash( - selectedTransactions, - notSelectedTransactions, - receipts, - cumulativeGasUsed, - cumulativeBlobGasUsed); + return Objects.hash(selectedTransactions, notSelectedTransactions, receipts, cumulativeGasUsed); } public String toTraceLog() { return "cumulativeGasUsed=" + cumulativeGasUsed - + ", cumulativeBlobGasUsed=" - + cumulativeBlobGasUsed + ", selectedTransactions=" + selectedTransactions.stream() .map(Transaction::toTraceLog) diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractStatefulTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractStatefulTransactionSelector.java new file mode 100644 index 000000000..00d143dec --- /dev/null +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractStatefulTransactionSelector.java @@ -0,0 +1,57 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; + +import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; + +/** + * This class represents an abstract transaction selector which provides methods to access and set + * the selector working state. + * + * @param The type of state specified by the implementing selector, not to be confused with the + * world state. + */ +public abstract class AbstractStatefulTransactionSelector extends AbstractTransactionSelector { + private final SelectorsStateManager selectorsStateManager; + + public AbstractStatefulTransactionSelector( + final BlockSelectionContext context, + final SelectorsStateManager selectorsStateManager, + final S initialState, + final SelectorsStateManager.StateDuplicator stateDuplicator) { + super(context); + this.selectorsStateManager = selectorsStateManager; + selectorsStateManager.createSelectorState(this, initialState, stateDuplicator); + } + + /** + * Get the working state specific to this selector. + * + * @return the working state of the selector + */ + protected S getWorkingState() { + return selectorsStateManager.getSelectorWorkingState(this); + } + + /** + * Set the working state specific to this selector. + * + * @param newState the new state of the selector + */ + protected void setWorkingState(final S newState) { + selectorsStateManager.setSelectorWorkingState(this, newState); + } +} diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractTransactionSelector.java index 120a9c4f8..50ca099f8 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractTransactionSelector.java @@ -16,15 +16,15 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.TransactionSelector; /** * This class represents an abstract transaction selector which provides methods to evaluate * transactions. */ -public abstract class AbstractTransactionSelector { +public abstract class AbstractTransactionSelector implements TransactionSelector { protected final BlockSelectionContext context; public AbstractTransactionSelector(final BlockSelectionContext context) { @@ -35,25 +35,21 @@ public abstract class AbstractTransactionSelector { * Evaluates a transaction in the context of other transactions in the same block. * * @param evaluationContext The current selection session data. - * @param blockTransactionResults The results of other transaction evaluations in the same block. * @return The result of the transaction evaluation */ public abstract TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults); + final TransactionEvaluationContext evaluationContext); /** * Evaluates a transaction considering other transactions in the same block and a transaction * processing result. * * @param evaluationContext The current selection session data. - * @param blockTransactionResults The results of other transaction evaluations in the same block. * @param processingResult The result of transaction processing. * @return The result of the transaction evaluation */ public abstract TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult); /** diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobPriceTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobPriceTransactionSelector.java index 57d4e7dca..31433b1ab 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobPriceTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobPriceTransactionSelector.java @@ -16,7 +16,6 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; @@ -40,13 +39,11 @@ public class BlobPriceTransactionSelector extends AbstractTransactionSelector { * Evaluates a transaction considering its blob price. * * @param evaluationContext The current selection session data. - * @param ignored The results of other transaction evaluations in the same block. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults ignored) { + final TransactionEvaluationContext evaluationContext) { if (transactionBlobPriceBelowMin(evaluationContext.getTransaction())) { return TransactionSelectionResult.BLOB_PRICE_BELOW_CURRENT_MIN; } @@ -56,7 +53,6 @@ public class BlobPriceTransactionSelector extends AbstractTransactionSelector { @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { // All necessary checks were done in the pre-processing method, so nothing to do here. return TransactionSelectionResult.SELECTED; diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java index d5fd4212e..9a8dc44be 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java @@ -16,9 +16,9 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,11 +28,12 @@ import org.slf4j.LoggerFactory; * evaluating transactions based on blobs size. It checks if a transaction supports blobs, and if * so, checks that there is enough remaining blob gas in the block to fit the blobs of the tx. */ -public class BlobSizeTransactionSelector extends AbstractTransactionSelector { +public class BlobSizeTransactionSelector extends AbstractStatefulTransactionSelector { private static final Logger LOG = LoggerFactory.getLogger(BlobSizeTransactionSelector.class); - public BlobSizeTransactionSelector(final BlockSelectionContext context) { - super(context); + public BlobSizeTransactionSelector( + final BlockSelectionContext context, final SelectorsStateManager selectorsStateManager) { + super(context, selectorsStateManager, 0L, SelectorsStateManager.StateDuplicator::duplicateLong); } /** @@ -43,21 +44,19 @@ public class BlobSizeTransactionSelector extends AbstractTransactionSelector { * number of blobs or not. * * @param evaluationContext The current selection session data. - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults transactionSelectionResults) { + final TransactionEvaluationContext evaluationContext) { final var tx = evaluationContext.getTransaction(); if (tx.getType().supportsBlob()) { + final var cumulativeBlobGasUsed = getWorkingState(); + final var remainingBlobGas = - context.gasLimitCalculator().currentBlobGasLimit() - - transactionSelectionResults.getCumulativeBlobGasUsed(); + context.gasLimitCalculator().currentBlobGasLimit() - cumulativeBlobGasUsed; if (remainingBlobGas == 0) { LOG.atTrace() @@ -88,9 +87,11 @@ public class BlobSizeTransactionSelector extends AbstractTransactionSelector { @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { - // All necessary checks were done in the pre-processing method, so nothing to do here. + final var tx = evaluationContext.getTransaction(); + if (tx.getType().supportsBlob()) { + setWorkingState(getWorkingState() + context.gasCalculator().blobGasCost(tx.getBlobCount())); + } return TransactionSelectionResult.SELECTED; } } diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java index 0da2b1386..e57af5eb6 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java @@ -16,10 +16,10 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,11 +29,15 @@ import org.slf4j.LoggerFactory; * evaluating transactions based on block size. It checks if a transaction is too large for the * block and determines the selection result accordingly. */ -public class BlockSizeTransactionSelector extends AbstractTransactionSelector { +public class BlockSizeTransactionSelector extends AbstractStatefulTransactionSelector { private static final Logger LOG = LoggerFactory.getLogger(BlockSizeTransactionSelector.class); - public BlockSizeTransactionSelector(final BlockSelectionContext context) { - super(context); + private final long blockGasLimit; + + public BlockSizeTransactionSelector( + final BlockSelectionContext context, final SelectorsStateManager selectorsStateManager) { + super(context, selectorsStateManager, 0L, SelectorsStateManager.StateDuplicator::duplicateLong); + this.blockGasLimit = context.pendingBlockHeader().getGasLimit(); } /** @@ -41,25 +45,23 @@ public class BlockSizeTransactionSelector extends AbstractTransactionSelector { * too large for the block returns a selection result based on block occupancy. * * @param evaluationContext The current selection session data. - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults transactionSelectionResults) { + final TransactionEvaluationContext evaluationContext) { - if (transactionTooLargeForBlock( - evaluationContext.getTransaction(), transactionSelectionResults)) { + final long cumulativeGasUsed = getWorkingState(); + + if (transactionTooLargeForBlock(evaluationContext.getTransaction(), cumulativeGasUsed)) { LOG.atTrace() .setMessage("Transaction {} too large to select for block creation") .addArgument(evaluationContext.getPendingTransaction()::toTraceLog) .log(); - if (blockOccupancyAboveThreshold(transactionSelectionResults)) { + if (blockOccupancyAboveThreshold(cumulativeGasUsed)) { LOG.trace("Block occupancy above threshold, completing operation"); return TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD; - } else if (blockFull(transactionSelectionResults)) { + } else if (blockFull(cumulativeGasUsed)) { LOG.trace("Block full, completing operation"); return TransactionSelectionResult.BLOCK_FULL; } else { @@ -72,49 +74,42 @@ public class BlockSizeTransactionSelector extends AbstractTransactionSelector { @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { - // All necessary checks were done in the pre-processing method, so nothing to do here. + final long gasUsedByTransaction = + evaluationContext.getTransaction().getGasLimit() - processingResult.getGasRemaining(); + setWorkingState(getWorkingState() + gasUsedByTransaction); + return TransactionSelectionResult.SELECTED; } /** * Checks if the transaction is too large for the block. * - * @param transaction The transaction to be checked. - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. + * @param transaction The transaction to be checked. block. + * @param cumulativeGasUsed The cumulative gas used by previous txs. * @return True if the transaction is too large for the block, false otherwise. */ private boolean transactionTooLargeForBlock( - final Transaction transaction, - final TransactionSelectionResults transactionSelectionResults) { + final Transaction transaction, final long cumulativeGasUsed) { - return transaction.getGasLimit() - > context.pendingBlockHeader().getGasLimit() - - transactionSelectionResults.getCumulativeGasUsed(); + return transaction.getGasLimit() > blockGasLimit - cumulativeGasUsed; } /** * Checks if the block occupancy is above the threshold. * - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. + * @param cumulativeGasUsed The cumulative gas used by previous txs. * @return True if the block occupancy is above the threshold, false otherwise. */ - private boolean blockOccupancyAboveThreshold( - final TransactionSelectionResults transactionSelectionResults) { - final long gasAvailable = context.pendingBlockHeader().getGasLimit(); - - final long gasUsed = transactionSelectionResults.getCumulativeGasUsed(); - final long gasRemaining = gasAvailable - gasUsed; - final double occupancyRatio = (double) gasUsed / (double) gasAvailable; + private boolean blockOccupancyAboveThreshold(final long cumulativeGasUsed) { + final long gasRemaining = blockGasLimit - cumulativeGasUsed; + final double occupancyRatio = (double) cumulativeGasUsed / (double) blockGasLimit; LOG.trace( "Min block occupancy ratio {}, gas used {}, available {}, remaining {}, used/available {}", context.miningConfiguration().getMinBlockOccupancyRatio(), - gasUsed, - gasAvailable, + cumulativeGasUsed, + blockGasLimit, gasRemaining, occupancyRatio); @@ -124,15 +119,11 @@ public class BlockSizeTransactionSelector extends AbstractTransactionSelector { /** * Checks if the block is full. * - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. + * @param cumulativeGasUsed The cumulative gas used by previous txs. * @return True if the block is full, false otherwise. */ - private boolean blockFull(final TransactionSelectionResults transactionSelectionResults) { - final long gasAvailable = context.pendingBlockHeader().getGasLimit(); - final long gasUsed = transactionSelectionResults.getCumulativeGasUsed(); - - final long gasRemaining = gasAvailable - gasUsed; + private boolean blockFull(final long cumulativeGasUsed) { + final long gasRemaining = blockGasLimit - cumulativeGasUsed; if (gasRemaining < context.gasCalculator().getMinimumTransactionCost()) { LOG.trace( diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelector.java index 88f74e2d0..b3fbe31eb 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelector.java @@ -17,7 +17,6 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; @@ -37,15 +36,12 @@ public class MinPriorityFeePerGasTransactionSelector extends AbstractTransaction * Evaluates a transaction before processing. * * @param evaluationContext The current selection session data. - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. * @return TransactionSelectionResult. If the priority fee is below the minimum, it returns an * invalid transient result. Otherwise, it returns a selected result. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults transactionSelectionResults) { + final TransactionEvaluationContext evaluationContext) { if (isPriorityFeePriceBelowMinimum(evaluationContext)) { return TransactionSelectionResult.PRIORITY_FEE_PER_GAS_BELOW_CURRENT_MIN; } @@ -82,7 +78,6 @@ public class MinPriorityFeePerGasTransactionSelector extends AbstractTransaction @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { return TransactionSelectionResult.SELECTED; } diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/PriceTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/PriceTransactionSelector.java index 0ede0bfae..7f4cacbde 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/PriceTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/PriceTransactionSelector.java @@ -16,7 +16,6 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; @@ -40,13 +39,11 @@ public class PriceTransactionSelector extends AbstractTransactionSelector { * minimum, it returns a selection result indicating the reason. * * @param evaluationContext The current selection session data. - * @param ignored The results of other transaction evaluations in the same block. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults ignored) { + final TransactionEvaluationContext evaluationContext) { if (transactionCurrentPriceBelowMin(evaluationContext)) { return TransactionSelectionResult.CURRENT_TX_PRICE_BELOW_MIN; } @@ -56,7 +53,6 @@ public class PriceTransactionSelector extends AbstractTransactionSelector { @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { // All necessary checks were done in the pre-processing method, so nothing to do here. return TransactionSelectionResult.SELECTED; diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/ProcessingResultTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/ProcessingResultTransactionSelector.java index 120ad372a..5d2d5a7b9 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/ProcessingResultTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/ProcessingResultTransactionSelector.java @@ -16,7 +16,6 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; @@ -41,8 +40,7 @@ public class ProcessingResultTransactionSelector extends AbstractTransactionSele @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults) { + final TransactionEvaluationContext evaluationContext) { // All checks depend on processingResult and will be done in the post-processing method, so // nothing to do here. return TransactionSelectionResult.SELECTED; @@ -54,14 +52,12 @@ public class ProcessingResultTransactionSelector extends AbstractTransactionSele * result. * * @param evaluationContext The current selection session data. - * @param blockTransactionResults The results of other transaction evaluations in the same block. * @param processingResult The processing result of the transaction. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { if (processingResult.isInvalid()) { diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/SkipSenderTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/SkipSenderTransactionSelector.java index d29f045b7..a83627579 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/SkipSenderTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/SkipSenderTransactionSelector.java @@ -17,34 +17,41 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; -import java.util.HashSet; -import java.util.Set; +import java.util.HashMap; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SkipSenderTransactionSelector extends AbstractTransactionSelector { private static final Logger LOG = LoggerFactory.getLogger(SkipSenderTransactionSelector.class); - private final Set
skippedSenders = new HashSet<>(); + private final Map skippedSenderNonceMap = new HashMap<>(); public SkipSenderTransactionSelector(final BlockSelectionContext context) { super(context); } + /** + * Check if this pending tx belongs to a sender with a previous pending tx not selected, in which + * case we can safely skip the evaluation due to the nonce gap + * + * @param evaluationContext The current selection session data. + * @return SENDER_WITH_PREVIOUS_TX_NOT_SELECTED if there is a nonce gas for this sender + */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults ignored) { + final TransactionEvaluationContext evaluationContext) { final var sender = evaluationContext.getTransaction().getSender(); - if (skippedSenders.contains(sender)) { + final var skippedNonce = skippedSenderNonceMap.get(sender); + if (nonceGap(evaluationContext, skippedNonce)) { LOG.atTrace() - .setMessage("Not selecting tx {} since its sender {} is in the skip list") + .setMessage("Not selecting tx {} since its sender {} is in the skip list with nonce {}") .addArgument(() -> evaluationContext.getPendingTransaction().toTraceLog()) .addArgument(sender) + .addArgument(skippedNonce) .log(); return TransactionSelectionResult.SENDER_WITH_PREVIOUS_TX_NOT_SELECTED; @@ -52,10 +59,14 @@ public class SkipSenderTransactionSelector extends AbstractTransactionSelector { return TransactionSelectionResult.SELECTED; } + private static boolean nonceGap( + final TransactionEvaluationContext evaluationContext, final Long skippedNonce) { + return skippedNonce != null && evaluationContext.getTransaction().getNonce() > skippedNonce; + } + @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { // All necessary checks were done in the pre-processing method, so nothing to do here. return TransactionSelectionResult.SELECTED; @@ -66,7 +77,7 @@ public class SkipSenderTransactionSelector extends AbstractTransactionSelector { * same sender, since it will never be selected due to the nonce gap, so we add the sender to the * skip list. * - * @param evaluationContext The current selection context + * @param evaluationContext The current evaluation context * @param transactionSelectionResult The transaction selection result */ @Override @@ -74,7 +85,27 @@ public class SkipSenderTransactionSelector extends AbstractTransactionSelector { final TransactionEvaluationContext evaluationContext, final TransactionSelectionResult transactionSelectionResult) { final var sender = evaluationContext.getTransaction().getSender(); - skippedSenders.add(sender); - LOG.trace("Sender {} added to the skip list", sender); + final var nonce = evaluationContext.getTransaction().getNonce(); + skippedSenderNonceMap.put(sender, nonce); + LOG.trace("Sender {} added to the skip list with nonce {}", sender, nonce); + } + + /** + * When a transaction is selected we can remove it from the list. This could happen when the same + * pending tx is present both in the internal pool and the plugin pool, and for example it is not + * selected by the plugin but could be later selected from the internal pool. + * + * @param evaluationContext The current evaluation context + * @param processingResult The transaction processing result + */ + @Override + public void onTransactionSelected( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + final var sender = evaluationContext.getTransaction().getSender(); + final var skippedNonce = skippedSenderNonceMap.remove(sender); + if (skippedNonce != null) { + LOG.trace("Sender {} removed from the skip list, skipped nonce was {}", sender, skippedNonce); + } } } diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java index 242694fa9..e42b07dce 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java @@ -90,6 +90,7 @@ import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.TransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; import org.hyperledger.besu.services.TransactionSelectionServiceImpl; import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; @@ -648,33 +649,41 @@ public abstract class AbstractBlockTransactionSelectorTest { final Transaction notSelectedInvalid = createTransaction(2, Wei.of(10), 21_000, SENDER2); ensureTransactionIsValid(notSelectedInvalid, 21_000, 0); - final PluginTransactionSelectorFactory transactionSelectorFactory = - () -> - new PluginTransactionSelector() { - @Override - public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext) { - if (evaluationContext - .getPendingTransaction() - .getTransaction() - .equals(notSelectedTransient)) - return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID_TRANSIENT; - if (evaluationContext - .getPendingTransaction() - .getTransaction() - .equals(notSelectedInvalid)) - return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID; - return SELECTED; - } + final PluginTransactionSelector pluginTransactionSelector = + new PluginTransactionSelector() { + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + if (evaluationContext + .getPendingTransaction() + .getTransaction() + .equals(notSelectedTransient)) + return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID_TRANSIENT; + if (evaluationContext + .getPendingTransaction() + .getTransaction() + .equals(notSelectedInvalid)) + return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID; + return SELECTED; + } + + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final org.hyperledger.besu.plugin.data.TransactionProcessingResult processingResult) { + return SELECTED; + } + }; + + final PluginTransactionSelectorFactory transactionSelectorFactory = + new PluginTransactionSelectorFactory() { + @Override + public PluginTransactionSelector create( + final SelectorsStateManager selectorsStateManager) { + return pluginTransactionSelector; + } + }; - @Override - public TransactionSelectionResult evaluateTransactionPostProcessing( - final TransactionEvaluationContext evaluationContext, - final org.hyperledger.besu.plugin.data.TransactionProcessingResult - processingResult) { - return SELECTED; - } - }; transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -717,30 +726,35 @@ public abstract class AbstractBlockTransactionSelectorTest { final Transaction notSelected = createTransaction(1, Wei.of(10), 30_000); ensureTransactionIsValid(notSelected, maxGasUsedByTransaction + 1, 0); - final Transaction selected3 = createTransaction(3, Wei.of(10), 21_000); - ensureTransactionIsValid(selected3, maxGasUsedByTransaction, 0); + final PluginTransactionSelector pluginTransactionSelector = + new PluginTransactionSelector() { + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + return SELECTED; + } + + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final org.hyperledger.besu.plugin.data.TransactionProcessingResult processingResult) { + // the transaction with max gas +1 should fail + if (processingResult.getEstimateGasUsedByTransaction() > maxGasUsedByTransaction) { + return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID_TRANSIENT; + } + return SELECTED; + } + }; final PluginTransactionSelectorFactory transactionSelectorFactory = - () -> - new PluginTransactionSelector() { - @Override - public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext) { - return SELECTED; - } + new PluginTransactionSelectorFactory() { + @Override + public PluginTransactionSelector create( + final SelectorsStateManager selectorsStateManager) { + return pluginTransactionSelector; + } + }; - @Override - public TransactionSelectionResult evaluateTransactionPostProcessing( - final TransactionEvaluationContext evaluationContext, - final org.hyperledger.besu.plugin.data.TransactionProcessingResult - processingResult) { - // the transaction with max gas +1 should fail - if (processingResult.getEstimateGasUsedByTransaction() > maxGasUsedByTransaction) { - return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID_TRANSIENT; - } - return SELECTED; - } - }; transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -776,7 +790,7 @@ public abstract class AbstractBlockTransactionSelectorTest { PluginTransactionSelector transactionSelector = mock(PluginTransactionSelector.class); when(transactionSelector.evaluateTransactionPreProcessing(any())).thenReturn(SELECTED); when(transactionSelector.evaluateTransactionPostProcessing(any(), any())).thenReturn(SELECTED); - when(transactionSelectorFactory.create()).thenReturn(transactionSelector); + when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector); transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -1070,7 +1084,7 @@ public abstract class AbstractBlockTransactionSelectorTest { final PluginTransactionSelectorFactory transactionSelectorFactory = mock(PluginTransactionSelectorFactory.class); - when(transactionSelectorFactory.create()).thenReturn(transactionSelector); + when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector); transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -1233,7 +1247,7 @@ public abstract class AbstractBlockTransactionSelectorTest { final PluginTransactionSelectorFactory transactionSelectorFactory = mock(PluginTransactionSelectorFactory.class); - when(transactionSelectorFactory.create()).thenReturn(transactionSelector); + when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector); transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -1320,6 +1334,7 @@ public abstract class AbstractBlockTransactionSelectorTest { final Wei blobGasPrice, final TransactionSelectionService transactionSelectionService) { + final var selectorsStateManager = new SelectorsStateManager(); final BlockTransactionSelector selector = new BlockTransactionSelector( miningConfiguration, @@ -1336,8 +1351,9 @@ public abstract class AbstractBlockTransactionSelectorTest { new LondonGasCalculator(), GasLimitCalculator.constant(), protocolSchedule.getByBlockHeader(blockHeader).getBlockHashProcessor(), - transactionSelectionService.createPluginTransactionSelector(), - ethScheduler); + transactionSelectionService.createPluginTransactionSelector(selectorsStateManager), + ethScheduler, + selectorsStateManager); return selector; } diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java index 7d9292935..309343ac5 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java @@ -16,10 +16,11 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOBS_FULL; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS; import static org.mockito.Answers.RETURNS_DEEP_STUBS; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import org.hyperledger.besu.crypto.KeyPair; @@ -36,12 +37,13 @@ import org.hyperledger.besu.datatypes.VersionedHash; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionTestFixture; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.evm.gascalculator.CancunGasCalculator; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import java.util.Optional; import java.util.stream.IntStream; @@ -50,6 +52,7 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes48; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -68,86 +71,103 @@ class BlobSizeTransactionSelectorTest { @Mock(answer = RETURNS_DEEP_STUBS) BlockSelectionContext blockSelectionContext; - @Mock TransactionSelectionResults selectionResults; + @Mock TransactionProcessingResult transactionProcessingResult; + + SelectorsStateManager selectorsStateManager; + BlobSizeTransactionSelector selector; + + @BeforeEach + void setup() { + when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS); + when(blockSelectionContext.gasCalculator().blobGasCost(anyLong())) + .thenAnswer(iom -> BLOB_GAS_PER_BLOB * iom.getArgument(0, Long.class)); + + selectorsStateManager = new SelectorsStateManager(); + selector = new BlobSizeTransactionSelector(blockSelectionContext, selectorsStateManager); + } @Test - void notBlobTransactionsAreSelectedWithoutAnyCheck() { - final var selector = new BlobSizeTransactionSelector(blockSelectionContext); - - final var nonBlobTx = createEIP1559PendingTransaction(); + void notBlobTransactionsAreAlwaysSelected() { + // this tx fills all the available blob space + final var firstBlobTx = createBlobPendingTransaction(MAX_BLOBS); final var txEvaluationContext = new TransactionEvaluationContext( - blockSelectionContext.pendingBlockHeader(), nonBlobTx, null, null, null); + blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext); - final var result = - selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults); - assertThat(result).isEqualTo(TransactionSelectionResult.SELECTED); - verifyNoInteractions(selectionResults); + // this non blob tx is selected regardless the blob space is already filled + final var nonBlobTx = createEIP1559PendingTransaction(); + + final var nonBlobTxEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), nonBlobTx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(nonBlobTxEvaluationContext); } @Test void firstBlobTransactionIsSelected() { - when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS); - when(blockSelectionContext.gasCalculator().blobGasCost(anyLong())) - .thenAnswer(iom -> BLOB_GAS_PER_BLOB * iom.getArgument(0, Long.class)); - - final var selector = new BlobSizeTransactionSelector(blockSelectionContext); - final var firstBlobTx = createBlobPendingTransaction(MAX_BLOBS); final var txEvaluationContext = new TransactionEvaluationContext( blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null); - - when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(0L); - - final var result = - selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults); - assertThat(result).isEqualTo(TransactionSelectionResult.SELECTED); - verify(selectionResults).getCumulativeBlobGasUsed(); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext); } @Test void returnsBlobsFullWhenMaxNumberOfBlobsAlreadyPresent() { - when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS); - - final var selector = new BlobSizeTransactionSelector(blockSelectionContext); - - final var firstBlobTx = createBlobPendingTransaction(1); - - final var txEvaluationContext = + final var blobTx1 = createBlobPendingTransaction(MAX_BLOBS); + final var txEvaluationContext1 = new TransactionEvaluationContext( - blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null); + blockSelectionContext.pendingBlockHeader(), blobTx1, null, null, null); - when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(MAX_BLOB_GAS); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext1); - final var result = - selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults); - assertThat(result).isEqualTo(TransactionSelectionResult.BLOBS_FULL); - verify(selectionResults).getCumulativeBlobGasUsed(); + final var blobTx2 = createBlobPendingTransaction(1); + final var txEvaluationContext2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), blobTx2, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertNotSelected(txEvaluationContext2, BLOBS_FULL); } @Test void returnsTooLargeForRemainingBlobGas() { - when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS); - when(blockSelectionContext.gasCalculator().blobGasCost(anyLong())) - .thenAnswer(iom -> BLOB_GAS_PER_BLOB * iom.getArgument(0, Long.class)); - - final var selector = new BlobSizeTransactionSelector(blockSelectionContext); - - final var firstBlobTx = createBlobPendingTransaction(MAX_BLOBS); - - final var txEvaluationContext = + // first tx only fill the space for one blob leaving space max MAX_BLOB_GAS-1 blobs to be added + // later + final var blobTx1 = createBlobPendingTransaction(1); + final var txEvaluationContext1 = new TransactionEvaluationContext( - blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null); + blockSelectionContext.pendingBlockHeader(), blobTx1, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext1); - when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(MAX_BLOB_GAS - 1); + final var blobTx2 = createBlobPendingTransaction(MAX_BLOBS); + final var txEvaluationContext2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), blobTx2, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertNotSelected(txEvaluationContext2, TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS); + } - final var result = - selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults); - assertThat(result).isEqualTo(TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS); - verify(selectionResults).getCumulativeBlobGasUsed(); + private void evaluateAndAssertSelected(final TransactionEvaluationContext txEvaluationContext) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)).isEqualTo(SELECTED); + assertThat( + selector.evaluateTransactionPostProcessing( + txEvaluationContext, transactionProcessingResult)) + .isEqualTo(SELECTED); + } + + private void evaluateAndAssertNotSelected( + final TransactionEvaluationContext txEvaluationContext, + final TransactionSelectionResult preProcessedResult) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)) + .isEqualTo(preProcessedResult); } private PendingTransaction createEIP1559PendingTransaction() { diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java new file mode 100644 index 000000000..8463f5e04 --- /dev/null +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java @@ -0,0 +1,272 @@ +/* + * 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.blockcreation.txselection.selectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOCK_FULL; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_GAS; +import static org.mockito.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; +import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; + +import java.util.Optional; +import java.util.stream.IntStream; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class BlockSizeTransactionSelectorTest { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final KeyPair KEYS = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final long TRANSFER_GAS_LIMIT = 21_000L; + private static final long BLOCK_GAS_LIMIT = 1_000_000L; + + @Mock(answer = RETURNS_DEEP_STUBS) + BlockSelectionContext blockSelectionContext; + + SelectorsStateManager selectorsStateManager; + BlockSizeTransactionSelector selector; + MiningConfiguration miningConfiguration; + + @BeforeEach + void setup() { + miningConfiguration = MiningConfiguration.newDefault(); + when(blockSelectionContext.pendingBlockHeader().getGasLimit()).thenReturn(BLOCK_GAS_LIMIT); + when(blockSelectionContext.miningConfiguration()).thenReturn(miningConfiguration); + + selectorsStateManager = new SelectorsStateManager(); + selector = new BlockSizeTransactionSelector(blockSelectionContext, selectorsStateManager); + } + + @Test + void singleTransactionBelowBlockGasLimitIsSelected() { + final var tx = createPendingTransaction(TRANSFER_GAS_LIMIT); + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext, remainingGas(0)); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT); + } + + @Test + void singleTransactionAboveBlockGasLimitIsNotSelected() { + final var tx = createPendingTransaction(BLOCK_GAS_LIMIT + 1); + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertNotSelected(txEvaluationContext, TX_TOO_LARGE_FOR_REMAINING_GAS); + + assertThat(selector.getWorkingState()).isEqualTo(0); + } + + @Test + void correctlyCumulatesOnlyTheEffectiveGasUsedAfterProcessing() { + final var tx = createPendingTransaction(TRANSFER_GAS_LIMIT * 2); + final long remainingGas = 100; + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext, remainingGas(remainingGas)); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * 2 - remainingGas); + } + + @Test + void moreTransactionsBelowBlockGasLimitAreSelected() { + selectorsStateManager.blockSelectionStarted(); + + final int txCount = 10; + + IntStream.range(0, txCount) + .forEach( + unused -> { + final var tx = createPendingTransaction(TRANSFER_GAS_LIMIT); + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + evaluateAndAssertSelected(txEvaluationContext, remainingGas(0)); + }); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); + } + + @Test + void moreTransactionsThanBlockCanFitOnlySomeAreSelected() { + selectorsStateManager.blockSelectionStarted(); + + final int txCount = 10; + + IntStream.range(0, txCount) + .forEach( + unused -> { + final var tx = createPendingTransaction(TRANSFER_GAS_LIMIT); + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + evaluateAndAssertSelected(txEvaluationContext, remainingGas(0)); + }); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); + + // last tx is too big for the remaining gas + final long tooBigGasLimit = BLOCK_GAS_LIMIT - (TRANSFER_GAS_LIMIT * txCount) + 1; + + final var bigTxEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), + createPendingTransaction(tooBigGasLimit), + null, + null, + null); + evaluateAndAssertNotSelected(bigTxEvaluationContext, TX_TOO_LARGE_FOR_REMAINING_GAS); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); + } + + @Test + void identifyWhenBlockOccupancyIsAboveThreshold() { + selectorsStateManager.blockSelectionStarted(); + + // create 2 txs with a gas limit just above the min block occupancy ratio + // so the first is accepted while the second not + final long justAboveOccupancyRatioGasLimit = + (long) (BLOCK_GAS_LIMIT * miningConfiguration.getMinBlockOccupancyRatio()) + 100; + final var tx1 = createPendingTransaction(justAboveOccupancyRatioGasLimit); + + final var txEvaluationContext1 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx1, null, null, null); + evaluateAndAssertSelected(txEvaluationContext1, remainingGas(0)); + + assertThat(selector.getWorkingState()).isEqualTo(justAboveOccupancyRatioGasLimit); + + final var tx2 = createPendingTransaction(justAboveOccupancyRatioGasLimit); + + final var txEvaluationContext2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx2, null, null, null); + evaluateAndAssertNotSelected(txEvaluationContext2, BLOCK_OCCUPANCY_ABOVE_THRESHOLD); + + assertThat(selector.getWorkingState()).isEqualTo(justAboveOccupancyRatioGasLimit); + } + + @Test + void identifyWhenBlockIsFull() { + when(blockSelectionContext.gasCalculator().getMinimumTransactionCost()) + .thenReturn(TRANSFER_GAS_LIMIT); + + selectorsStateManager.blockSelectionStarted(); + + // allow to completely fill the block + miningConfiguration.setMinBlockOccupancyRatio(1.0); + + // create 2 txs, where the first fill the block leaving less gas than the min required by a + // transfer + final long fillBlockGasLimit = BLOCK_GAS_LIMIT - TRANSFER_GAS_LIMIT + 1; + final var tx1 = createPendingTransaction(fillBlockGasLimit); + + final var txEvaluationContext1 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx1, null, null, null); + evaluateAndAssertSelected(txEvaluationContext1, remainingGas(0)); + + assertThat(selector.getWorkingState()).isEqualTo(fillBlockGasLimit); + + final var tx2 = createPendingTransaction(TRANSFER_GAS_LIMIT); + + final var txEvaluationContext2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx2, null, null, null); + evaluateAndAssertNotSelected(txEvaluationContext2, BLOCK_FULL); + + assertThat(selector.getWorkingState()).isEqualTo(fillBlockGasLimit); + } + + private void evaluateAndAssertSelected( + final TransactionEvaluationContext txEvaluationContext, + final TransactionProcessingResult transactionProcessingResult) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)).isEqualTo(SELECTED); + assertThat( + selector.evaluateTransactionPostProcessing( + txEvaluationContext, transactionProcessingResult)) + .isEqualTo(SELECTED); + } + + private void evaluateAndAssertNotSelected( + final TransactionEvaluationContext txEvaluationContext, + final TransactionSelectionResult preProcessedResult) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)) + .isEqualTo(preProcessedResult); + } + + private PendingTransaction createPendingTransaction(final long gasLimit) { + return PendingTransaction.newPendingTransaction( + createTransaction(TransactionType.EIP1559, gasLimit), false, false); + } + + private Transaction createTransaction(final TransactionType type, final long gasLimit) { + + var tx = + new TransactionTestFixture() + .to(Optional.of(Address.fromHexString("0x634316eA0EE79c701c6F67C53A4C54cBAfd2316d"))) + .nonce(0) + .gasLimit(gasLimit) + .type(type) + .maxFeePerGas(Optional.of(Wei.of(1000))) + .maxPriorityFeePerGas(Optional.of(Wei.of(100))); + + return tx.createTransaction(KEYS); + } + + private TransactionProcessingResult remainingGas(final long remainingGas) { + final var txProcessingResult = mock(TransactionProcessingResult.class); + when(txProcessingResult.getGasRemaining()).thenReturn(remainingGas); + return txProcessingResult; + } +} diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/MinPriorityFeePerGasTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelectorTest.java similarity index 92% rename from ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/MinPriorityFeePerGasTransactionSelectorTest.java rename to ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelectorTest.java index 29cea9d08..73d158a13 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/MinPriorityFeePerGasTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelectorTest.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.ethereum.blockcreation; +package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -22,8 +22,6 @@ import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.AbstractTransactionSelector; -import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.MinPriorityFeePerGasTransactionSelector; import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; import org.hyperledger.besu.ethereum.core.Transaction; @@ -85,8 +83,7 @@ public class MinPriorityFeePerGasTransactionSelectorTest { private void assertSelectionResult( final TransactionEvaluationContext evaluationContext, final TransactionSelectionResult expectedResult) { - var actualResult = - transactionSelector.evaluateTransactionPreProcessing(evaluationContext, null); + var actualResult = transactionSelector.evaluateTransactionPreProcessing(evaluationContext); assertThat(actualResult).isEqualTo(expectedResult); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java index 55ffd5ba7..4c543d224 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java @@ -16,9 +16,12 @@ package org.hyperledger.besu.ethereum.core; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.services.TransactionSelectionService; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import org.hyperledger.besu.util.number.PositiveNumber; import java.time.Duration; @@ -168,10 +171,16 @@ public abstract class MiningConfiguration { public TransactionSelectionService getTransactionSelectionService() { return new TransactionSelectionService() { @Override - public PluginTransactionSelector createPluginTransactionSelector() { + public PluginTransactionSelector createPluginTransactionSelector( + final SelectorsStateManager selectorsStateManager) { return PluginTransactionSelector.ACCEPT_ALL; } + @Override + public void selectPendingTransactions( + final BlockTransactionSelectionService selectionService, + final ProcessableBlockHeader pendingBlockHeader) {} + @Override public void registerPluginTransactionSelectorFactory( final PluginTransactionSelectorFactory transactionSelectorFactory) {} 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/AbstractDetermineCommonAncestorTaskParameterizedTest.java similarity index 90% rename from ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest.java rename to ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/AbstractDetermineCommonAncestorTaskParameterizedTest.java index 138106c5b..dbfa06e88 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/AbstractDetermineCommonAncestorTaskParameterizedTest.java @@ -1,5 +1,5 @@ /* - * Copyright ConsenSys AG. + * 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 @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryBlockchain; import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryWorldStateArchive; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import org.hyperledger.besu.ethereum.ConsensusContext; import org.hyperledger.besu.ethereum.ProtocolContext; @@ -49,31 +50,28 @@ import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.MetricsSystem; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; -public class DetermineCommonAncestorTaskParameterizedTest { +public abstract class AbstractDetermineCommonAncestorTaskParameterizedTest { private final ProtocolSchedule protocolSchedule = ProtocolScheduleFixture.MAINNET; private static final BlockDataGenerator blockDataGenerator = new BlockDataGenerator(); private final MetricsSystem metricsSystem = new NoOpMetricsSystem(); private static Block genesisBlock; private static MutableBlockchain localBlockchain; - private static final int chainHeight = 50; + protected static final int chainHeight = 50; private MutableBlockchain remoteBlockchain; private PeerTaskExecutor peerTaskExecutor; @@ -98,19 +96,7 @@ public class DetermineCommonAncestorTaskParameterizedTest { @BeforeEach public void setup() { remoteBlockchain = createInMemoryBlockchain(genesisBlock); - peerTaskExecutor = Mockito.mock(PeerTaskExecutor.class); - } - - public static Stream parameters() throws IOException { - final int[] requestSizes = {5, 12, chainHeight, chainHeight * 2}; - final Stream.Builder builder = Stream.builder(); - for (final int requestSize : requestSizes) { - for (int i = 0; i <= chainHeight; i++) { - builder.add(Arguments.of(requestSize, i, true)); - builder.add(Arguments.of(requestSize, i, false)); - } - } - return builder.build(); + peerTaskExecutor = mock(PeerTaskExecutor.class); } @ParameterizedTest(name = "requestSize={0}, commonAncestor={1}, isPeerTaskSystemEnabled={2}") @@ -187,10 +173,8 @@ public class DetermineCommonAncestorTaskParameterizedTest { .build(), metricsSystem); - Mockito.when( - peerTaskExecutor.executeAgainstPeer( - Mockito.any(GetHeadersFromPeerTask.class), - Mockito.eq(respondingEthPeer.getEthPeer()))) + when(peerTaskExecutor.executeAgainstPeer( + Mockito.any(GetHeadersFromPeerTask.class), Mockito.eq(respondingEthPeer.getEthPeer()))) .thenAnswer( (invocationOnMock) -> { GetHeadersFromPeerTask getHeadersTask = @@ -205,7 +189,7 @@ public class DetermineCommonAncestorTaskParameterizedTest { headers.add(remoteBlockchain.getBlockHeader(i).get()); } - return new PeerTaskExecutorResult>( + return new PeerTaskExecutorResult<>( Optional.of(headers), PeerTaskExecutorResponseCode.SUCCESS, Optional.of(respondingEthPeer.getEthPeer())); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest1.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest1.java new file mode 100644 index 000000000..bc8a0b209 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest1.java @@ -0,0 +1,34 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.eth.sync.tasks; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.provider.Arguments; + +public class DetermineCommonAncestorTaskParameterizedTest1 + extends AbstractDetermineCommonAncestorTaskParameterizedTest { + + public static Stream parameters() { + final int[] requestSizes = {5, 12, chainHeight, chainHeight * 2}; + final Stream.Builder builder = Stream.builder(); + for (final int requestSize : requestSizes) { + for (int i = 0; i <= chainHeight; i++) { + builder.add(Arguments.of(requestSize, i, true)); + } + } + return builder.build(); + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest2.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest2.java new file mode 100644 index 000000000..38328c7f2 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest2.java @@ -0,0 +1,34 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.eth.sync.tasks; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.provider.Arguments; + +public class DetermineCommonAncestorTaskParameterizedTest2 + extends AbstractDetermineCommonAncestorTaskParameterizedTest { + + public static Stream parameters() { + final int[] requestSizes = {5, 12, chainHeight, chainHeight * 2}; + final Stream.Builder builder = Stream.builder(); + for (final int requestSize : requestSizes) { + for (int i = 0; i <= chainHeight; i++) { + builder.add(Arguments.of(requestSize, i, false)); + } + } + return builder.build(); + } +} diff --git a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/badcode.json b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/badcode.json index dd6616d7a..2852b94ff 100644 --- a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/badcode.json +++ b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/badcode.json @@ -9,6 +9,6 @@ "stdout": [ {"pc":0,"op":96,"gas":"0x2540be400","gasCost":"0x3","memSize":0,"stack":[],"depth":1,"refund":0,"opName":"PUSH1"}, {"pc":2,"op":239,"gas":"0x2540be3fd","gasCost":"0x0","memSize":0,"stack":["0x0"],"depth":1,"refund":0,"opName":"INVALID","error":"Invalid opcode: 0xef"}, - {"stateRoot":"0xfc9dc1be50c1b0a497afa545d770cc7064f0d71efbc4338f002dc2e086965d98","output":"0x","gasUsed":"0x2540be400","pass":false,"fork":"Cancun"} + {"stateRoot":"0xfc9dc1be50c1b0a497afa545d770cc7064f0d71efbc4338f002dc2e086965d98","output":"0x","gasUsed":"0x2540be400","pass":false,"fork":"Prague"} ] } \ No newline at end of file diff --git a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/charge-intrinsic.json b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/charge-intrinsic.json index 9355c4728..1022ab2f7 100644 --- a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/charge-intrinsic.json +++ b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/charge-intrinsic.json @@ -70,6 +70,6 @@ {"pc":24,"op":243,"gas":"0x45","gasCost":"0x0","memSize":96,"stack":["0x1","0x40"],"depth":2,"refund":0,"opName":"RETURN"}, {"pc":22,"op":96,"gas":"0x71","gasCost":"0x3","memSize":96,"stack":["0x1"],"depth":1,"refund":0,"opName":"PUSH1"}, {"pc":24,"op":243,"gas":"0x6e","gasCost":"0x0","memSize":96,"stack":["0x1","0x40"],"depth":1,"refund":0,"opName":"RETURN"}, - {"stateRoot":"0xcb5e8e232189003640b6f131ea2c09b1791ffd2e8357f64610f638e9a11ab2d2","output":"0x40","gasUsed":"0x619e","pass":true,"fork":"Cancun"} + {"stateRoot":"0xcb5e8e232189003640b6f131ea2c09b1791ffd2e8357f64610f638e9a11ab2d2","output":"0x40","gasUsed":"0x619e","pass":true,"fork":"Prague"} ] } \ No newline at end of file diff --git a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/revert.json b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/revert.json index 32ee526fd..0fd1ec7bf 100644 --- a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/revert.json +++ b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/revert.json @@ -13,6 +13,6 @@ {"pc":8,"op":96,"gas":"0x2540be3f4","gasCost":"0x3","memSize":32,"stack":[],"depth":1,"refund":0,"opName":"PUSH1"}, {"pc":10,"op":96,"gas":"0x2540be3f1","gasCost":"0x3","memSize":32,"stack":["0x4"],"depth":1,"refund":0,"opName":"PUSH1"}, {"pc":12,"op":253,"gas":"0x2540be3ee","gasCost":"0x0","memSize":32,"stack":["0x4","0x1c"],"depth":1,"refund":0,"opName":"REVERT","error":"0x4e6f7065"}, - {"stateRoot":"0x405bbd98da2aca6dff77f79e0b270270c48d6a3e07b76db675b20e454b50bbcb","output":"0x4e6f7065","gasUsed":"0x12","pass":true,"fork":"Cancun"} + {"stateRoot":"0x405bbd98da2aca6dff77f79e0b270270c48d6a3e07b76db675b20e454b50bbcb","output":"0x4e6f7065","gasUsed":"0x12","pass":true,"fork":"Prague"} ] } \ No newline at end of file diff --git a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/warm-contract.json b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/warm-contract.json index 1c2a9c806..f13a90b20 100644 --- a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/warm-contract.json +++ b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/trace/warm-contract.json @@ -67,6 +67,6 @@ {"pc":24,"op":243,"gas":"0x45","gasCost":"0x0","memSize":96,"stack":["0x1","0x40"],"depth":2,"refund":0,"opName":"RETURN"}, {"pc":22,"op":96,"gas":"0x71","gasCost":"0x3","memSize":96,"stack":["0x1"],"depth":1,"refund":0,"opName":"PUSH1"}, {"pc":24,"op":243,"gas":"0x6e","gasCost":"0x0","memSize":96,"stack":["0x1","0x40"],"depth":1,"refund":0,"opName":"RETURN"}, - {"stateRoot":"0xcb5e8e232189003640b6f131ea2c09b1791ffd2e8357f64610f638e9a11ab2d2","output":"0x40","gasUsed":"0x619e","pass":true,"fork":"Cancun"} + {"stateRoot":"0xcb5e8e232189003640b6f131ea2c09b1791ffd2e8357f64610f638e9a11ab2d2","output":"0x40","gasUsed":"0x619e","pass":true,"fork":"Prague"} ] } \ No newline at end of file diff --git a/evm/src/test/java/org/hyperledger/besu/evm/fluent/EVMExecutorTest.java b/evm/src/test/java/org/hyperledger/besu/evm/fluent/EVMExecutorTest.java index 20426670b..0a0bdba33 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/fluent/EVMExecutorTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/fluent/EVMExecutorTest.java @@ -49,7 +49,7 @@ class EVMExecutorTest { @Test void currentEVM() { var subject = EVMExecutor.evm(); - assertThat(subject.getEVMVersion()).isEqualTo(EvmSpecVersion.CANCUN); + assertThat(subject.getEVMVersion()).isEqualTo(EvmSpecVersion.PRAGUE); } @ParameterizedTest diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6cd992d18..5b13593ad 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1903,6 +1903,14 @@ + + + + + + + + @@ -1913,6 +1921,11 @@ + + + + + @@ -1926,6 +1939,14 @@ + + + + + + + + @@ -1934,6 +1955,14 @@ + + + + + + + + @@ -1942,6 +1971,14 @@ + + + + + + + + @@ -1950,6 +1987,14 @@ + + + + + + + + @@ -1963,6 +2008,14 @@ + + + + + + + + @@ -1976,6 +2029,14 @@ + + + + + + + + @@ -1984,6 +2045,14 @@ + + + + + + + + @@ -1992,6 +2061,14 @@ + + + + + + + + @@ -2000,6 +2077,14 @@ + + + + + + + + @@ -2008,6 +2093,14 @@ + + + + + + + + @@ -2016,6 +2109,14 @@ + + + + + + + + @@ -2024,6 +2125,14 @@ + + + + + + + + @@ -2032,6 +2141,14 @@ + + + + + + + + @@ -2045,6 +2162,14 @@ + + + + + + + + @@ -2058,6 +2183,14 @@ + + + + + + + + @@ -2071,6 +2204,14 @@ + + + + + + + + @@ -2079,6 +2220,14 @@ + + + + + + + + @@ -2089,6 +2238,11 @@ + + + + + @@ -2102,6 +2256,14 @@ + + + + + + + + @@ -2115,6 +2277,14 @@ + + + + + + + + @@ -2123,6 +2293,14 @@ + + + + + + + + @@ -2134,6 +2312,17 @@ + + + + + + + + + + + @@ -2147,6 +2336,14 @@ + + + + + + + + @@ -2155,11 +2352,24 @@ + + + + + + + + + + + + + @@ -2173,6 +2383,14 @@ + + + + + + + + @@ -2181,6 +2399,14 @@ + + + + + + + + @@ -2189,6 +2415,14 @@ + + + + + + + + @@ -2211,6 +2445,23 @@ + + + + + + + + + + + + + + + + + @@ -2230,6 +2481,20 @@ + + + + + + + + + + + + + + @@ -2243,6 +2508,14 @@ + + + + + + + + @@ -2251,6 +2524,14 @@ + + + + + + + + @@ -2259,6 +2540,14 @@ + + + + + + + + @@ -2267,6 +2556,14 @@ + + + + + + + + @@ -6721,15 +7018,15 @@ - - - + + + - - + + - - + + diff --git a/platform/build.gradle b/platform/build.gradle index 3be52256c..cd42a832c 100644 --- a/platform/build.gradle +++ b/platform/build.gradle @@ -28,7 +28,7 @@ javaPlatform { dependencies { api platform('com.fasterxml.jackson:jackson-bom:2.18.0') api platform('io.grpc:grpc-bom:1.68.0') - api platform('io.netty:netty-bom:4.1.115.Final') + api platform('io.netty:netty-bom:4.1.118.Final') api platform('io.opentelemetry:opentelemetry-bom:1.43.0') api platform('io.prometheus:prometheus-metrics-bom:1.3.4') api platform('io.vertx:vertx-stack-depchain:4.5.10') @@ -96,6 +96,8 @@ dependencies { api 'info.picocli:picocli-codegen:4.7.6' api 'io.kubernetes:client-java:21.0.1-legacy' + // Temporarily locking in to avoid https://github.com/netplex/json-smart-v2/issues/240 + api 'net.minidev:json-smart:2.4.2' api 'io.opentelemetry.instrumentation:opentelemetry-okhttp-3.0:2.9.0-alpha' api 'io.opentelemetry.proto:opentelemetry-proto:1.3.2-alpha' @@ -178,7 +180,7 @@ dependencies { api 'tech.pegasys:jc-kzg-4844:1.0.0' - api 'tech.pegasys.discovery:discovery:24.9.1' + api 'tech.pegasys.discovery:discovery:25.2.0' } } diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index 040a7b128..b5c731228 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -71,7 +71,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'I2IrN2aLU610031Vw8xNr3hcT8/wb25pDrclwZUggE4=' + knownHash = 'D2ZMRGb2HS/9FgDE2mcizdxTQhFsGD4LS9lgngG/TnU=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java index daa07930d..21effcc5c 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java @@ -15,8 +15,11 @@ package org.hyperledger.besu.plugin.services; import org.hyperledger.besu.plugin.Unstable; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; /** Transaction selection service interface */ @Unstable @@ -25,9 +28,23 @@ public interface TransactionSelectionService extends BesuService { /** * Create a transaction selector plugin * + * @param selectorsStateManager the selectors state manager * @return the transaction selector plugin */ - PluginTransactionSelector createPluginTransactionSelector(); + PluginTransactionSelector createPluginTransactionSelector( + SelectorsStateManager selectorsStateManager); + + /** + * Called during the block creation to allow plugins to propose their own pending transactions for + * block inclusion + * + * @param blockTransactionSelectionService the service used by the plugin to evaluate pending + * transactions and commit or rollback changes + * @param pendingBlockHeader the header of the block being created + */ + void selectPendingTransactions( + BlockTransactionSelectionService blockTransactionSelectionService, + final ProcessableBlockHeader pendingBlockHeader); /** * Registers the transaction selector factory with the service diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/AbstractStatefulPluginTransactionSelector.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/AbstractStatefulPluginTransactionSelector.java new file mode 100644 index 000000000..9e8f6137b --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/AbstractStatefulPluginTransactionSelector.java @@ -0,0 +1,71 @@ +/* + * 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.plugin.services.txselection; + +/** + * This class represents an abstract plugin transaction selector which provides methods to manage + * the selector state. + * + * @param The type of the state used by the selector + */ +public abstract class AbstractStatefulPluginTransactionSelector + implements PluginTransactionSelector { + private final SelectorsStateManager selectorsStateManager; + + /** + * Initialize the plugin state to an initial value + * + * @param selectorsStateManager the selectors state manager + * @param initialState the initial value of the state + * @param stateDuplicator the function that duplicates the state + */ + public AbstractStatefulPluginTransactionSelector( + final SelectorsStateManager selectorsStateManager, + final S initialState, + final SelectorsStateManager.StateDuplicator stateDuplicator) { + this.selectorsStateManager = selectorsStateManager; + selectorsStateManager.createSelectorState(this, initialState, stateDuplicator); + } + + /** + * Get the working state for this selector. A working state contains changes that have not yet + * committed + * + * @return the working state of this selector + */ + protected S getWorkingState() { + return selectorsStateManager.getSelectorWorkingState(this); + } + + /** + * Set the working state for this selector. A working state contains changes that have not yet + * commited + * + * @param newState the new working state of this selector + */ + protected void setWorkingState(final S newState) { + selectorsStateManager.setSelectorWorkingState(this, newState); + } + + /** + * Get the commited state for this selector. A commited state contains changes that have been + * commited + * + * @return the commited state of this selector + */ + protected S getCommitedState() { + return selectorsStateManager.getSelectorCommittedState(this); + } +} diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/BlockTransactionSelectionService.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/BlockTransactionSelectionService.java new file mode 100644 index 000000000..a4ecf9e7f --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/BlockTransactionSelectionService.java @@ -0,0 +1,57 @@ +/* + * 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.plugin.services.txselection; + +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; + +/** + * A service that can be used by plugins to include their pending transactions during block + * creation. Proposed pending transactions need to be first evaluated and based on the result of the + * evaluation of one or more pending transactions the plugin can apply its logic to commit or not + * the inclusion of the evaluated in the block. + * + *

The process of including plugin proposed pending transactions starts when {@link + * PluginTransactionSelectorFactory#selectPendingTransactions(BlockTransactionSelectionService, + * ProcessableBlockHeader)} is called. + */ +public interface BlockTransactionSelectionService { + + /** + * Evaluate a plugin proposed pending transaction for block inclusion + * + * @param pendingTransaction the pending transaction proposed by the plugin + * @return the result of the evaluation + */ + TransactionSelectionResult evaluatePendingTransaction(PendingTransaction pendingTransaction); + + /** + * Commit the changes applied by all the evaluated pending transactions since the previous commit. + * As part of this {@link PluginTransactionSelector} state of commited. + * + * @return false only if a timeout occurred during the selection of the pending transaction, + * meaning that the pending transaction is not included in the current block + */ + boolean commit(); + + /** + * Rollback the changes applied by all the evaluated pending transactions since the previous + * commit. + * + *

As part of this {@link PluginTransactionSelector} state of rolled back. + */ + void rollback(); +} diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelector.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelector.java index 71503591d..9c1e5ceab 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelector.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelector.java @@ -23,7 +23,7 @@ import org.hyperledger.besu.plugin.services.tracer.BlockAwareOperationTracer; /** Interface for the transaction selector */ @Unstable -public interface PluginTransactionSelector { +public interface PluginTransactionSelector extends TransactionSelector { /** Plugin transaction selector that unconditionally select every transaction */ PluginTransactionSelector ACCEPT_ALL = new PluginTransactionSelector() { diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java index e0b7acc58..1cdec48c0 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java @@ -15,15 +15,39 @@ package org.hyperledger.besu.plugin.services.txselection; import org.hyperledger.besu.plugin.Unstable; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; -/** Interface for a factory that creates transaction selectors */ +/** + * Interface for a factory that creates transaction selector and propose pending transaction for + * block inclusion + */ @Unstable public interface PluginTransactionSelectorFactory { + /** + * A default factory that does not propose any pending transactions and does not apply any filter + */ + PluginTransactionSelectorFactory NO_OP_FACTORY = new PluginTransactionSelectorFactory() {}; /** - * Create a transaction selector + * Create a plugin transaction selector, that can be used during block creation to apply custom + * filters to proposed pending transactions * + * @param selectorsStateManager the selectors state manager * @return the transaction selector */ - PluginTransactionSelector create(); + default PluginTransactionSelector create(final SelectorsStateManager selectorsStateManager) { + return PluginTransactionSelector.ACCEPT_ALL; + } + + /** + * Called during the block creation to allow plugins to propose their own pending transactions for + * block inclusion + * + * @param blockTransactionSelectionService the service used by the plugin to evaluate pending + * transactions and commit or rollback changes + * @param pendingBlockHeader the header of the block being created + */ + default void selectPendingTransactions( + final BlockTransactionSelectionService blockTransactionSelectionService, + final ProcessableBlockHeader pendingBlockHeader) {} } diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/SelectorsStateManager.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/SelectorsStateManager.java new file mode 100644 index 000000000..b85bab7dc --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/SelectorsStateManager.java @@ -0,0 +1,222 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.plugin.services.txselection; + +import static com.google.common.base.Preconditions.checkState; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +/** + * Manages the state of transaction selectors (including the plugin transaction selector {@link + * PluginTransactionSelector}) during the block creation process. Some selectors have a state, for + * example the amount of gas used by selected pending transactions so far, and changes made to the + * state must be only commited after the evaluated pending transaction has been definitely selected + * for inclusion, until that point it will be always possible to rollback the changes to the state + * and return the previous commited state. + */ +@SuppressWarnings("rawtypes") +public class SelectorsStateManager { + private final List>>> + uncommitedStates = new ArrayList<>(); + private Map>> committedState = + new HashMap<>(); + private volatile boolean blockSelectionStarted = false; + + /** Create an empty selectors state manager, here to make javadoc linter happy. */ + public SelectorsStateManager() {} + + /** + * Create, initialize and track the state for a selector. + * + *

Call to this method must be performed before the block selection is stated with {@link + * #blockSelectionStarted()}, otherwise it fails. + * + * @param selector the selector + * @param initialState the initial value of the state + * @param duplicator the state duplicator + * @param the type of the selector state + * @param the type of the state duplicator + */ + public > void createSelectorState( + final TransactionSelector selector, final S initialState, final D duplicator) { + checkState( + !blockSelectionStarted, "Cannot create selector state after block selection is started"); + committedState.put(selector, new SelectorState<>(initialState, duplicator)); + } + + /** + * Called at the start of block selection, when the initialization is done, to prepare a new + * working state based on the initial state. + * + *

After this method is called, it is not possible to call anymore {@link + * #createSelectorState(TransactionSelector, Object, StateDuplicator)} + */ + public void blockSelectionStarted() { + blockSelectionStarted = true; + uncommitedStates.add(duplicateLastState()); + } + + private Map>> + duplicateLastState() { + return getLast().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().duplicated())); + } + + /** + * Get the working state for the specified selector + * + * @param selector the selector + * @return the working state of the selector + * @param the type of the selector state + */ + @SuppressWarnings("unchecked") + public T getSelectorWorkingState(final TransactionSelector selector) { + return (T) uncommitedStates.getLast().get(selector).get(); + } + + /** + * set the working state for the specified selector + * + * @param selector the selector + * @param newState the new state + * @param the type of the selector state + */ + @SuppressWarnings("unchecked") + public void setSelectorWorkingState(final TransactionSelector selector, final T newState) { + ((SelectorState>) uncommitedStates.getLast().get(selector)).set(newState); + } + + /** + * Get the commited state for the specified selector + * + * @param selector the selector + * @return the commited state of the selector + * @param the type of the selector state + */ + @SuppressWarnings("unchecked") + public T getSelectorCommittedState(final TransactionSelector selector) { + return (T) committedState.get(selector).get(); + } + + /** + * Commit the current working state and prepare a new working state based on the just commited + * state + */ + public void commit() { + committedState = getLast(); + uncommitedStates.clear(); + uncommitedStates.add(duplicateLastState()); + } + + /** + * Discards the current working state and prepare a new working state based on the just commited + * state + */ + public void rollback() { + uncommitedStates.clear(); + uncommitedStates.add(duplicateLastState()); + } + + private Map>> getLast() { + if (uncommitedStates.isEmpty()) { + return committedState; + } + return uncommitedStates.getLast(); + } + + /** + * A function that create a duplicate of the input object. The duplication must be a deep copy. + * + * @param the type of the object + */ + @FunctionalInterface + public interface StateDuplicator extends UnaryOperator { + /** + * Duplicate the input objet + * + * @param t the input object to duplicate + * @return a deep copy of the input object + */ + T duplicate(T t); + + @Override + default T apply(final T t) { + return duplicate(t); + } + + /** + * Utility to duplicate a long + * + * @param l a long + * @return a copy of the long + */ + static long duplicateLong(final long l) { + return l; + } + } + + /** + * A selector state object is one that is able to return, update and duplicate the state it + * contains + * + * @param the type of the state + */ + private static class SelectorState> { + private final D duplicator; + private S state; + + /** + * Create a selector state with the initial value + * + * @param initialState the initial initialState + */ + public SelectorState(final S initialState, final D duplicator) { + this.state = initialState; + this.duplicator = duplicator; + } + + /** + * The method that concrete classes must implement to create a deep copy of the state + * + * @return a new selector state with a deep copy of the state + */ + private SelectorState duplicated() { + return new SelectorState<>(duplicator.duplicate(state), duplicator); + } + + /** + * Get the current state + * + * @return the current state + */ + public S get() { + return state; + } + + /** + * Replace the current state with the new one + * + * @param newState the new state + */ + public void set(final S newState) { + this.state = newState; + } + } +} diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/TransactionSelector.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/TransactionSelector.java new file mode 100644 index 000000000..fa9628f91 --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/TransactionSelector.java @@ -0,0 +1,21 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.plugin.services.txselection; + +import org.hyperledger.besu.plugin.Unstable; + +/** Interface for the transaction selector */ +@Unstable +public interface TransactionSelector {} diff --git a/util/src/main/java/org/hyperledger/besu/util/log4j/plugin/StackTraceMatchFilter.java b/util/src/main/java/org/hyperledger/besu/util/log4j/plugin/StackTraceMatchFilter.java new file mode 100644 index 000000000..b280589a8 --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/log4j/plugin/StackTraceMatchFilter.java @@ -0,0 +1,120 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.log4j.plugin; + +import java.util.Arrays; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.filter.AbstractFilter; +import org.apache.logging.log4j.message.Message; + +/** Matches a text in the stack trace */ +@Plugin( + name = "StackTraceMatchFilter", + category = "Core", + elementType = "filter", + printObject = true) +public class StackTraceMatchFilter extends AbstractFilter { + private final String text; + + private StackTraceMatchFilter(final String text, final Result onMatch, final Result onMismatch) { + super(onMatch, onMismatch); + this.text = text; + } + + @Override + public Result filter( + final Logger logger, + final Level level, + final Marker marker, + final Object msg, + final Throwable t) { + return filter(t); + } + + @Override + public Result filter( + final Logger logger, + final Level level, + final Marker marker, + final Message msg, + final Throwable t) { + return filter(t); + } + + @Override + public Result filter(final LogEvent event) { + return filter(event.getThrown()); + } + + private Result filter(final Throwable t) { + if (t != null) { + return Arrays.stream(t.getStackTrace()) + .map(StackTraceElement::getClassName) + .anyMatch(cn -> cn.contains(text)) + ? onMatch + : onMismatch; + } + return Result.NEUTRAL; + } + + @Override + public String toString() { + return text; + } + + /** + * Create a new builder + * + * @return a new builder + */ + @PluginBuilderFactory + public static StackTraceMatchFilter.Builder newBuilder() { + return new StackTraceMatchFilter.Builder(); + } + + /** Builder for StackTraceMatchFilter */ + public static class Builder extends AbstractFilterBuilder + implements org.apache.logging.log4j.core.util.Builder { + @PluginBuilderAttribute private String text = ""; + + /** Default constructor */ + public Builder() { + // here to make javadoc happy + } + + /** + * Set the string to match in the stack trace + * + * @param text the match string + * @return this builder + */ + public StackTraceMatchFilter.Builder setMatchString(final String text) { + this.text = text; + return this; + } + + @Override + public StackTraceMatchFilter build() { + return new StackTraceMatchFilter(this.text, this.getOnMatch(), this.getOnMismatch()); + } + } +}