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 903e26c9602..17c61b19eb0 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -90,6 +90,7 @@ import org.hyperledger.besu.config.CheckpointConfigOptions; 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; @@ -234,6 +235,7 @@ 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; @@ -244,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; @@ -345,8 +348,11 @@ public class BesuCommand implements DefaultCommandValues, Runnable { private final Set allocatedPorts = new HashSet<>(); private final Supplier genesisConfigSupplier = Suppliers.memoize(this::readGenesisConfig); - private final Supplier genesisConfigOptionsSupplier = + + /** 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 = @@ -1397,17 +1403,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 (hasKzgFork(readGenesisConfigOptions())) { if (kzgTrustedSetupFile != null) { KZGPointEvalPrecompiledContract.init(kzgTrustedSetupFile); } else { @@ -1583,24 +1579,18 @@ private void validateChainDataPruningParams() { "--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)); + final var epochLengthOpt = getPoaEpochLength(genesisConfigOptions); + 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, + getConsensusMechanism(genesisConfigOptions))); + } } } } @@ -1612,7 +1602,7 @@ private GenesisConfig readGenesisConfig() { network.equals(EPHEMERY) ? EphemeryGenesisUpdater.updateGenesis(genesisConfigOverrides) : genesisFile != null - ? GenesisConfig.fromSource(genesisConfigSource(genesisFile)) + ? GenesisConfig.fromConfig(loadAndTransformGenesisFile(genesisFile)) : GenesisConfig.fromResource( Optional.ofNullable(network).orElse(MAINNET).getGenesisFile()); return effectiveGenesisFile.withOverrides(genesisConfigOverrides); @@ -1627,6 +1617,200 @@ private GenesisConfigOptions readGenesisConfigOptions() { } } + /** + * 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 @@ -2011,7 +2195,7 @@ private TransactionPoolConfiguration buildTransactionPoolConfiguration() { private MiningConfiguration getMiningParameters() { miningOptions.setTransactionSelectionService(transactionSelectionServiceImpl); final var miningParameters = miningOptions.toDomainObject(); - getGenesisBlockPeriodSeconds(genesisConfigOptionsSupplier.get()) + getBlockPeriodSeconds(readGenesisConfigOptions()) .ifPresent(miningParameters::setBlockPeriodSeconds); initMiningParametersMetrics(miningParameters); @@ -2079,23 +2263,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, @@ -2216,8 +2383,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { // If no chain id is found in the genesis, use mainnet network id try { builder.setNetworkId( - genesisConfigOptionsSupplier - .get() + readGenesisConfigOptions() .getChainId() .orElse(EthNetworkConfig.getNetworkConfig(MAINNET).networkId())); } catch (final DecodeException e) { @@ -2285,15 +2451,6 @@ 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); - } - } - /** * Returns data directory used by Besu. Visible as it is accessed by other subcommands. * @@ -2530,10 +2687,11 @@ public void setIgnorableStorageSegments() { private void validatePostMergeCheckpointBlockRequirements() { final SynchronizerConfiguration synchronizerConfiguration = unstableSynchronizerOptions.toDomainObject().build(); + final GenesisConfigOptions genesisConfigOptions = readGenesisConfigOptions(); final Optional terminalTotalDifficulty = - genesisConfigOptionsSupplier.get().getTerminalTotalDifficulty(); + genesisConfigOptions.getTerminalTotalDifficulty(); final CheckpointConfigOptions checkpointConfigOptions = - genesisConfigOptionsSupplier.get().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 b8025b00496..8ec6455e0c5 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/app/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -640,15 +640,14 @@ protected Vertx createVertx(final VertxOptions vertxOptions) { return vertx; } - @Override - public GenesisConfigOptions getGenesisConfigOptions() { - return super.getGenesisConfigOptions(); - } - 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 9d940c2fde3..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.getGenesisConfigOptions().isPoa()) { + if (besuCommand.getGenesisConfigOptionsSupplier().get().isPoa()) { miningConfiguration.setBlockPeriodSeconds(POA_BLOCK_PERIOD_SECONDS); } return miningConfiguration;