From a03c98bf9e4e5edc1c4eb1d9ef0973130e6f8a89 Mon Sep 17 00:00:00 2001 From: Gabriel-Trintinalia Date: Thu, 19 Dec 2024 13:01:07 +0800 Subject: [PATCH] eth_simulateV1 - Add BlockSimulator feature (#7941) Signed-off-by: Gabriel-Trintinalia --- .../org/hyperledger/besu/cli/BesuCommand.java | 11 + .../services/BlockSimulatorServiceImpl.java | 158 +++++++ .../besu/services/BlockchainServiceImpl.java | 2 +- .../parameters/BlockOverridesParameter.java | 76 ++++ .../jsonrpc/internal/results/BlockResult.java | 19 + .../results/CallProcessingResult.java | 95 ++++ .../AbstractJsonRpcHttpServiceTest.java | 3 + .../transaction/BlockSimulationException.java | 21 + .../transaction/BlockSimulationResult.java | 58 +++ .../ethereum/transaction/BlockSimulator.java | 423 ++++++++++++++++++ .../ethereum/transaction/BlockStateCall.java | 61 +++ .../transaction/TransactionSimulator.java | 23 + .../TransactionSimulatorResult.java | 6 + .../transaction/BlockSimulatorTest.java | 251 +++++++++++ plugin-api/build.gradle | 2 +- .../besu/plugin/data/BlockOverrides.java | 382 ++++++++++++++++ .../data/PluginBlockSimulationResult.java | 80 ++++ .../services/BlockSimulationService.java | 59 +++ .../plugin/services/BlockchainService.java | 3 +- 19 files changed, 1730 insertions(+), 3 deletions(-) create mode 100644 besu/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockOverridesParameter.java create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/CallProcessingResult.java create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationException.java create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationResult.java create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCall.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java create mode 100644 plugin-api/src/main/java/org/hyperledger/besu/plugin/data/BlockOverrides.java create mode 100644 plugin-api/src/main/java/org/hyperledger/besu/plugin/data/PluginBlockSimulationResult.java create mode 100644 plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BlockSimulationService.java diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 5f7047dcae3..029f87b005b 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -152,6 +152,7 @@ import org.hyperledger.besu.plugin.data.EnodeURL; import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.plugin.services.BesuEvents; +import org.hyperledger.besu.plugin.services.BlockSimulationService; import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.PermissioningService; @@ -178,6 +179,7 @@ import org.hyperledger.besu.services.BesuConfigurationImpl; import org.hyperledger.besu.services.BesuEventsImpl; import org.hyperledger.besu.services.BesuPluginContextImpl; +import org.hyperledger.besu.services.BlockSimulatorServiceImpl; import org.hyperledger.besu.services.BlockchainServiceImpl; import org.hyperledger.besu.services.MiningServiceImpl; import org.hyperledger.besu.services.P2PServiceImpl; @@ -1288,6 +1290,15 @@ private void startPlugins(final Runner runner) { besuPluginContext.addService( MiningService.class, new MiningServiceImpl(besuController.getMiningCoordinator())); + besuPluginContext.addService( + BlockSimulationService.class, + new BlockSimulatorServiceImpl( + besuController.getProtocolContext().getWorldStateArchive(), + miningParametersSupplier.get(), + besuController.getTransactionSimulator(), + besuController.getProtocolSchedule(), + besuController.getProtocolContext().getBlockchain())); + besuController.getAdditionalPluginServices().appendPluginServices(besuPluginContext); besuPluginContext.startPlugins(); } diff --git a/besu/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java b/besu/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java new file mode 100644 index 00000000000..f840cd30273 --- /dev/null +++ b/besu/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java @@ -0,0 +1,158 @@ +/* + * Copyright contributors to 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.services; + +import org.hyperledger.besu.datatypes.AccountOverrideMap; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.transaction.BlockSimulationResult; +import org.hyperledger.besu.ethereum.transaction.BlockSimulator; +import org.hyperledger.besu.ethereum.transaction.BlockStateCall; +import org.hyperledger.besu.ethereum.transaction.CallParameter; +import org.hyperledger.besu.ethereum.transaction.TransactionSimulator; +import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.plugin.Unstable; +import org.hyperledger.besu.plugin.data.BlockOverrides; +import org.hyperledger.besu.plugin.data.PluginBlockSimulationResult; +import org.hyperledger.besu.plugin.data.TransactionSimulationResult; +import org.hyperledger.besu.plugin.services.BlockSimulationService; + +import java.util.List; + +/** This class is a service that simulates the processing of a block */ +public class BlockSimulatorServiceImpl implements BlockSimulationService { + private final BlockSimulator blockSimulator; + private final WorldStateArchive worldStateArchive; + private final Blockchain blockchain; + + /** + * This constructor creates a BlockSimulatorServiceImpl object + * + * @param worldStateArchive the world state archive + * @param miningConfiguration the mining configuration + * @param transactionSimulator the transaction simulator + * @param protocolSchedule the protocol schedule + * @param blockchain the blockchain + */ + public BlockSimulatorServiceImpl( + final WorldStateArchive worldStateArchive, + final MiningConfiguration miningConfiguration, + final TransactionSimulator transactionSimulator, + final ProtocolSchedule protocolSchedule, + final Blockchain blockchain) { + this.blockchain = blockchain; + blockSimulator = + new BlockSimulator( + worldStateArchive, protocolSchedule, transactionSimulator, miningConfiguration); + this.worldStateArchive = worldStateArchive; + } + + /** + * Simulate the processing of a block given a header, a list of transactions, and blockOverrides. + * + * @param blockNumber the block number + * @param transactions the transactions to include in the block + * @param blockOverrides the blockSimulationOverride of the block + * @param accountOverrides state overrides of the block + * @return the block context + */ + @Override + public PluginBlockSimulationResult simulate( + final long blockNumber, + final List transactions, + final BlockOverrides blockOverrides, + final AccountOverrideMap accountOverrides) { + return processSimulation(blockNumber, transactions, blockOverrides, accountOverrides, false); + } + + /** + * This method is experimental and should be used with caution. Simulate the processing of a block + * given a header, a list of transactions, and blockOverrides and persist the WorldState + * + * @param blockNumber the block number + * @param transactions the transactions to include in the block + * @param blockOverrides block overrides for the block + * @param accountOverrides state overrides of the block + * @return the PluginBlockSimulationResult + */ + @Unstable + @Override + public PluginBlockSimulationResult simulateAndPersistWorldState( + final long blockNumber, + final List transactions, + final BlockOverrides blockOverrides, + final AccountOverrideMap accountOverrides) { + return processSimulation(blockNumber, transactions, blockOverrides, accountOverrides, true); + } + + private PluginBlockSimulationResult processSimulation( + final long blockNumber, + final List transactions, + final BlockOverrides blockOverrides, + final AccountOverrideMap accountOverrides, + final boolean persistWorldState) { + BlockHeader header = getBlockHeader(blockNumber); + List callParameters = + transactions.stream().map(CallParameter::fromTransaction).toList(); + BlockStateCall blockStateCall = + new BlockStateCall(callParameters, blockOverrides, accountOverrides, true); + try (final MutableWorldState ws = getWorldState(header, persistWorldState)) { + List results = + blockSimulator.process(header, List.of(blockStateCall), ws); + BlockSimulationResult result = results.getFirst(); + if (persistWorldState) { + ws.persist(result.getBlock().getHeader()); + } + return response(result); + } catch (final Exception e) { + throw new RuntimeException("Error simulating block", e); + } + } + + private BlockHeader getBlockHeader(final long blockNumber) { + return blockchain + .getBlockHeader(blockNumber) + .orElseThrow( + () -> + new IllegalArgumentException( + "Block header not found for block number: " + blockNumber)); + } + + private MutableWorldState getWorldState(final BlockHeader header, final boolean isPersisting) { + return worldStateArchive + .getMutable(header, isPersisting) + .orElseThrow( + () -> + new IllegalArgumentException( + "World state not available for block number (block hash): " + + header.toLogString())); + } + + private PluginBlockSimulationResult response(final BlockSimulationResult result) { + return new PluginBlockSimulationResult( + result.getBlockHeader(), + result.getBlockBody(), + result.getReceipts(), + result.getTransactionSimulations().stream() + .map( + simulation -> + new TransactionSimulationResult(simulation.transaction(), simulation.result())) + .toList()); + } +} diff --git a/besu/src/main/java/org/hyperledger/besu/services/BlockchainServiceImpl.java b/besu/src/main/java/org/hyperledger/besu/services/BlockchainServiceImpl.java index 76b4d7e1509..b1893ef37fb 100644 --- a/besu/src/main/java/org/hyperledger/besu/services/BlockchainServiceImpl.java +++ b/besu/src/main/java/org/hyperledger/besu/services/BlockchainServiceImpl.java @@ -108,7 +108,7 @@ public Optional> getReceiptsByBlockHash(final Hash bloc public void storeBlock( final BlockHeader blockHeader, final BlockBody blockBody, - final List receipts) { + final List receipts) { final org.hyperledger.besu.ethereum.core.BlockHeader coreHeader = (org.hyperledger.besu.ethereum.core.BlockHeader) blockHeader; final org.hyperledger.besu.ethereum.core.BlockBody coreBody = diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockOverridesParameter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockOverridesParameter.java new file mode 100644 index 00000000000..9cd6e880b42 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockOverridesParameter.java @@ -0,0 +1,76 @@ +/* + * Copyright contributors to 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.ethereum.api.jsonrpc.internal.parameters; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.datatypes.parameters.UnsignedLongParameter; +import org.hyperledger.besu.plugin.data.BlockOverrides; + +import java.math.BigInteger; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +public class BlockOverridesParameter extends BlockOverrides { + /** + * Constructs a new BlockOverrides instance. + * + * @param timestamp the optional timestamp + * @param blockNumber the optional block number + * @param blockHash the optional block hash + * @param prevRandao the optional previous Randao + * @param gasLimit the optional gas limit + * @param feeRecipient the optional fee recipient + * @param baseFeePerGas the optional base fee per gas + * @param blobBaseFee the optional blob base fee + * @param stateRoot the optional state root + * @param difficulty the optional difficulty + * @param extraData the optional extra data + * @param mixHashOrPrevRandao the optional mix hash or previous Randao + */ + @JsonCreator + public BlockOverridesParameter( + @JsonProperty("time") final Optional timestamp, + @JsonProperty("number") final Optional blockNumber, + @JsonProperty("hash") final Optional blockHash, + @JsonProperty("prevRandao") final Optional prevRandao, + @JsonProperty("gasLimit") final Optional gasLimit, + @JsonProperty("feeRecipient") final Optional
feeRecipient, + @JsonProperty("baseFeePerGas") final Optional baseFeePerGas, + @JsonProperty("blobBaseFee") final Optional blobBaseFee, + @JsonProperty("stateRoot") final Optional stateRoot, + @JsonProperty("difficulty") final Optional difficulty, + @JsonProperty("extraData") final Optional extraData, + @JsonProperty("mixHashOrPrevRandao") final Optional mixHashOrPrevRandao) { + super( + timestamp, + blockNumber, + blockHash, + prevRandao, + gasLimit, + feeRecipient, + baseFeePerGas, + blobBaseFee, + stateRoot, + difficulty, + extraData, + mixHashOrPrevRandao); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockResult.java index c2456f061de..e74cf2f4e0a 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockResult.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/BlockResult.java @@ -89,6 +89,7 @@ public class BlockResult implements JsonRpcResult { private final String excessBlobGas; private final String parentBeaconBlockRoot; private final String targetBlobsPerBlock; + private final List callProcessingResults; public BlockResult( final BlockHeader header, @@ -107,6 +108,18 @@ public BlockResult( final int size, final boolean includeCoinbase, final Optional> withdrawals) { + this(header, transactions, ommers, null, totalDifficulty, size, includeCoinbase, withdrawals); + } + + public BlockResult( + final BlockHeader header, + final List transactions, + final List ommers, + final List callProcessingResults, + final Difficulty totalDifficulty, + final int size, + final boolean includeCoinbase, + final Optional> withdrawals) { this.number = Quantity.create(header.getNumber()); this.hash = header.getHash().toString(); this.mixHash = header.getMixHash().toString(); @@ -128,6 +141,7 @@ public BlockResult( this.timestamp = Quantity.create(header.getTimestamp()); this.ommers = ommers; this.transactions = transactions; + this.callProcessingResults = callProcessingResults; this.coinbase = includeCoinbase ? header.getCoinbase().toString() : null; this.withdrawalsRoot = header.getWithdrawalsRoot().map(Hash::toString).orElse(null); this.withdrawals = @@ -282,4 +296,9 @@ public String getParentBeaconBlockRoot() { public String getTargetBlobsPerBlock() { return targetBlobsPerBlock; } + + @JsonGetter(value = "calls") + public List getTransactionProcessingResults() { + return callProcessingResults; + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/CallProcessingResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/CallProcessingResult.java new file mode 100644 index 00000000000..9015a61c854 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/CallProcessingResult.java @@ -0,0 +1,95 @@ +/* + * Copyright contributors to 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.ethereum.api.jsonrpc.internal.results; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.tuweni.bytes.Bytes; + +public class CallProcessingResult { + @JsonProperty("status") + private final String status; + + @JsonProperty("returnData") + private final String returnData; + + @JsonProperty("gasUsed") + private final String gasUsed; + + @JsonProperty("error") + private final ErrorDetails error; + + @JsonProperty("logs") + private final List logs; + + public CallProcessingResult( + @JsonProperty("status") final int status, + @JsonProperty("returnData") final Bytes returnData, + @JsonProperty("gasUsed") final long gasUsed, + @JsonProperty("error") final ErrorDetails error, + @JsonProperty("logs") final List logs) { + this.status = Quantity.create(status); + this.returnData = returnData.toString(); + + this.gasUsed = Quantity.create(gasUsed); + this.error = error; + this.logs = logs; + } + + public String getStatus() { + return status; + } + + public String getReturnData() { + return returnData; + } + + public String getGasUsed() { + return gasUsed; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public ErrorDetails getError() { + return error; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public List getLogs() { + return logs; + } + + public record ErrorDetails( + @JsonProperty("code") long code, + @JsonProperty("message") String message, + @JsonProperty("data") Bytes data) { + + @Override + public long code() { + return code; + } + + @Override + public String message() { + return message; + } + + @Override + public Bytes data() { + return data; + } + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java index 0da11a03528..5afbbbfebd8 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.when; import org.hyperledger.besu.config.StubGenesisConfigOptions; +import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.api.ApiConfiguration; import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration; @@ -147,6 +148,8 @@ protected Map getRpcMethods( .thenReturn(ValidationResult.invalid(TransactionInvalidReason.NONCE_TOO_LOW)); final PrivacyParameters privacyParameters = mock(PrivacyParameters.class); + when(miningConfiguration.getCoinbase()).thenReturn(Optional.of(Address.ZERO)); + final BlockchainQueries blockchainQueries = new BlockchainQueries( blockchainSetupUtil.getProtocolSchedule(), diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationException.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationException.java new file mode 100644 index 00000000000..c304ed9ae1a --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationException.java @@ -0,0 +1,21 @@ +/* + * Copyright contributors to 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.ethereum.transaction; + +public class BlockSimulationException extends RuntimeException { + public BlockSimulationException(final String message) { + super(message); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationResult.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationResult.java new file mode 100644 index 00000000000..da0846890f6 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationResult.java @@ -0,0 +1,58 @@ +/* + * Copyright contributors to 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.ethereum.transaction; + +import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.plugin.data.BlockBody; +import org.hyperledger.besu.plugin.data.BlockHeader; +import org.hyperledger.besu.plugin.data.TransactionReceipt; + +import java.util.ArrayList; +import java.util.List; + +public class BlockSimulationResult { + final Block block; + final List receipts; + List transactionSimulationResults; + + public BlockSimulationResult( + final Block block, + final List receipts, + final List transactionSimulationResults) { + this.block = block; + this.receipts = new ArrayList<>(receipts); + this.transactionSimulationResults = transactionSimulationResults; + } + + public BlockHeader getBlockHeader() { + return block.getHeader(); + } + + public BlockBody getBlockBody() { + return block.getBody(); + } + + public List getReceipts() { + return receipts; + } + + public List getTransactionSimulations() { + return transactionSimulationResults; + } + + public Block getBlock() { + return block; + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java new file mode 100644 index 00000000000..018ce76ca7b --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java @@ -0,0 +1,423 @@ +/* + * Copyright contributors to 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.ethereum.transaction; + +import org.hyperledger.besu.datatypes.AccountOverride; +import org.hyperledger.besu.datatypes.AccountOverrideMap; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.ethereum.core.BlockBody; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder; +import org.hyperledger.besu.ethereum.core.BlockHeaderFunctions; +import org.hyperledger.besu.ethereum.core.Difficulty; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.core.ParsedExtraData; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.mainnet.BodyValidation; +import org.hyperledger.besu.ethereum.mainnet.ImmutableTransactionValidationParams; +import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions; +import org.hyperledger.besu.ethereum.mainnet.MiningBeneficiaryCalculator; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; +import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket; +import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.tracing.OperationTracer; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; +import org.hyperledger.besu.plugin.data.BlockOverrides; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; + +/** + * Simulates the execution of a block, processing transactions and applying state overrides. This + * class is responsible for simulating the execution of a block, which involves processing + * transactions and applying state overrides. It provides a way to test and validate the behavior of + * a block without actually executing it on the blockchain. The simulator takes into account various + * factors, such as the block header, transaction calls, and state overrides, to simulate the + * execution of the block. It returns a list of simulation results, which include the final block + * header, transaction receipts, and other relevant information. + */ +public class BlockSimulator { + private final TransactionSimulator transactionSimulator; + private final WorldStateArchive worldStateArchive; + private final ProtocolSchedule protocolSchedule; + private final MiningConfiguration miningConfiguration; + + public BlockSimulator( + final WorldStateArchive worldStateArchive, + final ProtocolSchedule protocolSchedule, + final TransactionSimulator transactionSimulator, + final MiningConfiguration miningConfiguration) { + this.worldStateArchive = worldStateArchive; + this.protocolSchedule = protocolSchedule; + this.miningConfiguration = miningConfiguration; + this.transactionSimulator = transactionSimulator; + } + + /** + * Processes a list of BlockStateCalls sequentially, collecting the results. + * + * @param header The block header for all simulations. + * @param blockStateCalls The list of BlockStateCalls to process. + * @return A list of BlockSimulationResult objects from processing each BlockStateCall. + */ + public List process( + final BlockHeader header, final List blockStateCalls) { + try (final MutableWorldState ws = + worldStateArchive + .getMutable(header, false) + .orElseThrow( + () -> + new IllegalArgumentException( + "Public world state not available for block " + header.toLogString()))) { + return process(header, blockStateCalls, ws); + } catch (IllegalArgumentException e) { + throw e; + } catch (final Exception e) { + throw new RuntimeException("Error simulating block", e); + } + } + + /** + * Processes a list of BlockStateCalls sequentially, collecting the results. + * + * @param header The block header for all simulations. + * @param blockStateCalls The list of BlockStateCalls to process. + * @param worldState The initial MutableWorldState to start with. + * @return A list of BlockSimulationResult objects from processing each BlockStateCall. + */ + public List process( + final BlockHeader header, + final List blockStateCalls, + final MutableWorldState worldState) { + List simulationResults = new ArrayList<>(); + for (BlockStateCall blockStateCall : blockStateCalls) { + BlockSimulationResult simulationResult = + processSingleBlockStateCall(header, blockStateCall, worldState); + simulationResults.add(simulationResult); + } + return simulationResults; + } + + /** + * Processes a single BlockStateCall, simulating the block execution. + * + * @param header The block header for the simulation. + * @param blockStateCall The BlockStateCall to process. + * @param ws The MutableWorldState to use for the simulation. + * @return A BlockSimulationResult from processing the BlockStateCall. + */ + private BlockSimulationResult processSingleBlockStateCall( + final BlockHeader header, final BlockStateCall blockStateCall, final MutableWorldState ws) { + BlockOverrides blockOverrides = blockStateCall.getBlockOverrides(); + long timestamp = blockOverrides.getTimestamp().orElse(header.getTimestamp() + 1); + ProtocolSpec newProtocolSpec = protocolSchedule.getForNextBlockHeader(header, timestamp); + + // Apply block header overrides and state overrides + BlockHeader blockHeader = applyBlockHeaderOverrides(header, newProtocolSpec, blockOverrides); + blockStateCall.getAccountOverrides().ifPresent(overrides -> applyStateOverrides(overrides, ws)); + + // Override the mining beneficiary calculator if a fee recipient is specified, otherwise use the + // default + MiningBeneficiaryCalculator miningBeneficiaryCalculator = + getMiningBeneficiaryCalculator(blockOverrides, newProtocolSpec); + + List transactionSimulatorResults = + processTransactions(blockHeader, blockStateCall, ws, miningBeneficiaryCalculator); + + return finalizeBlock( + blockHeader, blockStateCall, ws, newProtocolSpec, transactionSimulatorResults); + } + + @VisibleForTesting + protected List processTransactions( + final BlockHeader blockHeader, + final BlockStateCall blockStateCall, + final MutableWorldState ws, + final MiningBeneficiaryCalculator miningBeneficiaryCalculator) { + + List transactionSimulations = new ArrayList<>(); + + for (CallParameter callParameter : blockStateCall.getCalls()) { + final WorldUpdater transactionUpdater = ws.updater(); + + final Optional transactionSimulatorResult = + transactionSimulator.processWithWorldUpdater( + callParameter, + Optional.empty(), // We have already applied state overrides on block level + buildTransactionValidationParams(blockStateCall.isValidate()), + OperationTracer.NO_TRACING, + blockHeader, + transactionUpdater, + miningBeneficiaryCalculator); + + if (transactionSimulatorResult.isEmpty()) { + throw new BlockSimulationException("Transaction simulator result is empty"); + } + + TransactionSimulatorResult result = transactionSimulatorResult.get(); + if (result.isInvalid()) { + throw new BlockSimulationException( + "Transaction simulator result is invalid: " + result.getInvalidReason().orElse(null)); + } + transactionSimulations.add(transactionSimulatorResult.get()); + transactionUpdater.commit(); + } + return transactionSimulations; + } + + @VisibleForTesting + protected BlockSimulationResult finalizeBlock( + final BlockHeader blockHeader, + final BlockStateCall blockStateCall, + final MutableWorldState ws, + final ProtocolSpec protocolSpec, + final List transactionSimulations) { + + long currentGasUsed = 0; + final var transactionReceiptFactory = protocolSpec.getTransactionReceiptFactory(); + + final List receipts = new ArrayList<>(); + final List transactions = new ArrayList<>(); + + for (TransactionSimulatorResult transactionSimulatorResult : transactionSimulations) { + + TransactionProcessingResult transactionProcessingResult = transactionSimulatorResult.result(); + final Transaction transaction = transactionSimulatorResult.transaction(); + + currentGasUsed += transaction.getGasLimit() - transactionProcessingResult.getGasRemaining(); + + final TransactionReceipt transactionReceipt = + transactionReceiptFactory.create( + transaction.getType(), transactionProcessingResult, ws, currentGasUsed); + + receipts.add(transactionReceipt); + transactions.add(transaction); + } + + BlockHeader finalBlockHeader = + createFinalBlockHeader( + blockHeader, + ws, + transactions, + blockStateCall.getBlockOverrides(), + receipts, + currentGasUsed); + Block block = new Block(finalBlockHeader, new BlockBody(transactions, List.of())); + return new BlockSimulationResult(block, receipts, transactionSimulations); + } + + /** + * Applies state overrides to the world state. + * + * @param accountOverrideMap The AccountOverrideMap containing the state overrides. + * @param ws The MutableWorldState to apply the overrides to. + */ + @VisibleForTesting + protected void applyStateOverrides( + final AccountOverrideMap accountOverrideMap, final MutableWorldState ws) { + var updater = ws.updater(); + for (Address accountToOverride : accountOverrideMap.keySet()) { + final AccountOverride override = accountOverrideMap.get(accountToOverride); + MutableAccount account = updater.getOrCreate(accountToOverride); + override.getNonce().ifPresent(account::setNonce); + if (override.getBalance().isPresent()) { + account.setBalance(override.getBalance().get()); + } + override.getCode().ifPresent(n -> account.setCode(Bytes.fromHexString(n))); + override + .getStateDiff() + .ifPresent( + d -> + d.forEach( + (key, value) -> + account.setStorageValue( + UInt256.fromHexString(key), UInt256.fromHexString(value)))); + } + updater.commit(); + } + + /** + * Applies block header overrides to the block header. + * + * @param header The original block header. + * @param newProtocolSpec The ProtocolSpec for the block. + * @param blockOverrides The BlockOverrides to apply. + * @return The modified block header. + */ + @VisibleForTesting + protected BlockHeader applyBlockHeaderOverrides( + final BlockHeader header, + final ProtocolSpec newProtocolSpec, + final BlockOverrides blockOverrides) { + long timestamp = blockOverrides.getTimestamp().orElse(header.getTimestamp() + 1); + long blockNumber = blockOverrides.getBlockNumber().orElse(header.getNumber() + 1); + + return BlockHeaderBuilder.createDefault() + .parentHash(header.getHash()) + .timestamp(timestamp) + .number(blockNumber) + .coinbase( + blockOverrides + .getFeeRecipient() + .orElseGet(() -> miningConfiguration.getCoinbase().orElseThrow())) + .difficulty( + blockOverrides.getDifficulty().isPresent() + ? Difficulty.of(blockOverrides.getDifficulty().get()) + : header.getDifficulty()) + .gasLimit( + blockOverrides + .getGasLimit() + .orElseGet(() -> getNextGasLimit(newProtocolSpec, header, blockNumber))) + .baseFee( + blockOverrides + .getBaseFeePerGas() + .orElseGet(() -> getNextBaseFee(newProtocolSpec, header, blockNumber))) + .mixHash(blockOverrides.getMixHashOrPrevRandao().orElse(Hash.EMPTY)) + .extraData(blockOverrides.getExtraData().orElse(Bytes.EMPTY)) + .blockHeaderFunctions(new SimulatorBlockHeaderFunctions(blockOverrides)) + .buildBlockHeader(); + } + + /** + * Creates the final block header after applying state changes and transaction processing. + * + * @param blockHeader The original block header. + * @param ws The MutableWorldState after applying state overrides. + * @param transactions The list of transactions in the block. + * @param blockOverrides The BlockOverrides to apply. + * @param receipts The list of transaction receipts. + * @param currentGasUsed The total gas used in the block. + * @return The final block header. + */ + private BlockHeader createFinalBlockHeader( + final BlockHeader blockHeader, + final MutableWorldState ws, + final List transactions, + final BlockOverrides blockOverrides, + final List receipts, + final long currentGasUsed) { + + return BlockHeaderBuilder.createDefault() + .populateFrom(blockHeader) + .ommersHash(BodyValidation.ommersHash(List.of())) + .stateRoot(blockOverrides.getStateRoot().orElse(ws.rootHash())) + .transactionsRoot(BodyValidation.transactionsRoot(transactions)) + .receiptsRoot(BodyValidation.receiptsRoot(receipts)) + .logsBloom(BodyValidation.logsBloom(receipts)) + .gasUsed(currentGasUsed) + .withdrawalsRoot(null) + .requestsHash(null) + .mixHash(blockOverrides.getMixHashOrPrevRandao().orElse(Hash.EMPTY)) + .extraData(blockOverrides.getExtraData().orElse(Bytes.EMPTY)) + .blockHeaderFunctions(new SimulatorBlockHeaderFunctions(blockOverrides)) + .buildBlockHeader(); + } + + /** + * Builds the TransactionValidationParams for the block simulation. + * + * @param shouldValidate Whether to validate transactions. + * @return The TransactionValidationParams for the block simulation. + */ + @VisibleForTesting + ImmutableTransactionValidationParams buildTransactionValidationParams( + final boolean shouldValidate) { + + if (shouldValidate) { + return ImmutableTransactionValidationParams.builder() + .from(TransactionValidationParams.processingBlock()) + .build(); + } + + return ImmutableTransactionValidationParams.builder() + .from(TransactionValidationParams.transactionSimulator()) + .isAllowExceedingBalance(true) + .build(); + } + + private long getNextGasLimit( + final ProtocolSpec protocolSpec, final BlockHeader parentHeader, final long blockNumber) { + return protocolSpec + .getGasLimitCalculator() + .nextGasLimit( + parentHeader.getGasLimit(), + miningConfiguration.getTargetGasLimit().orElse(parentHeader.getGasLimit()), + blockNumber); + } + + /** + * Override the mining beneficiary calculator if a fee recipient is specified, otherwise use the + * default + */ + private MiningBeneficiaryCalculator getMiningBeneficiaryCalculator( + final BlockOverrides blockOverrides, final ProtocolSpec newProtocolSpec) { + if (blockOverrides.getFeeRecipient().isPresent()) { + return blockHeader -> blockOverrides.getFeeRecipient().get(); + } else { + return newProtocolSpec.getMiningBeneficiaryCalculator(); + } + } + + private Wei getNextBaseFee( + final ProtocolSpec protocolSpec, final BlockHeader parentHeader, final long blockNumber) { + return Optional.of(protocolSpec.getFeeMarket()) + .filter(FeeMarket::implementsBaseFee) + .map(BaseFeeMarket.class::cast) + .map( + feeMarket -> + feeMarket.computeBaseFee( + blockNumber, + parentHeader.getBaseFee().orElse(Wei.ZERO), + parentHeader.getGasUsed(), + feeMarket.targetGasUsed(parentHeader))) + .orElse(null); + } + + private static class SimulatorBlockHeaderFunctions implements BlockHeaderFunctions { + + private final BlockOverrides blockOverrides; + private final MainnetBlockHeaderFunctions blockHeaderFunctions = + new MainnetBlockHeaderFunctions(); + + private SimulatorBlockHeaderFunctions(final BlockOverrides blockOverrides) { + this.blockOverrides = blockOverrides; + } + + @Override + public Hash hash(final BlockHeader header) { + return blockOverrides.getBlockHash().orElseGet(() -> blockHeaderFunctions.hash(header)); + } + + @Override + public ParsedExtraData parseExtraData(final BlockHeader header) { + return blockHeaderFunctions.parseExtraData(header); + } + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCall.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCall.java new file mode 100644 index 00000000000..5db5faee23c --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCall.java @@ -0,0 +1,61 @@ +/* + * Copyright contributors to 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.ethereum.transaction; + +import org.hyperledger.besu.datatypes.AccountOverrideMap; +import org.hyperledger.besu.plugin.data.BlockOverrides; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class BlockStateCall { + + private final BlockOverrides blockOverrides; + + private final List calls; + + private final AccountOverrideMap accountOverrides; + + private final boolean validation; + + public BlockStateCall( + final List calls, + final BlockOverrides blockOverrides, + final AccountOverrideMap accountOverrides, + final boolean validation) { + this.calls = calls != null ? calls : new ArrayList<>(); + this.blockOverrides = + blockOverrides != null ? blockOverrides : BlockOverrides.builder().build(); + this.accountOverrides = accountOverrides; + this.validation = validation; + } + + public boolean isValidate() { + return validation; + } + + public BlockOverrides getBlockOverrides() { + return blockOverrides; + } + + public Optional getAccountOverrides() { + return Optional.ofNullable(accountOverrides); + } + + public List getCalls() { + return calls; + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java index 57cef89006a..a9161922a08 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java @@ -34,6 +34,7 @@ import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; +import org.hyperledger.besu.ethereum.mainnet.MiningBeneficiaryCalculator; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; @@ -341,6 +342,28 @@ private MutableWorldState getWorldState(final BlockHeader header) { "Public world state not available for block " + header.toLogString())); } + @Nonnull + public Optional processWithWorldUpdater( + final CallParameter callParams, + final Optional maybeStateOverrides, + final TransactionValidationParams transactionValidationParams, + final OperationTracer operationTracer, + final BlockHeader header, + final WorldUpdater updater, + final MiningBeneficiaryCalculator miningBeneficiaryCalculator) { + + final Address miningBeneficiary = miningBeneficiaryCalculator.calculateBeneficiary(header); + + return processWithWorldUpdater( + callParams, + maybeStateOverrides, + transactionValidationParams, + operationTracer, + header, + updater, + miningBeneficiary); + } + @Nonnull public Optional processWithWorldUpdater( final CallParameter callParams, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorResult.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorResult.java index 853bc4611a3..d06c6722505 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorResult.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorResult.java @@ -18,6 +18,8 @@ import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import java.util.Optional; + import org.apache.tuweni.bytes.Bytes; public record TransactionSimulatorResult( @@ -42,4 +44,8 @@ public Bytes getOutput() { public ValidationResult getValidationResult() { return result.getValidationResult(); } + + public Optional getInvalidReason() { + return result.getInvalidReason(); + } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java new file mode 100644 index 00000000000..bb1225107ec --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java @@ -0,0 +1,251 @@ +/* + * Copyright contributors to 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.ethereum.transaction; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.AccountOverride; +import org.hyperledger.besu.datatypes.AccountOverrideMap; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.GasLimitCalculator; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder; +import org.hyperledger.besu.ethereum.core.Difficulty; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.mainnet.ImmutableTransactionValidationParams; +import org.hyperledger.besu.ethereum.mainnet.MiningBeneficiaryCalculator; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; +import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; +import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; +import org.hyperledger.besu.plugin.data.BlockOverrides; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class BlockSimulatorTest { + + @Mock private WorldStateArchive worldStateArchive; + @Mock private ProtocolSchedule protocolSchedule; + @Mock private TransactionSimulator transactionSimulator; + @Mock private MiningConfiguration miningConfiguration; + @Mock private MutableWorldState mutableWorldState; + private BlockHeader blockHeader; + + private BlockSimulator blockSimulator; + + @BeforeEach + public void setUp() { + blockSimulator = + new BlockSimulator( + worldStateArchive, protocolSchedule, transactionSimulator, miningConfiguration); + blockHeader = BlockHeaderBuilder.createDefault().buildBlockHeader(); + ProtocolSpec protocolSpec = mock(ProtocolSpec.class); + when(miningConfiguration.getCoinbase()) + .thenReturn(Optional.ofNullable(Address.fromHexString("0x1"))); + when(protocolSchedule.getForNextBlockHeader(any(), anyLong())).thenReturn(protocolSpec); + when(protocolSpec.getMiningBeneficiaryCalculator()) + .thenReturn(mock(MiningBeneficiaryCalculator.class)); + GasLimitCalculator gasLimitCalculator = mock(GasLimitCalculator.class); + when(protocolSpec.getGasLimitCalculator()).thenReturn(gasLimitCalculator); + when(gasLimitCalculator.nextGasLimit(anyLong(), anyLong(), anyLong())).thenReturn(1L); + when(protocolSpec.getFeeMarket()).thenReturn(mock(FeeMarket.class)); + } + + @Test + public void shouldProcessWithValidWorldState() { + when(worldStateArchive.getMutable(any(BlockHeader.class), eq(false))) + .thenReturn(Optional.of(mutableWorldState)); + + List results = + blockSimulator.process(blockHeader, Collections.emptyList()); + + assertNotNull(results); + verify(worldStateArchive).getMutable(any(BlockHeader.class), eq(false)); + } + + @Test + public void shouldNotProcessWithInvalidWorldState() { + when(worldStateArchive.getMutable(any(BlockHeader.class), eq(false))) + .thenReturn(Optional.empty()); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> blockSimulator.process(blockHeader, Collections.emptyList())); + + assertEquals( + String.format("Public world state not available for block %s", blockHeader.toLogString()), + exception.getMessage()); + } + + @Test + public void shouldStopWhenTransactionSimulationIsInvalid() { + + CallParameter callParameter = mock(CallParameter.class); + BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null, true); + + TransactionSimulatorResult transactionSimulatorResult = mock(TransactionSimulatorResult.class); + when(transactionSimulatorResult.isInvalid()).thenReturn(true); + when(transactionSimulatorResult.getInvalidReason()) + .thenReturn(Optional.of("Invalid Transaction")); + + when(transactionSimulator.processWithWorldUpdater( + any(), any(), any(), any(), any(), any(), any(MiningBeneficiaryCalculator.class))) + .thenReturn(Optional.of(transactionSimulatorResult)); + + BlockSimulationException exception = + assertThrows( + BlockSimulationException.class, + () -> blockSimulator.process(blockHeader, List.of(blockStateCall), mutableWorldState)); + + assertEquals( + "Transaction simulator result is invalid: Invalid Transaction", exception.getMessage()); + } + + @Test + public void shouldStopWhenTransactionSimulationIsEmpty() { + + CallParameter callParameter = mock(CallParameter.class); + BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null, true); + + when(transactionSimulator.processWithWorldUpdater( + any(), any(), any(), any(), any(), any(), any(MiningBeneficiaryCalculator.class))) + .thenReturn(Optional.empty()); + + BlockSimulationException exception = + assertThrows( + BlockSimulationException.class, + () -> blockSimulator.process(blockHeader, List.of(blockStateCall), mutableWorldState)); + + assertEquals("Transaction simulator result is empty", exception.getMessage()); + } + + @Test + public void shouldApplyStateOverridesCorrectly() { + AccountOverrideMap accountOverrideMap = mock(AccountOverrideMap.class); + Address address = mock(Address.class); + AccountOverride accountOverride = mock(AccountOverride.class); + MutableAccount mutableAccount = mock(MutableAccount.class); + + when(accountOverrideMap.keySet()).thenReturn(Set.of(address)); + when(accountOverrideMap.get(address)).thenReturn(accountOverride); + + WorldUpdater worldUpdater = mock(WorldUpdater.class); + when(mutableWorldState.updater()).thenReturn(worldUpdater); + + when(worldUpdater.getOrCreate(address)).thenReturn(mutableAccount); + + when(accountOverride.getNonce()).thenReturn(Optional.of(123L)); + when(accountOverride.getBalance()).thenReturn(Optional.of(Wei.of(456L))); + when(accountOverride.getCode()).thenReturn(Optional.of("")); + when(accountOverride.getStateDiff()) + .thenReturn(Optional.of(new HashMap<>(Map.of("0x0", "0x1")))); + + blockSimulator.applyStateOverrides(accountOverrideMap, mutableWorldState); + + verify(mutableAccount).setNonce(anyLong()); + verify(mutableAccount).setBalance(any(Wei.class)); + verify(mutableAccount).setCode(any(Bytes.class)); + verify(mutableAccount).setStorageValue(any(UInt256.class), any(UInt256.class)); + } + + @Test + public void shouldApplyBlockHeaderOverridesCorrectly() { + ProtocolSpec protocolSpec = mock(ProtocolSpec.class); + + var expectedTimestamp = 1L; + var expectedBlockNumber = 2L; + var expectedFeeRecipient = Address.fromHexString("0x1"); + var expectedBaseFeePerGas = Wei.of(7L); + var expectedGasLimit = 5L; + var expectedDifficulty = BigInteger.ONE; + var expectedMixHashOrPrevRandao = Hash.hash(Bytes.fromHexString("0x01")); + var expectedExtraData = Bytes.fromHexString("0x02"); + + BlockOverrides blockOverrides = + BlockOverrides.builder() + .timestamp(expectedTimestamp) + .blockNumber(expectedBlockNumber) + .feeRecipient(expectedFeeRecipient) + .baseFeePerGas(expectedBaseFeePerGas) + .gasLimit(expectedGasLimit) + .difficulty(expectedDifficulty) + .mixHashOrPrevRandao(expectedMixHashOrPrevRandao) + .extraData(expectedExtraData) + .build(); + + BlockHeader result = + blockSimulator.applyBlockHeaderOverrides(blockHeader, protocolSpec, blockOverrides); + + assertNotNull(result); + assertEquals(expectedTimestamp, result.getTimestamp()); + assertEquals(expectedBlockNumber, result.getNumber()); + assertEquals(expectedFeeRecipient, result.getCoinbase()); + assertEquals(Optional.of(expectedBaseFeePerGas), result.getBaseFee()); + assertEquals(expectedGasLimit, result.getGasLimit()); + assertThat(result.getDifficulty()).isEqualTo(Difficulty.of(expectedDifficulty)); + assertEquals(expectedMixHashOrPrevRandao, result.getMixHash()); + assertEquals(expectedExtraData, result.getExtraData()); + } + + @Test + public void testBuildTransactionValidationParams() { + var configWhenValidate = + ImmutableTransactionValidationParams.builder() + .from(TransactionValidationParams.processingBlock()) + .build(); + + ImmutableTransactionValidationParams params = + blockSimulator.buildTransactionValidationParams(true); + assertThat(params).isEqualTo(configWhenValidate); + assertThat(params.isAllowExceedingBalance()).isFalse(); + + params = blockSimulator.buildTransactionValidationParams(false); + assertThat(params.isAllowExceedingBalance()).isTrue(); + } +} diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index f8608f89325..070313150eb 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -71,7 +71,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'TPCo4SZ61OrJxRAa2SIcAIOAOjVTdRw+UOeHMuiJP84=' + knownHash = '+YR9PYN+gPCvXzK2w52ypz9dZ0FOy0G3I1PljZasOkU=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/BlockOverrides.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/BlockOverrides.java new file mode 100644 index 00000000000..703bb64b373 --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/BlockOverrides.java @@ -0,0 +1,382 @@ +/* + * Copyright contributors to 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.plugin.data; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.datatypes.parameters.UnsignedLongParameter; + +import java.math.BigInteger; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** BlockOverrides represents the block overrides for a block. */ +public class BlockOverrides { + private final Optional timestamp; + private final Optional blockNumber; + private final Optional blockHash; + private final Optional prevRandao; + private final Optional gasLimit; + private final Optional
feeRecipient; + private final Optional baseFeePerGas; + private final Optional blobBaseFee; + private final Optional stateRoot; + private final Optional difficulty; + private final Optional extraData; + private final Optional mixHashOrPrevRandao; + + /** + * Constructs a new BlockOverrides instance. + * + * @param timestamp the optional timestamp + * @param blockNumber the optional block number + * @param blockHash the optional block hash + * @param prevRandao the optional previous Randao + * @param gasLimit the optional gas limit + * @param feeRecipient the optional fee recipient + * @param baseFeePerGas the optional base fee per gas + * @param blobBaseFee the optional blob base fee + * @param stateRoot the optional state root + * @param difficulty the optional difficulty + * @param extraData the optional extra data + * @param mixHashOrPrevRandao the optional mix hash or previous Randao + */ + public BlockOverrides( + final Optional timestamp, + final Optional blockNumber, + final Optional blockHash, + final Optional prevRandao, + final Optional gasLimit, + final Optional
feeRecipient, + final Optional baseFeePerGas, + final Optional blobBaseFee, + final Optional stateRoot, + final Optional difficulty, + final Optional extraData, + final Optional mixHashOrPrevRandao) { + this.timestamp = timestamp.map(UnsignedLongParameter::getValue); + this.blockNumber = blockNumber.map(UnsignedLongParameter::getValue); + this.blockHash = blockHash; + this.prevRandao = prevRandao; + this.gasLimit = gasLimit.map(UnsignedLongParameter::getValue); + this.feeRecipient = feeRecipient; + this.baseFeePerGas = baseFeePerGas; + this.blobBaseFee = blobBaseFee.map(UnsignedLongParameter::getValue); + this.stateRoot = stateRoot; + this.difficulty = difficulty; + this.extraData = extraData; + this.mixHashOrPrevRandao = mixHashOrPrevRandao; + } + + /** + * Constructs a new BlockOverrides instance from a Builder. + * + * @param builder the builder to construct from + */ + private BlockOverrides(final Builder builder) { + this.blockNumber = Optional.ofNullable(builder.blockNumber); + this.blockHash = Optional.ofNullable(builder.blockHash); + this.prevRandao = Optional.ofNullable(builder.prevRandao); + this.timestamp = Optional.ofNullable(builder.timestamp); + this.gasLimit = Optional.ofNullable(builder.gasLimit); + this.feeRecipient = Optional.ofNullable(builder.feeRecipient); + this.baseFeePerGas = Optional.ofNullable(builder.baseFeePerGas); + this.blobBaseFee = Optional.ofNullable(builder.blobBaseFee); + this.stateRoot = Optional.ofNullable(builder.stateRoot); + this.difficulty = Optional.ofNullable(builder.difficulty); + this.extraData = Optional.ofNullable(builder.extraData); + this.mixHashOrPrevRandao = Optional.ofNullable(builder.mixHashOrPrevRandao); + } + + /** + * Gets the block number. + * + * @return the optional block number + */ + public Optional getBlockNumber() { + return blockNumber; + } + + /** + * Gets the block hash. + * + * @return the optional block hash + */ + public Optional getBlockHash() { + return blockHash; + } + + /** + * Gets the previous Randao. + * + * @return the optional previous Randao + */ + public Optional getPrevRandao() { + return prevRandao; + } + + /** + * Gets the timestamp. + * + * @return the optional timestamp + */ + public Optional getTimestamp() { + return timestamp; + } + + /** + * Gets the gas limit. + * + * @return the optional gas limit + */ + public Optional getGasLimit() { + return gasLimit; + } + + /** + * Gets the fee recipient. + * + * @return the optional fee recipient + */ + public Optional
getFeeRecipient() { + return feeRecipient; + } + + /** + * Gets the base fee per gas. + * + * @return the optional base fee per gas + */ + public Optional getBaseFeePerGas() { + return baseFeePerGas; + } + + /** + * Gets the blob base fee. + * + * @return the optional blob base fee + */ + public Optional getBlobBaseFee() { + return blobBaseFee; + } + + /** + * Gets the state root. + * + * @return the optional state root + */ + public Optional getStateRoot() { + return stateRoot; + } + + /** + * Gets the difficulty. + * + * @return the optional difficulty + */ + public Optional getDifficulty() { + return difficulty; + } + + /** + * Gets the extra data. + * + * @return the optional extra data + */ + public Optional getExtraData() { + return extraData; + } + + /** + * Gets the mix hash or previous Randao. + * + * @return the optional mix hash or previous Randao + */ + public Optional getMixHashOrPrevRandao() { + return mixHashOrPrevRandao; + } + + /** + * Creates a new Builder instance. + * + * @return a new Builder + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for BlockOverrides. */ + public static class Builder { + private Long timestamp; + private Long blockNumber; + private Hash blockHash; + private Bytes32 prevRandao; + private Long gasLimit; + private Address feeRecipient; + private Wei baseFeePerGas; + private Long blobBaseFee; + private Hash stateRoot; + private BigInteger difficulty; + private Bytes extraData; + private Hash mixHashOrPrevRandao; + + /** Constructs a new Builder instance. */ + public Builder() {} + + /** + * Sets the timestamp. + * + * @param timestamp the timestamp to set + * @return the builder instance + */ + public Builder timestamp(final Long timestamp) { + this.timestamp = timestamp; + return this; + } + + /** + * Sets the block number. + * + * @param blockNumber the block number to set + * @return the builder instance + */ + public Builder blockNumber(final Long blockNumber) { + this.blockNumber = blockNumber; + return this; + } + + /** + * Sets the block hash. + * + * @param blockHash the block hash to set + * @return the builder instance + */ + public Builder blockHash(final Hash blockHash) { + this.blockHash = blockHash; + return this; + } + + /** + * Sets the previous Randao. + * + * @param prevRandao the previous Randao to set + * @return the builder instance + */ + public Builder prevRandao(final Bytes32 prevRandao) { + this.prevRandao = prevRandao; + return this; + } + + /** + * Sets the gas limit. + * + * @param gasLimit the gas limit to set + * @return the builder instance + */ + public Builder gasLimit(final Long gasLimit) { + this.gasLimit = gasLimit; + return this; + } + + /** + * Sets the fee recipient. + * + * @param feeRecipient the fee recipient to set + * @return the builder instance + */ + public Builder feeRecipient(final Address feeRecipient) { + this.feeRecipient = feeRecipient; + return this; + } + + /** + * Sets the base fee per gas. + * + * @param baseFeePerGas the base fee per gas to set + * @return the builder instance + */ + public Builder baseFeePerGas(final Wei baseFeePerGas) { + this.baseFeePerGas = baseFeePerGas; + return this; + } + + /** + * Sets the blob base fee. + * + * @param blobBaseFee the blob base fee to set + * @return the builder instance + */ + public Builder blobBaseFee(final Long blobBaseFee) { + this.blobBaseFee = blobBaseFee; + return this; + } + + /** + * Sets the state root. + * + * @param stateRoot the state root to set + * @return the builder instance + */ + public Builder stateRoot(final Hash stateRoot) { + this.stateRoot = stateRoot; + return this; + } + + /** + * Sets the difficulty. + * + * @param difficulty the difficulty to set + * @return the builder instance + */ + public Builder difficulty(final BigInteger difficulty) { + this.difficulty = difficulty; + return this; + } + + /** + * Sets the extra data. + * + * @param extraData the extra data to set + * @return the builder instance + */ + public Builder extraData(final Bytes extraData) { + this.extraData = extraData; + return this; + } + + /** + * Sets the mix hash or previous Randao. + * + * @param mixHashOrPrevRandao the mix hash or previous Randao to set + * @return the builder instance + */ + public Builder mixHashOrPrevRandao(final Hash mixHashOrPrevRandao) { + this.mixHashOrPrevRandao = mixHashOrPrevRandao; + return this; + } + + /** + * Builds a new BlockOverrides instance. + * + * @return the new BlockOverrides instance + */ + public BlockOverrides build() { + return new BlockOverrides(this); + } + } +} diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/PluginBlockSimulationResult.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/PluginBlockSimulationResult.java new file mode 100644 index 00000000000..b2b2ab0e7be --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/PluginBlockSimulationResult.java @@ -0,0 +1,80 @@ +/* + * Copyright contributors to 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.plugin.data; + +import java.util.List; + +/** This class represents the result of simulating the processing of a block. */ +public class PluginBlockSimulationResult { + final BlockHeader blockHeader; + final BlockBody blockBody; + final List receipts; + final List transactionSimulationResults; + + /** + * Constructs a new BlockSimulationResult instance. + * + * @param blockHeader the block header + * @param blockBody the block body + * @param receipts the list of transaction receipts + * @param transactionSimulationResults the list of transaction simulation results + */ + public PluginBlockSimulationResult( + final BlockHeader blockHeader, + final BlockBody blockBody, + final List receipts, + final List transactionSimulationResults) { + this.blockHeader = blockHeader; + this.blockBody = blockBody; + this.receipts = receipts; + this.transactionSimulationResults = transactionSimulationResults; + } + + /** + * Gets the block header. + * + * @return the block header + */ + public BlockHeader getBlockHeader() { + return blockHeader; + } + + /** + * Gets the block body. + * + * @return the block body + */ + public BlockBody getBlockBody() { + return blockBody; + } + + /** + * Gets the list of transaction receipts. + * + * @return the list of transaction receipts + */ + public List getReceipts() { + return receipts; + } + + /** + * Gets the list of transaction simulation results. + * + * @return the list of transaction simulation results + */ + public List getTransactionSimulationResults() { + return transactionSimulationResults; + } +} diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BlockSimulationService.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BlockSimulationService.java new file mode 100644 index 00000000000..f0253186daf --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BlockSimulationService.java @@ -0,0 +1,59 @@ +/* + * Copyright contributors to 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.plugin.services; + +import org.hyperledger.besu.datatypes.AccountOverrideMap; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.Unstable; +import org.hyperledger.besu.plugin.data.BlockOverrides; +import org.hyperledger.besu.plugin.data.PluginBlockSimulationResult; + +import java.util.List; + +/** This class is a service that simulates the processing of a block */ +public interface BlockSimulationService extends BesuService { + + /** + * Simulate the processing of a block given a header, a list of transactions, and blockOverrides. + * + * @param blockNumber the block number + * @param transactions the transactions to include in the block + * @param blockOverrides the blockSimulationOverride of the block + * @param accountOverrides state overrides of the block + * @return the block context + */ + PluginBlockSimulationResult simulate( + long blockNumber, + List transactions, + BlockOverrides blockOverrides, + AccountOverrideMap accountOverrides); + + /** + * This method is experimental and should be used with caution. Simulate the processing of a block + * given a header, a list of transactions, and blockOverrides and persist the WorldState + * + * @param blockNumber the block number + * @param transactions the transactions to include in the block + * @param blockOverrides block overrides for the block + * @param accountOverrides state overrides of the block + * @return the PluginBlockSimulationResult + */ + @Unstable + PluginBlockSimulationResult simulateAndPersistWorldState( + long blockNumber, + List transactions, + BlockOverrides blockOverrides, + AccountOverrideMap accountOverrides); +} diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BlockchainService.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BlockchainService.java index a89d1144af7..12d21d789b3 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BlockchainService.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BlockchainService.java @@ -59,7 +59,8 @@ public interface BlockchainService extends BesuService { * @param blockBody the block body * @param receipts the transaction receipts */ - void storeBlock(BlockHeader blockHeader, BlockBody blockBody, List receipts); + void storeBlock( + BlockHeader blockHeader, BlockBody blockBody, List receipts); /** * Get the block header of the chain head