From b7aa552de83f224fff5292c33d23765863c86d3d Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Tue, 21 Oct 2025 11:40:36 +0200 Subject: [PATCH 1/2] added GenesisConfigService to BesuCommand, added preprocessing step for Geth like genesis files Signed-off-by: daniellehrner --- .../org/hyperledger/besu/cli/BesuCommand.java | 152 ++---- .../besu/cli/CommandTestAbstract.java | 6 +- .../besu/cli/options/MiningOptionsTest.java | 2 +- .../besu/config/GenesisConfigService.java | 326 +++++++++++++ .../besu/config/GenesisConfigServiceTest.java | 457 ++++++++++++++++++ 5 files changed, 838 insertions(+), 105 deletions(-) create mode 100644 config/src/main/java/org/hyperledger/besu/config/GenesisConfigService.java create mode 100644 config/src/test/java/org/hyperledger/besu/config/GenesisConfigServiceTest.java diff --git a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index abf13153d87..2646c3f8d47 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -88,8 +88,7 @@ import org.hyperledger.besu.cli.util.VersionProvider; import org.hyperledger.besu.components.BesuComponent; import org.hyperledger.besu.config.CheckpointConfigOptions; -import org.hyperledger.besu.config.GenesisConfig; -import org.hyperledger.besu.config.GenesisConfigOptions; +import org.hyperledger.besu.config.GenesisConfigService; import org.hyperledger.besu.config.MergeConfiguration; import org.hyperledger.besu.consensus.merge.blockcreation.MergeCoordinator; import org.hyperledger.besu.controller.BesuController; @@ -216,7 +215,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.math.BigInteger; -import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.GroupPrincipal; @@ -233,7 +231,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.OptionalInt; import java.util.Set; import java.util.TreeMap; import java.util.function.BiFunction; @@ -343,10 +340,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { new PreSynchronizationTaskRunner(); private final Set allocatedPorts = new HashSet<>(); - private final Supplier genesisConfigSupplier = - Suppliers.memoize(this::readGenesisConfig); - private final Supplier genesisConfigOptionsSupplier = - Suppliers.memoize(this::readGenesisConfigOptions); + private GenesisConfigService genesisConfigService; private final Supplier miningParametersSupplier = Suppliers.memoize(this::getMiningParameters); private final Supplier apiConfigurationSupplier = @@ -907,6 +901,8 @@ public void run() { System.exit(0); // Exit before any services are started } + genesisConfigService = createGenesisConfigService(); + // set merge config on the basis of genesis config setMergeConfigOptions(); @@ -1406,17 +1402,7 @@ void configureNativeLibs(final Optional configuredNetwork) { } } - if (genesisConfigOptionsSupplier.get().getCancunTime().isPresent() - || genesisConfigOptionsSupplier.get().getCancunEOFTime().isPresent() - || genesisConfigOptionsSupplier.get().getPragueTime().isPresent() - || genesisConfigOptionsSupplier.get().getOsakaTime().isPresent() - || genesisConfigOptionsSupplier.get().getBpo1Time().isPresent() - || genesisConfigOptionsSupplier.get().getBpo2Time().isPresent() - || genesisConfigOptionsSupplier.get().getBpo3Time().isPresent() - || genesisConfigOptionsSupplier.get().getBpo4Time().isPresent() - || genesisConfigOptionsSupplier.get().getBpo5Time().isPresent() - || genesisConfigOptionsSupplier.get().getAmsterdamTime().isPresent() - || genesisConfigOptionsSupplier.get().getFutureEipsTime().isPresent()) { + if (genesisConfigService.hasKzgFork()) { if (kzgTrustedSetupFile != null) { KZGPointEvalPrecompiledContract.init(kzgTrustedSetupFile); } else { @@ -1487,7 +1473,7 @@ private void validateApiOptions() { } private void validateTransactionPoolOptions() { - transactionPoolOptions.validate(commandLine, genesisConfigOptionsSupplier.get()); + transactionPoolOptions.validate(commandLine, genesisConfigService.getGenesisConfigOptions()); } private void validateDataStorageOptions() { @@ -1496,7 +1482,7 @@ private void validateDataStorageOptions() { private void validateMiningParams() { miningOptions.validate( - commandLine, genesisConfigOptionsSupplier.get(), isMergeEnabled(), logger); + commandLine, genesisConfigService.getGenesisConfigOptions(), isMergeEnabled(), logger); } private void validateP2POptions() { @@ -1584,58 +1570,30 @@ private void validateChainDataPruningParams() { Long chainDataPruningBlocksRetained = unstableChainPruningOptions.getChainDataPruningBlocksRetained(); if (unstableChainPruningOptions.getChainDataPruningEnabled()) { - final GenesisConfigOptions genesisConfigOptions = readGenesisConfigOptions(); if (chainDataPruningBlocksRetained < unstableChainPruningOptions.getChainDataPruningBlocksRetainedLimit()) { throw new ParameterException( this.commandLine, "--Xchain-pruning-blocks-retained must be >= " + unstableChainPruningOptions.getChainDataPruningBlocksRetainedLimit()); - } else if (genesisConfigOptions.isPoa()) { - long epochLength = 0L; - String consensusMechanism = ""; - if (genesisConfigOptions.isIbft2()) { - epochLength = genesisConfigOptions.getBftConfigOptions().getEpochLength(); - consensusMechanism = "IBFT2"; - } else if (genesisConfigOptions.isQbft()) { - epochLength = genesisConfigOptions.getQbftConfigOptions().getEpochLength(); - consensusMechanism = "QBFT"; - } else if (genesisConfigOptions.isClique()) { - epochLength = genesisConfigOptions.getCliqueConfigOptions().getEpochLength(); - consensusMechanism = "Clique"; - } - if (chainDataPruningBlocksRetained < epochLength) { - throw new ParameterException( - this.commandLine, - String.format( - "--Xchain-pruning-blocks-retained(%d) must be >= epochlength(%d) for %s", - chainDataPruningBlocksRetained, epochLength, consensusMechanism)); + } else if (genesisConfigService.isPoaConsensus()) { + final var epochLengthOpt = genesisConfigService.getPoaEpochLength(); + if (epochLengthOpt.isPresent()) { + final long epochLength = epochLengthOpt.getAsLong(); + if (chainDataPruningBlocksRetained < epochLength) { + throw new ParameterException( + this.commandLine, + String.format( + "--Xchain-pruning-blocks-retained(%d) must be >= epochlength(%d) for %s", + chainDataPruningBlocksRetained, + epochLength, + genesisConfigService.getConsensusMechanism())); + } } } } } - private GenesisConfig readGenesisConfig() { - GenesisConfig effectiveGenesisFile; - effectiveGenesisFile = - network.equals(EPHEMERY) - ? EphemeryGenesisUpdater.updateGenesis(genesisConfigOverrides) - : genesisFile != null - ? GenesisConfig.fromSource(genesisConfigSource(genesisFile)) - : GenesisConfig.fromResource( - Optional.ofNullable(network).orElse(MAINNET).getGenesisFile()); - return effectiveGenesisFile.withOverrides(genesisConfigOverrides); - } - - private GenesisConfigOptions readGenesisConfigOptions() { - try { - return genesisConfigSupplier.get().getConfigOptions(); - } catch (final Exception e) { - throw new ParameterException( - this.commandLine, "Unable to load genesis file. " + e.getCause()); - } - } - private void issueOptionWarnings() { // Check that P2P options are able to work @@ -1981,7 +1939,7 @@ private TransactionPoolConfiguration buildTransactionPoolConfiguration() { .from(txPoolConf) .saveFile((dataPath.resolve(txPoolConf.getSaveFile().getPath()).toFile())); - if (genesisConfigOptionsSupplier.get().isZeroBaseFee()) { + if (genesisConfigService.getGenesisConfigOptions().isZeroBaseFee()) { logger.warn( "Forcing price bump for transaction replacement to 0, since we are on a zero basefee network"); txPoolConfBuilder.priceBump(Percentage.ZERO); @@ -2020,8 +1978,7 @@ private TransactionPoolConfiguration buildTransactionPoolConfiguration() { private MiningConfiguration getMiningParameters() { miningOptions.setTransactionSelectionService(transactionSelectionServiceImpl); final var miningParameters = miningOptions.toDomainObject(); - getGenesisBlockPeriodSeconds(genesisConfigOptionsSupplier.get()) - .ifPresent(miningParameters::setBlockPeriodSeconds); + genesisConfigService.getBlockPeriodSeconds().ifPresent(miningParameters::setBlockPeriodSeconds); initMiningParametersMetrics(miningParameters); return miningParameters; @@ -2088,23 +2045,6 @@ private void initMiningParametersMetrics(final MiningConfiguration miningConfigu new MiningParametersMetrics(getMetricsSystem(), miningConfiguration); } - private OptionalInt getGenesisBlockPeriodSeconds( - final GenesisConfigOptions genesisConfigOptions) { - if (genesisConfigOptions.isClique()) { - return OptionalInt.of(genesisConfigOptions.getCliqueConfigOptions().getBlockPeriodSeconds()); - } - - if (genesisConfigOptions.isIbft2()) { - return OptionalInt.of(genesisConfigOptions.getBftConfigOptions().getBlockPeriodSeconds()); - } - - if (genesisConfigOptions.isQbft()) { - return OptionalInt.of(genesisConfigOptions.getQbftConfigOptions().getBlockPeriodSeconds()); - } - - return OptionalInt.empty(); - } - // Blockchain synchronization from peers. private Runner synchronize( final BesuController controller, @@ -2225,8 +2165,8 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { // If no chain id is found in the genesis, use mainnet network id try { builder.setNetworkId( - genesisConfigOptionsSupplier - .get() + genesisConfigService + .getGenesisConfigOptions() .getChainId() .orElse(EthNetworkConfig.getNetworkConfig(MAINNET).networkId())); } catch (final DecodeException e) { @@ -2246,7 +2186,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { builder.setDnsDiscoveryUrl(null); } - builder.setGenesisConfig(genesisConfigSupplier.get()); + builder.setGenesisConfig(genesisConfigService.getGenesisConfig()); if (networkId != null) { builder.setNetworkId(networkId); @@ -2260,7 +2200,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { builder.setDnsDiscoveryUrl(p2PDiscoveryOptions.discoveryDnsUrl); } else { final Optional discoveryDnsUrlFromGenesis = - genesisConfigOptionsSupplier.get().getDiscoveryOptions().getDiscoveryDnsUrl(); + genesisConfigService.getGenesisConfigOptions().getDiscoveryOptions().getDiscoveryDnsUrl(); discoveryDnsUrlFromGenesis.ifPresent(builder::setDnsDiscoveryUrl); } @@ -2279,7 +2219,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { } } else { final Optional> bootNodesFromGenesis = - genesisConfigOptionsSupplier.get().getDiscoveryOptions().getBootNodes(); + genesisConfigService.getGenesisConfigOptions().getDiscoveryOptions().getBootNodes(); if (bootNodesFromGenesis.isPresent()) { listBootNodes = buildEnodes(bootNodesFromGenesis.get(), getEnodeDnsConfiguration()); } @@ -2294,12 +2234,22 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { return builder.build(); } - private URL genesisConfigSource(final File genesisFile) { - try { - return genesisFile.toURI().toURL(); - } catch (final IOException e) { - throw new ParameterException( - this.commandLine, String.format("Unable to load genesis URL %s.", genesisFile), e); + /** + * Creates a GenesisConfigService instance based on the current configuration. This service + * handles loading genesis files and automatically detects and transforms Geth-format genesis + * files to Besu format. + * + * @return a new GenesisConfigService instance + */ + private GenesisConfigService createGenesisConfigService() { + if (network.equals(EPHEMERY)) { + final var config = EphemeryGenesisUpdater.updateGenesis(genesisConfigOverrides); + return GenesisConfigService.fromGenesisConfig(config); + } else if (genesisFile != null) { + return GenesisConfigService.fromFile(genesisFile, genesisConfigOverrides); + } else { + return GenesisConfigService.fromResource( + Optional.ofNullable(network).orElse(MAINNET).getGenesisFile(), genesisConfigOverrides); } } @@ -2511,21 +2461,21 @@ private Optional getEcCurveFromGenesisFile() { if (genesisFile == null) { return Optional.empty(); } - return genesisConfigOptionsSupplier.get().getEcCurve(); + return genesisConfigService.getEcCurve(); } /** - * Return the genesis config options + * Return the genesis config service * - * @return the genesis config options + * @return the genesis config service */ - protected GenesisConfigOptions getGenesisConfigOptions() { - return genesisConfigOptionsSupplier.get(); + protected GenesisConfigService getGenesisConfigService() { + return genesisConfigService; } private void setMergeConfigOptions() { MergeConfiguration.setMergeEnabled( - genesisConfigOptionsSupplier.get().getTerminalTotalDifficulty().isPresent()); + genesisConfigService.getGenesisConfigOptions().getTerminalTotalDifficulty().isPresent()); } /** Set ignorable segments in RocksDB Storage Provider plugin. */ @@ -2540,9 +2490,9 @@ private void validatePostMergeCheckpointBlockRequirements() { final SynchronizerConfiguration synchronizerConfiguration = unstableSynchronizerOptions.toDomainObject().build(); final Optional terminalTotalDifficulty = - genesisConfigOptionsSupplier.get().getTerminalTotalDifficulty(); + genesisConfigService.getGenesisConfigOptions().getTerminalTotalDifficulty(); final CheckpointConfigOptions checkpointConfigOptions = - genesisConfigOptionsSupplier.get().getCheckpointOptions(); + genesisConfigService.getGenesisConfigOptions().getCheckpointOptions(); if (synchronizerConfiguration.isCheckpointPostMergeEnabled()) { if (!checkpointConfigOptions.isValid()) { throw new InvalidConfigurationException( diff --git a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index aafc1155150..b3c23f9cac6 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -46,7 +46,7 @@ import org.hyperledger.besu.cli.options.TransactionPoolOptions; import org.hyperledger.besu.cli.options.storage.DataStorageOptions; import org.hyperledger.besu.components.BesuComponent; -import org.hyperledger.besu.config.GenesisConfigOptions; +import org.hyperledger.besu.config.GenesisConfigService; import org.hyperledger.besu.controller.BesuController; import org.hyperledger.besu.controller.BesuControllerBuilder; import org.hyperledger.besu.controller.NoopPluginServiceFactory; @@ -599,8 +599,8 @@ protected Vertx createVertx(final VertxOptions vertxOptions) { } @Override - public GenesisConfigOptions getGenesisConfigOptions() { - return super.getGenesisConfigOptions(); + public GenesisConfigService getGenesisConfigService() { + return super.getGenesisConfigService(); } public CommandSpec getSpec() { diff --git a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java index 9d940c2fde3..bf6d8d04732 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java @@ -491,7 +491,7 @@ protected String[] getNonOptionFields() { private MiningConfiguration runtimeConfiguration( final TestBesuCommand besuCommand, final MiningConfiguration miningConfiguration) { - if (besuCommand.getGenesisConfigOptions().isPoa()) { + if (besuCommand.getGenesisConfigService().isPoaConsensus()) { miningConfiguration.setBlockPeriodSeconds(POA_BLOCK_PERIOD_SECONDS); } return miningConfiguration; diff --git a/config/src/main/java/org/hyperledger/besu/config/GenesisConfigService.java b/config/src/main/java/org/hyperledger/besu/config/GenesisConfigService.java new file mode 100644 index 00000000000..0edfd15cbee --- /dev/null +++ b/config/src/main/java/org/hyperledger/besu/config/GenesisConfigService.java @@ -0,0 +1,326 @@ +/* + * 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.config; + +import java.io.File; +import java.net.URL; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Service for loading and transforming genesis configuration files. This service provides automatic + * detection and transformation of Geth-format genesis files to Besu format, enabling seamless + * compatibility between the two Ethereum client formats. + * + *

The service handles five main transformations for Geth genesis files: + * + *

    + *
  1. Adding the {@code ethash} field to the config (required for Besu's consensus detection) + *
  2. Mapping {@code mergeNetsplitBlock} to {@code preMergeForkBlock} (different field names for + * the same purpose) + *
  3. Adding {@code baseFeePerGas} when London fork is activated at genesis (block 0) + *
  4. Adding {@code withdrawalRequestContractAddress} if missing (EIP-7002) + *
  5. Adding {@code consolidationRequestContractAddress} if missing (EIP-7251) + *
+ */ +public class GenesisConfigService { + + private final GenesisConfig genesisConfig; + private final GenesisConfigOptions genesisConfigOptions; + + /** + * Private constructor. Use factory methods to create instances. + * + * @param genesisConfig the genesis configuration + */ + private GenesisConfigService(final GenesisConfig genesisConfig) { + this.genesisConfig = genesisConfig; + this.genesisConfigOptions = genesisConfig.getConfigOptions(); + } + + /** + * Create a GenesisConfigService from a file, with automatic Geth format detection and + * transformation. + * + * @param genesisFile the genesis file + * @param overrides configuration overrides to apply + * @return a new GenesisConfigService instance + */ + public static GenesisConfigService fromFile( + final File genesisFile, final Map overrides) { + try { + final URL url = genesisFile.toURI().toURL(); + final GenesisConfig config = loadAndTransform(url, overrides); + return new GenesisConfigService(config); + } catch (final Exception e) { + // Extract the root cause for better error reporting + Throwable rootCause = e; + while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { + rootCause = rootCause.getCause(); + } + throw new RuntimeException("Unable to load genesis file: " + genesisFile, rootCause); + } + } + + /** + * Create a GenesisConfigService from a resource name (no transformation needed for built-in + * networks). + * + * @param resourceName the resource name + * @param overrides configuration overrides to apply + * @return a new GenesisConfigService instance + */ + public static GenesisConfigService fromResource( + final String resourceName, final Map overrides) { + final GenesisConfig config = GenesisConfig.fromResource(resourceName).withOverrides(overrides); + return new GenesisConfigService(config); + } + + /** + * Create a GenesisConfigService from an existing GenesisConfig. + * + * @param genesisConfig the genesis configuration + * @return a new GenesisConfigService instance + */ + public static GenesisConfigService fromGenesisConfig(final GenesisConfig genesisConfig) { + return new GenesisConfigService(genesisConfig); + } + + /** + * Gets the genesis configuration. + * + * @return the genesis config + */ + public GenesisConfig getGenesisConfig() { + return genesisConfig; + } + + /** + * Gets the genesis configuration options. + * + * @return the genesis config options + */ + public GenesisConfigOptions getGenesisConfigOptions() { + return genesisConfigOptions; + } + + /** + * Gets the EC curve from the genesis configuration if specified. + * + * @return the EC curve, or empty if not specified + */ + public Optional getEcCurve() { + return genesisConfigOptions.getEcCurve(); + } + + /** + * Gets the block period in seconds based on the consensus mechanism. + * + * @return the block period in seconds, or empty if not applicable + */ + public OptionalInt getBlockPeriodSeconds() { + if (genesisConfigOptions.isClique()) { + return OptionalInt.of(genesisConfigOptions.getCliqueConfigOptions().getBlockPeriodSeconds()); + } + if (genesisConfigOptions.isIbft2()) { + return OptionalInt.of(genesisConfigOptions.getBftConfigOptions().getBlockPeriodSeconds()); + } + if (genesisConfigOptions.isQbft()) { + return OptionalInt.of(genesisConfigOptions.getQbftConfigOptions().getBlockPeriodSeconds()); + } + return OptionalInt.empty(); + } + + /** + * Checks if the genesis configuration includes any fork times that require KZG initialization. + * This includes Cancun and all subsequent forks that use KZG commitments for EIP-4844 blob + * transactions. + * + * @return true if any KZG-requiring fork time is present + */ + public boolean hasKzgFork() { + return genesisConfigOptions.getCancunTime().isPresent() + || genesisConfigOptions.getCancunEOFTime().isPresent() + || genesisConfigOptions.getPragueTime().isPresent() + || genesisConfigOptions.getOsakaTime().isPresent() + || genesisConfigOptions.getBpo1Time().isPresent() + || genesisConfigOptions.getBpo2Time().isPresent() + || genesisConfigOptions.getBpo3Time().isPresent() + || genesisConfigOptions.getBpo4Time().isPresent() + || genesisConfigOptions.getBpo5Time().isPresent() + || genesisConfigOptions.getAmsterdamTime().isPresent() + || genesisConfigOptions.getFutureEipsTime().isPresent(); + } + + /** + * Checks if the genesis configuration uses a Proof of Authority (PoA) consensus mechanism. + * + * @return true if the consensus is PoA (IBFT2, QBFT, or Clique) + */ + public boolean isPoaConsensus() { + return genesisConfigOptions.isPoa(); + } + + /** + * Gets the epoch length for PoA consensus mechanisms. + * + * @return epoch length if PoA consensus is configured, empty otherwise + */ + public OptionalLong getPoaEpochLength() { + if (genesisConfigOptions.isIbft2()) { + return OptionalLong.of(genesisConfigOptions.getBftConfigOptions().getEpochLength()); + } else if (genesisConfigOptions.isQbft()) { + return OptionalLong.of(genesisConfigOptions.getQbftConfigOptions().getEpochLength()); + } else if (genesisConfigOptions.isClique()) { + return OptionalLong.of(genesisConfigOptions.getCliqueConfigOptions().getEpochLength()); + } + return OptionalLong.empty(); + } + + /** + * Gets the name of the consensus mechanism configured in the genesis. + * + * @return the consensus mechanism name (e.g., "IBFT2", "QBFT", "Clique", "Ethash") + */ + public String getConsensusMechanism() { + if (genesisConfigOptions.isIbft2()) { + return "IBFT2"; + } else if (genesisConfigOptions.isQbft()) { + return "QBFT"; + } else if (genesisConfigOptions.isClique()) { + return "Clique"; + } else if (genesisConfigOptions.isEthHash()) { + return "Ethash"; + } + return "Unknown"; + } + + /** + * Loads a genesis file from URL and applies Geth-to-Besu transformation if needed. + * + * @param url the URL to load from + * @param overrides configuration overrides to apply + * @return the loaded and potentially transformed GenesisConfig + */ + private static GenesisConfig loadAndTransform( + final URL url, final Map overrides) { + // Load the raw JSON + final ObjectNode genesisRoot = JsonUtil.objectNodeFromURL(url, false); + + // Check if this is a Geth format genesis file + if (isGethFormat(genesisRoot)) { + // Transform it to Besu format + transformGethToBesu(genesisRoot); + } + + // Create GenesisConfig from the (potentially transformed) JSON + final GenesisConfig config = GenesisConfig.fromConfig(genesisRoot); + return config.withOverrides(overrides); + } + + /** + * Detects if a genesis file is in Geth format. + * + *

A genesis file is considered Geth format if: + * + *

    + *
  • It has a "config" section + *
  • The config has a "mergeNetsplitBlock" field (Geth-specific) + *
  • The config does NOT have an "ethash" field (Besu-specific) + *
+ * + * @param genesisRoot the root genesis JSON node + * @return true if this is a Geth format genesis file + */ + private static boolean isGethFormat(final ObjectNode genesisRoot) { + final Optional configNode = JsonUtil.getObjectNode(genesisRoot, "config"); + if (!configNode.isPresent()) { + return false; + } + + final ObjectNode config = configNode.get(); + final boolean hasMergeNetsplitBlock = config.has("mergeNetsplitBlock"); + final boolean hasEthash = config.has("ethash"); + + // It's Geth format if it has mergeNetsplitBlock but not ethash + return hasMergeNetsplitBlock && !hasEthash; + } + + /** + * Transforms a Geth-format genesis file to Besu format by applying five transformations. + * + *

Transformations applied: + * + *

    + *
  1. Add ethash field: Besu's {@code isEthHash()} method checks for the presence of + * this field in the JSON structure. Since this is a structural check, the overrides + * mechanism doesn't work - we must add it to the JSON. + *
  2. Map mergeNetsplitBlock to preMergeForkBlock: These fields serve identical purposes + * (marking the merge activation block) but use different names in Geth vs Besu. + *
  3. Add baseFeePerGas: When London fork is activated at genesis (block 0), Besu + * expects an explicit base fee. Geth may omit this field, so we add the standard default of + * 1 gwei (0x3B9ACA00). + *
  4. Add withdrawalRequestContractAddress: EIP-7002 withdrawal request contract address + * if missing. + *
  5. Add consolidationRequestContractAddress: EIP-7251 consolidation request contract + * address if missing. + *
+ * + * @param genesisRoot the root genesis JSON node (will be modified in place) + */ + private static void transformGethToBesu(final ObjectNode genesisRoot) { + final Optional configNode = JsonUtil.getObjectNode(genesisRoot, "config"); + if (!configNode.isPresent()) { + return; + } + + final ObjectNode config = configNode.get(); + + // Add ethash field if not present + if (!config.has("ethash")) { + config.set("ethash", JsonUtil.createEmptyObjectNode()); + } + + // Map mergeNetsplitBlock to preMergeForkBlock + if (config.has("mergeNetsplitBlock") && !config.has("preMergeForkBlock")) { + final long mergeBlock = config.get("mergeNetsplitBlock").asLong(); + config.put("preMergeForkBlock", mergeBlock); + } + + // Add baseFeePerGas if London is at genesis + if (!genesisRoot.has("baseFeePerGas") && config.has("londonBlock")) { + final long londonBlock = config.get("londonBlock").asLong(Long.MAX_VALUE); + if (londonBlock == 0) { + // Add default 1 gwei base fee + genesisRoot.put("baseFeePerGas", "0x3B9ACA00"); + } + } + + // Add withdrawalRequestContractAddress if missing (EIP-7002) + if (!config.has("withdrawalRequestContractAddress")) { + config.put("withdrawalRequestContractAddress", "0x00000961ef480eb55e80d19ad83579a64c007002"); + } + + // Add consolidationRequestContractAddress if missing (EIP-7251) + if (!config.has("consolidationRequestContractAddress")) { + config.put( + "consolidationRequestContractAddress", "0x0000bbddc7ce488642fb579f8b00f3a590007251"); + } + } +} diff --git a/config/src/test/java/org/hyperledger/besu/config/GenesisConfigServiceTest.java b/config/src/test/java/org/hyperledger/besu/config/GenesisConfigServiceTest.java new file mode 100644 index 00000000000..1de18b30204 --- /dev/null +++ b/config/src/test/java/org/hyperledger/besu/config/GenesisConfigServiceTest.java @@ -0,0 +1,457 @@ +/* + * 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.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class GenesisConfigServiceTest { + + @TempDir File tempFolder; + + @Test + void shouldLoadBesuFormatGenesisFile() throws IOException { + // Besu format has ethash field + final String besuGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1," + + " \"londonBlock\": 0," + + " \"ethash\": {}" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final File genesisFile = createTempGenesisFile(besuGenesis); + final GenesisConfigService service = + GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); + + assertThat(service.getGenesisConfig()).isNotNull(); + assertThat(service.getGenesisConfigOptions().isEthHash()).isTrue(); + assertThat(service.getGenesisConfigOptions().getChainId()) + .hasValue(java.math.BigInteger.valueOf(1)); + } + + @Test + void shouldDetectAndTransformGethFormatWithMergeNetsplitBlock() throws IOException { + // Geth format has mergeNetsplitBlock but no ethash + final String gethGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"londonBlock\": 0," + + " \"mergeNetsplitBlock\": 100" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final File genesisFile = createTempGenesisFile(gethGenesis); + final GenesisConfigService service = + GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); + + // Should have ethash field added + assertThat(service.getGenesisConfigOptions().isEthHash()).isTrue(); + + // Should have preMergeForkBlock mapped from mergeNetsplitBlock + assertThat(service.getGenesisConfigOptions().getMergeNetSplitBlockNumber()).hasValue(100L); + + // Should have withdrawal and consolidation request contract addresses added + assertThat(service.getGenesisConfigOptions().getWithdrawalRequestContractAddress()) + .hasValue( + org.hyperledger.besu.datatypes.Address.fromHexString( + "0x00000961ef480eb55e80d19ad83579a64c007002")); + assertThat(service.getGenesisConfigOptions().getConsolidationRequestContractAddress()) + .hasValue( + org.hyperledger.besu.datatypes.Address.fromHexString( + "0x0000bbddc7ce488642fb579f8b00f3a590007251")); + } + + @Test + void shouldAddBaseFeeWhenLondonAtGenesis() throws IOException { + // Geth format without baseFeePerGas, but London at genesis + final String gethGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"londonBlock\": 0," + + " \"mergeNetsplitBlock\": 100" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final File genesisFile = createTempGenesisFile(gethGenesis); + final GenesisConfigService service = + GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); + + // Should have baseFeePerGas added with default value (1 gwei = 0x3B9ACA00) + assertThat(service.getGenesisConfig().getGenesisBaseFeePerGas()) + .hasValue(GenesisConfig.BASEFEE_AT_GENESIS_DEFAULT_VALUE); + } + + @Test + void shouldNotAddBaseFeeWhenLondonNotAtGenesis() throws IOException { + // Geth format without baseFeePerGas, London not at genesis + final String gethGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"londonBlock\": 100," + + " \"mergeNetsplitBlock\": 200" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final File genesisFile = createTempGenesisFile(gethGenesis); + final GenesisConfigService service = + GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); + + // Should NOT add baseFeePerGas when London is not at genesis + assertThat(service.getGenesisConfig().getBaseFeePerGas()).isNotPresent(); + } + + @Test + void shouldPreserveExistingBaseFee() throws IOException { + // Geth format with explicit baseFeePerGas + final String gethGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"londonBlock\": 0," + + " \"mergeNetsplitBlock\": 100" + + " }," + + " \"baseFeePerGas\": \"0x2710\"," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final File genesisFile = createTempGenesisFile(gethGenesis); + final GenesisConfigService service = + GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); + + // Should preserve existing baseFeePerGas (0x2710 = 10000 wei) + assertThat(service.getGenesisConfig().getBaseFeePerGas()) + .hasValue(org.hyperledger.besu.datatypes.Wei.of(10000L)); + } + + @Test + void shouldNotTransformBesuFormat() throws IOException { + // Besu format already has ethash, no mergeNetsplitBlock + final String besuGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1," + + " \"londonBlock\": 0," + + " \"ethash\": {}" + + " }," + + " \"baseFeePerGas\": \"0x3B9ACA00\"," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final File genesisFile = createTempGenesisFile(besuGenesis); + final GenesisConfigService service = + GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); + + // Should load without transformation - already in Besu format + assertThat(service.getGenesisConfigOptions().isEthHash()).isTrue(); + assertThat(service.getGenesisConfig().getGenesisBaseFeePerGas()) + .hasValue(GenesisConfig.BASEFEE_AT_GENESIS_DEFAULT_VALUE); + } + + @Test + void shouldApplyOverrides() throws IOException { + final String gethGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"londonBlock\": 0," + + " \"mergeNetsplitBlock\": 100" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final File genesisFile = createTempGenesisFile(gethGenesis); + final GenesisConfigService service = + GenesisConfigService.fromFile(genesisFile, java.util.Map.of("chainId", "9999")); + + // Override should be applied + assertThat(service.getGenesisConfigOptions().getChainId()) + .hasValue(java.math.BigInteger.valueOf(9999)); + } + + @Test + void shouldLoadFromResource() { + final GenesisConfigService service = + GenesisConfigService.fromResource("/dev.json", Collections.emptyMap()); + + assertThat(service.getGenesisConfig()).isNotNull(); + assertThat(service.getGenesisConfigOptions().isEthHash()).isTrue(); + } + + @Test + void shouldLoadFromGenesisConfig() { + final GenesisConfig config = GenesisConfig.fromResource("/dev.json"); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.getGenesisConfig()).isEqualTo(config); + assertThat(service.getGenesisConfigOptions()).isNotNull(); + } + + @Test + void shouldGetEcCurve() { + final GenesisConfig config = + GenesisConfig.fromConfig("{\"config\":{\"ecCurve\":\"secp256r1\"}}"); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.getEcCurve()).hasValue("secp256r1"); + } + + @Test + void shouldReturnEmptyEcCurveWhenNotSpecified() { + final GenesisConfig config = GenesisConfig.fromConfig("{\"config\":{}}"); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.getEcCurve()).isEmpty(); + } + + @Test + void shouldGetBlockPeriodForClique() { + final String cliqueGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"clique\": {" + + " \"blockperiodseconds\": 5," + + " \"epochlength\": 30000" + + " }" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final GenesisConfig config = GenesisConfig.fromConfig(cliqueGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.getBlockPeriodSeconds()).hasValue(5); + } + + @Test + void shouldGetBlockPeriodForQbft() { + final String qbftGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"qbft\": {" + + " \"blockperiodseconds\": 2," + + " \"epochlength\": 30000" + + " }" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final GenesisConfig config = GenesisConfig.fromConfig(qbftGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.getBlockPeriodSeconds()).hasValue(2); + } + + @Test + void shouldReturnEmptyBlockPeriodForEthash() { + final String ethashGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1," + + " \"ethash\": {}" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final GenesisConfig config = GenesisConfig.fromConfig(ethashGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.getBlockPeriodSeconds()).isEmpty(); + } + + @Test + void shouldHandleInvalidGenesisFile() { + final File nonExistentFile = new File(tempFolder, "nonexistent.json"); + + assertThatThrownBy(() -> GenesisConfigService.fromFile(nonExistentFile, Collections.emptyMap())) + .isInstanceOf(RuntimeException.class) + .hasCauseInstanceOf(java.io.FileNotFoundException.class); + } + + @Test + void shouldDetectKzgForkForCancun() { + final String cancunGenesis = + "{\"config\":{\"chainId\":1,\"cancunTime\":1710338135},\"difficulty\":\"0x1\",\"gasLimit\":\"0x1000000\"}"; + + final GenesisConfig config = GenesisConfig.fromConfig(cancunGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.hasKzgFork()).isTrue(); + } + + @Test + void shouldDetectKzgForkForPrague() { + final String pragueGenesis = + "{\"config\":{\"chainId\":1,\"pragueTime\":1720000000},\"difficulty\":\"0x1\",\"gasLimit\":\"0x1000000\"}"; + + final GenesisConfig config = GenesisConfig.fromConfig(pragueGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.hasKzgFork()).isTrue(); + } + + @Test + void shouldDetectNoKzgForkForPreCancun() { + final String shanghaiGenesis = + "{\"config\":{\"chainId\":1,\"shanghaiTime\":1681338455},\"difficulty\":\"0x1\",\"gasLimit\":\"0x1000000\"}"; + + final GenesisConfig config = GenesisConfig.fromConfig(shanghaiGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.hasKzgFork()).isFalse(); + } + + @Test + void shouldDetectNoKzgForkWhenNoForksSpecified() { + final String basicGenesis = + "{\"config\":{\"chainId\":1},\"difficulty\":\"0x1\",\"gasLimit\":\"0x1000000\"}"; + + final GenesisConfig config = GenesisConfig.fromConfig(basicGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.hasKzgFork()).isFalse(); + } + + @Test + void shouldDetectPoaConsensusForClique() { + final String cliqueGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"clique\": {" + + " \"blockperiodseconds\": 5," + + " \"epochlength\": 30000" + + " }" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final GenesisConfig config = GenesisConfig.fromConfig(cliqueGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.isPoaConsensus()).isTrue(); + assertThat(service.getPoaEpochLength()).hasValue(30000L); + assertThat(service.getConsensusMechanism()).isEqualTo("Clique"); + } + + @Test + void shouldDetectPoaConsensusForQbft() { + final String qbftGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"qbft\": {" + + " \"blockperiodseconds\": 2," + + " \"epochlength\": 50000" + + " }" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final GenesisConfig config = GenesisConfig.fromConfig(qbftGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.isPoaConsensus()).isTrue(); + assertThat(service.getPoaEpochLength()).hasValue(50000L); + assertThat(service.getConsensusMechanism()).isEqualTo("QBFT"); + } + + @Test + void shouldNotDetectPoaConsensusForEthash() { + final String ethashGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1," + + " \"ethash\": {}" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final GenesisConfig config = GenesisConfig.fromConfig(ethashGenesis); + final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); + + assertThat(service.isPoaConsensus()).isFalse(); + assertThat(service.getPoaEpochLength()).isEmpty(); + assertThat(service.getConsensusMechanism()).isEqualTo("Ethash"); + } + + @Test + void shouldPreserveExistingContractAddresses() throws IOException { + // Geth format with custom contract addresses + final String gethGenesis = + "{" + + " \"config\": {" + + " \"chainId\": 1337," + + " \"londonBlock\": 0," + + " \"mergeNetsplitBlock\": 100," + + " \"withdrawalRequestContractAddress\": \"0x1111111111111111111111111111111111111111\"," + + " \"consolidationRequestContractAddress\": \"0x2222222222222222222222222222222222222222\"" + + " }," + + " \"difficulty\": \"0x1\"," + + " \"gasLimit\": \"0x1000000\"" + + "}"; + + final File genesisFile = createTempGenesisFile(gethGenesis); + final GenesisConfigService service = + GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); + + // Should preserve the custom addresses, not overwrite with defaults + assertThat(service.getGenesisConfigOptions().getWithdrawalRequestContractAddress()) + .hasValue( + org.hyperledger.besu.datatypes.Address.fromHexString( + "0x1111111111111111111111111111111111111111")); + assertThat(service.getGenesisConfigOptions().getConsolidationRequestContractAddress()) + .hasValue( + org.hyperledger.besu.datatypes.Address.fromHexString( + "0x2222222222222222222222222222222222222222")); + } + + private File createTempGenesisFile(final String content) throws IOException { + final File genesisFile = new File(tempFolder, "genesis.json"); + Files.writeString(genesisFile.toPath(), content); + return genesisFile; + } +} From ee7d4cfff2b07b45644eed18a1a1ae2f623ede0f Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Mon, 27 Oct 2025 12:47:43 +0100 Subject: [PATCH 2/2] remove GenesisConfigService and move logic to BesuCommand directly Signed-off-by: daniellehrner --- .../org/hyperledger/besu/cli/BesuCommand.java | 296 ++++++++++-- .../besu/cli/CommandTestAbstract.java | 11 +- .../besu/cli/options/MiningOptionsTest.java | 2 +- .../besu/config/GenesisConfigService.java | 326 ------------- .../besu/config/GenesisConfigServiceTest.java | 457 ------------------ 5 files changed, 258 insertions(+), 834 deletions(-) delete mode 100644 config/src/main/java/org/hyperledger/besu/config/GenesisConfigService.java delete mode 100644 config/src/test/java/org/hyperledger/besu/config/GenesisConfigServiceTest.java diff --git a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 2646c3f8d47..77263a2a17c 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -88,7 +88,9 @@ import org.hyperledger.besu.cli.util.VersionProvider; import org.hyperledger.besu.components.BesuComponent; import org.hyperledger.besu.config.CheckpointConfigOptions; -import org.hyperledger.besu.config.GenesisConfigService; +import org.hyperledger.besu.config.GenesisConfig; +import org.hyperledger.besu.config.GenesisConfigOptions; +import org.hyperledger.besu.config.JsonUtil; import org.hyperledger.besu.config.MergeConfiguration; import org.hyperledger.besu.consensus.merge.blockcreation.MergeCoordinator; import org.hyperledger.besu.controller.BesuController; @@ -215,6 +217,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.math.BigInteger; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.GroupPrincipal; @@ -231,6 +234,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.Set; import java.util.TreeMap; import java.util.function.BiFunction; @@ -241,6 +246,7 @@ import com.fasterxml.jackson.core.StreamReadConstraints; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Splitter; import com.google.common.base.Strings; @@ -340,7 +346,13 @@ public class BesuCommand implements DefaultCommandValues, Runnable { new PreSynchronizationTaskRunner(); private final Set allocatedPorts = new HashSet<>(); - private GenesisConfigService genesisConfigService; + private final Supplier genesisConfigSupplier = + Suppliers.memoize(this::readGenesisConfig); + + /** Memoized supplier for genesis configuration options. Protected to allow test access. */ + protected final Supplier genesisConfigOptionsSupplier = + Suppliers.memoize(this::readGenesisConfigOptions); + private final Supplier miningParametersSupplier = Suppliers.memoize(this::getMiningParameters); private final Supplier apiConfigurationSupplier = @@ -901,8 +913,6 @@ public void run() { System.exit(0); // Exit before any services are started } - genesisConfigService = createGenesisConfigService(); - // set merge config on the basis of genesis config setMergeConfigOptions(); @@ -1402,7 +1412,7 @@ void configureNativeLibs(final Optional configuredNetwork) { } } - if (genesisConfigService.hasKzgFork()) { + if (hasKzgFork(readGenesisConfigOptions())) { if (kzgTrustedSetupFile != null) { KZGPointEvalPrecompiledContract.init(kzgTrustedSetupFile); } else { @@ -1473,7 +1483,7 @@ private void validateApiOptions() { } private void validateTransactionPoolOptions() { - transactionPoolOptions.validate(commandLine, genesisConfigService.getGenesisConfigOptions()); + transactionPoolOptions.validate(commandLine, genesisConfigOptionsSupplier.get()); } private void validateDataStorageOptions() { @@ -1482,7 +1492,7 @@ private void validateDataStorageOptions() { private void validateMiningParams() { miningOptions.validate( - commandLine, genesisConfigService.getGenesisConfigOptions(), isMergeEnabled(), logger); + commandLine, genesisConfigOptionsSupplier.get(), isMergeEnabled(), logger); } private void validateP2POptions() { @@ -1570,14 +1580,15 @@ private void validateChainDataPruningParams() { Long chainDataPruningBlocksRetained = unstableChainPruningOptions.getChainDataPruningBlocksRetained(); if (unstableChainPruningOptions.getChainDataPruningEnabled()) { + final GenesisConfigOptions genesisConfigOptions = readGenesisConfigOptions(); if (chainDataPruningBlocksRetained < unstableChainPruningOptions.getChainDataPruningBlocksRetainedLimit()) { throw new ParameterException( this.commandLine, "--Xchain-pruning-blocks-retained must be >= " + unstableChainPruningOptions.getChainDataPruningBlocksRetainedLimit()); - } else if (genesisConfigService.isPoaConsensus()) { - final var epochLengthOpt = genesisConfigService.getPoaEpochLength(); + } else if (genesisConfigOptions.isPoa()) { + final var epochLengthOpt = getPoaEpochLength(genesisConfigOptions); if (epochLengthOpt.isPresent()) { final long epochLength = epochLengthOpt.getAsLong(); if (chainDataPruningBlocksRetained < epochLength) { @@ -1587,13 +1598,228 @@ private void validateChainDataPruningParams() { "--Xchain-pruning-blocks-retained(%d) must be >= epochlength(%d) for %s", chainDataPruningBlocksRetained, epochLength, - genesisConfigService.getConsensusMechanism())); + getConsensusMechanism(genesisConfigOptions))); } } } } } + private GenesisConfig readGenesisConfig() { + GenesisConfig effectiveGenesisFile; + effectiveGenesisFile = + network.equals(EPHEMERY) + ? EphemeryGenesisUpdater.updateGenesis(genesisConfigOverrides) + : genesisFile != null + ? GenesisConfig.fromConfig(loadAndTransformGenesisFile(genesisFile)) + : GenesisConfig.fromResource( + Optional.ofNullable(network).orElse(MAINNET).getGenesisFile()); + return effectiveGenesisFile.withOverrides(genesisConfigOverrides); + } + + private GenesisConfigOptions readGenesisConfigOptions() { + try { + return genesisConfigSupplier.get().getConfigOptions(); + } catch (final Exception e) { + throw new ParameterException( + this.commandLine, "Unable to load genesis file. " + e.getCause()); + } + } + + /** + * Loads a genesis file from File and applies Geth-to-Besu transformation if needed. + * + * @param genesisFile the genesis file + * @return the loaded and potentially transformed ObjectNode + */ + private ObjectNode loadAndTransformGenesisFile(final File genesisFile) { + try { + final URL url = genesisFile.toURI().toURL(); + final ObjectNode genesisRoot = JsonUtil.objectNodeFromURL(url, false); + + // Check if this is a Geth format genesis file and transform if needed + if (isGethFormat(genesisRoot)) { + transformGethToBesu(genesisRoot); + } + + return genesisRoot; + } catch (final Exception e) { + // Extract the root cause for better error reporting + Throwable rootCause = e; + while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { + rootCause = rootCause.getCause(); + } + throw new RuntimeException("Unable to load genesis file: " + genesisFile, rootCause); + } + } + + /** + * Detects if a genesis file is in Geth format. + * + *

A genesis file is considered Geth format if: + * + *

    + *
  • It has a "config" section + *
  • The config has a "mergeNetsplitBlock" field (Geth-specific) + *
  • The config does NOT have an "ethash" field (Besu-specific) + *
+ * + * @param genesisRoot the root genesis JSON node + * @return true if this is a Geth format genesis file + */ + private boolean isGethFormat(final ObjectNode genesisRoot) { + final Optional configNode = JsonUtil.getObjectNode(genesisRoot, "config"); + if (!configNode.isPresent()) { + return false; + } + + final ObjectNode config = configNode.get(); + final boolean hasMergeNetsplitBlock = config.has("mergeNetsplitBlock"); + final boolean hasEthash = config.has("ethash"); + + // It's Geth format if it has mergeNetsplitBlock but not ethash + return hasMergeNetsplitBlock && !hasEthash; + } + + /** + * Transforms a Geth-format genesis file to Besu format by applying five transformations. + * + *

Transformations applied: + * + *

    + *
  1. Add ethash field: Besu's {@code isEthHash()} method checks for the presence of + * this field in the JSON structure. Since this is a structural check, the overrides + * mechanism doesn't work - we must add it to the JSON. + *
  2. Map mergeNetsplitBlock to preMergeForkBlock: These fields serve identical purposes + * (marking the merge activation block) but use different names in Geth vs Besu. + *
  3. Add baseFeePerGas: When London fork is activated at genesis (block 0), Besu + * expects an explicit base fee. Geth may omit this field, so we add the standard default of + * 1 gwei (0x3B9ACA00). + *
  4. Add withdrawalRequestContractAddress: EIP-7002 withdrawal request contract address + * if missing. + *
  5. Add consolidationRequestContractAddress: EIP-7251 consolidation request contract + * address if missing. + *
+ * + * @param genesisRoot the root genesis JSON node (will be modified in place) + */ + private void transformGethToBesu(final ObjectNode genesisRoot) { + final Optional configNode = JsonUtil.getObjectNode(genesisRoot, "config"); + if (!configNode.isPresent()) { + return; + } + + final ObjectNode config = configNode.get(); + + // Add ethash field if not present + if (!config.has("ethash")) { + config.set("ethash", JsonUtil.createEmptyObjectNode()); + } + + // Map mergeNetsplitBlock to preMergeForkBlock + if (config.has("mergeNetsplitBlock") && !config.has("preMergeForkBlock")) { + final long mergeBlock = config.get("mergeNetsplitBlock").asLong(); + config.put("preMergeForkBlock", mergeBlock); + } + + // Add baseFeePerGas if London is at genesis + if (!genesisRoot.has("baseFeePerGas") && config.has("londonBlock")) { + final long londonBlock = config.get("londonBlock").asLong(Long.MAX_VALUE); + if (londonBlock == 0) { + // Add default 1 gwei base fee + genesisRoot.put("baseFeePerGas", "0x3B9ACA00"); + } + } + + // Add withdrawalRequestContractAddress if missing (EIP-7002) + if (!config.has("withdrawalRequestContractAddress")) { + config.put("withdrawalRequestContractAddress", "0x00000961ef480eb55e80d19ad83579a64c007002"); + } + + // Add consolidationRequestContractAddress if missing (EIP-7251) + if (!config.has("consolidationRequestContractAddress")) { + config.put( + "consolidationRequestContractAddress", "0x0000bbddc7ce488642fb579f8b00f3a590007251"); + } + } + + /** + * Checks if the genesis configuration includes any fork times that require KZG initialization. + * This includes Cancun and all subsequent forks that use KZG commitments for EIP-4844 blob + * transactions. + * + * @param genesisConfigOptions the genesis config options + * @return true if any KZG-requiring fork time is present + */ + private boolean hasKzgFork(final GenesisConfigOptions genesisConfigOptions) { + return genesisConfigOptions.getCancunTime().isPresent() + || genesisConfigOptions.getCancunEOFTime().isPresent() + || genesisConfigOptions.getPragueTime().isPresent() + || genesisConfigOptions.getOsakaTime().isPresent() + || genesisConfigOptions.getBpo1Time().isPresent() + || genesisConfigOptions.getBpo2Time().isPresent() + || genesisConfigOptions.getBpo3Time().isPresent() + || genesisConfigOptions.getBpo4Time().isPresent() + || genesisConfigOptions.getBpo5Time().isPresent() + || genesisConfigOptions.getAmsterdamTime().isPresent() + || genesisConfigOptions.getFutureEipsTime().isPresent(); + } + + /** + * Gets the block period in seconds based on the consensus mechanism. + * + * @param genesisConfigOptions the genesis config options + * @return the block period in seconds, or empty if not applicable + */ + private OptionalInt getBlockPeriodSeconds(final GenesisConfigOptions genesisConfigOptions) { + if (genesisConfigOptions.isClique()) { + return OptionalInt.of(genesisConfigOptions.getCliqueConfigOptions().getBlockPeriodSeconds()); + } + if (genesisConfigOptions.isIbft2()) { + return OptionalInt.of(genesisConfigOptions.getBftConfigOptions().getBlockPeriodSeconds()); + } + if (genesisConfigOptions.isQbft()) { + return OptionalInt.of(genesisConfigOptions.getQbftConfigOptions().getBlockPeriodSeconds()); + } + return OptionalInt.empty(); + } + + /** + * Gets the epoch length for PoA consensus mechanisms. + * + * @param genesisConfigOptions the genesis config options + * @return epoch length if PoA consensus is configured, empty otherwise + */ + private OptionalLong getPoaEpochLength(final GenesisConfigOptions genesisConfigOptions) { + if (genesisConfigOptions.isIbft2()) { + return OptionalLong.of(genesisConfigOptions.getBftConfigOptions().getEpochLength()); + } else if (genesisConfigOptions.isQbft()) { + return OptionalLong.of(genesisConfigOptions.getQbftConfigOptions().getEpochLength()); + } else if (genesisConfigOptions.isClique()) { + return OptionalLong.of(genesisConfigOptions.getCliqueConfigOptions().getEpochLength()); + } + return OptionalLong.empty(); + } + + /** + * Gets the name of the consensus mechanism configured in the genesis. + * + * @param genesisConfigOptions the genesis config options + * @return the consensus mechanism name (e.g., "IBFT2", "QBFT", "Clique", "Ethash") + */ + private String getConsensusMechanism(final GenesisConfigOptions genesisConfigOptions) { + if (genesisConfigOptions.isIbft2()) { + return "IBFT2"; + } else if (genesisConfigOptions.isQbft()) { + return "QBFT"; + } else if (genesisConfigOptions.isClique()) { + return "Clique"; + } else if (genesisConfigOptions.isEthHash()) { + return "Ethash"; + } + return "Unknown"; + } + private void issueOptionWarnings() { // Check that P2P options are able to work @@ -1939,7 +2165,7 @@ private TransactionPoolConfiguration buildTransactionPoolConfiguration() { .from(txPoolConf) .saveFile((dataPath.resolve(txPoolConf.getSaveFile().getPath()).toFile())); - if (genesisConfigService.getGenesisConfigOptions().isZeroBaseFee()) { + if (genesisConfigOptionsSupplier.get().isZeroBaseFee()) { logger.warn( "Forcing price bump for transaction replacement to 0, since we are on a zero basefee network"); txPoolConfBuilder.priceBump(Percentage.ZERO); @@ -1978,7 +2204,8 @@ private TransactionPoolConfiguration buildTransactionPoolConfiguration() { private MiningConfiguration getMiningParameters() { miningOptions.setTransactionSelectionService(transactionSelectionServiceImpl); final var miningParameters = miningOptions.toDomainObject(); - genesisConfigService.getBlockPeriodSeconds().ifPresent(miningParameters::setBlockPeriodSeconds); + getBlockPeriodSeconds(readGenesisConfigOptions()) + .ifPresent(miningParameters::setBlockPeriodSeconds); initMiningParametersMetrics(miningParameters); return miningParameters; @@ -2165,8 +2392,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { // If no chain id is found in the genesis, use mainnet network id try { builder.setNetworkId( - genesisConfigService - .getGenesisConfigOptions() + readGenesisConfigOptions() .getChainId() .orElse(EthNetworkConfig.getNetworkConfig(MAINNET).networkId())); } catch (final DecodeException e) { @@ -2186,7 +2412,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { builder.setDnsDiscoveryUrl(null); } - builder.setGenesisConfig(genesisConfigService.getGenesisConfig()); + builder.setGenesisConfig(genesisConfigSupplier.get()); if (networkId != null) { builder.setNetworkId(networkId); @@ -2200,7 +2426,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { builder.setDnsDiscoveryUrl(p2PDiscoveryOptions.discoveryDnsUrl); } else { final Optional discoveryDnsUrlFromGenesis = - genesisConfigService.getGenesisConfigOptions().getDiscoveryOptions().getDiscoveryDnsUrl(); + genesisConfigOptionsSupplier.get().getDiscoveryOptions().getDiscoveryDnsUrl(); discoveryDnsUrlFromGenesis.ifPresent(builder::setDnsDiscoveryUrl); } @@ -2219,7 +2445,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { } } else { final Optional> bootNodesFromGenesis = - genesisConfigService.getGenesisConfigOptions().getDiscoveryOptions().getBootNodes(); + genesisConfigOptionsSupplier.get().getDiscoveryOptions().getBootNodes(); if (bootNodesFromGenesis.isPresent()) { listBootNodes = buildEnodes(bootNodesFromGenesis.get(), getEnodeDnsConfiguration()); } @@ -2234,25 +2460,6 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { return builder.build(); } - /** - * Creates a GenesisConfigService instance based on the current configuration. This service - * handles loading genesis files and automatically detects and transforms Geth-format genesis - * files to Besu format. - * - * @return a new GenesisConfigService instance - */ - private GenesisConfigService createGenesisConfigService() { - if (network.equals(EPHEMERY)) { - final var config = EphemeryGenesisUpdater.updateGenesis(genesisConfigOverrides); - return GenesisConfigService.fromGenesisConfig(config); - } else if (genesisFile != null) { - return GenesisConfigService.fromFile(genesisFile, genesisConfigOverrides); - } else { - return GenesisConfigService.fromResource( - Optional.ofNullable(network).orElse(MAINNET).getGenesisFile(), genesisConfigOverrides); - } - } - /** * Returns data directory used by Besu. Visible as it is accessed by other subcommands. * @@ -2461,21 +2668,21 @@ private Optional getEcCurveFromGenesisFile() { if (genesisFile == null) { return Optional.empty(); } - return genesisConfigService.getEcCurve(); + return genesisConfigOptionsSupplier.get().getEcCurve(); } /** - * Return the genesis config service + * Return the genesis config options * - * @return the genesis config service + * @return the genesis config options */ - protected GenesisConfigService getGenesisConfigService() { - return genesisConfigService; + protected GenesisConfigOptions getGenesisConfigOptions() { + return genesisConfigOptionsSupplier.get(); } private void setMergeConfigOptions() { MergeConfiguration.setMergeEnabled( - genesisConfigService.getGenesisConfigOptions().getTerminalTotalDifficulty().isPresent()); + genesisConfigOptionsSupplier.get().getTerminalTotalDifficulty().isPresent()); } /** Set ignorable segments in RocksDB Storage Provider plugin. */ @@ -2489,10 +2696,11 @@ public void setIgnorableStorageSegments() { private void validatePostMergeCheckpointBlockRequirements() { final SynchronizerConfiguration synchronizerConfiguration = unstableSynchronizerOptions.toDomainObject().build(); + final GenesisConfigOptions genesisConfigOptions = readGenesisConfigOptions(); final Optional terminalTotalDifficulty = - genesisConfigService.getGenesisConfigOptions().getTerminalTotalDifficulty(); + genesisConfigOptions.getTerminalTotalDifficulty(); final CheckpointConfigOptions checkpointConfigOptions = - genesisConfigService.getGenesisConfigOptions().getCheckpointOptions(); + genesisConfigOptions.getCheckpointOptions(); if (synchronizerConfiguration.isCheckpointPostMergeEnabled()) { if (!checkpointConfigOptions.isValid()) { throw new InvalidConfigurationException( diff --git a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index b3c23f9cac6..23805d4db4e 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -46,7 +46,7 @@ import org.hyperledger.besu.cli.options.TransactionPoolOptions; import org.hyperledger.besu.cli.options.storage.DataStorageOptions; import org.hyperledger.besu.components.BesuComponent; -import org.hyperledger.besu.config.GenesisConfigService; +import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.controller.BesuController; import org.hyperledger.besu.controller.BesuControllerBuilder; import org.hyperledger.besu.controller.NoopPluginServiceFactory; @@ -598,15 +598,14 @@ protected Vertx createVertx(final VertxOptions vertxOptions) { return vertx; } - @Override - public GenesisConfigService getGenesisConfigService() { - return super.getGenesisConfigService(); - } - public CommandSpec getSpec() { return spec; } + public Supplier getGenesisConfigOptionsSupplier() { + return genesisConfigOptionsSupplier; + } + public NetworkingOptions getNetworkingOptions() { return unstableNetworkingOptions; } diff --git a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java index bf6d8d04732..054a43d2f38 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java @@ -491,7 +491,7 @@ protected String[] getNonOptionFields() { private MiningConfiguration runtimeConfiguration( final TestBesuCommand besuCommand, final MiningConfiguration miningConfiguration) { - if (besuCommand.getGenesisConfigService().isPoaConsensus()) { + if (besuCommand.getGenesisConfigOptionsSupplier().get().isPoa()) { miningConfiguration.setBlockPeriodSeconds(POA_BLOCK_PERIOD_SECONDS); } return miningConfiguration; diff --git a/config/src/main/java/org/hyperledger/besu/config/GenesisConfigService.java b/config/src/main/java/org/hyperledger/besu/config/GenesisConfigService.java deleted file mode 100644 index 0edfd15cbee..00000000000 --- a/config/src/main/java/org/hyperledger/besu/config/GenesisConfigService.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright contributors to Hyperledger Besu. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.hyperledger.besu.config; - -import java.io.File; -import java.net.URL; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.OptionalLong; - -import com.fasterxml.jackson.databind.node.ObjectNode; - -/** - * Service for loading and transforming genesis configuration files. This service provides automatic - * detection and transformation of Geth-format genesis files to Besu format, enabling seamless - * compatibility between the two Ethereum client formats. - * - *

The service handles five main transformations for Geth genesis files: - * - *

    - *
  1. Adding the {@code ethash} field to the config (required for Besu's consensus detection) - *
  2. Mapping {@code mergeNetsplitBlock} to {@code preMergeForkBlock} (different field names for - * the same purpose) - *
  3. Adding {@code baseFeePerGas} when London fork is activated at genesis (block 0) - *
  4. Adding {@code withdrawalRequestContractAddress} if missing (EIP-7002) - *
  5. Adding {@code consolidationRequestContractAddress} if missing (EIP-7251) - *
- */ -public class GenesisConfigService { - - private final GenesisConfig genesisConfig; - private final GenesisConfigOptions genesisConfigOptions; - - /** - * Private constructor. Use factory methods to create instances. - * - * @param genesisConfig the genesis configuration - */ - private GenesisConfigService(final GenesisConfig genesisConfig) { - this.genesisConfig = genesisConfig; - this.genesisConfigOptions = genesisConfig.getConfigOptions(); - } - - /** - * Create a GenesisConfigService from a file, with automatic Geth format detection and - * transformation. - * - * @param genesisFile the genesis file - * @param overrides configuration overrides to apply - * @return a new GenesisConfigService instance - */ - public static GenesisConfigService fromFile( - final File genesisFile, final Map overrides) { - try { - final URL url = genesisFile.toURI().toURL(); - final GenesisConfig config = loadAndTransform(url, overrides); - return new GenesisConfigService(config); - } catch (final Exception e) { - // Extract the root cause for better error reporting - Throwable rootCause = e; - while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { - rootCause = rootCause.getCause(); - } - throw new RuntimeException("Unable to load genesis file: " + genesisFile, rootCause); - } - } - - /** - * Create a GenesisConfigService from a resource name (no transformation needed for built-in - * networks). - * - * @param resourceName the resource name - * @param overrides configuration overrides to apply - * @return a new GenesisConfigService instance - */ - public static GenesisConfigService fromResource( - final String resourceName, final Map overrides) { - final GenesisConfig config = GenesisConfig.fromResource(resourceName).withOverrides(overrides); - return new GenesisConfigService(config); - } - - /** - * Create a GenesisConfigService from an existing GenesisConfig. - * - * @param genesisConfig the genesis configuration - * @return a new GenesisConfigService instance - */ - public static GenesisConfigService fromGenesisConfig(final GenesisConfig genesisConfig) { - return new GenesisConfigService(genesisConfig); - } - - /** - * Gets the genesis configuration. - * - * @return the genesis config - */ - public GenesisConfig getGenesisConfig() { - return genesisConfig; - } - - /** - * Gets the genesis configuration options. - * - * @return the genesis config options - */ - public GenesisConfigOptions getGenesisConfigOptions() { - return genesisConfigOptions; - } - - /** - * Gets the EC curve from the genesis configuration if specified. - * - * @return the EC curve, or empty if not specified - */ - public Optional getEcCurve() { - return genesisConfigOptions.getEcCurve(); - } - - /** - * Gets the block period in seconds based on the consensus mechanism. - * - * @return the block period in seconds, or empty if not applicable - */ - public OptionalInt getBlockPeriodSeconds() { - if (genesisConfigOptions.isClique()) { - return OptionalInt.of(genesisConfigOptions.getCliqueConfigOptions().getBlockPeriodSeconds()); - } - if (genesisConfigOptions.isIbft2()) { - return OptionalInt.of(genesisConfigOptions.getBftConfigOptions().getBlockPeriodSeconds()); - } - if (genesisConfigOptions.isQbft()) { - return OptionalInt.of(genesisConfigOptions.getQbftConfigOptions().getBlockPeriodSeconds()); - } - return OptionalInt.empty(); - } - - /** - * Checks if the genesis configuration includes any fork times that require KZG initialization. - * This includes Cancun and all subsequent forks that use KZG commitments for EIP-4844 blob - * transactions. - * - * @return true if any KZG-requiring fork time is present - */ - public boolean hasKzgFork() { - return genesisConfigOptions.getCancunTime().isPresent() - || genesisConfigOptions.getCancunEOFTime().isPresent() - || genesisConfigOptions.getPragueTime().isPresent() - || genesisConfigOptions.getOsakaTime().isPresent() - || genesisConfigOptions.getBpo1Time().isPresent() - || genesisConfigOptions.getBpo2Time().isPresent() - || genesisConfigOptions.getBpo3Time().isPresent() - || genesisConfigOptions.getBpo4Time().isPresent() - || genesisConfigOptions.getBpo5Time().isPresent() - || genesisConfigOptions.getAmsterdamTime().isPresent() - || genesisConfigOptions.getFutureEipsTime().isPresent(); - } - - /** - * Checks if the genesis configuration uses a Proof of Authority (PoA) consensus mechanism. - * - * @return true if the consensus is PoA (IBFT2, QBFT, or Clique) - */ - public boolean isPoaConsensus() { - return genesisConfigOptions.isPoa(); - } - - /** - * Gets the epoch length for PoA consensus mechanisms. - * - * @return epoch length if PoA consensus is configured, empty otherwise - */ - public OptionalLong getPoaEpochLength() { - if (genesisConfigOptions.isIbft2()) { - return OptionalLong.of(genesisConfigOptions.getBftConfigOptions().getEpochLength()); - } else if (genesisConfigOptions.isQbft()) { - return OptionalLong.of(genesisConfigOptions.getQbftConfigOptions().getEpochLength()); - } else if (genesisConfigOptions.isClique()) { - return OptionalLong.of(genesisConfigOptions.getCliqueConfigOptions().getEpochLength()); - } - return OptionalLong.empty(); - } - - /** - * Gets the name of the consensus mechanism configured in the genesis. - * - * @return the consensus mechanism name (e.g., "IBFT2", "QBFT", "Clique", "Ethash") - */ - public String getConsensusMechanism() { - if (genesisConfigOptions.isIbft2()) { - return "IBFT2"; - } else if (genesisConfigOptions.isQbft()) { - return "QBFT"; - } else if (genesisConfigOptions.isClique()) { - return "Clique"; - } else if (genesisConfigOptions.isEthHash()) { - return "Ethash"; - } - return "Unknown"; - } - - /** - * Loads a genesis file from URL and applies Geth-to-Besu transformation if needed. - * - * @param url the URL to load from - * @param overrides configuration overrides to apply - * @return the loaded and potentially transformed GenesisConfig - */ - private static GenesisConfig loadAndTransform( - final URL url, final Map overrides) { - // Load the raw JSON - final ObjectNode genesisRoot = JsonUtil.objectNodeFromURL(url, false); - - // Check if this is a Geth format genesis file - if (isGethFormat(genesisRoot)) { - // Transform it to Besu format - transformGethToBesu(genesisRoot); - } - - // Create GenesisConfig from the (potentially transformed) JSON - final GenesisConfig config = GenesisConfig.fromConfig(genesisRoot); - return config.withOverrides(overrides); - } - - /** - * Detects if a genesis file is in Geth format. - * - *

A genesis file is considered Geth format if: - * - *

    - *
  • It has a "config" section - *
  • The config has a "mergeNetsplitBlock" field (Geth-specific) - *
  • The config does NOT have an "ethash" field (Besu-specific) - *
- * - * @param genesisRoot the root genesis JSON node - * @return true if this is a Geth format genesis file - */ - private static boolean isGethFormat(final ObjectNode genesisRoot) { - final Optional configNode = JsonUtil.getObjectNode(genesisRoot, "config"); - if (!configNode.isPresent()) { - return false; - } - - final ObjectNode config = configNode.get(); - final boolean hasMergeNetsplitBlock = config.has("mergeNetsplitBlock"); - final boolean hasEthash = config.has("ethash"); - - // It's Geth format if it has mergeNetsplitBlock but not ethash - return hasMergeNetsplitBlock && !hasEthash; - } - - /** - * Transforms a Geth-format genesis file to Besu format by applying five transformations. - * - *

Transformations applied: - * - *

    - *
  1. Add ethash field: Besu's {@code isEthHash()} method checks for the presence of - * this field in the JSON structure. Since this is a structural check, the overrides - * mechanism doesn't work - we must add it to the JSON. - *
  2. Map mergeNetsplitBlock to preMergeForkBlock: These fields serve identical purposes - * (marking the merge activation block) but use different names in Geth vs Besu. - *
  3. Add baseFeePerGas: When London fork is activated at genesis (block 0), Besu - * expects an explicit base fee. Geth may omit this field, so we add the standard default of - * 1 gwei (0x3B9ACA00). - *
  4. Add withdrawalRequestContractAddress: EIP-7002 withdrawal request contract address - * if missing. - *
  5. Add consolidationRequestContractAddress: EIP-7251 consolidation request contract - * address if missing. - *
- * - * @param genesisRoot the root genesis JSON node (will be modified in place) - */ - private static void transformGethToBesu(final ObjectNode genesisRoot) { - final Optional configNode = JsonUtil.getObjectNode(genesisRoot, "config"); - if (!configNode.isPresent()) { - return; - } - - final ObjectNode config = configNode.get(); - - // Add ethash field if not present - if (!config.has("ethash")) { - config.set("ethash", JsonUtil.createEmptyObjectNode()); - } - - // Map mergeNetsplitBlock to preMergeForkBlock - if (config.has("mergeNetsplitBlock") && !config.has("preMergeForkBlock")) { - final long mergeBlock = config.get("mergeNetsplitBlock").asLong(); - config.put("preMergeForkBlock", mergeBlock); - } - - // Add baseFeePerGas if London is at genesis - if (!genesisRoot.has("baseFeePerGas") && config.has("londonBlock")) { - final long londonBlock = config.get("londonBlock").asLong(Long.MAX_VALUE); - if (londonBlock == 0) { - // Add default 1 gwei base fee - genesisRoot.put("baseFeePerGas", "0x3B9ACA00"); - } - } - - // Add withdrawalRequestContractAddress if missing (EIP-7002) - if (!config.has("withdrawalRequestContractAddress")) { - config.put("withdrawalRequestContractAddress", "0x00000961ef480eb55e80d19ad83579a64c007002"); - } - - // Add consolidationRequestContractAddress if missing (EIP-7251) - if (!config.has("consolidationRequestContractAddress")) { - config.put( - "consolidationRequestContractAddress", "0x0000bbddc7ce488642fb579f8b00f3a590007251"); - } - } -} diff --git a/config/src/test/java/org/hyperledger/besu/config/GenesisConfigServiceTest.java b/config/src/test/java/org/hyperledger/besu/config/GenesisConfigServiceTest.java deleted file mode 100644 index 1de18b30204..00000000000 --- a/config/src/test/java/org/hyperledger/besu/config/GenesisConfigServiceTest.java +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Copyright contributors to Hyperledger Besu. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.hyperledger.besu.config; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Collections; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class GenesisConfigServiceTest { - - @TempDir File tempFolder; - - @Test - void shouldLoadBesuFormatGenesisFile() throws IOException { - // Besu format has ethash field - final String besuGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1," - + " \"londonBlock\": 0," - + " \"ethash\": {}" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final File genesisFile = createTempGenesisFile(besuGenesis); - final GenesisConfigService service = - GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); - - assertThat(service.getGenesisConfig()).isNotNull(); - assertThat(service.getGenesisConfigOptions().isEthHash()).isTrue(); - assertThat(service.getGenesisConfigOptions().getChainId()) - .hasValue(java.math.BigInteger.valueOf(1)); - } - - @Test - void shouldDetectAndTransformGethFormatWithMergeNetsplitBlock() throws IOException { - // Geth format has mergeNetsplitBlock but no ethash - final String gethGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"londonBlock\": 0," - + " \"mergeNetsplitBlock\": 100" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final File genesisFile = createTempGenesisFile(gethGenesis); - final GenesisConfigService service = - GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); - - // Should have ethash field added - assertThat(service.getGenesisConfigOptions().isEthHash()).isTrue(); - - // Should have preMergeForkBlock mapped from mergeNetsplitBlock - assertThat(service.getGenesisConfigOptions().getMergeNetSplitBlockNumber()).hasValue(100L); - - // Should have withdrawal and consolidation request contract addresses added - assertThat(service.getGenesisConfigOptions().getWithdrawalRequestContractAddress()) - .hasValue( - org.hyperledger.besu.datatypes.Address.fromHexString( - "0x00000961ef480eb55e80d19ad83579a64c007002")); - assertThat(service.getGenesisConfigOptions().getConsolidationRequestContractAddress()) - .hasValue( - org.hyperledger.besu.datatypes.Address.fromHexString( - "0x0000bbddc7ce488642fb579f8b00f3a590007251")); - } - - @Test - void shouldAddBaseFeeWhenLondonAtGenesis() throws IOException { - // Geth format without baseFeePerGas, but London at genesis - final String gethGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"londonBlock\": 0," - + " \"mergeNetsplitBlock\": 100" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final File genesisFile = createTempGenesisFile(gethGenesis); - final GenesisConfigService service = - GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); - - // Should have baseFeePerGas added with default value (1 gwei = 0x3B9ACA00) - assertThat(service.getGenesisConfig().getGenesisBaseFeePerGas()) - .hasValue(GenesisConfig.BASEFEE_AT_GENESIS_DEFAULT_VALUE); - } - - @Test - void shouldNotAddBaseFeeWhenLondonNotAtGenesis() throws IOException { - // Geth format without baseFeePerGas, London not at genesis - final String gethGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"londonBlock\": 100," - + " \"mergeNetsplitBlock\": 200" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final File genesisFile = createTempGenesisFile(gethGenesis); - final GenesisConfigService service = - GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); - - // Should NOT add baseFeePerGas when London is not at genesis - assertThat(service.getGenesisConfig().getBaseFeePerGas()).isNotPresent(); - } - - @Test - void shouldPreserveExistingBaseFee() throws IOException { - // Geth format with explicit baseFeePerGas - final String gethGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"londonBlock\": 0," - + " \"mergeNetsplitBlock\": 100" - + " }," - + " \"baseFeePerGas\": \"0x2710\"," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final File genesisFile = createTempGenesisFile(gethGenesis); - final GenesisConfigService service = - GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); - - // Should preserve existing baseFeePerGas (0x2710 = 10000 wei) - assertThat(service.getGenesisConfig().getBaseFeePerGas()) - .hasValue(org.hyperledger.besu.datatypes.Wei.of(10000L)); - } - - @Test - void shouldNotTransformBesuFormat() throws IOException { - // Besu format already has ethash, no mergeNetsplitBlock - final String besuGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1," - + " \"londonBlock\": 0," - + " \"ethash\": {}" - + " }," - + " \"baseFeePerGas\": \"0x3B9ACA00\"," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final File genesisFile = createTempGenesisFile(besuGenesis); - final GenesisConfigService service = - GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); - - // Should load without transformation - already in Besu format - assertThat(service.getGenesisConfigOptions().isEthHash()).isTrue(); - assertThat(service.getGenesisConfig().getGenesisBaseFeePerGas()) - .hasValue(GenesisConfig.BASEFEE_AT_GENESIS_DEFAULT_VALUE); - } - - @Test - void shouldApplyOverrides() throws IOException { - final String gethGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"londonBlock\": 0," - + " \"mergeNetsplitBlock\": 100" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final File genesisFile = createTempGenesisFile(gethGenesis); - final GenesisConfigService service = - GenesisConfigService.fromFile(genesisFile, java.util.Map.of("chainId", "9999")); - - // Override should be applied - assertThat(service.getGenesisConfigOptions().getChainId()) - .hasValue(java.math.BigInteger.valueOf(9999)); - } - - @Test - void shouldLoadFromResource() { - final GenesisConfigService service = - GenesisConfigService.fromResource("/dev.json", Collections.emptyMap()); - - assertThat(service.getGenesisConfig()).isNotNull(); - assertThat(service.getGenesisConfigOptions().isEthHash()).isTrue(); - } - - @Test - void shouldLoadFromGenesisConfig() { - final GenesisConfig config = GenesisConfig.fromResource("/dev.json"); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.getGenesisConfig()).isEqualTo(config); - assertThat(service.getGenesisConfigOptions()).isNotNull(); - } - - @Test - void shouldGetEcCurve() { - final GenesisConfig config = - GenesisConfig.fromConfig("{\"config\":{\"ecCurve\":\"secp256r1\"}}"); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.getEcCurve()).hasValue("secp256r1"); - } - - @Test - void shouldReturnEmptyEcCurveWhenNotSpecified() { - final GenesisConfig config = GenesisConfig.fromConfig("{\"config\":{}}"); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.getEcCurve()).isEmpty(); - } - - @Test - void shouldGetBlockPeriodForClique() { - final String cliqueGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"clique\": {" - + " \"blockperiodseconds\": 5," - + " \"epochlength\": 30000" - + " }" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final GenesisConfig config = GenesisConfig.fromConfig(cliqueGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.getBlockPeriodSeconds()).hasValue(5); - } - - @Test - void shouldGetBlockPeriodForQbft() { - final String qbftGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"qbft\": {" - + " \"blockperiodseconds\": 2," - + " \"epochlength\": 30000" - + " }" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final GenesisConfig config = GenesisConfig.fromConfig(qbftGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.getBlockPeriodSeconds()).hasValue(2); - } - - @Test - void shouldReturnEmptyBlockPeriodForEthash() { - final String ethashGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1," - + " \"ethash\": {}" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final GenesisConfig config = GenesisConfig.fromConfig(ethashGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.getBlockPeriodSeconds()).isEmpty(); - } - - @Test - void shouldHandleInvalidGenesisFile() { - final File nonExistentFile = new File(tempFolder, "nonexistent.json"); - - assertThatThrownBy(() -> GenesisConfigService.fromFile(nonExistentFile, Collections.emptyMap())) - .isInstanceOf(RuntimeException.class) - .hasCauseInstanceOf(java.io.FileNotFoundException.class); - } - - @Test - void shouldDetectKzgForkForCancun() { - final String cancunGenesis = - "{\"config\":{\"chainId\":1,\"cancunTime\":1710338135},\"difficulty\":\"0x1\",\"gasLimit\":\"0x1000000\"}"; - - final GenesisConfig config = GenesisConfig.fromConfig(cancunGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.hasKzgFork()).isTrue(); - } - - @Test - void shouldDetectKzgForkForPrague() { - final String pragueGenesis = - "{\"config\":{\"chainId\":1,\"pragueTime\":1720000000},\"difficulty\":\"0x1\",\"gasLimit\":\"0x1000000\"}"; - - final GenesisConfig config = GenesisConfig.fromConfig(pragueGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.hasKzgFork()).isTrue(); - } - - @Test - void shouldDetectNoKzgForkForPreCancun() { - final String shanghaiGenesis = - "{\"config\":{\"chainId\":1,\"shanghaiTime\":1681338455},\"difficulty\":\"0x1\",\"gasLimit\":\"0x1000000\"}"; - - final GenesisConfig config = GenesisConfig.fromConfig(shanghaiGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.hasKzgFork()).isFalse(); - } - - @Test - void shouldDetectNoKzgForkWhenNoForksSpecified() { - final String basicGenesis = - "{\"config\":{\"chainId\":1},\"difficulty\":\"0x1\",\"gasLimit\":\"0x1000000\"}"; - - final GenesisConfig config = GenesisConfig.fromConfig(basicGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.hasKzgFork()).isFalse(); - } - - @Test - void shouldDetectPoaConsensusForClique() { - final String cliqueGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"clique\": {" - + " \"blockperiodseconds\": 5," - + " \"epochlength\": 30000" - + " }" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final GenesisConfig config = GenesisConfig.fromConfig(cliqueGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.isPoaConsensus()).isTrue(); - assertThat(service.getPoaEpochLength()).hasValue(30000L); - assertThat(service.getConsensusMechanism()).isEqualTo("Clique"); - } - - @Test - void shouldDetectPoaConsensusForQbft() { - final String qbftGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"qbft\": {" - + " \"blockperiodseconds\": 2," - + " \"epochlength\": 50000" - + " }" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final GenesisConfig config = GenesisConfig.fromConfig(qbftGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.isPoaConsensus()).isTrue(); - assertThat(service.getPoaEpochLength()).hasValue(50000L); - assertThat(service.getConsensusMechanism()).isEqualTo("QBFT"); - } - - @Test - void shouldNotDetectPoaConsensusForEthash() { - final String ethashGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1," - + " \"ethash\": {}" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final GenesisConfig config = GenesisConfig.fromConfig(ethashGenesis); - final GenesisConfigService service = GenesisConfigService.fromGenesisConfig(config); - - assertThat(service.isPoaConsensus()).isFalse(); - assertThat(service.getPoaEpochLength()).isEmpty(); - assertThat(service.getConsensusMechanism()).isEqualTo("Ethash"); - } - - @Test - void shouldPreserveExistingContractAddresses() throws IOException { - // Geth format with custom contract addresses - final String gethGenesis = - "{" - + " \"config\": {" - + " \"chainId\": 1337," - + " \"londonBlock\": 0," - + " \"mergeNetsplitBlock\": 100," - + " \"withdrawalRequestContractAddress\": \"0x1111111111111111111111111111111111111111\"," - + " \"consolidationRequestContractAddress\": \"0x2222222222222222222222222222222222222222\"" - + " }," - + " \"difficulty\": \"0x1\"," - + " \"gasLimit\": \"0x1000000\"" - + "}"; - - final File genesisFile = createTempGenesisFile(gethGenesis); - final GenesisConfigService service = - GenesisConfigService.fromFile(genesisFile, Collections.emptyMap()); - - // Should preserve the custom addresses, not overwrite with defaults - assertThat(service.getGenesisConfigOptions().getWithdrawalRequestContractAddress()) - .hasValue( - org.hyperledger.besu.datatypes.Address.fromHexString( - "0x1111111111111111111111111111111111111111")); - assertThat(service.getGenesisConfigOptions().getConsolidationRequestContractAddress()) - .hasValue( - org.hyperledger.besu.datatypes.Address.fromHexString( - "0x2222222222222222222222222222222222222222")); - } - - private File createTempGenesisFile(final String content) throws IOException { - final File genesisFile = new File(tempFolder, "genesis.json"); - Files.writeString(genesisFile.toPath(), content); - return genesisFile; - } -}