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 9e3e06870eb..0c0bdf22585 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 @@ -287,7 +287,8 @@ public BlockProcessingResult processBlock( blockHashLookup, blobGasPrice, blockAccessListBuilder, - blockAccessList); + blockAccessList, + maybeParentHeader); boolean parallelizedTxFound = false; int nbParallelTx = 0; @@ -666,7 +667,8 @@ Optional run( final BlockHashLookup blockHashLookup, final Wei blobGasPrice, final Optional blockAccessListBuilder, - final Optional maybeBlockBal); + final Optional maybeBlockBal, + final Optional maybeParentHeader); class NoPreprocessing implements PreprocessingFunction { @@ -679,7 +681,8 @@ public Optional run( final BlockHashLookup blockHashLookup, final Wei blobGasPrice, final Optional blockAccessListBuilder, - final Optional maybeBlockBal) { + final Optional maybeBlockBal, + final Optional maybeParentHeader) { return Optional.empty(); } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalConcurrentTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalConcurrentTransactionProcessor.java index f680dfd0294..afcfa1a30e9 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalConcurrentTransactionProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalConcurrentTransactionProcessor.java @@ -75,10 +75,17 @@ protected ParallelizedTransactionContext runTransaction( final Address miningBeneficiary, final BlockHashLookup blockHashLookup, final Wei blobGasPrice, - final Optional blockAccessListBuilder) { + final Optional blockAccessListBuilder, + final Optional maybeParentHeader) { - final BonsaiWorldState ws = getWorldState(protocolContext, blockHeader); - if (ws == null) return null; + if (maybeParentHeader.isEmpty()) { + return null; + } + final BonsaiWorldState ws = + getWorldState(protocolContext, maybeParentHeader.get()).orElse(null); + if (ws == null) { + return null; + } try { ws.disableCacheMerkleTrieLoader(); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockTransactionProcessor.java index 8de120c0175..a92261afd6e 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockTransactionProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelBlockTransactionProcessor.java @@ -45,7 +45,8 @@ public void runAsyncBlock( final BlockHashLookup blockHashLookup, final Wei blobGasPrice, final Executor executor, - final Optional blockAccessListBuilder) { + final Optional blockAccessListBuilder, + final Optional maybeParentHeader) { futures = new CompletableFuture[transactions.size()]; @@ -64,25 +65,19 @@ public void runAsyncBlock( miningBeneficiary, blockHashLookup, blobGasPrice, - blockAccessListBuilder), + blockAccessListBuilder, + maybeParentHeader), executor); } } - protected BonsaiWorldState getWorldState( - final ProtocolContext protocolContext, final BlockHeader blockHeader) { - - final BlockHeader chainHeadHeader = protocolContext.getBlockchain().getChainHeadHeader(); - if (!chainHeadHeader.getHash().equals(blockHeader.getParentHash())) { - return null; - } - - return (BonsaiWorldState) - protocolContext - .getWorldStateArchive() - .getWorldState( - WorldStateQueryParams.withBlockHeaderAndNoUpdateNodeHead(chainHeadHeader)) - .orElse(null); + /** World state at the parent block. Call only when the parent header is known to be present. */ + protected Optional getWorldState( + final ProtocolContext protocolContext, final BlockHeader parentHeader) { + return protocolContext + .getWorldStateArchive() + .getWorldState(WorldStateQueryParams.withBlockHeaderAndNoUpdateNodeHead(parentHeader)) + .map(BonsaiWorldState.class::cast); } protected abstract ParallelizedTransactionContext runTransaction( @@ -93,7 +88,8 @@ protected abstract ParallelizedTransactionContext runTransaction( Address miningBeneficiary, BlockHashLookup blockHashLookup, Wei blobGasPrice, - Optional blockAccessListBuilder); + Optional blockAccessListBuilder, + Optional maybeParentHeader); public abstract Optional getProcessingResult( MutableWorldState worldState, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelTransactionPreprocessing.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelTransactionPreprocessing.java index c71d534fdcd..dfa575d064c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelTransactionPreprocessing.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelTransactionPreprocessing.java @@ -55,7 +55,8 @@ public Optional run( final BlockHashLookup blockHashLookup, final Wei blobGasPrice, final Optional blockAccessListBuilder, - final Optional maybeBlockBal) { + final Optional maybeBlockBal, + final Optional maybeParentHeader) { if (!(protocolContext.getWorldStateArchive() instanceof PathBasedWorldStateProvider)) { return Optional.empty(); } @@ -78,7 +79,8 @@ public Optional run( blockHashLookup, blobGasPrice, executor, - blockAccessListBuilder); + blockAccessListBuilder, + maybeParentHeader); return Optional.of(new PreprocessingContext(parallelProcessor)); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelizedConcurrentTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelizedConcurrentTransactionProcessor.java index a32e19b768d..b5eebe1920f 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelizedConcurrentTransactionProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/parallelization/ParallelizedConcurrentTransactionProcessor.java @@ -83,10 +83,17 @@ protected ParallelizedTransactionContext runTransaction( final Address miningBeneficiary, final BlockHashLookup blockHashLookup, final Wei blobGasPrice, - final Optional blockAccessListBuilder) { + final Optional blockAccessListBuilder, + final Optional maybeParentHeader) { - final BonsaiWorldState ws = getWorldState(protocolContext, blockHeader); - if (ws == null) return null; + if (maybeParentHeader.isEmpty()) { + return null; + } + final BonsaiWorldState ws = + getWorldState(protocolContext, maybeParentHeader.get()).orElse(null); + if (ws == null) { + return null; + } try { ws.disableCacheMerkleTrieLoader(); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractParallelBlockProcessorIntegrationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractParallelBlockProcessorIntegrationTest.java index 717ebbcf9ec..f0fa9fa7c9b 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractParallelBlockProcessorIntegrationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractParallelBlockProcessorIntegrationTest.java @@ -47,6 +47,7 @@ import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BalanceChange; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiAccount; +import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; @@ -180,7 +181,21 @@ protected Block createBlock( final Wei baseFee, final Address coinbase, final Transaction... txs) { - final BlockHeader parentHeader = ctx.getBlockchain().getChainHeadHeader(); + return createBlock( + ctx, ctx.getBlockchain().getChainHeadHeader(), stateRoot, baseFee, coinbase, txs); + } + + /** + * Builds a block whose parent is {@code parentHeader} (not necessarily the chain head). Block + * number is {@code parentHeader.getNumber() + 1}. + */ + protected Block createBlock( + final ExecutionContextTestFixture ctx, + final BlockHeader parentHeader, + final Hash stateRoot, + final Wei baseFee, + final Address coinbase, + final Transaction... txs) { final BlockHeader blockHeader = new BlockHeaderTestFixture() .number(parentHeader.getNumber() + 1L) @@ -204,8 +219,26 @@ protected Hash discoverStateRoot(final Wei baseFee, final Transaction... txs) { protected Hash discoverStateRoot( final Wei baseFee, final Address coinbase, final Transaction... txs) { final ExecutionContextTestFixture ctx = createFreshContext(); - final MutableWorldState ws = ctx.getStateArchive().getWorldState(); - final Block block = createBlock(ctx, Hash.ZERO, baseFee, coinbase, txs); + return discoverStateRootAtParent( + ctx, ctx.getBlockchain().getChainHeadHeader(), baseFee, coinbase, txs); + } + + /** + * Discovers the post-execution state root for {@code txs} when the block's parent is {@code + * parentHeader}, using a mutable world state layered on that parent's state (same basis as block + * import). + */ + protected Hash discoverStateRootAtParent( + final ExecutionContextTestFixture ctx, + final BlockHeader parentHeader, + final Wei baseFee, + final Address coinbase, + final Transaction... txs) { + final MutableWorldState ws = + ctx.getStateArchive() + .getWorldState(WorldStateQueryParams.withBlockHeaderAndUpdateNodeHead(parentHeader)) + .orElseThrow(); + final Block block = createBlock(ctx, parentHeader, Hash.ZERO, baseFee, coinbase, txs); final BlockProcessor processor = createSequentialProcessor(ctx); final BlockProcessingResult result = processor.processBlock(ctx.getProtocolContext(), ctx.getBlockchain(), ws, block); @@ -225,8 +258,160 @@ protected Hash discoverStateRoot( return Hash.fromHexString(msg.substring(idx + marker.length())); } + /** Header of the parent of the current chain head (i.e. chain head number minus one). */ + protected BlockHeader parentOfChainHead(final ExecutionContextTestFixture ctx) { + final BlockHeader head = ctx.getBlockchain().getChainHeadHeader(); + return ctx.getBlockchain() + .getBlockHeader(head.getParentHash()) + .orElseThrow( + () -> + new IllegalStateException( + "Chain head has no recorded parent; advance the chain by one block first")); + } + + /** + * Appends one empty canonical block on top of the current head (same rules as normal import: + * sequential process then {@link + * org.hyperledger.besu.ethereum.chain.MutableBlockchain#appendBlock}). + */ + protected void advanceCanonicalChainWithOneEmptyBlock( + final ExecutionContextTestFixture ctx, final Wei baseFee) { + final BlockHeader parent = ctx.getBlockchain().getChainHeadHeader(); + final Hash stateRoot = discoverStateRootAtParent(ctx, parent, baseFee, MINING_BENEFICIARY); + final MutableWorldState ws = + ctx.getStateArchive() + .getWorldState(WorldStateQueryParams.withBlockHeaderAndUpdateNodeHead(parent)) + .orElseThrow(); + final Block block = createBlock(ctx, parent, stateRoot, baseFee, MINING_BENEFICIARY); + final BlockProcessor processor = createSequentialProcessor(ctx); + final BlockProcessingResult result = + processor.processBlock(ctx.getProtocolContext(), ctx.getBlockchain(), ws, block); + assertTrue( + result.isSuccessful(), + "Empty block advance failed: " + result.errorMessage.orElse("(no message)")); + ctx.getBlockchain() + .appendBlock( + block, result.getYield().orElseThrow().getReceipts(), getBlockAccessList(result)); + } + + /** + * Like {@link #executeAndCompare(Wei, Transaction...)} but the transaction block is built on the + * parent of the chain head (head minus one): the chain is advanced by one empty block + * first, then transfers are executed as a sibling at the same height as that empty block, with + * world state loaded from that parent (mirroring {@link + * org.hyperledger.besu.ethereum.MainnetBlockValidator}). + */ + protected ComparisonResult executeAndCompareParentOfChainHead( + final Wei baseFee, final Transaction... txs) { + final ExecutionContextTestFixture discoveryCtx = createFreshContext(); + advanceCanonicalChainWithOneEmptyBlock(discoveryCtx, baseFee); + final Hash stateRoot = + discoverStateRootAtParent( + discoveryCtx, parentOfChainHead(discoveryCtx), baseFee, MINING_BENEFICIARY, txs); + + final ExecutionContextTestFixture seqCtx = createFreshContext(); + advanceCanonicalChainWithOneEmptyBlock(seqCtx, baseFee); + final BlockHeader seqTxParent = parentOfChainHead(seqCtx); + final MutableWorldState seqWs = + seqCtx + .getStateArchive() + .getWorldState(WorldStateQueryParams.withBlockHeaderAndUpdateNodeHead(seqTxParent)) + .orElseThrow(); + final Block seqBlock = + createBlock(seqCtx, seqTxParent, stateRoot, baseFee, MINING_BENEFICIARY, txs); + final BlockProcessor seqProcessor = createSequentialProcessor(seqCtx); + final BlockProcessingResult seqResult = + seqProcessor.processBlock( + seqCtx.getProtocolContext(), seqCtx.getBlockchain(), seqWs, seqBlock); + assertTrue( + seqResult.isSuccessful(), + "Sequential processing failed: " + seqResult.errorMessage.orElse("(no message)")); + + final ExecutionContextTestFixture parCtx = createFreshContext(); + advanceCanonicalChainWithOneEmptyBlock(parCtx, baseFee); + final BlockHeader parTxParent = parentOfChainHead(parCtx); + final MutableWorldState parWs = + parCtx + .getStateArchive() + .getWorldState(WorldStateQueryParams.withBlockHeaderAndUpdateNodeHead(parTxParent)) + .orElseThrow(); + final Block parBlock = + createBlock(parCtx, parTxParent, stateRoot, baseFee, MINING_BENEFICIARY, txs); + final ProtocolSpec spec = + parCtx + .getProtocolSchedule() + .getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader()); + final BlockProcessor parProcessor = createParallelProcessor(parCtx); + final ParallelTransactionPreprocessing preprocessing = + createParallelPreprocessing(spec.getTransactionProcessor()); + final BlockProcessingResult parResult = + parProcessor.processBlock( + parCtx.getProtocolContext(), parCtx.getBlockchain(), parWs, parBlock, preprocessing); + assertTrue( + parResult.isSuccessful(), + getVariantName() + + " parallel processing failed: " + + parResult.errorMessage.orElse("(no message)")); + assertParallelizationRecordedInResults(seqResult, parResult, txs.length); + + assertThat(parWs.rootHash()) + .as(getVariantName() + " parallel state root must match sequential") + .isEqualTo(seqWs.rootHash()); + + final Optional seqBal = getBlockAccessList(seqResult); + final Optional parBal = getBlockAccessList(parResult); + assertThat(seqBal).as("Sequential BAL should be present").isPresent(); + assertThat(parBal).as(getVariantName() + " parallel BAL should be present").isPresent(); + assertThat(BodyValidation.balHash(parBal.get())) + .as(getVariantName() + " parallel BAL hash must match sequential") + .isEqualTo(BodyValidation.balHash(seqBal.get())); + + return new ComparisonResult( + seqWs.rootHash(), parWs.rootHash(), seqResult, parResult, seqWs, parWs); + } + // ==================== Core Comparison ==================== + /** + * {@link BlockProcessingResult#getNbParallelizedTransactions()} is populated by {@link + * org.hyperledger.besu.ethereum.mainnet.AbstractBlockProcessor} when at least one transaction was + * applied via the parallel path ({@code isProcessedInParallel} on the tx result). + * + *

With perfect parallelization (BAL), the parallel processor must report this for non-empty + * blocks. With optimistic collision handling, {@link + * org.hyperledger.besu.ethereum.mainnet.parallelization.MainnetParallelBlockProcessor} may fall + * back to fully sequential processing for the whole block, in which case the field stays empty; + * we then only assert that any reported count is positive. + */ + protected void assertParallelizationRecordedInResults( + final BlockProcessingResult sequentialResult, + final BlockProcessingResult parallelResult, + final int transactionCount) { + if (transactionCount < 1) { + return; + } + assertThat(sequentialResult.getNbParallelizedTransactions()) + .as("Sequential MainnetBlockProcessor must not report parallelized transaction metrics") + .isEmpty(); + final Optional nbParallel = parallelResult.getNbParallelizedTransactions(); + if (getBalConfiguration().isPerfectParallelizationEnabled()) { + assertThat(nbParallel) + .as( + getVariantName() + + " parallel run must report nbParallelizedTransactions (parallel path was used)") + .isPresent() + .hasValueSatisfying(n -> assertThat(n).isEqualTo(transactionCount)); + } else { + nbParallel.ifPresent( + n -> + assertThat(n) + .as( + getVariantName() + + " when parallel metrics are present, count must be positive") + .isPositive()); + } + } + protected ComparisonResult executeAndCompare(final Wei baseFee, final Transaction... txs) { final Hash stateRoot = discoverStateRoot(baseFee, txs); @@ -261,6 +446,7 @@ protected ComparisonResult executeAndCompare(final Wei baseFee, final Transactio getVariantName() + " parallel processing failed: " + parResult.errorMessage.orElse("(no message)")); + assertParallelizationRecordedInResults(seqResult, parResult, txs.length); // State root comparison assertThat(parWs.rootHash()) diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractSimpleTransferTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractSimpleTransferTest.java index a8cee7f38f7..4027e4b707f 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractSimpleTransferTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AbstractSimpleTransferTest.java @@ -67,6 +67,35 @@ void independentTransfers() { .isEqualTo(Wei.of(2_000_000_000_000_000_000L)); } + @Test + @DisplayName( + "Independent transfers from different senders match when parent is chain head minus one") + void independentTransfersWithParentAtHeadMinusOne() { + final Transaction tx1 = + createTransferTransaction( + 0, 1_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_2, ACCOUNT_GENESIS_1_KEYPAIR); + final Transaction tx2 = + createTransferTransaction( + 0, 2_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_3, ACCOUNT_GENESIS_2_KEYPAIR); + + final ComparisonResult result = executeAndCompareParentOfChainHead(Wei.of(5), tx1, tx2); + + final Address addr2 = Address.fromHexStringStrict(ACCOUNT_2); + final Address addr3 = Address.fromHexStringStrict(ACCOUNT_3); + final Address sender1 = Address.fromHexStringStrict(ACCOUNT_GENESIS_1); + final Address sender2 = Address.fromHexStringStrict(ACCOUNT_GENESIS_2); + + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr2); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), addr3); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), sender1); + assertAccountsMatch(result.seqWorldState(), result.parWorldState(), sender2); + + assertThat(((BonsaiAccount) result.seqWorldState().get(addr2)).getBalance()) + .isEqualTo(Wei.of(1_000_000_000_000_000_000L)); + assertThat(((BonsaiAccount) result.seqWorldState().get(addr3)).getBalance()) + .isEqualTo(Wei.of(2_000_000_000_000_000_000L)); + } + @Test @DisplayName("Multiple transactions from the same sender produce matching state") void sameSenderConflict() { diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalParallelBlockProcessorIntegrationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalParallelBlockProcessorIntegrationTest.java index e89e85fa0dd..6a2b23e1d19 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalParallelBlockProcessorIntegrationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalParallelBlockProcessorIntegrationTest.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.mainnet.parallelization; import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.MINING_BENEFICIARY; import static org.junit.jupiter.api.Assertions.assertTrue; import org.hyperledger.besu.datatypes.Address; @@ -35,6 +36,7 @@ import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList; +import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams; import org.hyperledger.besu.evm.blockhash.BlockHashLookup; import java.util.List; @@ -150,6 +152,7 @@ protected ComparisonResult executeAndCompare(final Wei baseFee, final Transactio assertTrue( parResult.isSuccessful(), "BAL parallel import failed: " + parResult.errorMessage.orElse("(no message)")); + assertParallelizationRecordedInResults(seqResult, parResult, txs.length); // ========== Step 3: Compare state roots ========== assertThat(parWs.rootHash()) @@ -168,6 +171,101 @@ protected ComparisonResult executeAndCompare(final Wei baseFee, final Transactio return new ComparisonResult( seqWs.rootHash(), parWs.rootHash(), seqResult, parResult, seqWs, parWs); } + + @Override + protected ComparisonResult executeAndCompareParentOfChainHead( + final Wei baseFee, final Transaction... txs) { + final ExecutionContextTestFixture discoveryCtx = createFreshContext(); + advanceCanonicalChainWithOneEmptyBlock(discoveryCtx, baseFee); + final Hash stateRoot = + discoverStateRootAtParent( + discoveryCtx, parentOfChainHead(discoveryCtx), baseFee, MINING_BENEFICIARY, txs); + + final ExecutionContextTestFixture seqCtx = createFreshContext(); + advanceCanonicalChainWithOneEmptyBlock(seqCtx, baseFee); + final BlockHeader seqTxParent = parentOfChainHead(seqCtx); + final MutableWorldState seqWs = + seqCtx + .getStateArchive() + .getWorldState(WorldStateQueryParams.withBlockHeaderAndUpdateNodeHead(seqTxParent)) + .orElseThrow(); + final Block seqBlock = + createBlock(seqCtx, seqTxParent, stateRoot, baseFee, MINING_BENEFICIARY, txs); + final ProtocolSpec spec = + seqCtx + .getProtocolSchedule() + .getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader()); + final MainnetTransactionProcessor txProcessor = spec.getTransactionProcessor(); + + final BlockProcessor seqProcessor = + new MainnetBlockProcessor( + txProcessor, + spec.getTransactionReceiptFactory(), + Wei.ZERO, + BlockHeader::getCoinbase, + true, + seqCtx.getProtocolSchedule(), + SEQUENTIAL_CONFIG); + + final BlockProcessingResult seqResult = + seqProcessor.processBlock( + seqCtx.getProtocolContext(), seqCtx.getBlockchain(), seqWs, seqBlock); + assertTrue( + seqResult.isSuccessful(), + "Sequential execution failed: " + seqResult.errorMessage.orElse("(no message)")); + + final Optional generatedBal = getBlockAccessList(seqResult); + assertThat(generatedBal).as("Sequential execution should produce a BAL").isPresent(); + + final ExecutionContextTestFixture parCtx = createFreshContext(); + advanceCanonicalChainWithOneEmptyBlock(parCtx, baseFee); + final BlockHeader parTxParent = parentOfChainHead(parCtx); + final MutableWorldState parWs = + parCtx + .getStateArchive() + .getWorldState(WorldStateQueryParams.withBlockHeaderAndUpdateNodeHead(parTxParent)) + .orElseThrow(); + final Block parBlock = + createBlock(parCtx, parTxParent, stateRoot, baseFee, MINING_BENEFICIARY, txs); + final ProtocolSpec parSpec = + parCtx + .getProtocolSchedule() + .getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader()); + final MainnetTransactionProcessor parTxProcessor = parSpec.getTransactionProcessor(); + + final BlockProcessor parProcessor = createParallelProcessor(parCtx); + final ParallelTransactionPreprocessing balImportPreprocessing = + new ParallelTransactionPreprocessingWithBal( + parTxProcessor, generatedBal.get(), BalConfiguration.DEFAULT); + + final BlockProcessingResult parResult = + parProcessor.processBlock( + parCtx.getProtocolContext(), + parCtx.getBlockchain(), + parWs, + parBlock, + balImportPreprocessing); + assertTrue( + parResult.isSuccessful(), + "BAL parallel import failed: " + parResult.errorMessage.orElse("(no message)")); + assertParallelizationRecordedInResults(seqResult, parResult, txs.length); + + assertThat(parWs.rootHash()) + .as("BAL parallel import state root must match sequential reference") + .isEqualTo(seqWs.rootHash()); + + final Optional parBal = getBlockAccessList(parResult); + assertThat(parBal).as("Parallel import should also produce a BAL").isPresent(); + + final Hash seqBalHash = BodyValidation.balHash(generatedBal.get()); + final Hash parBalHash = BodyValidation.balHash(parBal.get()); + assertThat(parBalHash) + .as("BAL hash from parallel import must match sequential") + .isEqualTo(seqBalHash); + + return new ComparisonResult( + seqWs.rootHash(), parWs.rootHash(), seqResult, parResult, seqWs, parWs); + } } /** @@ -196,7 +294,8 @@ public Optional run( final BlockHashLookup blockHashLookup, final Wei blobGasPrice, final Optional blockAccessListBuilder, - final Optional maybeBlockBal) { + final Optional maybeBlockBal, + final Optional maybeParentHeader) { return super.run( protocolContext, blockHeader, @@ -205,7 +304,8 @@ public Optional run( blockHashLookup, blobGasPrice, blockAccessListBuilder, - Optional.of(preComputedBal)); + Optional.of(preComputedBal), + maybeParentHeader); } } @@ -227,6 +327,12 @@ protected ParallelTransactionPreprocessing createParallelPreprocessing( protected ComparisonResult executeAndCompare(final Wei baseFee, final Transaction... txs) { return new BalTestBase() {}.executeAndCompare(baseFee, txs); } + + @Override + protected ComparisonResult executeAndCompareParentOfChainHead( + final Wei baseFee, final Transaction... txs) { + return new BalTestBase() {}.executeAndCompareParentOfChainHead(baseFee, txs); + } } @Nested diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalTransactionProcessorUnitTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalTransactionProcessorUnitTest.java index 59bbd46c95f..02438bca648 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalTransactionProcessorUnitTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/BalTransactionProcessorUnitTest.java @@ -18,8 +18,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -29,7 +31,6 @@ import org.hyperledger.besu.datatypes.StorageSlotKey; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.chain.MutableBlockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; import org.hyperledger.besu.ethereum.core.Transaction; @@ -45,6 +46,7 @@ import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoopBonsaiCachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldState; +import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams; import org.hyperledger.besu.ethereum.trie.pathbased.common.trielog.NoOpTrieLogManager; import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; @@ -85,7 +87,11 @@ class BalTransactionProcessorUnitTest { @Mock private MainnetTransactionProcessor transactionProcessor; private record TestEnvironment( - ProtocolContext protocolContext, BlockHeader blockHeader, BonsaiWorldState worldState) {} + ProtocolContext protocolContext, + BlockHeader blockHeader, + Optional maybeParentHeader, + WorldStateArchive worldStateArchive, + BonsaiWorldState worldState) {} private BonsaiWorldState createEmptyWorldState() { final BonsaiWorldStateKeyValueStorage storage = @@ -106,21 +112,18 @@ private BonsaiWorldState createEmptyWorldState() { private TestEnvironment createTestEnvironment() { final ProtocolContext protocolContext = mock(ProtocolContext.class); - final MutableBlockchain blockchain = mock(MutableBlockchain.class); - final BlockHeader chainHeadBlockHeader = mock(BlockHeader.class); + final BlockHeader parentHeader = mock(BlockHeader.class); final BlockHeader blockHeader = mock(BlockHeader.class); final WorldStateArchive worldStateArchive = mock(WorldStateArchive.class); final BonsaiWorldState worldState = createEmptyWorldState(); - when(protocolContext.getBlockchain()).thenReturn(blockchain); - when(blockchain.getChainHeadHeader()).thenReturn(chainHeadBlockHeader); - when(chainHeadBlockHeader.getHash()).thenReturn(Hash.ZERO); - when(chainHeadBlockHeader.getStateRoot()).thenReturn(Hash.EMPTY_TRIE_HASH); - when(blockHeader.getParentHash()).thenReturn(Hash.ZERO); when(protocolContext.getWorldStateArchive()).thenReturn(worldStateArchive); when(worldStateArchive.getWorldState(any())).thenReturn(Optional.of(worldState)); + when(parentHeader.getBlockHash()).thenReturn(Hash.ZERO); + when(parentHeader.getStateRoot()).thenReturn(Hash.EMPTY_TRIE_HASH); - return new TestEnvironment(protocolContext, blockHeader, worldState); + return new TestEnvironment( + protocolContext, blockHeader, Optional.of(parentHeader), worldStateArchive, worldState); } private Transaction mockTransaction() { @@ -172,7 +175,8 @@ void transactionProcessorCalledWithCorrectParams() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); verify(transactionProcessor, times(1)) .processTransaction( @@ -209,7 +213,8 @@ void allTransactionsProcessed() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); verify(transactionProcessor, times(3)) .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); @@ -235,7 +240,8 @@ void processingResultReturnedForSuccessfulTransaction() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); final Optional result = processor.getProcessingResult( @@ -251,6 +257,115 @@ void processingResultReturnedForSuccessfulTransaction() { } } + @Nested + @DisplayName("Parent header and world state loading") + class ParentHeaderAndWorldStateTests { + + @Test + @DisplayName("Does not query archive or process transaction when parent header is absent") + void skipsProcessingWhenParentHeaderAbsent() { + final ProtocolContext protocolContext = mock(ProtocolContext.class); + final BlockHeader blockHeader = mock(BlockHeader.class); + final BlockAccessList blockAccessList = mock(BlockAccessList.class); + final Transaction transaction = mock(Transaction.class); + final BonsaiWorldState worldStateForResult = createEmptyWorldState(); + final BalConcurrentTransactionProcessor processor = + new BalConcurrentTransactionProcessor( + transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); + + processor.runAsyncBlock( + protocolContext, + blockHeader, + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty(), + Optional.empty()); + + verify(protocolContext, never()).getWorldStateArchive(); + verify(transactionProcessor, never()) + .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); + assertTrue( + processor + .getProcessingResult( + worldStateForResult, + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()) + .isEmpty()); + } + + @Test + @DisplayName("Does not process transaction when world state archive returns empty") + void skipsProcessingWhenArchiveHasNoWorldState() { + final TestEnvironment env = createTestEnvironment(); + when(env.worldStateArchive().getWorldState(any())).thenReturn(Optional.empty()); + final BlockAccessList blockAccessList = mock(BlockAccessList.class); + final Transaction transaction = mock(Transaction.class); + final BalConcurrentTransactionProcessor processor = + new BalConcurrentTransactionProcessor( + transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty(), + env.maybeParentHeader()); + + verify(env.worldStateArchive(), times(1)).getWorldState(any()); + verify(transactionProcessor, never()) + .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); + assertTrue( + processor + .getProcessingResult( + env.worldState(), + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()) + .isEmpty()); + } + + @Test + @DisplayName("World state query uses the parent block header") + void loadWorldStateUsesParentBlockHeader() { + final TestEnvironment env = createTestEnvironment(); + stubSuccessfulTransaction(); + final BlockAccessList blockAccessList = mockEmptyBlockAccessList(); + final Transaction transaction = mockTransaction(); + final BlockHeader parent = env.maybeParentHeader().orElseThrow(); + final BalConcurrentTransactionProcessor processor = + new BalConcurrentTransactionProcessor( + transactionProcessor, blockAccessList, BalConfiguration.DEFAULT); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty(), + env.maybeParentHeader()); + + verify(env.worldStateArchive()) + .getWorldState(argThat((WorldStateQueryParams p) -> p.getBlockHeader() == parent)); + verify(transactionProcessor, times(1)) + .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); + } + } + @Nested @DisplayName("Pre-State Setup") class PreStateSetupTests { @@ -388,7 +503,8 @@ void preStateSetupFromBlockAccessList() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); final Transaction[] txs = new Transaction[] {tx0, tx1, tx2}; for (int i = 0; i < txs.length; i++) { @@ -438,7 +554,8 @@ void returnsEmptyWhenParallelContextIsNull() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); final Optional result = processor.getProcessingResult( diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticTransactionProcessorUnitTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticTransactionProcessorUnitTest.java index c0f081b8c30..32e27c308bc 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticTransactionProcessorUnitTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/OptimisticTransactionProcessorUnitTest.java @@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -28,7 +29,6 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.chain.MutableBlockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; import org.hyperledger.besu.ethereum.core.Transaction; @@ -43,6 +43,7 @@ import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.cache.NoopBonsaiCachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.storage.BonsaiWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldState; +import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams; import org.hyperledger.besu.ethereum.trie.pathbased.common.trielog.NoOpTrieLogManager; import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; @@ -65,6 +66,8 @@ 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; /** * Unit tests for ParallelizedConcurrentTransactionProcessor (Optimistic strategy). Tests verify: - @@ -92,7 +95,11 @@ void setUp() { } private record TestEnvironment( - ProtocolContext protocolContext, BlockHeader blockHeader, BonsaiWorldState worldState) {} + ProtocolContext protocolContext, + BlockHeader blockHeader, + Optional maybeParentHeader, + WorldStateArchive worldStateArchive, + BonsaiWorldState worldState) {} private BonsaiWorldState createEmptyWorldState() { final BonsaiWorldStateKeyValueStorage storage = @@ -113,21 +120,18 @@ private BonsaiWorldState createEmptyWorldState() { private TestEnvironment createTestEnvironment() { final ProtocolContext protocolContext = mock(ProtocolContext.class); - final MutableBlockchain blockchain = mock(MutableBlockchain.class); - final BlockHeader chainHeadBlockHeader = mock(BlockHeader.class); + final BlockHeader parentHeader = mock(BlockHeader.class); final BlockHeader blockHeader = mock(BlockHeader.class); final WorldStateArchive worldStateArchive = mock(WorldStateArchive.class); final BonsaiWorldState worldState = createEmptyWorldState(); - when(protocolContext.getBlockchain()).thenReturn(blockchain); - when(blockchain.getChainHeadHeader()).thenReturn(chainHeadBlockHeader); - when(chainHeadBlockHeader.getHash()).thenReturn(Hash.ZERO); - when(chainHeadBlockHeader.getStateRoot()).thenReturn(Hash.EMPTY_TRIE_HASH); - when(blockHeader.getParentHash()).thenReturn(Hash.ZERO); when(protocolContext.getWorldStateArchive()).thenReturn(worldStateArchive); when(worldStateArchive.getWorldState(any())).thenReturn(Optional.of(worldState)); + when(parentHeader.getBlockHash()).thenReturn(Hash.ZERO); + when(parentHeader.getStateRoot()).thenReturn(Hash.EMPTY_TRIE_HASH); - return new TestEnvironment(protocolContext, blockHeader, worldState); + return new TestEnvironment( + protocolContext, blockHeader, Optional.of(parentHeader), worldStateArchive, worldState); } private Transaction mockTransaction() { @@ -162,7 +166,8 @@ void transactionProcessorCalledWithCorrectParams() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); verify(transactionProcessor, times(1)) .processTransaction( @@ -193,13 +198,111 @@ void transactionProcessorCalledOncePerTransaction() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); verify(transactionProcessor, times(3)) .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); } } + @Nested + @DisplayName("Parent header and world state loading") + @MockitoSettings( + strictness = Strictness.LENIENT) // outer @BeforeEach builds env unused by parent-absent test + class ParentHeaderAndWorldStateTests { + + @Test + @DisplayName("Does not query archive or process transaction when parent header is absent") + void skipsProcessingWhenParentHeaderAbsent() { + final ProtocolContext protocolContext = mock(ProtocolContext.class); + final BlockHeader blockHeader = mock(BlockHeader.class); + final Transaction transaction = mock(Transaction.class); + final BonsaiWorldState worldStateForResult = createEmptyWorldState(); + + processor.runAsyncBlock( + protocolContext, + blockHeader, + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty(), + Optional.empty()); + + verify(protocolContext, never()).getWorldStateArchive(); + verify(transactionProcessor, never()) + .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); + assertTrue( + processor + .getProcessingResult( + worldStateForResult, + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()) + .isEmpty()); + } + + @Test + @DisplayName("Does not process transaction when world state archive returns empty") + void skipsProcessingWhenArchiveHasNoWorldState() { + when(env.worldStateArchive().getWorldState(any())).thenReturn(Optional.empty()); + final Transaction transaction = mock(Transaction.class); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty(), + env.maybeParentHeader()); + + verify(env.worldStateArchive(), times(1)).getWorldState(any()); + verify(transactionProcessor, never()) + .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); + assertTrue( + processor + .getProcessingResult( + env.worldState(), + MINING_BENEFICIARY, + transaction, + 0, + Optional.empty(), + Optional.empty()) + .isEmpty()); + } + + @Test + @DisplayName("World state query uses the parent block header") + void loadWorldStateUsesParentBlockHeader() { + final Transaction transaction = mockTransaction(); + stubSuccessfulTransaction(Optional.empty()); + final BlockHeader parent = env.maybeParentHeader().orElseThrow(); + + processor.runAsyncBlock( + env.protocolContext(), + env.blockHeader(), + Collections.singletonList(transaction), + MINING_BENEFICIARY, + (__, ___) -> Hash.EMPTY, + BLOB_GAS_PRICE, + sameThreadExecutor, + Optional.empty(), + env.maybeParentHeader()); + + verify(env.worldStateArchive()) + .getWorldState(argThat((WorldStateQueryParams p) -> p.getBlockHeader() == parent)); + verify(transactionProcessor, times(1)) + .processTransaction(any(), any(), any(), any(), any(), any(), any(), any(), any()); + } + } + @Nested @DisplayName("Collision Detection") class CollisionDetectionTests { @@ -219,7 +322,8 @@ void collisionDetectorCalledWithCorrectTransaction() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); processor.getProcessingResult( env.worldState(), MINING_BENEFICIARY, transaction, 0, Optional.empty(), Optional.empty()); @@ -248,7 +352,8 @@ void collisionDetectorCalledWithCorrectMiningBeneficiary() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); processor.getProcessingResult( env.worldState(), customBeneficiary, transaction, 0, Optional.empty(), Optional.empty()); @@ -274,7 +379,8 @@ void collisionDetectorCalledForEachTransaction() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); processor.getProcessingResult( env.worldState(), MINING_BENEFICIARY, tx1, 0, Optional.empty(), Optional.empty()); @@ -310,7 +416,8 @@ void returnsEmptyWhenCollisionDetected() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); when(collisionDetector.hasCollision(any(), any(), any(), any())).thenReturn(true); @@ -343,7 +450,8 @@ void returnsEmptyWhenParallelContextIsNull() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); final Optional result = processor.getProcessingResult( @@ -373,7 +481,8 @@ void returnsResultWhenNoCollision() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); final Optional result = processor.getProcessingResult( @@ -403,7 +512,8 @@ void partialFallbackOnSecondTransaction() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.empty()); + Optional.empty(), + env.maybeParentHeader()); when(collisionDetector.hasCollision(eq(tx1), any(), any(), any())).thenReturn(false); when(collisionDetector.hasCollision(eq(tx2), any(), any(), any())).thenReturn(true); @@ -440,7 +550,8 @@ void accessLocationTrackerPassedWithBalBuilder() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.of(balBuilder)); + Optional.of(balBuilder), + env.maybeParentHeader()); verify(transactionProcessor) .processTransaction( @@ -475,7 +586,8 @@ void partialBlockAccessViewPreservedInResult() { (__, ___) -> Hash.EMPTY, BLOB_GAS_PRICE, sameThreadExecutor, - Optional.of(balBuilder)); + Optional.of(balBuilder), + env.maybeParentHeader()); final Optional maybeResult = processor.getProcessingResult(