Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.AMSTERDAM;
import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.CANCUN;
import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.PARIS;
import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.PRAGUE;
import static org.hyperledger.besu.datatypes.HardforkId.MainnetHardforkId.SHANGHAI;

import org.hyperledger.besu.config.GenesisConfig;
Expand All @@ -31,6 +32,7 @@
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockProcessor;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.mainnet.blockhash.PraguePreExecutionProcessor;
import org.hyperledger.besu.evm.internal.EvmConfiguration;
import org.hyperledger.besu.evm.operation.InvalidOperation;
import org.hyperledger.besu.evm.operation.PrevRanDaoOperation;
Expand Down Expand Up @@ -269,4 +271,76 @@ public void amsterdamHasBlockAccessListFactoryWithForkActivated() {
.withFailMessage("BlockAccessListFactory should be present for Amsterdam, but it was empty")
.isPresent();
}

/**
* Verifies that a Clique-to-PoS network (with TTD set) uses PraguePreExecutionProcessor for
* post-merge Prague blocks, not FrontierPreExecutionProcessor. This is a regression test for a
* bug where isPoAConsensus() returned true for Clique genesis configs even when TTD was set,
* causing EIP-2935 and EIP-4788 system calls to be skipped for post-merge blocks.
*/
@Test
public void cliqueToPoSNetworkUsesPraguePreExecutionProcessorAfterMerge() {
final String jsonInput =
"{\"config\": "
+ "{\"chainId\": 59139,\n"
+ "\"homesteadBlock\": 0,\n"
+ "\"eip150Block\": 0,\n"
+ "\"eip155Block\": 0,\n"
+ "\"eip158Block\": 0,\n"
+ "\"byzantiumBlock\": 0,\n"
+ "\"constantinopleBlock\": 0,\n"
+ "\"petersburgBlock\": 0,\n"
+ "\"istanbulBlock\": 0,\n"
+ "\"berlinBlock\": 0,\n"
+ "\"londonBlock\": 0,\n"
+ "\"terminalTotalDifficulty\": 17628883,\n"
+ "\"shanghaiTime\": 1755165600,\n"
+ "\"pragueTime\": 1755770400,\n"
+ "\"clique\": {\n"
+ " \"blockperiodseconds\": 1,\n"
+ " \"epochlength\": 30000,\n"
+ " \"createemptyblocks\": true\n"
+ "},\n"
+ "\"depositContractAddress\": \"0x45152B0bD93Dc1e4c84d70e24edA3CEb12b1a1D3\",\n"
+ "\"withdrawalRequestContractAddress\": \"0xF7B4391C85B1ad1eAF38cb4B45a235cDF9295a7D\",\n"
+ "\"consolidationRequestContractAddress\": \"0x0bbfd3E844Cc1D63D4D86498Ca91FF6de417Ed73\"\n"
+ "}}";

final GenesisConfigOptions config = GenesisConfig.fromConfig(jsonInput).getConfigOptions();

// Verify that the genesis is detected as Clique (this is expected)
assertThat(config.isClique()).isTrue();
// Verify TTD is set (this is a Clique-to-PoS network)
assertThat(config.getTerminalTotalDifficulty()).isPresent();

final ProtocolSchedule protocolSchedule =
MergeProtocolSchedule.create(
config,
false,
MiningConfiguration.MINING_DISABLED,
new BadBlockManager(),
false,
BalConfiguration.DEFAULT,
new NoOpMetricsSystem(),
EvmConfiguration.DEFAULT);

// Get Prague spec at post-merge timestamp
final ProtocolSpec pragueSpec =
protocolSchedule.getByBlockHeader(
new BlockHeaderTestFixture().number(18000000).timestamp(1755770400).buildHeader());

assertThat(pragueSpec.getHardforkId()).isEqualTo(PRAGUE);
assertProofOfStakeConfigIsEnabled(pragueSpec);

// The critical assertion: Prague blocks on a Clique-to-PoS network must use
// PraguePreExecutionProcessor (for EIP-2935 and EIP-4788 system calls),
// NOT FrontierPreExecutionProcessor
assertThat(pragueSpec.getPreExecutionProcessor())
.withFailMessage(
"Prague blocks on a Clique-to-PoS network should use PraguePreExecutionProcessor, "
+ "but got %s. This means EIP-2935 blockhash storage and EIP-4788 beacon root "
+ "system calls are being skipped, causing stateroot mismatches.",
pragueSpec.getPreExecutionProcessor().getClass().getSimpleName())
Comment thread
daniellehrner marked this conversation as resolved.
.isInstanceOf(PraguePreExecutionProcessor.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import org.hyperledger.besu.ethereum.mainnet.blockhash.CancunPreExecutionProcessor;
import org.hyperledger.besu.ethereum.mainnet.blockhash.FrontierPreExecutionProcessor;
import org.hyperledger.besu.ethereum.mainnet.blockhash.PraguePreExecutionProcessor;
import org.hyperledger.besu.ethereum.mainnet.blockhash.PreExecutionProcessor;
import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket;
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket;
import org.hyperledger.besu.ethereum.mainnet.parallelization.MainnetParallelBlockProcessor;
Expand Down Expand Up @@ -875,13 +876,28 @@ static ProtocolSpecBuilder cancunDefinition(
evm.getMaxInitcodeSize()))
.precompileContractRegistryBuilder(MainnetPrecompiledContractRegistries::cancun)
.blockHeaderValidatorBuilder(MainnetBlockHeaderValidator::blobAwareBlockHeaderValidator)
.preExecutionProcessor(
isPoAConsensus(genesisConfigOptions)
? new FrontierPreExecutionProcessor()
: new CancunPreExecutionProcessor())
.preExecutionProcessor(getPreExecutionProcessor(genesisConfigOptions))
Comment thread
daniellehrner marked this conversation as resolved.
.hardforkId(CANCUN);
}

private static PreExecutionProcessor getPreExecutionProcessor(
final GenesisConfigOptions genesisConfigOptions) {
if (isPoAConsensus(genesisConfigOptions) && !hasSystemContractAddresses(genesisConfigOptions)) {
return new FrontierPreExecutionProcessor();
}

return new CancunPreExecutionProcessor();
}
Comment thread
daniellehrner marked this conversation as resolved.

private static PreExecutionProcessor getPraguePreExecutionProcessor(
final GenesisConfigOptions genesisConfigOptions) {
if (isPoAConsensus(genesisConfigOptions) && !hasSystemContractAddresses(genesisConfigOptions)) {
return new FrontierPreExecutionProcessor();
}

return new PraguePreExecutionProcessor();
}

static ProtocolSpecBuilder pragueDefinition(
final Optional<BigInteger> chainId,
final boolean enableRevertReason,
Expand Down Expand Up @@ -959,14 +975,11 @@ static ProtocolSpecBuilder pragueDefinition(
new CodeDelegationService()))
.build())
// EIP-2935 Blockhash processor
.preExecutionProcessor(
isPoAConsensus(genesisConfigOptions)
? new FrontierPreExecutionProcessor()
: new PraguePreExecutionProcessor())
.preExecutionProcessor(getPraguePreExecutionProcessor(genesisConfigOptions))
.hardforkId(PRAGUE);
if (isPoAConsensus(genesisConfigOptions)) {
LOG.debug(
"Skipping system contract request processors for PoA consensus (clique/ibft/qbft).");
if (isPoAConsensus(genesisConfigOptions) && !hasSystemContractAddresses(genesisConfigOptions)) {
LOG.warn(
"Skipping system contract request processors for PoA consensus (clique/ibft/qbft) without system contract addresses.");
pragueSpecBuilder.requestProcessorCoordinator(RequestProcessorCoordinator.noOp());
} else {
try {
Expand All @@ -990,6 +1003,13 @@ private static boolean isPoAConsensus(final GenesisConfigOptions genesisConfigOp
|| genesisConfigOptions.isQbft();
}

private static boolean hasSystemContractAddresses(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may seem a little surprising to the user that all system contracts must be specified for any of them to activate, as conceptually they are indepenent, but I think it's fine if it's clearly communicated whether they are active or not.

I agree it does make sense to require specifying all of them, as that's the current use case on mainnet and apparently on the PoA networks that do use them. Also some of the existing code (like RequestContractAddresses) already assumes that. This logic may change in the future but it's fine for now.

Copy link
Copy Markdown
Contributor

@Filter94 Filter94 Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic may change in the future

I doubt, because that would break backwards compatibility. For the clients that used to support clique, in order to be compatible with the PoA networks, they had to implement the strictest rules. I.e. If some chain would disable some system contract, since Nethermind, for example, supports it, it would disable Geth nodes syncing to this chain. So it seems like all clients have to stick with the strictest validity rule amongst the client, that rule being "All system contracts have to be set"

Copy link
Copy Markdown
Contributor

@mirgee mirgee Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant addition of further system contracts in future forks might change this logic ("all system contracts have to be set"), exactly to preserve backwards compatibility.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I would leave it as is for now

final GenesisConfigOptions genesisConfigOptions) {
return genesisConfigOptions.getDepositContractAddress().isPresent()
&& genesisConfigOptions.getWithdrawalRequestContractAddress().isPresent()
&& genesisConfigOptions.getConsolidationRequestContractAddress().isPresent();
Comment thread
daniellehrner marked this conversation as resolved.
}

static ProtocolSpecBuilder osakaDefinition(
final Optional<BigInteger> chainId,
final boolean enableRevertReason,
Expand Down
Loading