diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java index 516379730a9..41baf774a85 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java @@ -45,6 +45,7 @@ import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.mainnet.BlockAccessListValidator; import org.hyperledger.besu.ethereum.mainnet.BlockGasAccountingStrategy; import org.hyperledger.besu.ethereum.mainnet.BlockGasUsedValidator; import org.hyperledger.besu.ethereum.mainnet.CancunTargetingGasLimitCalculator; @@ -178,6 +179,7 @@ public String description() { true, Optional.empty(), Optional.empty(), + BlockAccessListValidator.ALWAYS_REJECT_BAL, new DefaultStateRootCommitterFactory(), BlockGasAccountingStrategy.FRONTIER, BlockGasUsedValidator.FRONTIER); @@ -214,6 +216,7 @@ public String description() { true, Optional.empty(), Optional.empty(), + BlockAccessListValidator.ALWAYS_REJECT_BAL, new DefaultStateRootCommitterFactory(), BlockGasAccountingStrategy.FRONTIER, BlockGasUsedValidator.FRONTIER); diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java index 204fb8ff985..6779363e29a 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java @@ -30,6 +30,7 @@ import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.AbstractTransactionSelector; import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.BlobPriceTransactionSelector; import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.BlobSizeTransactionSelector; +import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.BlockAccessListItemBudgetTransactionSelector; import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.BlockRlpSizeTransactionSelector; import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.BlockSizeTransactionSelector; import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.MinPriorityFeePerGasTransactionSelector; @@ -125,6 +126,7 @@ public class BlockTransactionSelector implements BlockTransactionSelectionServic private final long blockTxsSelectionMaxTimeNanos; private final long pluginTxsSelectionMaxTimeNanos; private final Optional maybeBlockAccessListBuilder; + private WorldUpdater blockWorldStateUpdater; private WorldUpdater txWorldStateUpdater; private volatile TransactionEvaluationContext currTxEvaluationContext; @@ -165,7 +167,8 @@ public BlockTransactionSelector( this.selectorsStateManager = selectorsStateManager; this.transactionSelectionService = miningConfiguration.getTransactionSelectionService(); this.transactionSelectors = - createTransactionSelectors(blockSelectionContext, selectorsStateManager); + createTransactionSelectors( + blockSelectionContext, selectorsStateManager, maybeBlockAccessListBuilder); this.pluginTransactionSelector = pluginTransactionSelector; this.operationTracer = new InterruptibleOperationTracer(pluginTransactionSelector.getOperationTracer()); @@ -180,7 +183,9 @@ public BlockTransactionSelector( } private List createTransactionSelectors( - final BlockSelectionContext context, final SelectorsStateManager selectorsStateManager) { + final BlockSelectionContext context, + final SelectorsStateManager selectorsStateManager, + final Optional maybeBlockAccessListBuilder) { return List.of( new SkipSenderTransactionSelector(context), new BlockSizeTransactionSelector(context, selectorsStateManager), @@ -189,7 +194,8 @@ private List createTransactionSelectors( new BlobPriceTransactionSelector(context), new MinPriorityFeePerGasTransactionSelector(context), new BlockRlpSizeTransactionSelector(context, selectorsStateManager), - new ProcessingResultTransactionSelector(context)); + new ProcessingResultTransactionSelector(context), + new BlockAccessListItemBudgetTransactionSelector(context, maybeBlockAccessListBuilder)); } /** diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockAccessListItemBudgetTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockAccessListItemBudgetTransactionSelector.java new file mode 100644 index 00000000000..f2bfa14bbb6 --- /dev/null +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockAccessListItemBudgetTransactionSelector.java @@ -0,0 +1,86 @@ +/* + * 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.blockcreation.txselection.selectors; + +import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; +import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; +import org.hyperledger.besu.ethereum.mainnet.BlockAccessListItemSizeCheck; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Rejects a transaction when applying its partial block access view would exceed the EIP-7928 item + * budget for the block (same rule as block import in {@link + * org.hyperledger.besu.ethereum.mainnet.AbstractBlockProcessor}). + */ +public class BlockAccessListItemBudgetTransactionSelector extends AbstractTransactionSelector { + + private static final Logger LOG = + LoggerFactory.getLogger(BlockAccessListItemBudgetTransactionSelector.class); + + private final Optional maybeBlockAccessListBuilder; + + public BlockAccessListItemBudgetTransactionSelector( + final BlockSelectionContext context, + final Optional maybeBlockAccessListBuilder) { + super(context); + this.maybeBlockAccessListBuilder = maybeBlockAccessListBuilder; + } + + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + return TransactionSelectionResult.SELECTED; + } + + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + if (maybeBlockAccessListBuilder.isEmpty()) { + return TransactionSelectionResult.SELECTED; + } + final BlockAccessList.BlockAccessListBuilder mainBuilder = maybeBlockAccessListBuilder.get(); + final long itemCount; + if (processingResult.getPartialBlockAccessView().isEmpty()) { + itemCount = mainBuilder.eip7928ItemCount(); + } else { + final BlockAccessList committedSnapshot = mainBuilder.build(); + final BlockAccessList.BlockAccessListBuilder probe = BlockAccessList.builder(); + probe.mergeFrom(committedSnapshot); + probe.apply(processingResult.getPartialBlockAccessView().get()); + itemCount = probe.eip7928ItemCount(); + } + final BlockAccessListItemSizeCheck itemSizeCheck = + context + .protocolSpec() + .getBlockAccessListValidator() + .validateExecutedBlockAccessListItemSize( + itemCount, context.pendingBlockHeader(), context.protocolSpec()); + if (itemSizeCheck.isOverBudget()) { + LOG.trace( + "Transaction not selected: {}", + itemSizeCheck.overBudgetError().orElseThrow().errorMessage()); + return TransactionSelectionResult.BLOCK_ACCESS_LIST_ITEM_BUDGET_EXCEEDED; + } + return TransactionSelectionResult.SELECTED; + } +} diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java index 8035aff6493..ec276142067 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java @@ -75,6 +75,7 @@ import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStoragePrefixedKeyBlockchainStorage; import org.hyperledger.besu.ethereum.storage.keyvalue.VariablesKeyValueStorage; @@ -1682,7 +1683,27 @@ protected BlockTransactionSelector createBlockSelector( final Address miningBeneficiary, final Wei blobGasPrice, final TransactionSelectionService transactionSelectionService) { - ProtocolSpec protocolSpec = protocolSchedule.getByBlockHeader(blockchain.getChainHeadHeader()); + return createBlockSelector( + miningConfiguration, + transactionProcessor, + blockHeader, + miningBeneficiary, + blobGasPrice, + transactionSelectionService, + protocolSchedule, + Optional.empty()); + } + + protected BlockTransactionSelector createBlockSelector( + final MiningConfiguration miningConfiguration, + final MainnetTransactionProcessor transactionProcessor, + final ProcessableBlockHeader blockHeader, + final Address miningBeneficiary, + final Wei blobGasPrice, + final TransactionSelectionService transactionSelectionService, + final ProtocolSchedule schedule, + final Optional maybeBalBuilder) { + ProtocolSpec protocolSpec = schedule.getByBlockHeader(blockchain.getChainHeadHeader()); final var selectorsStateManager = new SelectorsStateManager(); final BlockTransactionSelector selector = new BlockTransactionSelector( @@ -1692,7 +1713,7 @@ protected BlockTransactionSelector createBlockSelector( worldState, transactionPool, blockHeader, - protocolSchedule.getByBlockHeader(blockHeader).getTransactionReceiptFactory(), + schedule.getByBlockHeader(blockHeader).getTransactionReceiptFactory(), miningBeneficiary, blobGasPrice, protocolSpec, @@ -1700,7 +1721,7 @@ protected BlockTransactionSelector createBlockSelector( blockHeader, selectorsStateManager), ethScheduler, selectorsStateManager, - Optional.empty()); + maybeBalBuilder); return selector; } @@ -1934,7 +1955,7 @@ public static TransactionSelectionResult invalid(final String invalidReason) { } } - protected enum Sender { + public enum Sender { // it is important to keep the addresses of the senders sorted, to make the tests reproducible, // since a different sender address can change the order in which txs are selected, // if all the other sorting fields are equal diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AmsterdamBalBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AmsterdamBalBlockTransactionSelectorTest.java new file mode 100644 index 00000000000..bd094f53664 --- /dev/null +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AmsterdamBalBlockTransactionSelectorTest.java @@ -0,0 +1,438 @@ +/* + * 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.blockcreation; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER1; +import static org.hyperledger.besu.ethereum.blockcreation.AbstractBlockTransactionSelectorTest.Sender.SENDER2; +import static org.hyperledger.besu.ethereum.core.MiningConfiguration.DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOCK_ACCESS_LIST_ITEM_BUDGET_EXCEEDED; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.config.GenesisConfig; +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.blockcreation.txselection.BlockTransactionSelector; +import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; +import org.hyperledger.besu.ethereum.chain.BadBlockManager; +import org.hyperledger.besu.ethereum.chain.DefaultBlockchain; +import org.hyperledger.besu.ethereum.chain.GenesisState; +import org.hyperledger.besu.ethereum.chain.MutableBlockchain; +import org.hyperledger.besu.ethereum.core.AddressHelpers; +import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder; +import org.hyperledger.besu.ethereum.core.Difficulty; +import org.hyperledger.besu.ethereum.core.ImmutableMiningConfiguration; +import org.hyperledger.besu.ethereum.core.ImmutableMiningConfiguration.MutableInitValues; +import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.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.PendingTransactions; +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.BaseFeePendingTransactionsSorter; +import org.hyperledger.besu.ethereum.mainnet.BalConfiguration; +import org.hyperledger.besu.ethereum.mainnet.BodyValidation; +import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions; +import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ProtocolScheduleBuilder; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpecAdapters; +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.PartialBlockAccessView; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStoragePrefixedKeyBlockchainStorage; +import org.hyperledger.besu.ethereum.storage.keyvalue.VariablesKeyValueStorage; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.CodeCache; +import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.internal.EvmConfiguration; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.TransactionSelectionService; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.hyperledger.besu.services.TransactionSelectionServiceImpl; +import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; +import org.hyperledger.besu.testutil.TestClock; +import org.hyperledger.besu.util.number.Fraction; +import org.hyperledger.besu.util.number.PositiveNumber; + +import java.math.BigInteger; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * EIP-7928 BAL item budget during block building requires Amsterdam (or later); this class is + * standalone so it does not inherit the full {@link AbstractBlockTransactionSelectorTest} suite + * (which targets London-era genesis). + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class AmsterdamBalBlockTransactionSelectorTest { + + private static final BigInteger CHAIN_ID = BigInteger.valueOf(42L); + + private final MetricsSystem metricsSystem = new NoOpMetricsSystem(); + private GenesisConfig genesisConfig; + private MutableBlockchain blockchain; + private TransactionPool transactionPool; + private MutableWorldState worldState; + private ProtocolSchedule protocolSchedule; + private TransactionSelectionService transactionSelectionService; + private MiningConfiguration defaultTestMiningConfiguration; + + @Mock private EthScheduler ethScheduler; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ProtocolContext protocolContext; + + @Mock private MainnetTransactionProcessor transactionProcessor; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private EthContext ethContext; + + @BeforeEach + void setUp() { + genesisConfig = + GenesisConfig.fromResource("/block-transaction-selector/amsterdam-genesis.json"); + protocolSchedule = + new ProtocolScheduleBuilder( + genesisConfig.getConfigOptions(), + Optional.of(CHAIN_ID), + ProtocolSpecAdapters.create(0, Function.identity()), + false, + EvmConfiguration.DEFAULT, + MiningConfiguration.MINING_DISABLED, + new BadBlockManager(), + false, + BalConfiguration.DEFAULT, + new NoOpMetricsSystem()) + .createProtocolSchedule(); + transactionSelectionService = new TransactionSelectionServiceImpl(); + defaultTestMiningConfiguration = + createMiningParameters( + transactionSelectionService, Wei.ZERO, DEFAULT_POS_BLOCK_TXS_SELECTION_MAX_TIME); + + final Block genesisBlock = + GenesisState.fromConfig(genesisConfig, protocolSchedule, new CodeCache()).getBlock(); + + blockchain = + DefaultBlockchain.createMutable( + genesisBlock, + new KeyValueStoragePrefixedKeyBlockchainStorage( + new InMemoryKeyValueStorage(), + new VariablesKeyValueStorage(new InMemoryKeyValueStorage()), + new MainnetBlockHeaderFunctions(), + false), + new NoOpMetricsSystem(), + 0); + + when(protocolContext.getBlockchain()).thenReturn(blockchain); + + worldState = InMemoryKeyValueStorageProvider.createInMemoryWorldState(); + final var worldStateUpdater = worldState.updater(); + Arrays.stream(AbstractBlockTransactionSelectorTest.Sender.values()) + .map(AbstractBlockTransactionSelectorTest.Sender::address) + .forEach(address -> worldStateUpdater.createAccount(address, 0, Wei.of(1_000_000_000L))); + worldStateUpdater.commit(); + + when(protocolContext.getWorldStateArchive().getWorldState(any(WorldStateQueryParams.class))) + .thenReturn(Optional.of(worldState)); + when(ethContext.getEthPeers().subscribeConnect(any())).thenReturn(1L); + when(ethScheduler.scheduleBlockCreationTask(anyLong(), any(Runnable.class))) + .thenAnswer(invocation -> CompletableFuture.runAsync(invocation.getArgument(1))); + when(ethScheduler.scheduleFutureTask(any(Runnable.class), any(Duration.class))) + .thenAnswer( + invocation -> { + final Duration delay = invocation.getArgument(1); + CompletableFuture.delayedExecutor(delay.toMillis(), MILLISECONDS) + .execute(invocation.getArgument(0)); + return null; + }); + + transactionPool = createTransactionPool(); + transactionPool.setEnabled(); + } + + private MiningConfiguration createMiningParameters( + final TransactionSelectionService transactionSelectionService, + final Wei minGasPrice, + final PositiveNumber txsSelectionMaxTime) { + return ImmutableMiningConfiguration.builder() + .mutableInitValues(MutableInitValues.builder().minTransactionGasPrice(minGasPrice).build()) + .transactionSelectionService(transactionSelectionService) + .posBlockTxsSelectionMaxTime(txsSelectionMaxTime) + .build(); + } + + private TransactionPool createTransactionPool() { + final TransactionPoolConfiguration poolConf = + ImmutableTransactionPoolConfiguration.builder() + .txPoolMaxSize(5) + .txPoolLimitByAccountPercentage(Fraction.fromFloat(1f)) + .minGasPrice(Wei.ONE) + .build(); + final PendingTransactions pendingTransactions = + new BaseFeePendingTransactionsSorter( + poolConf, + TestClock.system(ZoneId.systemDefault()), + metricsSystem, + blockchain::getChainHeadHeader); + + return new TransactionPool( + () -> pendingTransactions, + protocolSchedule, + protocolContext, + mock(TransactionBroadcaster.class), + ethContext, + new TransactionPoolMetrics(metricsSystem), + poolConf, + new BlobCache()); + } + + private ProcessableBlockHeader createBlock(final long gasLimit, final Wei baseFee) { + return BlockHeaderBuilder.create() + .parentHash(Hash.EMPTY) + .coinbase(Address.fromHexString(String.format("%020x", 1))) + .difficulty(Difficulty.ONE) + .number(1) + .gasLimit(gasLimit) + .timestamp(Instant.now().toEpochMilli()) + .baseFee(baseFee) + .buildProcessableBlockHeader(); + } + + private BlockTransactionSelector createBlockSelector( + final MiningConfiguration miningConfiguration, + final MainnetTransactionProcessor transactionProcessor, + final ProcessableBlockHeader blockHeader, + final Address miningBeneficiary, + final Wei blobGasPrice, + final TransactionSelectionService transactionSelectionService, + final ProtocolSchedule schedule, + final Optional maybeBalBuilder) { + final ProtocolSpec protocolSpec = schedule.getByBlockHeader(blockchain.getChainHeadHeader()); + final var selectorsStateManager = new SelectorsStateManager(); + return new BlockTransactionSelector( + miningConfiguration, + transactionProcessor, + blockchain, + worldState, + transactionPool, + blockHeader, + schedule.getByBlockHeader(blockHeader).getTransactionReceiptFactory(), + miningBeneficiary, + blobGasPrice, + protocolSpec, + transactionSelectionService.createPluginTransactionSelector( + blockHeader, selectorsStateManager), + ethScheduler, + selectorsStateManager, + maybeBalBuilder); + } + + private Transaction createTransaction( + final int nonce, + final Wei gasPrice, + final long gasLimit, + final AbstractBlockTransactionSelectorTest.Sender sender) { + return Transaction.builder() + .gasLimit(gasLimit) + .gasPrice(gasPrice) + .nonce(nonce) + .payload(Bytes.EMPTY) + .to(Address.ID) + .value(Wei.of(nonce)) + .sender(sender.address()) + .chainId(CHAIN_ID) + .guessType() + .signAndBuild(sender.keyPair()); + } + + @Test + void secondTransactionNotSelectedWhenBlockAccessListItemBudgetWouldBeExceeded() { + final ProtocolSpec chainSpec = + spy(protocolSchedule.getByBlockHeader(blockchain.getChainHeadHeader())); + final GasCalculator realGasCalculator = chainSpec.getGasCalculator(); + final GasCalculator gasCalculator = mock(GasCalculator.class, delegatesTo(realGasCalculator)); + lenient().when(gasCalculator.getBlockAccessListItemCost()).thenReturn(2_000_000L); + doReturn(gasCalculator).when(chainSpec).getGasCalculator(); + final ProtocolSchedule scheduleStub = mock(ProtocolSchedule.class); + when(scheduleStub.getByBlockHeader(any())).thenReturn(chainSpec); + + final ProcessableBlockHeader blockHeader = createBlock(6_000_000, Wei.ONE); + final BlockAccessList.BlockAccessListBuilder sharedBalBuilder = BlockAccessList.builder(); + final BlockTransactionSelector selector = + createBlockSelector( + defaultTestMiningConfiguration, + transactionProcessor, + blockHeader, + AddressHelpers.ofValue(1), + Wei.ZERO, + transactionSelectionService, + scheduleStub, + Optional.of(sharedBalBuilder)); + + final Transaction tx1 = createTransaction(0, Wei.of(5), 100_000, SENDER1); + final Transaction tx2 = createTransaction(0, Wei.of(5), 100_000, SENDER2); + + final AtomicInteger evaluationOrder = new AtomicInteger(); + when(transactionProcessor.processTransaction( + any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenAnswer( + inv -> { + final int txIndex = evaluationOrder.getAndIncrement(); + return TransactionProcessingResult.successful( + List.of(), + 50_000L, + 50_000L, + Bytes.EMPTY, + Optional.of(balPartialAddingTwoEip7928Items(txIndex)), + ValidationResult.valid()); + }); + + transactionPool.addRemoteTransactions(List.of(tx1, tx2)); + + final TransactionSelectionResults results = selector.buildTransactionListForBlock(); + + assertThat(results.getSelectedTransactions()).containsExactly(tx1); + assertThat(results.getNotSelectedTransactions()) + .containsExactly(entry(tx2, BLOCK_ACCESS_LIST_ITEM_BUDGET_EXCEEDED)); + + final BlockAccessList committedBal = sharedBalBuilder.build(); + assertThat(committedBal.eip7928ItemCount()).isEqualTo(2); + + final Address tx2PartialAccount = Address.fromHexString(String.format("0x%040x", 1L + 100L)); + assertThat(committedBal.accountChanges()) + .extracting(BlockAccessList.AccountChanges::address) + .doesNotContain(tx2PartialAccount); + + final BlockAccessList.BlockAccessListBuilder expectedBuilder = BlockAccessList.builder(); + expectedBuilder.apply(balPartialAddingTwoEip7928Items(0)); + final BlockAccessList expectedBalOnlyTx1 = expectedBuilder.build(); + assertThat(BodyValidation.balHash(committedBal)) + .isEqualTo(BodyValidation.balHash(expectedBalOnlyTx1)); + } + + @Test + void bothTransactionsSelectedWhenBlockAccessListItemBudgetAllowsThem() { + final ProtocolSpec chainSpec = + spy(protocolSchedule.getByBlockHeader(blockchain.getChainHeadHeader())); + final GasCalculator realGasCalculator = chainSpec.getGasCalculator(); + final GasCalculator gasCalculator = mock(GasCalculator.class, delegatesTo(realGasCalculator)); + lenient().when(gasCalculator.getBlockAccessListItemCost()).thenReturn(1_000_000L); + doReturn(gasCalculator).when(chainSpec).getGasCalculator(); + final ProtocolSchedule scheduleStub = mock(ProtocolSchedule.class); + when(scheduleStub.getByBlockHeader(any())).thenReturn(chainSpec); + + final ProcessableBlockHeader blockHeader = createBlock(6_000_000, Wei.ONE); + final BlockAccessList.BlockAccessListBuilder sharedBalBuilder = BlockAccessList.builder(); + final BlockTransactionSelector selector = + createBlockSelector( + defaultTestMiningConfiguration, + transactionProcessor, + blockHeader, + AddressHelpers.ofValue(1), + Wei.ZERO, + transactionSelectionService, + scheduleStub, + Optional.of(sharedBalBuilder)); + + final Transaction tx1 = createTransaction(0, Wei.of(5), 100_000, SENDER1); + final Transaction tx2 = createTransaction(0, Wei.of(5), 100_000, SENDER2); + + final AtomicInteger evaluationOrder = new AtomicInteger(); + when(transactionProcessor.processTransaction( + any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenAnswer( + inv -> { + final int txIndex = evaluationOrder.getAndIncrement(); + return TransactionProcessingResult.successful( + List.of(), + 50_000L, + 50_000L, + Bytes.EMPTY, + Optional.of(balPartialAddingTwoEip7928Items(txIndex)), + ValidationResult.valid()); + }); + + transactionPool.addRemoteTransactions(List.of(tx1, tx2)); + + final TransactionSelectionResults results = selector.buildTransactionListForBlock(); + + assertThat(results.getSelectedTransactions()).containsExactly(tx1, tx2); + assertThat(results.getNotSelectedTransactions()).isEmpty(); + + final BlockAccessList committedBal = sharedBalBuilder.build(); + assertThat(committedBal.eip7928ItemCount()).isEqualTo(4L); + + final BlockAccessList.BlockAccessListBuilder expectedBuilder = BlockAccessList.builder(); + expectedBuilder.apply(balPartialAddingTwoEip7928Items(0)); + expectedBuilder.apply(balPartialAddingTwoEip7928Items(1)); + final BlockAccessList expectedBalBothTxs = expectedBuilder.build(); + assertThat(BodyValidation.balHash(committedBal)) + .isEqualTo(BodyValidation.balHash(expectedBalBothTxs)); + } + + private static PartialBlockAccessView balPartialAddingTwoEip7928Items(final int txIndex) { + final Address addr = Address.fromHexString(String.format("0x%040x", txIndex + 100L)); + final PartialBlockAccessView.PartialBlockAccessViewBuilder builder = + new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(txIndex); + builder + .getOrCreateAccountBuilder(addr) + .addStorageChange(new StorageSlotKey(UInt256.ONE), UInt256.ZERO); + return builder.build(); + } +} diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockAccessListItemBudgetTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockAccessListItemBudgetTransactionSelectorTest.java new file mode 100644 index 00000000000..415f8914bae --- /dev/null +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockAccessListItemBudgetTransactionSelectorTest.java @@ -0,0 +1,192 @@ +/* + * 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.blockcreation.txselection.selectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOCK_ACCESS_LIST_ITEM_BUDGET_EXCEEDED; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.StorageSlotKey; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; +import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.mainnet.BlockAccessListItemSizeCheck; +import org.hyperledger.besu.ethereum.mainnet.BlockAccessListValidationError; +import org.hyperledger.besu.ethereum.mainnet.BlockAccessListValidator; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +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.PartialBlockAccessView; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; + +import java.util.List; +import java.util.Optional; + +import com.google.common.base.Stopwatch; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit tests for {@link BlockAccessListItemBudgetTransactionSelector}: when a BAL builder is + * present, post-processing probes committed state + the current tx partial and delegates the + * EIP-7928 item budget to {@link BlockAccessListValidator#validateExecutedBlockAccessListItemSize}; + * when no builder is configured, the selector is a no-op. + */ +@ExtendWith(MockitoExtension.class) +class BlockAccessListItemBudgetTransactionSelectorTest { + + @Mock private ProcessableBlockHeader pendingBlockHeader; + @Mock private ProtocolSpec protocolSpec; + @Mock private BlockAccessListValidator balValidator; + @Mock private PendingTransaction pendingTransaction; + + @BeforeEach + void setUp() { + lenient().when(protocolSpec.getBlockAccessListValidator()).thenReturn(balValidator); + } + + /** + * This selector never applies the EIP-7928 item budget in the pre-processing phase, so a + * transaction is never dropped at pre-processing for that reason (budget is enforced only in + * post-processing when a BAL builder is present). + */ + @Test + void preProcessingAlwaysSelectsRegardlessOfBalBuilderPresence() { + final TransactionEvaluationContext ctx = evalContext(); + + final BlockAccessListItemBudgetTransactionSelector withoutBuilder = + new BlockAccessListItemBudgetTransactionSelector(context(), Optional.empty()); + assertThat(withoutBuilder.evaluateTransactionPreProcessing(ctx)).isEqualTo(SELECTED); + + final BlockAccessListItemBudgetTransactionSelector withBuilder = + new BlockAccessListItemBudgetTransactionSelector( + context(), Optional.of(BlockAccessList.builder())); + assertThat(withBuilder.evaluateTransactionPreProcessing(ctx)).isEqualTo(SELECTED); + } + + /** + * Empty committed BAL + partial adding two EIP-7928 items: the probe item count passed to the + * validator is 2; when the validator accepts, the selector returns {@code SELECTED}. + */ + @Test + void postProcessingWithinBudgetPassesValidatorAndReturnsSelected() { + when(balValidator.validateExecutedBlockAccessListItemSize( + anyLong(), eq(pendingBlockHeader), eq(protocolSpec))) + .thenReturn(BlockAccessListItemSizeCheck.withinBudget()); + + final BlockAccessList.BlockAccessListBuilder balBuilder = BlockAccessList.builder(); + final BlockAccessListItemBudgetTransactionSelector selector = + new BlockAccessListItemBudgetTransactionSelector(context(), Optional.of(balBuilder)); + + final TransactionProcessingResult result = + TransactionProcessingResult.successful( + List.of(), + 21_000L, + 0L, + Bytes.EMPTY, + Optional.of(twoItemPartial(0)), + ValidationResult.valid()); + + assertThat(selector.evaluateTransactionPostProcessing(evalContext(), result)) + .isEqualTo(SELECTED); + + final ArgumentCaptor countCaptor = ArgumentCaptor.forClass(Long.class); + verify(balValidator) + .validateExecutedBlockAccessListItemSize( + countCaptor.capture(), eq(pendingBlockHeader), eq(protocolSpec)); + assertThat(countCaptor.getValue()).isEqualTo(2L); + } + + /** + * Committed builder already holds 2 items; the candidate partial adds 2 more (probe total 4). + * Stubbed validator rejects counts {@code > 3}, so the selector returns {@code + * BLOCK_ACCESS_LIST_ITEM_BUDGET_EXCEEDED} without mutating the main builder’s committed view + * beyond what the test applied before the selector ran. + */ + @Test + void postProcessingWhenValidatorRejectsReturnsBudgetExceeded() { + when(balValidator.validateExecutedBlockAccessListItemSize( + anyLong(), eq(pendingBlockHeader), eq(protocolSpec))) + .thenAnswer( + inv -> + ((long) inv.getArgument(0)) > 3L + ? BlockAccessListItemSizeCheck.overBudget( + new BlockAccessListValidationError("over budget")) + : BlockAccessListItemSizeCheck.withinBudget()); + + final BlockAccessList.BlockAccessListBuilder balBuilder = BlockAccessList.builder(); + balBuilder.apply(twoItemPartial(0)); + + final BlockAccessListItemBudgetTransactionSelector selector = + new BlockAccessListItemBudgetTransactionSelector(context(), Optional.of(balBuilder)); + + final TransactionProcessingResult result = + TransactionProcessingResult.successful( + List.of(), + 21_000L, + 0L, + Bytes.EMPTY, + Optional.of(twoItemPartial(1)), + ValidationResult.valid()); + + assertThat(selector.evaluateTransactionPostProcessing(evalContext(), result)) + .isEqualTo(BLOCK_ACCESS_LIST_ITEM_BUDGET_EXCEEDED); + } + + private BlockSelectionContext context() { + return new BlockSelectionContext( + MiningConfiguration.newDefault(), + pendingBlockHeader, + protocolSpec, + Wei.ZERO, + Address.fromHexString("0x0000000000000000000000000000000000000001"), + null); + } + + private TransactionEvaluationContext evalContext() { + return new TransactionEvaluationContext( + pendingBlockHeader, + pendingTransaction, + Stopwatch.createStarted(), + Wei.ONE, + Wei.ONE, + () -> false); + } + + /** One account + one storage write → 2 EIP-7928 items (same shape as other BAL tests). */ + private static PartialBlockAccessView twoItemPartial(final int txIndex) { + final Address addr = Address.fromHexString(String.format("0x%040x", txIndex + 100L)); + final PartialBlockAccessView.PartialBlockAccessViewBuilder b = + new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(txIndex); + b.getOrCreateAccountBuilder(addr) + .addStorageChange(new StorageSlotKey(UInt256.ONE), UInt256.ZERO); + return b.build(); + } +} diff --git a/ethereum/blockcreation/src/test/resources/block-transaction-selector/amsterdam-genesis.json b/ethereum/blockcreation/src/test/resources/block-transaction-selector/amsterdam-genesis.json new file mode 100644 index 00000000000..69bfd3aede3 --- /dev/null +++ b/ethereum/blockcreation/src/test/resources/block-transaction-selector/amsterdam-genesis.json @@ -0,0 +1,39 @@ +{ + "config": { + "chainId": 42, + "homesteadBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "mergeNetsplitBlock": 0, + "terminalTotalDifficulty": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "pragueTime": 0, + "osakaTime": 0, + "bpo1Time": 0, + "bpo2Time": 0, + "bpo3Time": 0, + "bpo4Time": 0, + "bpo5Time": 0, + "amsterdamTime": 0, + "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa", + "withdrawalRequestContractAddress": "0x00000961ef480eb55e80d19ad83579a64c007002", + "consolidationRequestContractAddress": "0x0000bbddc7ce488642fb579f8b00f3a590007251" + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "gasLimit": "0x989680", + "baseFeePerGas": "0x1", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "slotnumber": "0x42" +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java index 0c0bdf22585..e4f4e51209c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java @@ -326,9 +326,6 @@ public BlockProcessingResult processBlock( blockHashLookup, transactionLocationTracker); - applyPartialBlockAccessView( - transactionProcessingResult.getPartialBlockAccessView(), blockAccessListBuilder); - if (transactionProcessingResult.isInvalid()) { String errorMessage = MessageFormat.format( @@ -343,6 +340,26 @@ public BlockProcessingResult processBlock( return new BlockProcessingResult(Optional.empty(), errorMessage); } + applyPartialBlockAccessView( + transactionProcessingResult.getPartialBlockAccessView(), blockAccessListBuilder); + + if (blockAccessListBuilder.isPresent()) { + final BlockAccessListItemSizeCheck itemSizeCheck = + protocolSpec + .getBlockAccessListValidator() + .validateExecutedBlockAccessListItemSize( + blockAccessListBuilder.get().eip7928ItemCount(), blockHeader, protocolSpec); + if (itemSizeCheck.isOverBudget()) { + final String errorMessage = + itemSizeCheck.overBudgetError().orElseThrow().errorMessage(); + LOG.error(errorMessage); + if (worldState instanceof BonsaiWorldState) { + ((BonsaiWorldStateUpdateAccumulator) blockUpdater).reset(); + } + return new BlockProcessingResult(Optional.empty(), errorMessage); + } + } + if (transactionUpdater instanceof StackedUpdater) { transactionUpdater.commit(); } @@ -493,34 +510,23 @@ public BlockProcessingResult processBlock( try { if (blockAccessListBuilder.isPresent()) { final BlockAccessList bal = blockAccessListBuilder.get().build(); - final Optional headerBalHash = block.getHeader().getBalHash(); - if (headerBalHash.isPresent()) { - final Hash expectedHash = BodyValidation.balHash(bal); - if (!headerBalHash.get().equals(expectedHash)) { - final String errorMessage = - String.format( - "Block access list hash mismatch, calculated: %s header: %s", - expectedHash.getBytes().toHexString(), - headerBalHash.get().getBytes().toHexString()); - LOG.error(errorMessage); - - if (balConfiguration.shouldLogBalsOnMismatch()) { - final String constructedBalStr = bal.toString(); - final String blockBalStr = - blockAccessList.map(Object::toString).orElse(""); - LOG.error( - "--- BAL constructed during execution ---\n{}\n" - + "--- BAL supplied for block ---\n{}", - constructedBalStr, - blockBalStr); - } - - if (worldState instanceof BonsaiWorldState) { - ((BonsaiWorldStateUpdateAccumulator) worldState.updater()).reset(); - } - return new BlockProcessingResult( - Optional.empty(), errorMessage, false, Optional.of(bal)); + final Optional constructedBalError = + protocolSpec + .getBlockAccessListValidator() + .validateExecutedBlockAccessListAfterBuild( + bal, + blockHeader, + blockAccessList, + balConfiguration.shouldLogBalsOnMismatch()); + if (constructedBalError.isPresent()) { + if (worldState instanceof BonsaiWorldState) { + ((BonsaiWorldStateUpdateAccumulator) worldState.updater()).reset(); } + return new BlockProcessingResult( + Optional.empty(), + constructedBalError.get().errorMessage(), + false, + Optional.of(bal)); } maybeBlockAccessList = Optional.of(bal); blockProcessingMetrics.recordBlockAccessListMetrics(bal); @@ -528,7 +534,7 @@ public BlockProcessingResult processBlock( maybeBlockAccessList = Optional.empty(); } } catch (Exception e) { - LOG.error("Error validating BAL hash", e); + LOG.error("Error validating block access list", e); if (worldState instanceof BonsaiWorldState) { ((BonsaiWorldStateUpdateAccumulator) worldState.updater()).reset(); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListItemSizeCheck.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListItemSizeCheck.java new file mode 100644 index 00000000000..551c8fb4b69 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListItemSizeCheck.java @@ -0,0 +1,63 @@ +/* + * 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.mainnet; + +import java.util.Optional; + +/** + * Outcome of {@link BlockAccessListValidator#validateExecutedBlockAccessListItemSize} (EIP-7928 + * item budget vs gas limit). + */ +public sealed interface BlockAccessListItemSizeCheck + permits BlockAccessListItemSizeCheck.WithinBudget, BlockAccessListItemSizeCheck.OverBudget { + + /** {@code true} when the running item count exceeds the EIP-7928 budget for the header. */ + boolean isOverBudget(); + + /** Present only when {@link #isOverBudget()} is {@code true}. */ + Optional overBudgetError(); + + record WithinBudget() implements BlockAccessListItemSizeCheck { + @Override + public boolean isOverBudget() { + return false; + } + + @Override + public Optional overBudgetError() { + return Optional.empty(); + } + } + + record OverBudget(BlockAccessListValidationError error) implements BlockAccessListItemSizeCheck { + @Override + public boolean isOverBudget() { + return true; + } + + @Override + public Optional overBudgetError() { + return Optional.of(error); + } + } + + static WithinBudget withinBudget() { + return new WithinBudget(); + } + + static OverBudget overBudget(final BlockAccessListValidationError error) { + return new OverBudget(error); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListValidationError.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListValidationError.java new file mode 100644 index 00000000000..d9d3abbdead --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListValidationError.java @@ -0,0 +1,23 @@ +/* + * 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.mainnet; + +/** + * Failure payload from {@link BlockAccessListValidator} (e.g. {@link + * BlockAccessListValidator#validateExecutedBlockAccessListAfterBuild} or {@link + * BlockAccessListItemSizeCheck.OverBudget}) and import-time boolean {@link + * BlockAccessListValidator#validate}. + */ +public record BlockAccessListValidationError(String errorMessage) {} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListValidator.java index 31ed5360ec9..e0fe5a093da 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockAccessListValidator.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.mainnet; import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; import java.util.Optional; @@ -31,7 +32,8 @@ public interface BlockAccessListValidator { blockAccessList.isEmpty() && header.getBalHash().isEmpty(); /** - * Validates a block access list against protocol constraints. + * Full validation of an imported block access list (e.g. from a peer): hash, EIP-7928 + * item size, canonical ordering, and index constraints per EIP-7928. * * @param blockAccessList the optional block access list to validate (empty if block has no BAL) * @param blockHeader the block header containing gas limit and other context @@ -40,4 +42,56 @@ public interface BlockAccessListValidator { */ boolean validate( Optional blockAccessList, BlockHeader blockHeader, int nbTransactions); + + /** + * During block execution: EIP-7928 item-size budget only (running count of addresses plus storage + * keys vs block gas limit). Call after each merged transaction view for fail-fast. After + * post-execution updates, size is checked again together with the header hash in {@link + * #validateExecutedBlockAccessListAfterBuild}. + * + * @param itemCount current BAL item count while merging partial views + * @param blockHeader block or pending header (gas limit); during mining this is a {@link + * ProcessableBlockHeader} + */ + default BlockAccessListItemSizeCheck validateExecutedBlockAccessListItemSize( + final long itemCount, final ProcessableBlockHeader blockHeader) { + return BlockAccessListItemSizeCheck.withinBudget(); + } + + /** + * Same as {@link #validateExecutedBlockAccessListItemSize(long, ProcessableBlockHeader)} but uses + * {@code protocolSpecForItemCost} for {@link + * org.hyperledger.besu.evm.gascalculator.GasCalculator#getBlockAccessListItemCost()} so block + * building can align with the mining {@link ProtocolSpec} instead of only the validator's + * schedule. + * + * @param protocolSpecForItemCost spec whose gas calculator defines the EIP-7928 item cost + */ + default BlockAccessListItemSizeCheck validateExecutedBlockAccessListItemSize( + final long itemCount, + final ProcessableBlockHeader blockHeader, + final ProtocolSpec protocolSpecForItemCost) { + return validateExecutedBlockAccessListItemSize(itemCount, blockHeader); + } + + /** + * After the complete executed BAL is built (post-withdrawals/requests, etc.): when the header + * omits {@code balHash}, returns empty. Otherwise EIP-7928 item size on the built list, then + * verify {@code balHash} matches. + * + * @param executedBal BAL built from execution traces + * @param blockHeader block header (gas limit, optional {@code balHash}) + * @param suppliedBlockAccessList BAL supplied with the block payload (e.g. engine), for mismatch + * diagnostics only + * @param logBalDetailsOnHashMismatch when true, log executed and supplied BAL bodies on hash + * mismatch + * @return empty if valid; otherwise a {@link BlockAccessListValidationError} + */ + default Optional validateExecutedBlockAccessListAfterBuild( + final BlockAccessList executedBal, + final BlockHeader blockHeader, + final Optional suppliedBlockAccessList, + final boolean logBalDetailsOnHashMismatch) { + return Optional.empty(); + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockAccessListValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockAccessListValidator.java index d70d0a27d2f..9164cc2dd21 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockAccessListValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockAccessListValidator.java @@ -18,6 +18,7 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.StorageSlotKey; import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; import java.util.HashSet; @@ -35,12 +36,6 @@ public class MainnetBlockAccessListValidator implements BlockAccessListValidator private static final Logger LOG = LoggerFactory.getLogger(MainnetBlockAccessListValidator.class); - /** Canonical slot order (by slot key bytes), consistent with BlockAccessListBuilder. */ - private static int compareSlotKeysByCanonicalOrder( - final StorageSlotKey a, final StorageSlotKey b) { - return a.getSlotKey().orElseThrow().toBytes().compareTo(b.getSlotKey().orElseThrow().toBytes()); - } - private final ProtocolSchedule protocolSchedule; /** @@ -63,6 +58,66 @@ public MainnetBlockAccessListValidator(final ProtocolSchedule protocolSchedule) this.protocolSchedule = protocolSchedule; } + @Override + public BlockAccessListItemSizeCheck validateExecutedBlockAccessListItemSize( + final long itemCount, final ProcessableBlockHeader blockHeader) { + return validateExecutedBlockAccessListItemSize( + itemCount, blockHeader, protocolSchedule.getByBlockHeader(blockHeader)); + } + + @Override + public BlockAccessListItemSizeCheck validateExecutedBlockAccessListItemSize( + final long itemCount, + final ProcessableBlockHeader blockHeader, + final ProtocolSpec protocolSpecForItemCost) { + final long itemCost = protocolSpecForItemCost.getGasCalculator().getBlockAccessListItemCost(); + if (itemCost <= 0) { + return BlockAccessListItemSizeCheck.withinBudget(); + } + final long maxItems = blockHeader.getGasLimit() / itemCost; + if (itemCount <= maxItems) { + return BlockAccessListItemSizeCheck.withinBudget(); + } + final String blockRef = + blockHeader instanceof BlockHeader fullHeader + ? fullHeader.getBlockHash().toShortLogString() + : String.format("pending#%d", blockHeader.getNumber()); + return BlockAccessListItemSizeCheck.overBudget( + new BlockAccessListValidationError( + String.format( + "Block access list size exceeds maximum allowed items for block %s with gas limit %d" + + " (items %d, max %d)", + blockRef, blockHeader.getGasLimit(), itemCount, maxItems))); + } + + @Override + public Optional validateExecutedBlockAccessListAfterBuild( + final BlockAccessList executedBal, + final BlockHeader blockHeader, + final Optional suppliedBlockAccessList, + final boolean logBalDetailsOnHashMismatch) { + if (blockHeader.getBalHash().isEmpty()) { + return Optional.empty(); + } + final BlockAccessListItemSizeCheck sizeCheck = + validateExecutedBlockAccessListItemSize( + executedBal.eip7928ItemCount(), + blockHeader, + protocolSchedule.getByBlockHeader(blockHeader)); + if (sizeCheck.isOverBudget()) { + final BlockAccessListValidationError error = sizeCheck.overBudgetError().orElseThrow(); + LOG.error(error.errorMessage()); + return Optional.of(error); + } + + return balHashMismatchAgainstHeaderIfAny( + executedBal, + blockHeader.getBalHash(), + suppliedBlockAccessList, + logBalDetailsOnHashMismatch, + true); + } + @Override public boolean validate( final Optional blockAccessList, @@ -86,34 +141,17 @@ public boolean validate( return false; } - final Hash providedBalHash = BodyValidation.balHash(bal); - if (!headerBalHash.get().equals(providedBalHash)) { - LOG.warn( - "Block access list hash mismatch for block {}: provided={}, header={}", - blockHeader.getBlockHash(), - providedBalHash, - headerBalHash.get()); + final BlockAccessListItemSizeCheck lightSizeCheck = + validateExecutedBlockAccessListItemSize( + bal.eip7928ItemCount(), blockHeader, protocolSchedule.getByBlockHeader(blockHeader)); + if (lightSizeCheck.isOverBudget()) { + LOG.warn(lightSizeCheck.overBudgetError().orElseThrow().errorMessage()); return false; } - final ProtocolSpec protocolSpec = protocolSchedule.getByBlockHeader(blockHeader); - final long itemCost = protocolSpec.getGasCalculator().getBlockAccessListItemCost(); - if (itemCost > 0) { - long totalStorageKeys = 0; - for (BlockAccessList.AccountChanges accountChange : bal.accountChanges()) { - totalStorageKeys += accountChange.storageChanges().size(); - totalStorageKeys += accountChange.storageReads().size(); - } - final long totalAddresses = bal.accountChanges().size(); - final long balItems = totalStorageKeys + totalAddresses; - final long maxItems = blockHeader.getGasLimit() / itemCost; - if (balItems > maxItems) { - LOG.warn( - "Block access list size exceeds maximum allowed items for block {} with gas limit {}", - blockHeader.getBlockHash(), - blockHeader.getGasLimit()); - return false; - } + if (balHashMismatchAgainstHeaderIfAny(bal, headerBalHash, Optional.empty(), false, false) + .isPresent()) { + return false; } final long maxIndex = (long) nbTransactions + 1L; @@ -124,6 +162,56 @@ public boolean validate( return true; } + private void logBalHashMismatch( + final String message, + final boolean logAsError, + final BlockAccessList balForDetails, + final Optional supplied, + final boolean logDetails) { + if (logAsError) { + LOG.error(message); + } else { + LOG.warn(message); + } + if (logDetails) { + LOG.error( + "--- BAL constructed during execution ---\n{}\n--- BAL supplied for block ---\n{}", + balForDetails.toString(), + supplied.map(Object::toString).orElse("")); + } + } + + /** + * When the header carries a {@code balHash}, returns empty if it matches {@code bal}; otherwise + * logs and returns a {@link BlockAccessListValidationError}. + */ + private Optional balHashMismatchAgainstHeaderIfAny( + final BlockAccessList bal, + final Optional headerBalHashOpt, + final Optional suppliedBlockAccessList, + final boolean logBalDetailsOnHashMismatch, + final boolean logPrimaryMessageAsError) { + if (headerBalHashOpt.isEmpty()) { + return Optional.empty(); + } + final Hash headerBalHash = headerBalHashOpt.get(); + final Hash computedHash = BodyValidation.balHash(bal); + if (computedHash.equals(headerBalHash)) { + return Optional.empty(); + } + final String errorMessage = + String.format( + "Block access list hash mismatch, calculated: %s header: %s", + computedHash.getBytes().toHexString(), headerBalHash.getBytes().toHexString()); + logBalHashMismatch( + errorMessage, + logPrimaryMessageAsError, + bal, + suppliedBlockAccessList, + logBalDetailsOnHashMismatch); + return Optional.of(new BlockAccessListValidationError(errorMessage)); + } + /** * Validates index range (indices in [0, maxIndex]) and canonical ordering (EIP-7928) in one * traversal. Strict ordering implies uniqueness; the only set kept is change slots to detect @@ -295,4 +383,9 @@ && compareSlotKeysByCanonicalOrder(prevStorageSlot, slot) >= 0) { } return true; } + + /** Canonical slot order (by slot key bytes), consistent with BlockAccessListBuilder. */ + private int compareSlotKeysByCanonicalOrder(final StorageSlotKey a, final StorageSlotKey b) { + return a.getSlotKey().orElseThrow().toBytes().compareTo(b.getSlotKey().orElseThrow().toBytes()); + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpec.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpec.java index 4db2ccf53ef..f1bc4c94fd0 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpec.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpec.java @@ -90,6 +90,7 @@ public class ProtocolSpec { private final Optional transactionPoolPreProcessor; private final Optional blockAccessListFactory; + private final BlockAccessListValidator blockAccessListValidator; private final StateRootCommitterFactory stateRootCommitterFactory; private final BlockGasAccountingStrategy blockGasAccountingStrategy; private final BlockGasUsedValidator blockGasUsedValidator; @@ -128,6 +129,7 @@ public class ProtocolSpec { * protection * @param blockGasAccountingStrategy the strategy for calculating block gas usage * @param blockGasUsedValidator the strategy for validating block gas used + * @param blockAccessListValidator the block access list validator for this fork */ public ProtocolSpec( final HardforkId hardforkId, @@ -161,6 +163,7 @@ public ProtocolSpec( final boolean isReplayProtectionSupported, final Optional transactionPoolPreProcessor, final Optional blockAccessListFactory, + final BlockAccessListValidator blockAccessListValidator, final StateRootCommitterFactory stateRootCommitterFactory, final BlockGasAccountingStrategy blockGasAccountingStrategy, final BlockGasUsedValidator blockGasUsedValidator) { @@ -195,6 +198,7 @@ public ProtocolSpec( this.isReplayProtectionSupported = isReplayProtectionSupported; this.transactionPoolPreProcessor = transactionPoolPreProcessor; this.blockAccessListFactory = blockAccessListFactory; + this.blockAccessListValidator = blockAccessListValidator; this.stateRootCommitterFactory = stateRootCommitterFactory; this.blockGasAccountingStrategy = blockGasAccountingStrategy; this.blockGasUsedValidator = blockGasUsedValidator; @@ -435,6 +439,10 @@ public boolean isBlockAccessListEnabled() { return blockAccessListFactory.isPresent(); } + public BlockAccessListValidator getBlockAccessListValidator() { + return blockAccessListValidator; + } + public StateRootCommitterFactory getStateRootCommitterFactory() { return stateRootCommitterFactory; } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpecBuilder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpecBuilder.java index 908f63b0c06..7920bf23c1e 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpecBuilder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ProtocolSpecBuilder.java @@ -459,6 +459,7 @@ public ProtocolSpec build(final ProtocolSchedule protocolSchedule) { isReplayProtectionSupported, Optional.ofNullable(transactionPoolPreProcessor), Optional.ofNullable(blockAccessListFactory), + blockAccessListValidator, stateRootCommitterFactory, blockGasAccountingStrategy, blockGasUsedValidator); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessList.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessList.java index 97d776453f9..82a7813bcc6 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessList.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessList.java @@ -59,6 +59,15 @@ public boolean isEmpty() { return accountChanges.isEmpty(); } + public long eip7928ItemCount() { + long totalStorageKeys = 0; + for (AccountChanges accountChange : accountChanges) { + totalStorageKeys += accountChange.storageChanges().size(); + totalStorageKeys += accountChange.storageReads().size(); + } + return (long) accountChanges.size() + totalStorageKeys; + } + public void writeTo(final RLPOutput out) { BlockAccessListEncoder.encode(this, out); } @@ -214,6 +223,33 @@ public void apply(final PartialBlockAccessView partialBlockAccessView) { }); } + /** + * Replays an immutable {@link BlockAccessList} into this builder so {@link #eip7928ItemCount()} + * matches incremental {@link #apply(PartialBlockAccessView)} calls that produced {@code bal}. + */ + public void mergeFrom(final BlockAccessList bal) { + for (AccountChanges ac : bal.accountChanges()) { + final AccountBuilder ab = getOrCreateAccountBuilder(ac.address()); + for (SlotChanges sc : ac.storageChanges()) { + for (StorageChange change : sc.changes()) { + ab.addStorageWrite(sc.slot(), change.txIndex(), change.newValue()); + } + } + for (SlotRead sr : ac.storageReads()) { + ab.addStorageRead(sr.slot()); + } + for (BalanceChange bc : ac.balanceChanges()) { + ab.addBalanceChange(bc.txIndex(), bc.postBalance()); + } + for (NonceChange nc : ac.nonceChanges()) { + ab.addNonceChange(nc.txIndex(), nc.newNonce()); + } + for (CodeChange cc : ac.codeChanges()) { + ab.addCodeChange(cc.txIndex(), cc.newCode()); + } + } + } + public BlockAccessList build() { return new BlockAccessList( @@ -223,6 +259,14 @@ public BlockAccessList build() { .toList()); } + public long eip7928ItemCount() { + long count = accountChangesBuilders.size(); + for (AccountBuilder ab : accountChangesBuilders.values()) { + count += (long) ab.slotWrites.size() + ab.slotReads.size(); + } + return count; + } + public static class AccountBuilder { final Address address; final Map> slotWrites = new TreeMap<>(); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/SyncBlockBodyTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/SyncBlockBodyTest.java index bc03acbe125..fa83470f21c 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/SyncBlockBodyTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/SyncBlockBodyTest.java @@ -23,6 +23,7 @@ import org.hyperledger.besu.datatypes.HardforkId; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.GasLimitCalculator; +import org.hyperledger.besu.ethereum.mainnet.BlockAccessListValidator; import org.hyperledger.besu.ethereum.mainnet.BlockGasAccountingStrategy; import org.hyperledger.besu.ethereum.mainnet.BlockGasUsedValidator; import org.hyperledger.besu.ethereum.mainnet.BodyValidation; @@ -224,6 +225,7 @@ private static ProtocolSpec getProtocolSpec() { true, Optional.empty(), Optional.empty(), + BlockAccessListValidator.ALWAYS_REJECT_BAL, new DefaultStateRootCommitterFactory(), BlockGasAccountingStrategy.FRONTIER, BlockGasUsedValidator.FRONTIER); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorBalValidationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorBalValidationTest.java new file mode 100644 index 00000000000..acbadcc4370 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorBalValidationTest.java @@ -0,0 +1,329 @@ +/* + * 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.mainnet; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.StorageSlotKey; +import org.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.BlockProcessingResult; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.ethereum.core.BlockBody; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.AccessLocationTracker; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessListFactory; +import org.hyperledger.besu.ethereum.mainnet.block.access.list.PartialBlockAccessView; +import org.hyperledger.besu.ethereum.mainnet.blockhash.FrontierPreExecutionProcessor; +import org.hyperledger.besu.ethereum.mainnet.parallelization.PreprocessingContext; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.DefaultStateRootCommitterFactory; +import org.hyperledger.besu.ethereum.mainnet.systemcall.BlockProcessingContext; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.ethereum.referencetests.ReferenceTestBlockchain; +import org.hyperledger.besu.ethereum.referencetests.ReferenceTestWorldState; +import org.hyperledger.besu.evm.blockhash.BlockHashLookup; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntFunction; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Verifies EIP-7928 BAL checks wired in {@link AbstractBlockProcessor}: per-transaction item budget + * (fail-fast) and post-build hash + size validation. + */ +@ExtendWith(MockitoExtension.class) +class AbstractBlockProcessorBalValidationTest { + + @Mock private ProtocolContext protocolContext; + @Mock private MainnetTransactionProcessor transactionProcessor; + @Mock private AbstractBlockProcessor.TransactionReceiptFactory transactionReceiptFactory; + @Mock private ProtocolSchedule protocolSchedule; + @Mock private ProtocolSpec protocolSpec; + @Mock private GasCalculator gasCalculator; + + private final Blockchain blockchain = new ReferenceTestBlockchain(); + private final MutableWorldState worldState = ReferenceTestWorldState.create(emptyMap()); + + @BeforeEach + void wireProtocolSpec() { + lenient().when(protocolSchedule.getByBlockHeader(any())).thenReturn(protocolSpec); + lenient() + .when(protocolSpec.getPreExecutionProcessor()) + .thenReturn(new FrontierPreExecutionProcessor()); + lenient() + .when(protocolSpec.getStateRootCommitterFactory()) + .thenReturn(new DefaultStateRootCommitterFactory()); + lenient() + .when(protocolSpec.getBlockGasAccountingStrategy()) + .thenReturn(BlockGasAccountingStrategy.FRONTIER); + lenient().when(protocolSpec.getGasCalculator()).thenReturn(gasCalculator); + lenient().when(gasCalculator.getBlobGasPerBlob()).thenReturn(1L); + lenient().when(protocolSpec.getWithdrawalsProcessor()).thenReturn(Optional.empty()); + lenient().when(protocolSpec.getRequestProcessorCoordinator()).thenReturn(Optional.empty()); + lenient() + .when(protocolSpec.getBlockAccessListFactory()) + .thenReturn(Optional.of(new BlockAccessListFactory())); + lenient() + .when(protocolSpec.getBlockAccessListValidator()) + .thenAnswer(__ -> MainnetBlockAccessListValidator.create(protocolSchedule)); + lenient() + .when(transactionReceiptFactory.create(any(), any(), any(), anyLong())) + .thenAnswer(invocation -> mock(TransactionReceipt.class, Answers.RETURNS_DEEP_STUBS)); + } + + @Test + void successfulBlockReturnsBuiltBalMatchingHeaderHash() { + lenient().when(gasCalculator.getBlockAccessListItemCost()).thenReturn(2000L); + final PartialBlockAccessView partial = partialOneAccountOneSlot(0, testAddress(1), 1L); + final BlockAccessList.BlockAccessListBuilder builder = BlockAccessList.builder(); + builder.apply(partial); + final BlockAccessList expectedBal = builder.build(); + final BlockHeader header = + new BlockHeaderTestFixture() + .gasLimit(6000L) + .balHash(BodyValidation.balHash(expectedBal)) + .buildHeader(); + + final AtomicInteger txCalls = new AtomicInteger(0); + final AbstractBlockProcessor processor = + new BalStubBlockProcessor( + transactionProcessor, + transactionReceiptFactory, + Wei.ZERO, + BlockHeader::getCoinbase, + true, + protocolSchedule, + BalConfiguration.DEFAULT, + loc -> { + txCalls.incrementAndGet(); + assertThat(loc).isZero(); + return TransactionProcessingResult.successful( + List.of(), + 1000L, + 0L, + Bytes.EMPTY, + Optional.of(partial), + ValidationResult.valid()); + }); + + final BlockProcessingResult result = + processor.processBlock( + protocolContext, + blockchain, + worldState, + blockWithTxs(header, 1, 5_000L), + Optional.empty()); + + assertThat(result.isSuccessful()).isTrue(); + assertThat(txCalls).hasValue(1); + assertThat(result.getYield().flatMap(y -> y.getBlockAccessList())) + .isPresent() + .get() + .satisfies( + bal -> + assertThat(BodyValidation.balHash(bal)) + .isEqualTo(header.getBalHash().orElseThrow())); + } + + @Test + void perTransactionBalSizeFailFastDoesNotRunFollowingTransactions() { + lenient().when(gasCalculator.getBlockAccessListItemCost()).thenReturn(2000L); + final long gasLimit = 16_000L; + final int maxItems = 8; + assertThat(gasLimit / 2000L).isEqualTo(maxItems); + + final BlockHeader header = new BlockHeaderTestFixture().gasLimit(gasLimit).buildHeader(); + + final AtomicInteger txCalls = new AtomicInteger(0); + final IntFunction partialForIndex = + loc -> partialOneAccountOneSlot(loc, testAddress(loc + 1L), loc * 100L + 1L); + + final AbstractBlockProcessor processor = + new BalStubBlockProcessor( + transactionProcessor, + transactionReceiptFactory, + Wei.ZERO, + BlockHeader::getCoinbase, + true, + protocolSchedule, + BalConfiguration.DEFAULT, + loc -> { + txCalls.incrementAndGet(); + return TransactionProcessingResult.successful( + List.of(), + 1000L, + 0L, + Bytes.EMPTY, + Optional.of(partialForIndex.apply(loc)), + ValidationResult.valid()); + }); + + final BlockProcessingResult result = + processor.processBlock( + protocolContext, + blockchain, + worldState, + blockWithTxs(header, 6, 2000L), + Optional.empty()); + + assertThat(result.isSuccessful()).isFalse(); + assertThat(result.errorMessage.orElse("")).contains("Block access list size exceeds maximum"); + assertThat(txCalls).hasValue(5); + } + + @Test + void afterBuildFailsWhenHeaderBalHashDoesNotMatchConstructedBal() { + lenient().when(gasCalculator.getBlockAccessListItemCost()).thenReturn(2000L); + final PartialBlockAccessView partial = partialOneAccountOneSlot(0, testAddress(1), 1L); + final BlockHeader header = + new BlockHeaderTestFixture() + .gasLimit(30_000_000L) + .balHash(Hash.fromHexString("cd".repeat(32))) + .buildHeader(); + + final AbstractBlockProcessor processor = + new BalStubBlockProcessor( + transactionProcessor, + transactionReceiptFactory, + Wei.ZERO, + BlockHeader::getCoinbase, + true, + protocolSchedule, + BalConfiguration.DEFAULT, + loc -> + TransactionProcessingResult.successful( + List.of(), + 1000L, + 0L, + Bytes.EMPTY, + Optional.of(partial), + ValidationResult.valid())); + + final BlockProcessingResult result = + processor.processBlock( + protocolContext, + blockchain, + worldState, + blockWithTxs(header, 1, 500_000L), + Optional.empty()); + + assertThat(result.isSuccessful()).isFalse(); + assertThat(result.errorMessage.orElse("")).contains("hash mismatch"); + } + + private static Address testAddress(final long suffix) { + return Address.fromHexString(String.format("0x%040x", suffix)); + } + + private static PartialBlockAccessView partialOneAccountOneSlot( + final int txIndex, final Address addr, final long slotBase) { + final PartialBlockAccessView.PartialBlockAccessViewBuilder builder = + new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(txIndex); + builder + .getOrCreateAccountBuilder(addr) + .addStorageChange(new StorageSlotKey(UInt256.valueOf(slotBase)), UInt256.ZERO); + return builder.build(); + } + + private static Block blockWithTxs( + final BlockHeader header, final int txCount, final long txGasLimit) { + final Transaction tx = mock(Transaction.class); + lenient().when(tx.getGasLimit()).thenReturn(txGasLimit); + lenient().when(tx.getHash()).thenReturn(Hash.EMPTY); + lenient().when(tx.getType()).thenReturn(TransactionType.FRONTIER); + lenient().when(tx.getVersionedHashes()).thenReturn(Optional.empty()); + final List txs = new ArrayList<>(); + for (int i = 0; i < txCount; i++) { + txs.add(tx); + } + return new Block(header, new BlockBody(txs, emptyList(), Optional.empty())); + } + + private static final class BalStubBlockProcessor extends AbstractBlockProcessor { + + private final IntFunction resultByTxIndex; + + BalStubBlockProcessor( + final MainnetTransactionProcessor transactionProcessor, + final TransactionReceiptFactory transactionReceiptFactory, + final Wei blockReward, + final MiningBeneficiaryCalculator miningBeneficiaryCalculator, + final boolean skipZeroBlockRewards, + final ProtocolSchedule protocolSchedule, + final BalConfiguration balConfiguration, + final IntFunction resultByTxIndex) { + super( + transactionProcessor, + transactionReceiptFactory, + blockReward, + miningBeneficiaryCalculator, + skipZeroBlockRewards, + protocolSchedule, + balConfiguration); + this.resultByTxIndex = resultByTxIndex; + } + + @Override + protected boolean rewardCoinbase( + final MutableWorldState worldState, + final BlockHeader header, + final List ommers, + final boolean skipZeroBlockRewards) { + return true; + } + + @Override + protected TransactionProcessingResult getTransactionProcessingResult( + final Optional preProcessingContext, + final BlockProcessingContext blockProcessingContext, + final WorldUpdater transactionUpdater, + final Wei blobGasPrice, + final Address miningBeneficiary, + final Transaction transaction, + final int location, + final BlockHashLookup blockHashLookup, + final Optional accessLocationTracker) { + return resultByTxIndex.apply(location); + } + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessListBuilderEip7928Test.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessListBuilderEip7928Test.java new file mode 100644 index 00000000000..b4f58946656 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/block/access/list/BlockAccessListBuilderEip7928Test.java @@ -0,0 +1,80 @@ +/* + * 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.mainnet.block.access.list; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.StorageSlotKey; +import org.hyperledger.besu.datatypes.Wei; + +import org.apache.tuweni.units.bigints.UInt256; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class BlockAccessListBuilderEip7928Test { + + private static final Address ADDR_1 = + Address.fromHexString("0x1000000000000000000000000000000000000001"); + private static final Address ADDR_2 = + Address.fromHexString("0x2000000000000000000000000000000000000002"); + private static final StorageSlotKey SLOT_1 = new StorageSlotKey(UInt256.ONE); + + @Test + void builderEip7928ItemCountMatchesBuiltList() { + final BlockAccessList.BlockAccessListBuilder builder = BlockAccessList.builder(); + final BlockAccessList.BlockAccessListBuilder.AccountBuilder ab1 = + builder.getOrCreateAccountBuilder(ADDR_1); + ab1.addStorageRead(SLOT_1); + ab1.addBalanceChange(0, Wei.ONE); + builder.getOrCreateAccountBuilder(ADDR_2); + final BlockAccessList built = builder.build(); + Assertions.assertThat(builder.eip7928ItemCount()).isEqualTo(built.eip7928ItemCount()); + } + + /** + * Tx selection probes the EIP-7928 budget by snapshotting the committed builder ({@code + * mergeFrom(build())}) then applying the candidate partial; that path must agree with applying + * partials incrementally on one builder, or the probe count (and accept/reject decision) would be + * wrong. + */ + @Test + void mergeFromReplayMatchesIncrementalApply() { + final PartialBlockAccessView p0 = partialWithOneAccountAndStorageWrite(0); + final PartialBlockAccessView p1 = partialWithOneAccountAndStorageWrite(1); + final BlockAccessList.BlockAccessListBuilder main = BlockAccessList.builder(); + main.apply(p0); + final BlockAccessList snap = main.build(); + + final BlockAccessList.BlockAccessListBuilder viaMerge = BlockAccessList.builder(); + viaMerge.mergeFrom(snap); + viaMerge.apply(p1); + + final BlockAccessList.BlockAccessListBuilder direct = BlockAccessList.builder(); + direct.apply(p0); + direct.apply(p1); + + Assertions.assertThat(viaMerge.eip7928ItemCount()) + .isEqualTo(direct.eip7928ItemCount()) + .isEqualTo(4L); + } + + private static PartialBlockAccessView partialWithOneAccountAndStorageWrite(final int txIndex) { + final Address addr = Address.fromHexString(String.format("0x%040x", txIndex + 100L)); + final PartialBlockAccessView.PartialBlockAccessViewBuilder b = + new PartialBlockAccessView.PartialBlockAccessViewBuilder().withTxIndex(txIndex); + b.getOrCreateAccountBuilder(addr) + .addStorageChange(new StorageSlotKey(UInt256.ONE), UInt256.ZERO); + return b.build(); + } +} diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index 3da3a15a2ca..330ba8e26fb 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -71,7 +71,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'fDkUshUOXLQDBHsugUujwwJkJtYVYnqipuScb8CIof0=' + knownHash = '749MmcZwu8KYYM4VDykj0baszULRuBFGrw3jWBuYwmM=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSelectionResult.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSelectionResult.java index 07883c21d9b..807365ecf38 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSelectionResult.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/TransactionSelectionResult.java @@ -192,6 +192,14 @@ public boolean penalize() { public static final TransactionSelectionResult TOO_LARGE_FOR_REMAINING_BLOCK_SIZE = TransactionSelectionResult.invalidTransient("TOO_LARGE_FOR_REMAINING_BLOCK_SIZE"); + /** + * The transaction has not been selected because merging its execution access view would exceed + * the EIP-7928 block access list item budget for the pending block gas limit, but selection + * should continue. + */ + public static final TransactionSelectionResult BLOCK_ACCESS_LIST_ITEM_BUDGET_EXCEEDED = + TransactionSelectionResult.invalidTransient("BLOCK_ACCESS_LIST_ITEM_BUDGET_EXCEEDED"); + /** * The transaction has not been selected since its current price is below the configured min * price, but the selection should continue.