diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcApis.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcApis.java index 3bef2f85ec8..a90134dac22 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcApis.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcApis.java @@ -33,7 +33,8 @@ public enum RpcApis { CLIQUE, IBFT, ENGINE, - QBFT; + QBFT, + TESTING; public static final List DEFAULT_RPC_APIS = Arrays.asList("ETH", "NET", "WEB3"); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java index 272aa9122e7..f060f0ea93f 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java @@ -177,7 +177,8 @@ public enum RpcMethod { TX_POOL_BESU_PENDING_TRANSACTIONS("txpool_besuPendingTransactions"), WEB3_CLIENT_VERSION("web3_clientVersion"), WEB3_SHA3("web3_sha3"), - PLUGINS_RELOAD_CONFIG("plugins_reloadPluginConfig"); + PLUGINS_RELOAD_CONFIG("plugins_reloadPluginConfig"), + TESTING_BUILD_BLOCK_V1("testing_buildBlockV1"); private final String methodName; diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/testing/TestingBlockCreator.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/testing/TestingBlockCreator.java new file mode 100644 index 00000000000..20a5bac5f06 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/testing/TestingBlockCreator.java @@ -0,0 +1,104 @@ +/* + * 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.methods.testing; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.blockcreation.AbstractBlockCreator; +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.SealableBlockHeader; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.Withdrawal; +import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** + * A block creator for testing purposes that only includes the provided transactions and does not + * use the local transaction pool. When explicit transactions are provided, the + * AbstractBlockCreator's evaluateTransactions method is used instead of + * buildTransactionListForBlock, which means the transaction pool is not queried for additional + * transactions. + */ +class TestingBlockCreator extends AbstractBlockCreator { + + public TestingBlockCreator( + final MiningConfiguration miningConfiguration, + final Address coinbase, + final Bytes extraData, + final TransactionPool transactionPool, + final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, + final EthScheduler ethScheduler) { + super( + miningConfiguration, + (__, ___) -> coinbase, + parent -> extraData, + transactionPool, + protocolContext, + protocolSchedule, + ethScheduler); + } + + public BlockCreationResult createBlock( + final Optional> maybeTransactions, + final Bytes32 random, + final long timestamp, + final Optional> withdrawals, + final Optional parentBeaconBlockRoot, + final Optional slotNumber, + final BlockHeader parentHeader) { + + return createBlock( + maybeTransactions, + Optional.of(Collections.emptyList()), + withdrawals, + Optional.of(random), + parentBeaconBlockRoot, + slotNumber, + timestamp, + false, + parentHeader); + } + + @Override + public BlockCreationResult createBlock( + final Optional> maybeTransactions, + final Optional> maybeOmmers, + final long timestamp, + final BlockHeader parentHeader) { + throw new UnsupportedOperationException("random is required for testing block creation"); + } + + @Override + protected BlockHeader createFinalBlockHeader(final SealableBlockHeader sealableBlockHeader) { + return BlockHeaderBuilder.create() + .difficulty(Difficulty.ZERO) + .populateFrom(sealableBlockHeader) + .nonce(0L) + .blockHeaderFunctions(blockHeaderFunctions) + .buildBlockHeader(); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/testing/TestingBuildBlockV1.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/testing/TestingBuildBlockV1.java new file mode 100644 index 00000000000..97bfbab7d02 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/testing/TestingBuildBlockV1.java @@ -0,0 +1,311 @@ +/* + * Copyright contributors to Hyperledger 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.methods.testing; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.EnginePayloadAttributesParameter; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.WithdrawalParameter; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlobsBundleV2; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.EngineGetPayloadResultV6; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity; +import org.hyperledger.besu.ethereum.api.util.DomainObjectDecodeUtils; +import org.hyperledger.besu.ethereum.blockcreation.BlockCreator.BlockCreationResult; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.Request; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.Withdrawal; +import org.hyperledger.besu.ethereum.core.encoding.EncodingContext; +import org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder; +import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ValidationResult; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; +import org.hyperledger.besu.util.HexUtils; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The testing_buildBlockV1 RPC method is a debugging and testing tool that simplifies the block + * production process into a single call. It is intended to replace the multi-step workflow of + * sending transactions, calling engine_forkchoiceUpdated with payloadAttributes, and then calling + * engine_getPayload. + * + *

This method is considered sensitive and is intended for testing environments only. + */ +public class TestingBuildBlockV1 implements JsonRpcMethod { + + private static final Logger LOG = LoggerFactory.getLogger(TestingBuildBlockV1.class); + + private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; + private final MiningConfiguration miningConfiguration; + private final TransactionPool transactionPool; + private final EthScheduler ethScheduler; + + public TestingBuildBlockV1( + final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, + final MiningConfiguration miningConfiguration, + final TransactionPool transactionPool, + final EthScheduler ethScheduler) { + this.protocolContext = protocolContext; + this.protocolSchedule = protocolSchedule; + this.miningConfiguration = miningConfiguration; + this.transactionPool = transactionPool; + this.ethScheduler = ethScheduler; + } + + @Override + public String getName() { + return RpcMethod.TESTING_BUILD_BLOCK_V1.getMethodName(); + } + + @Override + public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + final Object requestId = requestContext.getRequest().getId(); + + // Parameter 0: parentBlockHash (required) + final Hash parentBlockHash; + try { + final String parentHashHex = requestContext.getRequiredParameter(0, String.class); + parentBlockHash = Hash.fromHexString(parentHashHex); + } catch (JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid parentBlockHash parameter (index 0)", RpcErrorType.INVALID_PARAMS, e); + } + + // Parameter 1: payloadAttributes (required) + final EnginePayloadAttributesParameter payloadAttributes; + try { + payloadAttributes = + requestContext.getRequiredParameter(1, EnginePayloadAttributesParameter.class); + } catch (JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid payloadAttributes parameter (index 1)", RpcErrorType.INVALID_PARAMS, e); + } + + // Parameter 2: transactions (can be null or array) + // - null -> use txpool + // - [] or [...] -> use provided transactions + final String[] txArray; + try { + txArray = requestContext.getOptionalParameter(2, String[].class).orElse(null); + } catch (JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid transactions parameter (index 2)", RpcErrorType.INVALID_PARAMS, e); + } + final List rawTransactions = txArray != null ? List.of(txArray) : List.of(); + final boolean transactionsProvided = txArray != null; + + // Parameter 3: extraData (optional) + final Bytes extraData; + try { + extraData = + requestContext + .getOptionalParameter(3, String.class) + .filter(s -> !s.isEmpty()) + .map(Bytes::fromHexString) + .orElse(Bytes.EMPTY); + } catch (JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid extraData parameter (index 3)", RpcErrorType.INVALID_PARAMS, e); + } + + final Blockchain blockchain = protocolContext.getBlockchain(); + final Optional maybeParentHeader = blockchain.getBlockHeader(parentBlockHash); + + if (maybeParentHeader.isEmpty()) { + return new JsonRpcErrorResponse( + requestId, + ValidationResult.invalid( + RpcErrorType.INVALID_PARAMS, "Parent block not found: " + parentBlockHash)); + } + + final BlockHeader parentHeader = maybeParentHeader.get(); + + if (payloadAttributes == null) { + return new JsonRpcErrorResponse( + requestId, + ValidationResult.invalid(RpcErrorType.INVALID_PARAMS, "Missing payloadAttributes field")); + } + + final ValidationResult attributesValidation = + validatePayloadAttributes(payloadAttributes); + if (!attributesValidation.isValid()) { + return new JsonRpcErrorResponse(requestId, attributesValidation); + } + + final List transactions = new ArrayList<>(); + for (String rawTx : rawTransactions) { + try { + transactions.add(DomainObjectDecodeUtils.decodeRawTransaction(rawTx)); + } catch (Exception e) { + LOG.debug("Failed to decode transaction: {}", rawTx, e); + return new JsonRpcErrorResponse( + requestId, + ValidationResult.invalid( + RpcErrorType.INVALID_TRANSACTION_PARAMS, + "Failed to decode transaction: " + e.getMessage())); + } + } + + // Determine how to handle transactions based on go-ethereum semantics: + // - If transactions field was not provided (null in JSON) -> use txpool (Optional.empty()) + // - If transactions field is empty array [] -> build empty block (Optional.of(emptyList)) + // - If transactions field has items -> use those (Optional.of(transactions)) + final boolean useTransactionsFromTxPool = !transactionsProvided; + final Optional> maybeTransactions = + useTransactionsFromTxPool ? Optional.empty() : Optional.of(transactions); + + final List withdrawals = + payloadAttributes.getWithdrawals() != null + ? payloadAttributes.getWithdrawals().stream() + .map(WithdrawalParameter::toWithdrawal) + .collect(Collectors.toList()) + : List.of(); + + final Bytes32 prevRandao = payloadAttributes.getPrevRandao(); + final Bytes32 parentBeaconBlockRoot = payloadAttributes.getParentBeaconBlockRoot(); + final Long timestamp = payloadAttributes.getTimestamp(); + final Long slotNumber = payloadAttributes.getSlotNumber(); + + try { + final Address coinbase = payloadAttributes.getSuggestedFeeRecipient(); + + final TestingBlockCreator blockCreator = + new TestingBlockCreator( + miningConfiguration, + coinbase, + extraData, + transactionPool, + protocolContext, + protocolSchedule, + ethScheduler); + + final BlockCreationResult result = + blockCreator.createBlock( + maybeTransactions, + prevRandao, + timestamp, + Optional.of(withdrawals), + Optional.ofNullable(parentBeaconBlockRoot), + Optional.ofNullable(slotNumber), + parentHeader); + + final Block block = result.getBlock(); + + final List txsAsHex = + block.getBody().getTransactions().stream() + .map(tx -> TransactionEncoder.encodeOpaqueBytes(tx, EncodingContext.BLOCK_BODY)) + .map(b -> HexUtils.toFastHex(b, true)) + .collect(Collectors.toList()); + + final Optional> executionRequests = getExecutionRequests(result); + + final BlobsBundleV2 blobsBundle = new BlobsBundleV2(block.getBody().getTransactions()); + + final String blockAccessListHex = encodeBlockAccessList(result.getBlockAccessList()); + + final String slotNumberHex = + block.getHeader().getOptionalSlotNumber().map(Quantity::create).orElse(null); + + final EngineGetPayloadResultV6 responsePayload = + new EngineGetPayloadResultV6( + block.getHeader(), + txsAsHex, + block.getBody().getWithdrawals(), + executionRequests, + Quantity.create(Wei.ZERO), + blobsBundle, + blockAccessListHex, + slotNumberHex); + + return new JsonRpcSuccessResponse(requestId, responsePayload); + + } catch (Exception e) { + LOG.error("Error building block", e); + return new JsonRpcErrorResponse( + requestId, + ValidationResult.invalid( + RpcErrorType.INTERNAL_ERROR, "Error building block: " + e.getMessage())); + } + } + + private ValidationResult validatePayloadAttributes( + final EnginePayloadAttributesParameter attributes) { + if (attributes.getTimestamp() == null || attributes.getTimestamp() == 0) { + return ValidationResult.invalid( + RpcErrorType.INVALID_PARAMS, "Missing or invalid timestamp field"); + } + if (attributes.getPrevRandao() == null) { + return ValidationResult.invalid(RpcErrorType.INVALID_PARAMS, "Missing prevRandao field"); + } + if (attributes.getSuggestedFeeRecipient() == null) { + return ValidationResult.invalid( + RpcErrorType.INVALID_PARAMS, "Missing suggestedFeeRecipient field"); + } + return ValidationResult.valid(); + } + + private Optional> getExecutionRequests(final BlockCreationResult result) { + return result + .getRequests() + .map( + requests -> + requests.stream() + .sorted(Comparator.comparing(Request::getType)) + .filter(r -> !r.getData().isEmpty()) + .map(Request::getEncodedRequest) + .map(b -> HexUtils.toFastHex(b, true)) + .toList()); + } + + private String encodeBlockAccessList(final Optional maybeBlockAccessList) { + return maybeBlockAccessList + .map( + bal -> { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + bal.writeTo(output); + return output.encoded().toHexString(); + }) + .orElse(null); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadResultV6.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadResultV6.java index 8d9aa0c07d0..f59bb05de9d 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadResultV6.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadResultV6.java @@ -83,6 +83,27 @@ public List getExecutionRequests() { return executionRequests; } + @JsonPropertyOrder({ + "parentHash", + "feeRecipient", + "stateRoot", + "receiptsRoot", + "logsBloom", + "prevRandao", + "gasLimit", + "gasUsed", + "timestamp", + "extraData", + "baseFeePerGas", + "excessBlobGas", + "blobGasUsed", + "blockAccessList", + "slotNumber", + "transactions", + "withdrawals", + "blockNumber", + "blockHash" + }) public static class PayloadResult { protected final String blockHash; diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/JsonRpcMethodsFactory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/JsonRpcMethodsFactory.java index 1221211364f..a73a084f5c6 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/JsonRpcMethodsFactory.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/JsonRpcMethodsFactory.java @@ -159,7 +159,13 @@ public Map methods( metricsSystem, ethScheduler), new TxPoolJsonRpcMethods(transactionPool), - new PluginsJsonRpcMethods(namedPlugins)); + new PluginsJsonRpcMethods(namedPlugins), + new TestingJsonRpcMethods( + protocolContext, + protocolSchedule, + miningConfiguration, + transactionPool, + ethScheduler)); for (final JsonRpcMethods apiGroup : availableApiGroups) { enabled.putAll(apiGroup.create(rpcApis)); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TestingJsonRpcMethods.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TestingJsonRpcMethods.java new file mode 100644 index 00000000000..e99c26c7550 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TestingJsonRpcMethods.java @@ -0,0 +1,64 @@ +/* + * Copyright contributors to Hyperledger 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.methods; + +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.testing.TestingBuildBlockV1; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; + +import java.util.Map; + +/** + * Testing JSON-RPC methods. These methods are intended for testing environments only and should not + * be exposed on public-facing RPC APIs. + */ +public class TestingJsonRpcMethods extends ApiGroupJsonRpcMethods { + + private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; + private final MiningConfiguration miningConfiguration; + private final TransactionPool transactionPool; + private final EthScheduler ethScheduler; + + public TestingJsonRpcMethods( + final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, + final MiningConfiguration miningConfiguration, + final TransactionPool transactionPool, + final EthScheduler ethScheduler) { + this.protocolContext = protocolContext; + this.protocolSchedule = protocolSchedule; + this.miningConfiguration = miningConfiguration; + this.transactionPool = transactionPool; + this.ethScheduler = ethScheduler; + } + + @Override + protected String getApiGroup() { + return RpcApis.TESTING.name(); + } + + @Override + protected Map create() { + return mapOf( + new TestingBuildBlockV1( + protocolContext, protocolSchedule, miningConfiguration, transactionPool, ethScheduler)); + } +} 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 cf9d2a48124..9353fabed87 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 @@ -22,6 +22,7 @@ import org.hyperledger.besu.config.StubGenesisConfigOptions; import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.api.ApiConfiguration; import org.hyperledger.besu.ethereum.api.ImmutableApiConfiguration; @@ -37,6 +38,8 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.blockcreation.NoopMiningCoordinator; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; +import org.hyperledger.besu.ethereum.core.ImmutableMiningConfiguration; +import org.hyperledger.besu.ethereum.core.ImmutableMiningConfiguration.MutableInitValues; import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.core.Synchronizer; import org.hyperledger.besu.ethereum.core.Transaction; @@ -71,6 +74,7 @@ import io.vertx.core.VertxOptions; import okhttp3.MediaType; import okhttp3.OkHttpClient; +import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; @@ -90,7 +94,8 @@ public abstract class AbstractJsonRpcHttpServiceTest { RpcApis.NET.name(), RpcApis.WEB3.name(), RpcApis.DEBUG.name(), - RpcApis.TRACE.name()); + RpcApis.TRACE.name(), + RpcApis.TESTING.name()); protected final Vertx vertx = Vertx.vertx(); protected final Vertx syncVertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(1)); @@ -139,22 +144,39 @@ protected ApiConfiguration createApiConfiguration() { return ImmutableApiConfiguration.builder().gasCap(0L).build(); } - protected Map getRpcMethods( - final JsonRpcConfiguration config, final BlockchainSetupUtil blockchainSetupUtil) { - final ProtocolContext protocolContext = mock(ProtocolContext.class); - final Synchronizer synchronizerMock = mock(Synchronizer.class); - final P2PNetwork peerDiscoveryMock = mock(P2PNetwork.class); + protected TransactionPool createTransactionPoolMock() { final TransactionPool transactionPoolMock = mock(TransactionPool.class); - final MiningConfiguration miningConfiguration = mock(MiningConfiguration.class); - final ApiConfiguration apiConfiguration = createApiConfiguration(); when(transactionPoolMock.addTransactionViaApi(any(Transaction.class))) .thenReturn(ValidationResult.valid()); // nonce too low tests uses a tx with nonce=16 when(transactionPoolMock.addTransactionViaApi(argThat(tx -> tx.getNonce() == 16))) .thenReturn(ValidationResult.invalid(TransactionInvalidReason.NONCE_TOO_LOW)); + return transactionPoolMock; + } - when(miningConfiguration.getCoinbase()).thenReturn(Optional.of(Address.ZERO)); + protected MiningConfiguration createMiningConfiguration() { + return ImmutableMiningConfiguration.builder() + .mutableInitValues( + MutableInitValues.builder() + .extraData(Bytes.EMPTY) + .minTransactionGasPrice(Wei.ONE) + .minBlockOccupancyRatio(0d) + .coinbase(Address.ZERO) + .build()) + .build(); + } + + protected Map getRpcMethods( + final JsonRpcConfiguration config, final BlockchainSetupUtil blockchainSetupUtil) { + final ProtocolContext protocolContext = mock(ProtocolContext.class); + final Synchronizer synchronizerMock = mock(Synchronizer.class); + final P2PNetwork peerDiscoveryMock = mock(P2PNetwork.class); + final TransactionPool transactionPoolMock = createTransactionPoolMock(); + final MiningConfiguration miningConfiguration = createMiningConfiguration(); + final ApiConfiguration apiConfiguration = createApiConfiguration(); + when(protocolContext.getBlockchain()).thenReturn(blockchainSetupUtil.getBlockchain()); + when(protocolContext.getWorldStateArchive()).thenReturn(blockchainSetupUtil.getWorldArchive()); final BlockchainQueries blockchainQueries = new BlockchainQueries( blockchainSetupUtil.getProtocolSchedule(), diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TestingBuildBlockJsonRpcHttpBySpecTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TestingBuildBlockJsonRpcHttpBySpecTest.java new file mode 100644 index 00000000000..b53d368bce3 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/bonsai/TestingBuildBlockJsonRpcHttpBySpecTest.java @@ -0,0 +1,118 @@ +/* + * 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.bonsai; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPPrivateKey; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.jsonrpc.AbstractJsonRpcHttpBySpecTest; +import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; + +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class TestingBuildBlockJsonRpcHttpBySpecTest extends AbstractJsonRpcHttpBySpecTest { + + private static final BigInteger CHAIN_ID = BigInteger.valueOf(3503995874084926L); + private static final String PRIVATE_KEY = + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; + private static final Address RECIPIENT = + Address.fromHexString("0x627306090abaB3A6e1400e9345bC60c78a8BEf57"); + + @Override + @BeforeEach + public void setup() throws Exception { + blockchainSetupUtil = getBlockchainSetupUtil(DataStorageFormat.BONSAI); + blockchainSetupUtil.importAllBlocks(); + startService(); + } + + @Override + protected TransactionPool createTransactionPoolMock() { + final TransactionPool transactionPoolMock = super.createTransactionPoolMock(); + + final Transaction pendingTx = createTestTransaction(); + final PendingTransaction pending = + PendingTransaction.newPendingTransaction(pendingTx, false, false, (byte) 0); + + doAnswer( + invocation -> { + final PendingTransactions.PendingTransactionsSelector selector = + invocation.getArgument(0); + selector.evaluatePendingTransactions(List.of(pending)); + return null; + }) + .when(transactionPoolMock) + .selectTransactions(any()); + + return transactionPoolMock; + } + + private Transaction createTestTransaction() { + final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); + final SECPPrivateKey privateKey = + signatureAlgorithm.createPrivateKey(Bytes32.fromHexString(PRIVATE_KEY)); + final KeyPair keyPair = signatureAlgorithm.createKeyPair(privateKey); + + return new TransactionTestFixture() + .type(TransactionType.EIP1559) + .chainId(Optional.of(CHAIN_ID)) + .nonce(0) + .maxFeePerGas(Optional.of(Wei.of(16))) + .maxPriorityFeePerGas(Optional.of(Wei.ZERO)) + .gasLimit(21000) + .to(Optional.of(RECIPIENT)) + .value(Wei.of(1000)) + .createTransaction(keyPair); + } + + @Override + protected BlockchainSetupUtil getBlockchainSetupUtil(final DataStorageFormat storageFormat) { + return createBlockchainSetupUtil( + "testing_buildBlockV1/chain-data/genesis.json", + "testing_buildBlockV1/chain-data/blocks.bin", + storageFormat); + } + + public static Object[][] specs() { + return findSpecFiles(new String[] {"testing_buildBlockV1"}); + } + + @Test + void dryRunDetector() { + assertThat(true) + .withFailMessage("This test is here so gradle --dry-run executes this class") + .isTrue(); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/testing/TestingBuildBlockV1Test.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/testing/TestingBuildBlockV1Test.java new file mode 100644 index 00000000000..18d8a25555a --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/testing/TestingBuildBlockV1Test.java @@ -0,0 +1,369 @@ +/* + * 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.methods.testing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.StorageSlotKey; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import org.hyperledger.besu.ethereum.chain.MutableBlockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.AccountChanges; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BalanceChange; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.NonceChange; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.SlotChanges; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.SlotRead; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.StorageChange; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +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) +class TestingBuildBlockV1Test { + + private static final long DEFAULT_TIMESTAMP = 100L; + + @Mock private ProtocolContext protocolContext; + @Mock private ProtocolSchedule protocolSchedule; + @Mock private MiningConfiguration miningConfiguration; + @Mock private TransactionPool transactionPool; + @Mock private EthScheduler ethScheduler; + @Mock private MutableBlockchain blockchain; + + private TestingBuildBlockV1 method; + + @BeforeEach + void setUp() { + when(protocolContext.getBlockchain()).thenReturn(blockchain); + method = + new TestingBuildBlockV1( + protocolContext, protocolSchedule, miningConfiguration, transactionPool, ethScheduler); + } + + @Test + void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo("testing_buildBlockV1"); + } + + @Test + void shouldReturnErrorWhenParentBlockNotFound() { + when(blockchain.getBlockHeader(any(Hash.class))).thenReturn(Optional.empty()); + + final JsonRpcResponse response = + method.response(requestWithParentHash(Hash.ZERO.toHexString())); + + assertThat(response).isInstanceOf(JsonRpcErrorResponse.class); + final JsonRpcErrorResponse errorResponse = (JsonRpcErrorResponse) response; + assertThat(errorResponse.getError().getCode()).isEqualTo(RpcErrorType.INVALID_PARAMS.getCode()); + } + + @Test + void shouldReturnErrorForInvalidTransactionRlp() { + final BlockHeader parentHeader = + new BlockHeaderTestFixture().timestamp(DEFAULT_TIMESTAMP).buildHeader(); + when(blockchain.getBlockHeader(any(Hash.class))).thenReturn(Optional.of(parentHeader)); + + final JsonRpcResponse response = + method.response( + requestWithTransactions( + parentHeader.getHash().toHexString(), + new String[] {"0xINVALIDRLP"}, + DEFAULT_TIMESTAMP + 1)); + + assertThat(response).isInstanceOf(JsonRpcErrorResponse.class); + final JsonRpcErrorResponse errorResponse = (JsonRpcErrorResponse) response; + assertThat(errorResponse.getError().getCode()) + .isEqualTo(RpcErrorType.INVALID_TRANSACTION_PARAMS.getCode()); + } + + @Test + void shouldReturnErrorWhenZeroTimestamp() { + final BlockHeader parentHeader = new BlockHeaderTestFixture().buildHeader(); + when(blockchain.getBlockHeader(any(Hash.class))).thenReturn(Optional.of(parentHeader)); + + final JsonRpcResponse response = + method.response(requestWithTimestamp(parentHeader.getHash().toHexString(), 0L)); + + assertThat(response).isInstanceOf(JsonRpcErrorResponse.class); + final JsonRpcErrorResponse errorResponse = (JsonRpcErrorResponse) response; + assertThat(errorResponse.getError().getCode()).isEqualTo(RpcErrorType.INVALID_PARAMS.getCode()); + } + + @Test + void shouldReturnErrorWhenMissingSuggestedFeeRecipient() { + final BlockHeader parentHeader = + new BlockHeaderTestFixture().timestamp(DEFAULT_TIMESTAMP).buildHeader(); + when(blockchain.getBlockHeader(any(Hash.class))).thenReturn(Optional.of(parentHeader)); + + final JsonRpcResponse response = + method.response( + requestWithMissingSuggestedFeeRecipient( + parentHeader.getHash().toHexString(), DEFAULT_TIMESTAMP + 1)); + + assertThat(response).isInstanceOf(JsonRpcErrorResponse.class); + final JsonRpcErrorResponse errorResponse = (JsonRpcErrorResponse) response; + assertThat(errorResponse.getError().getCode()).isEqualTo(RpcErrorType.INVALID_PARAMS.getCode()); + } + + @Test + void verifyBlockAccessListEncodingFormat() { + final BlockAccessList blockAccessList = createSampleBlockAccessList(); + final String encoded = encodeBlockAccessList(blockAccessList); + + assertThat(encoded).isNotNull(); + assertThat(encoded).startsWith("0x"); + assertThat(encoded.length()).isGreaterThan(2); + } + + @Test + void verifyBlockAccessListEncodingWithMultipleAccounts() { + final BlockAccessList blockAccessList = createBlockAccessListWithMultipleAccounts(); + final String encoded = encodeBlockAccessList(blockAccessList); + + assertThat(encoded).isNotNull(); + assertThat(encoded).startsWith("0x"); + } + + @Test + void verifyEmptyBlockAccessListEncoding() { + final BlockAccessList emptyBlockAccessList = new BlockAccessList(List.of()); + final String encoded = encodeBlockAccessList(emptyBlockAccessList); + + assertThat(encoded).isNotNull(); + assertThat(encoded).startsWith("0x"); + } + + @Test + void verifyBlockAccessListWithOnlyBalanceChanges() { + final Address address = Address.fromHexString("0x0000000000000000000000000000000000000001"); + final BlockAccessList blockAccessList = + new BlockAccessList( + List.of( + new AccountChanges( + address, + List.of(), + List.of(), + List.of( + new BalanceChange(0, Wei.fromEth(1)), new BalanceChange(1, Wei.fromEth(2))), + List.of(), + List.of()))); + + final String encoded = encodeBlockAccessList(blockAccessList); + + assertThat(encoded).isNotNull(); + assertThat(encoded).startsWith("0x"); + } + + @Test + void verifyBlockAccessListWithStorageChanges() { + final Address address = Address.fromHexString("0x0000000000000000000000000000000000000001"); + final StorageSlotKey slotKey1 = new StorageSlotKey(UInt256.ONE); + final StorageSlotKey slotKey2 = new StorageSlotKey(UInt256.valueOf(2)); + + final BlockAccessList blockAccessList = + new BlockAccessList( + List.of( + new AccountChanges( + address, + List.of( + new SlotChanges( + slotKey1, + List.of( + new StorageChange(0, UInt256.valueOf(100)), + new StorageChange(1, UInt256.valueOf(200)))), + new SlotChanges( + slotKey2, List.of(new StorageChange(0, UInt256.valueOf(300))))), + List.of(), + List.of(), + List.of(), + List.of()))); + + final String encoded = encodeBlockAccessList(blockAccessList); + + assertThat(encoded).isNotNull(); + assertThat(encoded).startsWith("0x"); + } + + @Test + void verifyBlockAccessListWithStorageReads() { + final Address address = Address.fromHexString("0x0000000000000000000000000000000000000001"); + final StorageSlotKey slotKey1 = new StorageSlotKey(UInt256.ONE); + final StorageSlotKey slotKey2 = new StorageSlotKey(UInt256.valueOf(2)); + + final BlockAccessList blockAccessList = + new BlockAccessList( + List.of( + new AccountChanges( + address, + List.of(), + List.of(new SlotRead(slotKey1), new SlotRead(slotKey2)), + List.of(), + List.of(), + List.of()))); + + final String encoded = encodeBlockAccessList(blockAccessList); + + assertThat(encoded).isNotNull(); + assertThat(encoded).startsWith("0x"); + } + + @Test + void verifyBlockAccessListWithNonceChanges() { + final Address address = Address.fromHexString("0x0000000000000000000000000000000000000001"); + final BlockAccessList blockAccessList = + new BlockAccessList( + List.of( + new AccountChanges( + address, + List.of(), + List.of(), + List.of(), + List.of(new NonceChange(0, 1L), new NonceChange(1, 2L)), + List.of()))); + + final String encoded = encodeBlockAccessList(blockAccessList); + + assertThat(encoded).isNotNull(); + assertThat(encoded).startsWith("0x"); + } + + private static BlockAccessList createSampleBlockAccessList() { + final Address address = Address.fromHexString("0x0000000000000000000000000000000000000001"); + final StorageSlotKey slotKey = new StorageSlotKey(UInt256.ONE); + final SlotChanges slotChanges = + new SlotChanges(slotKey, List.of(new StorageChange(0, UInt256.valueOf(2)))); + return new BlockAccessList( + List.of( + new AccountChanges( + address, + List.of(slotChanges), + List.of(new SlotRead(slotKey)), + List.of(new BalanceChange(0, Wei.ONE)), + List.of(new NonceChange(0, 1L)), + List.of()))); + } + + private static BlockAccessList createBlockAccessListWithMultipleAccounts() { + final Address address1 = Address.fromHexString("0x0000000000000000000000000000000000000001"); + final Address address2 = Address.fromHexString("0x0000000000000000000000000000000000000002"); + final Address address3 = Address.fromHexString("0x0000000000000000000000000000000000000003"); + + final StorageSlotKey slotKey1 = new StorageSlotKey(UInt256.ONE); + final StorageSlotKey slotKey2 = new StorageSlotKey(UInt256.valueOf(2)); + + return new BlockAccessList( + List.of( + new AccountChanges( + address1, + List.of( + new SlotChanges(slotKey1, List.of(new StorageChange(0, UInt256.valueOf(10))))), + List.of(), + List.of(new BalanceChange(0, Wei.fromEth(1))), + List.of(new NonceChange(0, 1L)), + List.of()), + new AccountChanges( + address2, + List.of(), + List.of(new SlotRead(slotKey2)), + List.of(new BalanceChange(0, Wei.fromEth(2))), + List.of(), + List.of()), + new AccountChanges( + address3, + List.of(), + List.of(), + List.of(new BalanceChange(0, Wei.fromEth(3))), + List.of(), + List.of()))); + } + + private static String encodeBlockAccessList(final BlockAccessList blockAccessList) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + blockAccessList.writeTo(output); + return output.encoded().toHexString(); + } + + private JsonRpcRequestContext requestWithParentHash(final String parentHash) { + return requestWithTransactions(parentHash, new String[0], DEFAULT_TIMESTAMP + 1); + } + + private JsonRpcRequestContext requestWithTimestamp( + final String parentHash, final long timestamp) { + return requestWithTransactions(parentHash, new String[0], timestamp); + } + + private JsonRpcRequestContext requestWithTransactions( + final String parentHash, final String[] transactions, final long timestamp) { + final Map payloadAttributes = new LinkedHashMap<>(); + payloadAttributes.put("timestamp", Bytes.ofUnsignedLong(timestamp).toQuantityHexString()); + payloadAttributes.put("prevRandao", Hash.ZERO.toHexString()); + payloadAttributes.put("suggestedFeeRecipient", "0x0000000000000000000000000000000000000000"); + payloadAttributes.put("withdrawals", Collections.emptyList()); + payloadAttributes.put("parentBeaconBlockRoot", Bytes32.ZERO.toHexString()); + + return new JsonRpcRequestContext( + new JsonRpcRequest( + "2.0", + "testing_buildBlockV1", + new Object[] {parentHash, payloadAttributes, transactions, null})); + } + + private JsonRpcRequestContext requestWithMissingSuggestedFeeRecipient( + final String parentHash, final long timestamp) { + final Map payloadAttributes = new LinkedHashMap<>(); + payloadAttributes.put("timestamp", Bytes.ofUnsignedLong(timestamp).toQuantityHexString()); + payloadAttributes.put("prevRandao", Hash.ZERO.toHexString()); + payloadAttributes.put("withdrawals", Collections.emptyList()); + payloadAttributes.put("parentBeaconBlockRoot", Bytes32.ZERO.toHexString()); + + return new JsonRpcRequestContext( + new JsonRpcRequest( + "2.0", + "testing_buildBlockV1", + new Object[] {parentHash, payloadAttributes, new String[0], null})); + } +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/01_parent_not_found.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/01_parent_not_found.json new file mode 100644 index 00000000000..199139608b7 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/01_parent_not_found.json @@ -0,0 +1,30 @@ +{ + "description": "Error case: parentBlockHash refers to a block that does not exist in the chain. Expects INVALID_PARAMS with 'Parent block not found' message.", + "request": { + "id": 1, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + "0x3b8fb240d288781d4f1e1d32f4c15500beefdeadbeefdeadbeefdeadbeefdead", + { + "timestamp": "0x6705D918", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000", + "withdrawals": [], + "parentBeaconBlockRoot": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884365149a42212e8822" + }, + [], + "0x" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32602, + "message": "Invalid params", + "data": "Parent block not found: 0x3b8fb240d288781d4f1e1d32f4c15500beefdeadbeefdeadbeefdeadbeefdead" + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/02_missing_parent_hash.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/02_missing_parent_hash.json new file mode 100644 index 00000000000..2955352165e --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/02_missing_parent_hash.json @@ -0,0 +1,29 @@ +{ + "description": "Error case: parentBlockHash (index 0) is null. Expects INVALID_PARAMS error.", + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + null, + { + "timestamp": "0x6705D918", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000", + "withdrawals": [], + "parentBeaconBlockRoot": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884365149a42212e8822" + }, + [], + "0x" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "error": { + "code": -32602, + "message": "Invalid params" + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/03_missing_payload_attributes.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/03_missing_payload_attributes.json new file mode 100644 index 00000000000..475e91ff978 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/03_missing_payload_attributes.json @@ -0,0 +1,23 @@ +{ + "description": "Error case: payloadAttributes (index 1) is null. Expects INVALID_PARAMS error.", + "request": { + "id": 3, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + null, + [], + "0x" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "error": { + "code": -32602, + "message": "Invalid params" + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/04_empty_params.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/04_empty_params.json new file mode 100644 index 00000000000..ee9ae73ef3c --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/04_empty_params.json @@ -0,0 +1,18 @@ +{ + "description": "Error case: params array is empty (no parameter object at index 0). Expects INVALID_PARAMS error.", + "request": { + "id": 4, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 4, + "error": { + "code": -32602, + "message": "Invalid params" + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/05_valid_parent_hash.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/05_valid_parent_hash.json new file mode 100644 index 00000000000..1240b80fa21 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/05_valid_parent_hash.json @@ -0,0 +1,56 @@ +{ + "description": "Build an empty block on top of the genesis block. No transactions are included (gasUsed=0). Verifies that the block is correctly assembled with the genesis stateRoot as parent and that Amsterdam fork is active (baseFeePerGas=0x7).", + "request": { + "id": 6, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + { + "timestamp": "0x1", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000", + "withdrawals": [], + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + [], + "0x" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 6, + "result": { + "executionPayload": { + "parentHash": "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + "feeRecipient": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x2e71e705fe1545f51b471f7f19e1209d42fb5a2d831835fb203ec71b04b4c0e1", + "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x47e7c40", + "gasUsed": "0x0", + "timestamp": "0x1", + "extraData": "0x", + "baseFeePerGas": "0x7", + "excessBlobGas": "0x0", + "blobGasUsed": "0x0", + "blockAccessList": "0xf8a4de9400000961ef480eb55e80d19ad83579a64c007002c0c480010203c0c0c0de940000bbddc7ce488642fb579f8b00f3a590007251c0c480010203c0c0c0f840940000f90827f1c53a10cb7a02335b175320002935e6e580e3e280a0937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6c0c0c0c0e394000f3df6d732807ef1319fb7b8bb8522d0beac02c6c501c3c28001c3822000c0c0c0", + "slotNumber": null, + "transactions": [], + "withdrawals": [], + "blockNumber": "0x1", + "blockHash": "0x7b4e4ab65d3ad3791d91eeccffdbb9e5ae1fa5063727eb5ede51b3a82dfd445c" + }, + "blockValue": "0x0", + "blobsBundle": { + "commitments": [], + "proofs": [], + "blobs": [] + }, + "shouldOverrideBuilder": false, + "executionRequests": [] + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/06_with_transaction.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/06_with_transaction.json new file mode 100644 index 00000000000..2c0766922ee --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/06_with_transaction.json @@ -0,0 +1,61 @@ +{ + "description": "Build a block containing one EIP-1559 ETH transfer. Sender 0xfe3b557e (funded with ~12.5 ETH in genesis) sends 1000 wei to 0x627306090a. The transaction is included: gasUsed=21000 (0x5208), nonce=0, maxFeePerGas=0x10 (> genesis baseFee 0x7), chainId=3503995874084926.", + "request": { + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + { + "timestamp": "0x1", + "slotNumber": "0x2a", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000", + "withdrawals": [], + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + [ + "0x02f86b870c72dd9d5e883e80801082520894627306090abab3a6e1400e9345bc60c78a8bef578203e880c080a087b5ee5be96494e587c6f97c5fdb819c798cf7c5f4fad99ab1c365133c1b2feaa05a1bd572f15f968195d8a7afa535a04897a43f98f005ad581c92503fef4c6b1a" + ], + "0x" + ], + "id": 7 + }, + "response": { + "jsonrpc": "2.0", + "id": 7, + "result": { + "executionPayload": { + "parentHash": "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + "feeRecipient": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x6dfe5615c5bfa96c38d4c19c32f4ee7a80c96b039794e1401a649416b99d1e5c", + "receiptsRoot": "0x6d3683d8764c1015a7d49cf3e3d4c295074cb7855dddb9324863bf59ffdda8b8", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000020000000000000000000000000010000008000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000010000000002000000000000000000000000000000000000000000000000000000000000000000000002000000000208000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x47e7c40", + "gasUsed": "0x5208", + "timestamp": "0x1", + "extraData": "0x", + "baseFeePerGas": "0x7", + "excessBlobGas": "0x0", + "blobGasUsed": "0x0", + "blockAccessList": "0xf90116da940000000000000000000000000000000000000000c0c0c0c0c0de9400000961ef480eb55e80d19ad83579a64c007002c0c480010203c0c0c0de940000bbddc7ce488642fb579f8b00f3a590007251c0c480010203c0c0c0f840940000f90827f1c53a10cb7a02335b175320002935e6e580e3e280a0937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6c0c0c0c0e394000f3df6d732807ef1319fb7b8bb8522d0beac02c6c501c3c28001c3822000c0c0c0ec94627306090abab3a6e1400e9345bc60c78a8bef57c0c0d2d1018fc097ce7bc90715b34b9f10000003e8c0c0e994fe3b557e8fb62b89f4916b721be55ceb828dbd73c0c0cccb01890ad78ebc5ac61dbde0c3c20101c0", + "slotNumber": "0x2a", + "transactions": [ + "0x02f86b870c72dd9d5e883e80801082520894627306090abab3a6e1400e9345bc60c78a8bef578203e880c080a087b5ee5be96494e587c6f97c5fdb819c798cf7c5f4fad99ab1c365133c1b2feaa05a1bd572f15f968195d8a7afa535a04897a43f98f005ad581c92503fef4c6b1a" + ], + "withdrawals": [], + "blockNumber": "0x1", + "blockHash": "0x4982fbd45800a9a6aa767edaec0922eaf0b72d4d820bb16bff13d6f49f95b62d" + }, + "blockValue": "0x0", + "blobsBundle": { + "commitments": [], + "proofs": [], + "blobs": [] + }, + "shouldOverrideBuilder": false, + "executionRequests": [] + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/07_invalid_timestamp_zero.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/07_invalid_timestamp_zero.json new file mode 100644 index 00000000000..6b9b7bb983f --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/07_invalid_timestamp_zero.json @@ -0,0 +1,30 @@ +{ + "description": "Error case: timestamp is 0 in payloadAttributes. Expects INVALID_PARAMS with 'Missing or invalid timestamp field'. Note: with amsterdamTime=0 in genesis, fork validation passes (0>=0), then attribute validation rejects timestamp=0.", + "request": { + "id": 8, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + { + "timestamp": "0x0", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000", + "withdrawals": [], + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + [], + "0x" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 8, + "error": { + "code": -32602, + "message": "Invalid params", + "data": "Missing or invalid timestamp field" + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/08_missing_prev_randao.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/08_missing_prev_randao.json new file mode 100644 index 00000000000..6079dbb0d91 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/08_missing_prev_randao.json @@ -0,0 +1,28 @@ +{ + "description": "Error case: prevRandao field is absent from payloadAttributes. Note: the error has no 'data' field because Jackson deserialization fails before reaching validatePayloadAttributes(), producing a generic INVALID_PARAMS response.", + "request": { + "id": 9, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + { + "timestamp": "0x1", + "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000", + "withdrawals": [], + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + [], + "0x" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 9, + "error": { + "code": -32602, + "message": "Invalid params" + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/09_missing_suggested_fee_recipient.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/09_missing_suggested_fee_recipient.json new file mode 100644 index 00000000000..95e0ed7a0d1 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/09_missing_suggested_fee_recipient.json @@ -0,0 +1,29 @@ +{ + "description": "Error case: suggestedFeeRecipient field is absent from payloadAttributes. Expects INVALID_PARAMS with 'Missing suggestedFeeRecipient field'.", + "request": { + "id": 10, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + { + "timestamp": "0x1", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "withdrawals": [], + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + [], + "0x" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 10, + "error": { + "code": -32602, + "message": "Invalid params", + "data": "Missing suggestedFeeRecipient field" + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/10_txpool_with_transaction.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/10_txpool_with_transaction.json new file mode 100644 index 00000000000..bc7b53bb728 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/10_txpool_with_transaction.json @@ -0,0 +1,59 @@ +{ + "description": "Build a block using transactions from the transaction pool. When transactions parameter is null, the block creator should pull pending transactions from the mempool. The txpool contains one transaction which is included in the block (gasUsed=0x5208).", + "request": { + "id": 12, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + { + "timestamp": "0x1", + "slotNumber": "0x2a", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000", + "withdrawals": [], + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + null, + "0x" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 12, + "result": { + "executionPayload": { + "parentHash": "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + "feeRecipient": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x6dfe5615c5bfa96c38d4c19c32f4ee7a80c96b039794e1401a649416b99d1e5c", + "receiptsRoot": "0x6d3683d8764c1015a7d49cf3e3d4c295074cb7855dddb9324863bf59ffdda8b8", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000020000000000000000000000000010000008000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000010000000002000000000000000000000000000000000000000000000000000000000000000000000002000000000208000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x47e7c40", + "gasUsed": "0x5208", + "timestamp": "0x1", + "extraData": "0x", + "baseFeePerGas": "0x7", + "excessBlobGas": "0x0", + "blobGasUsed": "0x0", + "blockAccessList": "0xf90116da940000000000000000000000000000000000000000c0c0c0c0c0de9400000961ef480eb55e80d19ad83579a64c007002c0c480010203c0c0c0de940000bbddc7ce488642fb579f8b00f3a590007251c0c480010203c0c0c0f840940000f90827f1c53a10cb7a02335b175320002935e6e580e3e280a0937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6c0c0c0c0e394000f3df6d732807ef1319fb7b8bb8522d0beac02c6c501c3c28001c3822000c0c0c0ec94627306090abab3a6e1400e9345bc60c78a8bef57c0c0d2d1018fc097ce7bc90715b34b9f10000003e8c0c0e994fe3b557e8fb62b89f4916b721be55ceb828dbd73c0c0cccb01890ad78ebc5ac61dbde0c3c20101c0", + "slotNumber": "0x2a", + "transactions": [ + "0x02f86b870c72dd9d5e883e80801082520894627306090abab3a6e1400e9345bc60c78a8bef578203e880c080a087b5ee5be96494e587c6f97c5fdb819c798cf7c5f4fad99ab1c365133c1b2feaa05a1bd572f15f968195d8a7afa535a04897a43f98f005ad581c92503fef4c6b1a" + ], + "withdrawals": [], + "blockNumber": "0x1", + "blockHash": "0x4982fbd45800a9a6aa767edaec0922eaf0b72d4d820bb16bff13d6f49f95b62d" + }, + "blockValue": "0x0", + "blobsBundle": { + "commitments": [], + "proofs": [], + "blobs": [] + }, + "shouldOverrideBuilder": false, + "executionRequests": [] + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/11_invalid_transaction.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/11_invalid_transaction.json new file mode 100644 index 00000000000..f1feea88baa --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/11_invalid_transaction.json @@ -0,0 +1,32 @@ +{ + "description": "Build a block with an invalid raw transaction. The transaction hex is malformed and should fail to decode.", + "request": { + "id": 13, + "jsonrpc": "2.0", + "method": "testing_buildBlockV1", + "params": [ + "0x937c8f382f4945fc6122652239426484d97b400d66c122aea45f2dd4a8afeab6", + { + "timestamp": "0x1", + "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", + "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000", + "withdrawals": [], + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + [ + "0xdeadbeef" + ], + "0x" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 13, + "error": { + "code": -32602, + "message": "Invalid transaction params (missing or incorrect)", + "data": "Failed to decode transaction: Invalid RLP in raw transaction hex" + } + }, + "statusCode": 200 +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/chain-data/blocks.bin b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/chain-data/blocks.bin new file mode 100644 index 00000000000..4d564f201e6 Binary files /dev/null and b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/chain-data/blocks.bin differ diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/chain-data/genesis.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/chain-data/genesis.json new file mode 100644 index 00000000000..214875a35a5 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/testing_buildBlockV1/chain-data/genesis.json @@ -0,0 +1,81 @@ +{ + "config": { + "ethash": {}, + "chainId": 3503995874084926, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "constantinopleFixBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "mergeNetsplitBlock": 0, + "terminalTotalDifficulty": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "pragueTime": 0, + "amsterdamTime": 0, + "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa", + "withdrawalRequestContractAddress": "0x00000961ef480eb55e80d19ad83579a64c007002", + "consolidationRequestContractAddress": "0x0000bbddc7ce488642fb579f8b00f3a590007251", + "blobSchedule": { + "cancun": { + "target": 3, + "max": 6, + "baseFeeUpdateFraction": 3338477 + }, + "prague": { + "target": 6, + "max": 9, + "baseFeeUpdateFraction": 5007716 + } + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x", + "gasLimit": "0x47e7c40", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "00000000219ab540356cbb839cbe05303d7705fa": { + "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179f6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806117d56029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526026815260200180611779602691396040019150506040519081900390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117456033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061181b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a08082018352602080820196875281019490945260608085019390935260808085019290925281518083038201815292830181528151918201919091206000546001546002546003546004546040805196875260208701959095528585019390935260608501919091526080840152519081900360a0019a509098509650505050505050a1505050505050505050565b6060806040519050835180825260208201818101602087015b8282101561062957600081526020016105ee565b50505090500190506060836040519080825280601f01601f19166020018201604052801561066657602081008301600160200360800101838501826000602002015b8151815260200183019190815260200161064755565b505050905060006201000090505b83518110156106d857828282815181101561068b57fe5b60209101015160f81c60028302016201000002178152505060028282815181101561068b57fe5b6020818101805160028702016201000001179052505b8151811015610702576000905060018101905061066457565b600091905060008490505b8681101561073357808101808c821a179250505b60208101905061070d565b60008202905060006101000a81549060ff021916905550855160208501808301805160200160408701604089016060890160e0880184870260208002018051838501965b828110156107a857878101600802835160c081018103510151805160018901945095505050506107705250505050505050565b506040805180820190915290810186825b60208101905061073357600060019081018082555b815181101561080857805160ff1660018082016003820102179190915550506002810190508290506107d5565b6108186020600186900302610e25565b905090565b604080519050835180825260208201818101602087015b82821015610848576000815260200161083557565b50505090500190506060836040519080825280601f01601f191660200182016040528015610885578160200160208202803683370190505b5090506000816020820201905083518101905060005b82518110156108cc57838181518110156108b1578181518110156108c757600052602051602081510151602052603051515050505b5060018101905061089b565b806000806101000a81549060ff021916905560608401805160208086019190915260408601805160408087019190915260608701805160608801919091520190528101600052602051602081510151602052603051602081510151512090506109346020546114ba565b604080519080825280601f01601f19166020018201604052801561097157602081008301600160200360800101838501826000602002015b81518152602001830191908152602001610958565b505050905060004660018282600052602081510151602052603051602081510151512091505b8481101561090057828181518110156108c757818151811015610a1a57600052602051602081510151602052603051515050505b50600181019050610a1a565b50505050610dac565b6000816040519080825280601f01601f191660200182016040528015610a5457602081008301600160200360800101838501826000602002015b8151815260200183019190915260200190565b506000610a766000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610ab56000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610af46000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610b336000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610b726000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610bb16000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610bf06000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610c2f6000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610c6e6000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610cad6000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610cec6000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610d2b6000600190810190816020026040518082905b8082101561062957600081526020016105ee565b91505060609050600060609050600060209050610d6a6000600190810190816020026040518082905b8082101561062957600081526020016105ee565b9150506060905050505050905090505050905050909192939495969798909192939495969798", + "balance": "0x0" + }, + "00000961ef480eb55e80d19ad83579a64c007002": { + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f457600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603814608857366101f457346101f4575f5260205ff35b34106101f457600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101835782810160030260040181604c02815460601b8152601401816001015481526020019060020154807fffffffffffffffffffffffffffffffff00000000000000000000000000000000168252906010019060401c908160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160e1565b910180921461019557906002556101a0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101cd57505f5b6001546002828201116101e25750505f6101e8565b01600290035b5f555f600155604c025ff35b5f5ffd", + "balance": "0x1" + }, + "0000bbddc7ce488642fb579f8b00f3a590007251": { + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd", + "balance": "0x1" + }, + "0000f90827f1c53a10cb7a02335b175320002935": { + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500", + "balance": "0x1" + }, + "000f3df6d732807ef1319fb7b8bb8522d0beac02": { + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500", + "balance": "0x2a" + }, + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "known test account", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": "0x7", + "excessBlobGas": "0x0", + "blobGasUsed": "0x0" +} diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java index 9433e45f98d..c030aa2e43a 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java @@ -354,7 +354,8 @@ public BlockCreationResult createBlock( operationTracer.traceEndBlock(blockHeader, blockBody); timings.register("blockAssembled"); - return new BlockCreationResult(block, transactionResults, timings, blockAccessList); + return new BlockCreationResult( + block, transactionResults, timings, blockAccessList, maybeRequests); } catch (final SecurityModuleException ex) { throw new IllegalStateException("Failed to create block signature", ex); } catch (final CancellationException | StorageException ex) { diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/BlockCreator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/BlockCreator.java index faf822f5db8..e2286e955b8 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/BlockCreator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/BlockCreator.java @@ -17,6 +17,7 @@ import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.Request; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; @@ -29,16 +30,27 @@ class BlockCreationResult { private final TransactionSelectionResults transactionSelectionResults; private final BlockCreationTiming blockCreationTiming; private final Optional blockAccessList; + private final Optional> requests; public BlockCreationResult( final Block block, final TransactionSelectionResults transactionSelectionResults, final BlockCreationTiming timings, final Optional blockAccessList) { + this(block, transactionSelectionResults, timings, blockAccessList, Optional.empty()); + } + + public BlockCreationResult( + final Block block, + final TransactionSelectionResults transactionSelectionResults, + final BlockCreationTiming timings, + final Optional blockAccessList, + final Optional> requests) { this.block = block; this.transactionSelectionResults = transactionSelectionResults; this.blockCreationTiming = timings; this.blockAccessList = blockAccessList; + this.requests = requests; } public Block getBlock() { @@ -56,6 +68,10 @@ public BlockCreationTiming getBlockCreationTimings() { public Optional getBlockAccessList() { return blockAccessList; } + + public Optional> getRequests() { + return requests; + } } BlockCreationResult createBlock(final long timestamp, final BlockHeader parentHeader); diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java new file mode 100644 index 00000000000..de598bac6f0 --- /dev/null +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/TestingBuildBlockIntegrationTest.java @@ -0,0 +1,513 @@ +/* + * 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.blockcreation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.config.GenesisAccount; +import org.hyperledger.besu.config.GenesisConfig; +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPPrivateKey; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.blockcreation.BlockCreator.BlockCreationResult; +import org.hyperledger.besu.ethereum.chain.BadBlockManager; +import org.hyperledger.besu.ethereum.chain.MutableBlockchain; +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.ExecutionContextTestFixture; +import org.hyperledger.besu.ethereum.core.ImmutableMiningConfiguration; +import org.hyperledger.besu.ethereum.core.ImmutableMiningConfiguration.MutableInitValues; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.SealableBlockHeader; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.ethereum.eth.manager.EthContext; +import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; +import org.hyperledger.besu.ethereum.eth.transactions.BlobCache; +import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionBroadcaster; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; +import org.hyperledger.besu.ethereum.eth.transactions.sorter.AbstractPendingTransactionsSorter; +import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter; +import org.hyperledger.besu.ethereum.mainnet.ImmutableBalConfiguration; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ProtocolScheduleBuilder; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpecAdapters; +import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; +import org.hyperledger.besu.ethereum.mainnet.TransactionValidator; +import org.hyperledger.besu.ethereum.mainnet.TransactionValidatorFactory; +import org.hyperledger.besu.ethereum.mainnet.ValidationResult; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.AccountChanges; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessListFactory; +import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; +import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.internal.EvmConfiguration; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; +import org.hyperledger.besu.testutil.DeterministicEthScheduler; + +import java.math.BigInteger; +import java.time.Clock; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * Integration tests for block creation with and without Block Access List (BAL). These tests verify + * the full block creation flow similar to what testing_buildBlockV1 would do. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class TestingBuildBlockIntegrationTest { + + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + protected final GenesisConfig genesisConfig = + GenesisConfig.fromResource("/block-creation-genesis.json"); + + protected final List accounts = + genesisConfig.streamAllocations().filter(ga -> ga.privateKey() != null).toList(); + + protected EthScheduler ethScheduler = new DeterministicEthScheduler(); + + @Test + void shouldCreateBlockWithBAL() { + final TestContext context = createTestContextWithBAL(); + final GenesisAccount sender = accounts.get(1); + final GenesisAccount recipient = accounts.get(2); + final KeyPair keyPair = + SIGNATURE_ALGORITHM + .get() + .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + + final Transaction txn = + new TransactionTestFixture() + .sender(sender.address()) + .to(Optional.of(recipient.address())) + .value(Wei.fromEth(1)) + .gasLimit(21_000L) + .nonce(sender.nonce()) + .createTransaction(keyPair); + + final BlockCreationResult result = + context.blockCreator.createBlock( + Optional.of(List.of(txn)), + Optional.empty(), + context.parentHeader.getTimestamp() + 1L, + context.parentHeader); + + assertThat(result).isNotNull(); + assertThat(result.getBlock()).isNotNull(); + assertThat(result.getBlock().getBody().getTransactions()).hasSize(1); + + final Optional maybeBAL = result.getBlockAccessList(); + assertThat(maybeBAL).isPresent(); + + final BlockAccessList bal = maybeBAL.get(); + assertThat(bal.accountChanges()).isNotEmpty(); + + final List accountChanges = bal.accountChanges(); + assertThat(accountChanges.size()).isGreaterThanOrEqualTo(2); + + boolean foundSender = false; + boolean foundRecipient = false; + for (AccountChanges ac : accountChanges) { + if (ac.address().equals(sender.address())) { + foundSender = true; + assertThat(ac.balanceChanges()).isNotEmpty(); + assertThat(ac.nonceChanges()).isNotEmpty(); + } + if (ac.address().equals(recipient.address())) { + foundRecipient = true; + assertThat(ac.balanceChanges()).isNotEmpty(); + } + } + assertThat(foundSender).isTrue(); + assertThat(foundRecipient).isTrue(); + } + + @Test + void shouldCreateBlockWithoutBAL() { + final TestContext context = createTestContextWithoutBAL(); + final GenesisAccount sender = accounts.get(1); + final GenesisAccount recipient = accounts.get(2); + final KeyPair keyPair = + SIGNATURE_ALGORITHM + .get() + .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + + final Transaction txn = + new TransactionTestFixture() + .sender(sender.address()) + .to(Optional.of(recipient.address())) + .value(Wei.fromEth(1)) + .gasLimit(21_000L) + .nonce(sender.nonce()) + .createTransaction(keyPair); + + final BlockCreationResult result = + context.blockCreator.createBlock( + Optional.of(List.of(txn)), + Optional.empty(), + context.parentHeader.getTimestamp() + 1L, + context.parentHeader); + + assertThat(result).isNotNull(); + assertThat(result.getBlock()).isNotNull(); + assertThat(result.getBlock().getBody().getTransactions()).hasSize(1); + + final Optional maybeBAL = result.getBlockAccessList(); + assertThat(maybeBAL).isEmpty(); + } + + @Test + void shouldCreateEmptyBlockWithBAL() { + final TestContext context = createTestContextWithBAL(); + + final BlockCreationResult result = + context.blockCreator.createBlock( + Optional.of(Collections.emptyList()), + Optional.empty(), + context.parentHeader.getTimestamp() + 1L, + context.parentHeader); + + assertThat(result).isNotNull(); + assertThat(result.getBlock()).isNotNull(); + assertThat(result.getBlock().getBody().getTransactions()).isEmpty(); + + final Optional maybeBAL = result.getBlockAccessList(); + assertThat(maybeBAL).isPresent(); + } + + @Test + void shouldCreateEmptyBlockWithoutBAL() { + final TestContext context = createTestContextWithoutBAL(); + + final BlockCreationResult result = + context.blockCreator.createBlock( + Optional.of(Collections.emptyList()), + Optional.empty(), + context.parentHeader.getTimestamp() + 1L, + context.parentHeader); + + assertThat(result).isNotNull(); + assertThat(result.getBlock()).isNotNull(); + assertThat(result.getBlock().getBody().getTransactions()).isEmpty(); + + final Optional maybeBAL = result.getBlockAccessList(); + assertThat(maybeBAL).isEmpty(); + } + + @Test + void shouldHaveValidBALStructure() { + final TestContext context = createTestContextWithBAL(); + final GenesisAccount sender = accounts.get(1); + final GenesisAccount recipient = accounts.get(2); + final KeyPair keyPair = + SIGNATURE_ALGORITHM + .get() + .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + + final Transaction txn = + new TransactionTestFixture() + .sender(sender.address()) + .to(Optional.of(recipient.address())) + .value(Wei.fromEth(1)) + .gasLimit(21_000L) + .nonce(sender.nonce()) + .createTransaction(keyPair); + + final BlockCreationResult result = + context.blockCreator.createBlock( + Optional.of(List.of(txn)), + Optional.empty(), + context.parentHeader.getTimestamp() + 1L, + context.parentHeader); + + final Optional maybeBAL = result.getBlockAccessList(); + assertThat(maybeBAL).isPresent(); + + final BlockAccessList bal = maybeBAL.get(); + assertThat(bal.accountChanges()).isNotNull(); + assertThat(bal.accountChanges()).isNotEmpty(); + + for (AccountChanges ac : bal.accountChanges()) { + assertThat(ac.address()).isNotNull(); + assertThat(ac.balanceChanges()).isNotNull(); + assertThat(ac.nonceChanges()).isNotNull(); + assertThat(ac.storageChanges()).isNotNull(); + assertThat(ac.storageReads()).isNotNull(); + assertThat(ac.codeChanges()).isNotNull(); + } + } + + @Test + void shouldIncludeMultipleTransactionsInBAL() { + final TestContext context = createTestContextWithBAL(); + final GenesisAccount sender = accounts.get(1); + final GenesisAccount recipient1 = accounts.get(2); + final GenesisAccount recipient2 = accounts.get(0); + final KeyPair keyPair = + SIGNATURE_ALGORITHM + .get() + .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + + final Transaction txn1 = + new TransactionTestFixture() + .sender(sender.address()) + .to(Optional.of(recipient1.address())) + .value(Wei.fromEth(1)) + .gasLimit(21_000L) + .nonce(sender.nonce()) + .createTransaction(keyPair); + + final Transaction txn2 = + new TransactionTestFixture() + .sender(sender.address()) + .to(Optional.of(recipient2.address())) + .value(Wei.fromEth(1)) + .gasLimit(21_000L) + .nonce(sender.nonce() + 1) + .createTransaction(keyPair); + + final BlockCreationResult result = + context.blockCreator.createBlock( + Optional.of(List.of(txn1, txn2)), + Optional.empty(), + context.parentHeader.getTimestamp() + 1L, + context.parentHeader); + + assertThat(result).isNotNull(); + assertThat(result.getBlock()).isNotNull(); + assertThat(result.getBlock().getBody().getTransactions()).hasSize(2); + + final Optional maybeBAL = result.getBlockAccessList(); + assertThat(maybeBAL).isPresent(); + + final BlockAccessList bal = maybeBAL.get(); + assertThat(bal.accountChanges().size()).isGreaterThanOrEqualTo(3); + } + + @Test + void shouldTrackNonceChangesInBAL() { + final TestContext context = createTestContextWithBAL(); + final GenesisAccount sender = accounts.get(1); + final GenesisAccount recipient = accounts.get(2); + final KeyPair keyPair = + SIGNATURE_ALGORITHM + .get() + .createKeyPair(SECPPrivateKey.create(sender.privateKey(), "ECDSA")); + + final Transaction txn = + new TransactionTestFixture() + .sender(sender.address()) + .to(Optional.of(recipient.address())) + .value(Wei.fromEth(1)) + .gasLimit(21_000L) + .nonce(sender.nonce()) + .createTransaction(keyPair); + + final BlockCreationResult result = + context.blockCreator.createBlock( + Optional.of(List.of(txn)), + Optional.empty(), + context.parentHeader.getTimestamp() + 1L, + context.parentHeader); + + final Optional maybeBAL = result.getBlockAccessList(); + assertThat(maybeBAL).isPresent(); + + final BlockAccessList bal = maybeBAL.get(); + final Optional senderChanges = + bal.accountChanges().stream() + .filter(ac -> ac.address().equals(sender.address())) + .findFirst(); + + assertThat(senderChanges).isPresent(); + assertThat(senderChanges.get().nonceChanges()).isNotEmpty(); + assertThat(senderChanges.get().nonceChanges().get(0).newNonce()).isEqualTo(sender.nonce() + 1); + } + + record TestContext(AbstractBlockCreator blockCreator, BlockHeader parentHeader) {} + + private TestContext createTestContextWithBAL() { + return createTestContext(true); + } + + private TestContext createTestContextWithoutBAL() { + return createTestContext(false); + } + + private TestContext createTestContext(final boolean withBAL) { + final var alwaysValidTransactionValidatorFactory = mock(TransactionValidatorFactory.class); + when(alwaysValidTransactionValidatorFactory.get()) + .thenReturn(new AlwaysValidTransactionValidator()); + + final ProtocolSpecAdapters protocolSpecAdapters = + ProtocolSpecAdapters.create( + 0, + specBuilder -> { + specBuilder.isReplayProtectionSupported(true); + if (withBAL) { + specBuilder.blockAccessListFactory(new BlockAccessListFactory()); + } + specBuilder.transactionValidatorFactoryBuilder( + (evm, gasLimitCalculator, feeMarket) -> alwaysValidTransactionValidatorFactory); + return specBuilder; + }); + + final ExecutionContextTestFixture executionContextTestFixture = + ExecutionContextTestFixture.builder(genesisConfig) + .protocolSchedule( + new ProtocolScheduleBuilder( + genesisConfig.getConfigOptions(), + Optional.of(BigInteger.valueOf(42)), + protocolSpecAdapters, + false, + EvmConfiguration.DEFAULT, + MiningConfiguration.MINING_DISABLED, + new BadBlockManager(), + false, + ImmutableBalConfiguration.builder() + .isBalOptimisationEnabled(withBAL) + .build(), + new NoOpMetricsSystem()) + .createProtocolSchedule()) + .dataStorageFormat(DataStorageFormat.BONSAI) + .build(); + + final MutableBlockchain blockchain = executionContextTestFixture.getBlockchain(); + final BlockHeader parentHeader = blockchain.getChainHeadHeader(); + final TransactionPoolConfiguration poolConf = + ImmutableTransactionPoolConfiguration.builder().txPoolMaxSize(100).build(); + final AbstractPendingTransactionsSorter sorter = + new GasPricePendingTransactionsSorter( + poolConf, + Clock.systemUTC(), + new NoOpMetricsSystem(), + Suppliers.ofInstance(parentHeader)); + + final EthContext ethContext = mock(EthContext.class, RETURNS_DEEP_STUBS); + when(ethContext.getEthPeers().subscribeConnect(any())).thenReturn(1L); + + final TransactionPool transactionPool = + new TransactionPool( + () -> sorter, + executionContextTestFixture.getProtocolSchedule(), + executionContextTestFixture.getProtocolContext(), + mock(TransactionBroadcaster.class), + ethContext, + new TransactionPoolMetrics(new NoOpMetricsSystem()), + poolConf, + new BlobCache()); + transactionPool.setEnabled(); + + final MiningConfiguration miningConfiguration = + ImmutableMiningConfiguration.builder() + .mutableInitValues( + MutableInitValues.builder() + .extraData(Bytes.fromHexString("deadbeef")) + .minTransactionGasPrice(Wei.ONE) + .minBlockOccupancyRatio(0d) + .coinbase(Address.ZERO) + .build()) + .build(); + + final TestBlockCreator blockCreator = + new TestBlockCreator( + miningConfiguration, + (__, ___) -> Address.ZERO, + __ -> Bytes.fromHexString("deadbeef"), + transactionPool, + executionContextTestFixture.getProtocolContext(), + executionContextTestFixture.getProtocolSchedule(), + ethScheduler); + + return new TestContext(blockCreator, parentHeader); + } + + static class TestBlockCreator extends AbstractBlockCreator { + + protected TestBlockCreator( + final MiningConfiguration miningConfiguration, + final MiningBeneficiaryCalculator miningBeneficiaryCalculator, + final ExtraDataCalculator extraDataCalculator, + final TransactionPool transactionPool, + final org.hyperledger.besu.ethereum.ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, + final EthScheduler ethScheduler) { + super( + miningConfiguration, + miningBeneficiaryCalculator, + extraDataCalculator, + transactionPool, + protocolContext, + protocolSchedule, + ethScheduler); + } + + @Override + protected BlockHeader createFinalBlockHeader(final SealableBlockHeader sealableBlockHeader) { + return BlockHeaderBuilder.create() + .difficulty(Difficulty.ZERO) + .populateFrom(sealableBlockHeader) + .mixHash(org.hyperledger.besu.datatypes.Hash.EMPTY) + .nonce(0L) + .blockHeaderFunctions(blockHeaderFunctions) + .buildBlockHeader(); + } + } + + static class AlwaysValidTransactionValidator implements TransactionValidator { + + @Override + public ValidationResult validate( + final Transaction transaction, + final Optional baseFee, + final Optional blobBaseFee, + final TransactionValidationParams transactionValidationParams) { + return ValidationResult.valid(); + } + + @Override + public ValidationResult validateForSender( + final Transaction transaction, + final Account sender, + final TransactionValidationParams validationParams) { + return ValidationResult.valid(); + } + } +}