Skip to content
282 changes: 220 additions & 62 deletions app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -345,8 +348,11 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
private final Set<Integer> allocatedPorts = new HashSet<>();
private final Supplier<GenesisConfig> genesisConfigSupplier =
Suppliers.memoize(this::readGenesisConfig);
private final Supplier<GenesisConfigOptions> genesisConfigOptionsSupplier =

/** Memoized supplier for genesis configuration options. Protected to allow test access. */
protected final Supplier<GenesisConfigOptions> genesisConfigOptionsSupplier =
Suppliers.memoize(this::readGenesisConfigOptions);

private final Supplier<MiningConfiguration> miningParametersSupplier =
Suppliers.memoize(this::getMiningParameters);
private final Supplier<ApiConfiguration> apiConfigurationSupplier =
Expand Down Expand Up @@ -1397,17 +1403,7 @@ void configureNativeLibs(final Optional<NetworkName> 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 {
Expand Down Expand Up @@ -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)));
}
}
}
}
Expand All @@ -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);
Expand All @@ -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.
*
* <p>A genesis file is considered Geth format if:
*
* <ul>
* <li>It has a "config" section
* <li>The config has a "mergeNetsplitBlock" field (Geth-specific)
* <li>The config does NOT have an "ethash" field (Besu-specific)
* </ul>
*
* @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<ObjectNode> 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.
*
* <p>Transformations applied:
*
* <ol>
* <li><b>Add ethash field:</b> 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.
* <li><b>Map mergeNetsplitBlock to preMergeForkBlock:</b> These fields serve identical purposes
* (marking the merge activation block) but use different names in Geth vs Besu.
* <li><b>Add baseFeePerGas:</b> 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).
* <li><b>Add withdrawalRequestContractAddress:</b> EIP-7002 withdrawal request contract address
* if missing.
* <li><b>Add consolidationRequestContractAddress:</b> EIP-7251 consolidation request contract
* address if missing.
* </ol>
*
* @param genesisRoot the root genesis JSON node (will be modified in place)
*/
private void transformGethToBesu(final ObjectNode genesisRoot) {
final Optional<ObjectNode> 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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -2530,10 +2687,11 @@ public void setIgnorableStorageSegments() {
private void validatePostMergeCheckpointBlockRequirements() {
final SynchronizerConfiguration synchronizerConfiguration =
unstableSynchronizerOptions.toDomainObject().build();
final GenesisConfigOptions genesisConfigOptions = readGenesisConfigOptions();
final Optional<UInt256> terminalTotalDifficulty =
genesisConfigOptionsSupplier.get().getTerminalTotalDifficulty();
genesisConfigOptions.getTerminalTotalDifficulty();
final CheckpointConfigOptions checkpointConfigOptions =
genesisConfigOptionsSupplier.get().getCheckpointOptions();
genesisConfigOptions.getCheckpointOptions();
if (synchronizerConfiguration.isCheckpointPostMergeEnabled()) {
if (!checkpointConfigOptions.isValid()) {
throw new InvalidConfigurationException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GenesisConfigOptions> getGenesisConfigOptionsSupplier() {
return genesisConfigOptionsSupplier;
}

public NetworkingOptions getNetworkingOptions() {
return unstableNetworkingOptions;
}
Expand Down
Loading