eth_simulateV1 - Add BlockSimulator feature (#7941)

Signed-off-by: Gabriel-Trintinalia <gabriel.trintinalia@consensys.net>
This commit is contained in:
Gabriel-Trintinalia
2024-12-19 13:01:07 +08:00
committed by GitHub
parent 0448af8d24
commit a03c98bf9e
19 changed files with 1730 additions and 3 deletions

View File

@@ -152,6 +152,7 @@ import org.hyperledger.besu.nat.NatMethod;
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.plugin.services.transactionpool.TransactionPoolServi
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 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
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();
}

View File

@@ -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<? extends Transaction> 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<? extends Transaction> transactions,
final BlockOverrides blockOverrides,
final AccountOverrideMap accountOverrides) {
return processSimulation(blockNumber, transactions, blockOverrides, accountOverrides, true);
}
private PluginBlockSimulationResult processSimulation(
final long blockNumber,
final List<? extends Transaction> transactions,
final BlockOverrides blockOverrides,
final AccountOverrideMap accountOverrides,
final boolean persistWorldState) {
BlockHeader header = getBlockHeader(blockNumber);
List<CallParameter> callParameters =
transactions.stream().map(CallParameter::fromTransaction).toList();
BlockStateCall blockStateCall =
new BlockStateCall(callParameters, blockOverrides, accountOverrides, true);
try (final MutableWorldState ws = getWorldState(header, persistWorldState)) {
List<BlockSimulationResult> 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());
}
}

View File

@@ -108,7 +108,7 @@ public class BlockchainServiceImpl implements BlockchainService {
public void storeBlock(
final BlockHeader blockHeader,
final BlockBody blockBody,
final List<TransactionReceipt> receipts) {
final List<? extends TransactionReceipt> receipts) {
final org.hyperledger.besu.ethereum.core.BlockHeader coreHeader =
(org.hyperledger.besu.ethereum.core.BlockHeader) blockHeader;
final org.hyperledger.besu.ethereum.core.BlockBody coreBody =

View File

@@ -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<UnsignedLongParameter> timestamp,
@JsonProperty("number") final Optional<UnsignedLongParameter> blockNumber,
@JsonProperty("hash") final Optional<Hash> blockHash,
@JsonProperty("prevRandao") final Optional<Bytes32> prevRandao,
@JsonProperty("gasLimit") final Optional<UnsignedLongParameter> gasLimit,
@JsonProperty("feeRecipient") final Optional<Address> feeRecipient,
@JsonProperty("baseFeePerGas") final Optional<Wei> baseFeePerGas,
@JsonProperty("blobBaseFee") final Optional<UnsignedLongParameter> blobBaseFee,
@JsonProperty("stateRoot") final Optional<Hash> stateRoot,
@JsonProperty("difficulty") final Optional<BigInteger> difficulty,
@JsonProperty("extraData") final Optional<Bytes> extraData,
@JsonProperty("mixHashOrPrevRandao") final Optional<Hash> mixHashOrPrevRandao) {
super(
timestamp,
blockNumber,
blockHash,
prevRandao,
gasLimit,
feeRecipient,
baseFeePerGas,
blobBaseFee,
stateRoot,
difficulty,
extraData,
mixHashOrPrevRandao);
}
}

View File

@@ -89,6 +89,7 @@ public class BlockResult implements JsonRpcResult {
private final String excessBlobGas;
private final String parentBeaconBlockRoot;
private final String targetBlobsPerBlock;
private final List<CallProcessingResult> callProcessingResults;
public BlockResult(
final BlockHeader header,
@@ -107,6 +108,18 @@ public class BlockResult implements JsonRpcResult {
final int size,
final boolean includeCoinbase,
final Optional<List<Withdrawal>> withdrawals) {
this(header, transactions, ommers, null, totalDifficulty, size, includeCoinbase, withdrawals);
}
public BlockResult(
final BlockHeader header,
final List<TransactionResult> transactions,
final List<JsonNode> ommers,
final List<CallProcessingResult> callProcessingResults,
final Difficulty totalDifficulty,
final int size,
final boolean includeCoinbase,
final Optional<List<Withdrawal>> withdrawals) {
this.number = Quantity.create(header.getNumber());
this.hash = header.getHash().toString();
this.mixHash = header.getMixHash().toString();
@@ -128,6 +141,7 @@ public class BlockResult implements JsonRpcResult {
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 class BlockResult implements JsonRpcResult {
public String getTargetBlobsPerBlock() {
return targetBlobsPerBlock;
}
@JsonGetter(value = "calls")
public List<CallProcessingResult> getTransactionProcessingResults() {
return callProcessingResults;
}
}

View File

@@ -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<LogResult> 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<LogResult> 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<LogResult> 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;
}
}
}

View File

@@ -21,6 +21,7 @@ import static org.mockito.Mockito.mock;
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 @@ public abstract class AbstractJsonRpcHttpServiceTest {
.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(),

View File

@@ -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);
}
}

View File

@@ -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<TransactionReceipt> receipts;
List<TransactionSimulatorResult> transactionSimulationResults;
public BlockSimulationResult(
final Block block,
final List<? extends TransactionReceipt> receipts,
final List<TransactionSimulatorResult> 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<? extends TransactionReceipt> getReceipts() {
return receipts;
}
public List<TransactionSimulatorResult> getTransactionSimulations() {
return transactionSimulationResults;
}
public Block getBlock() {
return block;
}
}

View File

@@ -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<BlockSimulationResult> process(
final BlockHeader header, final List<? extends BlockStateCall> 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<BlockSimulationResult> process(
final BlockHeader header,
final List<? extends BlockStateCall> blockStateCalls,
final MutableWorldState worldState) {
List<BlockSimulationResult> 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<TransactionSimulatorResult> transactionSimulatorResults =
processTransactions(blockHeader, blockStateCall, ws, miningBeneficiaryCalculator);
return finalizeBlock(
blockHeader, blockStateCall, ws, newProtocolSpec, transactionSimulatorResults);
}
@VisibleForTesting
protected List<TransactionSimulatorResult> processTransactions(
final BlockHeader blockHeader,
final BlockStateCall blockStateCall,
final MutableWorldState ws,
final MiningBeneficiaryCalculator miningBeneficiaryCalculator) {
List<TransactionSimulatorResult> transactionSimulations = new ArrayList<>();
for (CallParameter callParameter : blockStateCall.getCalls()) {
final WorldUpdater transactionUpdater = ws.updater();
final Optional<TransactionSimulatorResult> 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<TransactionSimulatorResult> transactionSimulations) {
long currentGasUsed = 0;
final var transactionReceiptFactory = protocolSpec.getTransactionReceiptFactory();
final List<TransactionReceipt> receipts = new ArrayList<>();
final List<Transaction> 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<Transaction> transactions,
final BlockOverrides blockOverrides,
final List<TransactionReceipt> 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);
}
}
}

View File

@@ -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<? extends CallParameter> calls;
private final AccountOverrideMap accountOverrides;
private final boolean validation;
public BlockStateCall(
final List<? extends CallParameter> 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<AccountOverrideMap> getAccountOverrides() {
return Optional.ofNullable(accountOverrides);
}
public List<? extends CallParameter> getCalls() {
return calls;
}
}

View File

@@ -34,6 +34,7 @@ import org.hyperledger.besu.ethereum.core.MutableWorldState;
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 @@ public class TransactionSimulator {
"Public world state not available for block " + header.toLogString()));
}
@Nonnull
public Optional<TransactionSimulatorResult> processWithWorldUpdater(
final CallParameter callParams,
final Optional<AccountOverrideMap> 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<TransactionSimulatorResult> processWithWorldUpdater(
final CallParameter callParams,

View File

@@ -18,6 +18,8 @@ import org.hyperledger.besu.ethereum.core.Transaction;
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 record TransactionSimulatorResult(
public ValidationResult<TransactionInvalidReason> getValidationResult() {
return result.getValidationResult();
}
public Optional<String> getInvalidReason() {
return result.getInvalidReason();
}
}

View File

@@ -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<BlockSimulationResult> 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();
}
}

View File

@@ -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')

View File

@@ -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<Long> timestamp;
private final Optional<Long> blockNumber;
private final Optional<Hash> blockHash;
private final Optional<Bytes32> prevRandao;
private final Optional<Long> gasLimit;
private final Optional<Address> feeRecipient;
private final Optional<Wei> baseFeePerGas;
private final Optional<Long> blobBaseFee;
private final Optional<Hash> stateRoot;
private final Optional<BigInteger> difficulty;
private final Optional<Bytes> extraData;
private final Optional<Hash> 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<UnsignedLongParameter> timestamp,
final Optional<UnsignedLongParameter> blockNumber,
final Optional<Hash> blockHash,
final Optional<Bytes32> prevRandao,
final Optional<UnsignedLongParameter> gasLimit,
final Optional<Address> feeRecipient,
final Optional<Wei> baseFeePerGas,
final Optional<UnsignedLongParameter> blobBaseFee,
final Optional<Hash> stateRoot,
final Optional<BigInteger> difficulty,
final Optional<Bytes> extraData,
final Optional<Hash> 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<Long> getBlockNumber() {
return blockNumber;
}
/**
* Gets the block hash.
*
* @return the optional block hash
*/
public Optional<Hash> getBlockHash() {
return blockHash;
}
/**
* Gets the previous Randao.
*
* @return the optional previous Randao
*/
public Optional<Bytes32> getPrevRandao() {
return prevRandao;
}
/**
* Gets the timestamp.
*
* @return the optional timestamp
*/
public Optional<Long> getTimestamp() {
return timestamp;
}
/**
* Gets the gas limit.
*
* @return the optional gas limit
*/
public Optional<Long> getGasLimit() {
return gasLimit;
}
/**
* Gets the fee recipient.
*
* @return the optional fee recipient
*/
public Optional<Address> getFeeRecipient() {
return feeRecipient;
}
/**
* Gets the base fee per gas.
*
* @return the optional base fee per gas
*/
public Optional<Wei> getBaseFeePerGas() {
return baseFeePerGas;
}
/**
* Gets the blob base fee.
*
* @return the optional blob base fee
*/
public Optional<Long> getBlobBaseFee() {
return blobBaseFee;
}
/**
* Gets the state root.
*
* @return the optional state root
*/
public Optional<Hash> getStateRoot() {
return stateRoot;
}
/**
* Gets the difficulty.
*
* @return the optional difficulty
*/
public Optional<BigInteger> getDifficulty() {
return difficulty;
}
/**
* Gets the extra data.
*
* @return the optional extra data
*/
public Optional<Bytes> getExtraData() {
return extraData;
}
/**
* Gets the mix hash or previous Randao.
*
* @return the optional mix hash or previous Randao
*/
public Optional<Hash> 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);
}
}
}

View File

@@ -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<? extends TransactionReceipt> receipts;
final List<TransactionSimulationResult> 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<? extends TransactionReceipt> receipts,
final List<TransactionSimulationResult> 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<? extends TransactionReceipt> getReceipts() {
return receipts;
}
/**
* Gets the list of transaction simulation results.
*
* @return the list of transaction simulation results
*/
public List<TransactionSimulationResult> getTransactionSimulationResults() {
return transactionSimulationResults;
}
}

View File

@@ -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<? extends Transaction> 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<? extends Transaction> transactions,
BlockOverrides blockOverrides,
AccountOverrideMap accountOverrides);
}

View File

@@ -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<TransactionReceipt> receipts);
void storeBlock(
BlockHeader blockHeader, BlockBody blockBody, List<? extends TransactionReceipt> receipts);
/**
* Get the block header of the chain head