Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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