diff --git a/README.md b/README.md index 0f0466ea4e..bb6a556136 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Linea tracer (zkEVM) -This repository hosts a Linea tracing implementation for -[Besu](https://github.com/hyperledger/besu) based on an - implementation in Go. +This repository hosts a Linea tracing implementation for +[Besu](https://github.com/hyperledger/besu) based on an +implementation in Go. Tracing refers to the process of extracting data from the execution of an EVM client in order to construct large matrices known as execution traces. Execution traces are subject to the constraint system specified in the [linea-specification](https://github.com/Consensys/linea-specification) repo and implemented in the [linea-constraints](https://github.com/Consensys/linea-constraints) repo. -It serves developers by making the Linea tech stack open source under +It serves developers by making the Linea tech stack open source under the [Apache 2.0 license](LICENSE). ## What is Linea? @@ -19,15 +19,17 @@ If you already have an understanding of the tech stack, use our [Get Started](do ### Looking for plugins? -Discover [existing plugins](docs/plugins.md) and understand the [plugin release process](docs/plugin-release.md). +Discover [existing plugins](docs/plugins.md) and understand the [plugin release process](docs/plugin-release.md). ## Looking for the Linea code? Linea's stack is made up of multiple repositories, these include: + - This repo, [linea-tracer](https://github.com/Consensys/linea-tracer): Linea-Besu plugin which produces the traces that the constraint system applies and that serve as inputs to the prover -> + > This repository contains the elements of the Linea stack responsible for this process. -- [linea-monorepo](https://github.com/Consensys/linea-monorepo): The main repository for the Linea stack & network + +- [linea-monorepo](https://github.com/Consensys/linea-monorepo): The main repository for the Linea stack & network - [besu](https://github.com/hyperledger/besu): Besu client - [linea-sequencer](https://github.com/Consensys/linea-sequencer): A set of Linea-Besu plugins for the sequencer and RPC nodes - [linea-constraints](https://github.com/Consensys/linea-constraints): Implementation of the constraint system from the specification @@ -46,11 +48,15 @@ Linea abstracts away the complexity of this technical architecture to allow deve Contributions are welcome! ### Guidelines for Non-Code and other Trivial Contributions + Please keep in mind that we do not accept non-code contributions like fixing comments, typos or some other trivial fixes. Although we appreciate the extra help, managing lots of these small contributions is unfeasible, and puts extra pressure in our continuous delivery systems (running all tests, etc). Feel free to open an issue pointing to any of those errors, and we will batch them into a single change. 1. [Create an issue](https://github.com/Consensys/linea-tracer/issues). + > If the proposed update requires input, also tag us for discussion. -2. Submit the update as a pull request from your [fork of this repo](https://github.com/Consensys/linea-tracer/fork), and tag us for review. + +2. Submit the update as a pull request from your [fork of this repo](https://github.com/Consensys/linea-tracer/fork), and tag us for review. + > Include the issue number in the pull request description and (optionally) in the branch name. Consider starting with a ["good first issue"](https://github.com/ConsenSys/linea-tracer/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). @@ -62,7 +68,6 @@ Before contributing, ensure you're familiar with: - The [Besu contribution guide](https://wiki.hyperledger.org/display/BESU/Coding+Conventions), for Besu:Linea related contributions - Our [Security policy](https://github.com/Consensys/linea-monorepo/blob/main/docs/security.md) - ### Useful links - [Linea docs](https://docs.linea.build) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/txndata/osaka/OsakaUserTransaction.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/txndata/osaka/OsakaUserTransaction.java index 453fbc242c..b5a7f54d0c 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/txndata/osaka/OsakaUserTransaction.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/txndata/osaka/OsakaUserTransaction.java @@ -15,7 +15,7 @@ package net.consensys.linea.zktracer.module.txndata.osaka; -import static net.consensys.linea.zktracer.Trace.EIP_7825_TRANSACTION_GAS_LIMIT_CAP; +import static net.consensys.linea.zktracer.TraceOsaka.EIP_7825_TRANSACTION_GAS_LIMIT_CAP; import net.consensys.linea.zktracer.module.txndata.cancun.CancunTxnData; import net.consensys.linea.zktracer.module.txndata.cancun.rows.computationRows.WcpRow; diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/EmptyBlockTests.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/EmptyBlockTests.java index de6f99f5e8..5d80be0176 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/EmptyBlockTests.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/EmptyBlockTests.java @@ -38,163 +38,299 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.core.Transaction; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class EmptyBlockTests extends TracerTestBase { - @Test - void mixOfEmptyAndNonEmptyBlocks(TestInfo testInfo) { + enum BlockType { + EMPTY_BLOCK, + MONO_TRANSACTION_BLOCK___STORING, + MONO_TRANSACTION_BLOCK___READING, + } + + final KeyPair senderKeyPair = new SECP256K1().generateKeyPair(); + final Address senderAddress = + Address.extract(Hash.hash(senderKeyPair.getPublicKey().getEncodedBytes())); + + final ToyAccount senderAccount = + ToyAccount.builder().balance(Wei.fromEth(128)).nonce(5).address(senderAddress).build(); + + final ToyAccount receivingAccount = + ToyAccount.builder() + .balance(Wei.fromEth(1)) + .nonce(116) + .address(Address.fromHexStringStrict("0x1122334455667788990011223344556677889900")) + .code( + BytecodeCompiler.newProgram(chainConfig) + .push(0) // ret size + .push(0) // ret offset + .push(0) // size + .push(0) // offset + + // address: + .op(OpCode.CALLDATASIZE) // size + .push(0) // offset + .push(0) // dest offset + .op(OpCode.CALLDATACOPY) + .push(0) // offset + .op(OpCode.MLOAD) + .push(100000) // gas + .op(OpCode.DELEGATECALL) + .compile()) + .build(); + + final ToyAccount storingNumber = + ToyAccount.builder() + .balance(Wei.fromEth(1)) + .nonce(116) + .address(Address.wrap(leftPadTo(Bytes.minimalBytes(1111111), 20))) + .code( + BytecodeCompiler.newProgram(chainConfig) + .op(OpCode.NUMBER) // value + .push(Bytes.fromHexString("c7e0")) // key + .op(OpCode.SSTORE) + .compile()) + .build(); + + final ToyAccount logging = + ToyAccount.builder() + .balance(Wei.fromEth(1)) + .nonce(116) + .address(Address.wrap(leftPadTo(Bytes.minimalBytes(222222), 20))) + .code( + BytecodeCompiler.newProgram(chainConfig) + .push(2) + .op(OpCode.NUMBER) + .op(OpCode.SUB) + .push(Bytes.fromHexString("c7e0")) // key + .op(OpCode.SLOAD) + .op(OpCode.SUB) + // We now have on the stack one arg, (NUMBER -2 ) - SLOAD + .push(18) // counter to go (and there STOP) if condition is true, as we want + // to log if false + .op(OpCode.JUMPI) + // LOG to check the output + .push(0) + .push(0) + .op(OpCode.LOG0) + .op(OpCode.STOP) + .op(OpCode.JUMPDEST) + .op(OpCode.STOP) + .compile()) + .build(); + + final Transaction storing = + ToyTransaction.builder() + .sender(senderAccount) + .to(receivingAccount) + .payload(Bytes32.leftPad(storingNumber.getAddress())) + .keyPair(senderKeyPair) + .value(Wei.of(123)) + .build(); + + final Transaction reading = + ToyTransaction.builder() + .sender(senderAccount) + .to(receivingAccount) + .payload(Bytes32.leftPad(logging.getAddress())) + .keyPair(senderKeyPair) + .value(Wei.of(123)) + .nonce(senderAccount.getNonce() + 1) + .build(); + + /** + * Mix of empty and non empty blocks + * + * + * + * @param runWithBesu we either run the empty block tests with a MultiBlockExecutionEnvironment or + * with a Besu node. Runnning them with a Besu node verifies the behavior of a conflation + * trace generated through RPC containing empty blocks. It can't however operate check on the + * tracer per say, like retrieve the hub. In the case where we run with a Besu node, we don't + * perform check on the hub values. + * @param testInfo + */ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void mixOfEmptyAndNonEmptyBlocks_EENENE(boolean runWithBesu, TestInfo testInfo) { // Empty block are allowed only after Cancun - if (isPostCancun(chainConfig.fork)) { - - final ToyAccount receivingAccount = - ToyAccount.builder() - .balance(Wei.fromEth(1)) - .nonce(116) - .address(Address.fromHexStringStrict("0x1122334455667788990011223344556677889900")) - .code( - BytecodeCompiler.newProgram(chainConfig) - .push(0) // ret size - .push(0) // ret offset - .push(0) // size - .push(0) // offset - - // address: - .op(OpCode.CALLDATASIZE) // size - .push(0) // offset - .push(0) // dest offset - .op(OpCode.CALLDATACOPY) - .push(0) // offset - .op(OpCode.MLOAD) - .push(100000) // gas - .op(OpCode.DELEGATECALL) - .compile()) - .build(); - - final ToyAccount storingNumber = - ToyAccount.builder() - .balance(Wei.fromEth(1)) - .nonce(116) - .address(Address.wrap(leftPadTo(Bytes.minimalBytes(1111111), 20))) - .code( - BytecodeCompiler.newProgram(chainConfig) - .op(OpCode.NUMBER) // value - .push(Bytes.fromHexString("c7e0")) // key - .op(OpCode.SSTORE) - .compile()) - .build(); - - final ToyAccount logging = - ToyAccount.builder() - .balance(Wei.fromEth(1)) - .nonce(116) - .address(Address.wrap(leftPadTo(Bytes.minimalBytes(222222), 20))) - .code( - BytecodeCompiler.newProgram(chainConfig) - .push(2) - .op(OpCode.NUMBER) - .op(OpCode.SUB) - .push(Bytes.fromHexString("c7e0")) // key - .op(OpCode.SLOAD) - .op(OpCode.SUB) - // We now have on the stack one arg, (NUMBER -2 ) - SLOAD - .push(18) // counter to go (and there STOP) if condition is true, as we want - // to log if false - .op(OpCode.JUMPI) - // LOG to check the output - .push(0) - .push(0) - .op(OpCode.LOG0) - .op(OpCode.STOP) - .op(OpCode.JUMPDEST) - .op(OpCode.STOP) - .compile()) - .build(); - - final KeyPair senderKeyPair = new SECP256K1().generateKeyPair(); - final Address senderAddress = - Address.extract(Hash.hash(senderKeyPair.getPublicKey().getEncodedBytes())); - final ToyAccount senderAccount = - ToyAccount.builder().balance(Wei.fromEth(128)).nonce(5).address(senderAddress).build(); - - final Transaction storing = - ToyTransaction.builder() - .sender(senderAccount) - .to(receivingAccount) - .payload(Bytes32.leftPad(storingNumber.getAddress())) - .keyPair(senderKeyPair) - .value(Wei.of(123)) - .build(); - - final Transaction reading = - ToyTransaction.builder() - .sender(senderAccount) - .to(receivingAccount) - .payload(Bytes32.leftPad(logging.getAddress())) - .keyPair(senderKeyPair) - .value(Wei.of(123)) - .nonce(senderAccount.getNonce() + 1) - .build(); - - final MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = - MultiBlockExecutionEnvironment.builder(chainConfig, testInfo) - .accounts(List.of(senderAccount, storingNumber, logging, receivingAccount)); - - // Two blocks are empty - builder.addBlock(List.of()); - builder.addBlock(List.of()); - - // Third block has a single transaction - builder.addBlock(List.of(storing)); - - // One more empty block - builder.addBlock(List.of()); - - // Fifth block has a single transaction - builder.addBlock(List.of(reading)); - - // One more empty block - builder.addBlock(List.of()); - - final MultiBlockExecutionEnvironment env = builder.build(); + if (isPostCancun(fork)) { + + MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = + builderFromBlockTypeList( + List.of( + BlockType.EMPTY_BLOCK, + BlockType.EMPTY_BLOCK, + BlockType.MONO_TRANSACTION_BLOCK___STORING, + BlockType.EMPTY_BLOCK, + BlockType.MONO_TRANSACTION_BLOCK___READING, + BlockType.EMPTY_BLOCK), + testInfo); + + final MultiBlockExecutionEnvironment env = builder.runWithBesuNode(runWithBesu).build(); env.run(); - final State hub = env.getHub().state(); - short nbOfLog = 0; - for (State.HubTransactionState state : hub.getState().getAll()) { - for (TraceSection section : state.traceSections().trace()) { - if (section instanceof LogSection) { - nbOfLog += 1; + if (!runWithBesu) { + final State hub = env.getHub().state(); + short nbOfLog = 0; + for (State.HubTransactionState state : hub.getState().getAll()) { + for (TraceSection section : state.traceSections().trace()) { + if (section instanceof LogSection) { + nbOfLog += 1; + } } } + /** + * The idea is in one block to SSTORE the blockNumber, and few block after to SLOAD, compare + * it with the actual block number, and if we have a match, then do a logging we could + * check. The idea is to ensue that empty blocks are handled well, ie that the number of the + * block number, known by besu and the tracer is updating how we assume it + */ + checkArgument(nbOfLog == 1, "There should be exactly one log section"); } - /** - * The idea is in one block to SSTORE the blockNumber, and few block after to SLOAD, compare - * it with the actual block number, and if we have a match, then do a logging we could check. - * The idea is to ensue that empty blocks are handled well, ie that the number of the block - * number, known by besu and the tracer is updating how we assume it - */ - checkArgument(nbOfLog == 1, "There should be exactly one log section"); } } - @Test - void onlyOneEmptyBlock(TestInfo testInfo) { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void mixOfEmptyAndNonEmptyBlocks_NEEN(boolean runWithBesu, TestInfo testInfo) { // Empty block are allowed only after Cancun - if (isPostCancun(chainConfig.fork)) { + if (isPostCancun(fork)) { - final MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = - MultiBlockExecutionEnvironment.builder(chainConfig, testInfo); + MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = + builderFromBlockTypeList( + List.of( + BlockType.MONO_TRANSACTION_BLOCK___STORING, + BlockType.EMPTY_BLOCK, + BlockType.EMPTY_BLOCK, + BlockType.MONO_TRANSACTION_BLOCK___READING), + testInfo); - // One empty block - builder.addBlock(List.of()); + final MultiBlockExecutionEnvironment env = builder.runWithBesuNode(runWithBesu).build(); + env.run(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void mixOfEmptyAndNonEmptyBlocks_NEEE(boolean runWithBesu, TestInfo testInfo) { + // Empty block are allowed only after Cancun + if (isPostCancun(fork)) { + + MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = + builderFromBlockTypeList( + List.of( + BlockType.EMPTY_BLOCK, + BlockType.EMPTY_BLOCK, + BlockType.EMPTY_BLOCK, + BlockType.MONO_TRANSACTION_BLOCK___STORING), + testInfo); - final MultiBlockExecutionEnvironment env = builder.build(); + final MultiBlockExecutionEnvironment env = builder.runWithBesuNode(runWithBesu).build(); env.run(); - checkArgument( - env.getHub().txStack().transactions().isEmpty(), - "There should be no transaction in the state"); } } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void mixOfEmptyAndNonEmptyBlocks_ENNE(boolean runWithBesu, TestInfo testInfo) { + // Empty block are allowed only after Cancun + if (isPostCancun(fork)) { + + MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = + builderFromBlockTypeList( + List.of( + BlockType.EMPTY_BLOCK, + BlockType.MONO_TRANSACTION_BLOCK___STORING, + BlockType.MONO_TRANSACTION_BLOCK___READING, + BlockType.EMPTY_BLOCK), + testInfo); + + final MultiBlockExecutionEnvironment env = builder.runWithBesuNode(runWithBesu).build(); + env.run(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void mixOfEmptyAndNonEmptyBlocks_EEEE(boolean runWithBesu, TestInfo testInfo) { + // Empty block are allowed only after Cancun + if (isPostCancun(fork)) { + + MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = + builderFromBlockTypeList( + List.of( + BlockType.EMPTY_BLOCK, + BlockType.EMPTY_BLOCK, + BlockType.EMPTY_BLOCK, + BlockType.EMPTY_BLOCK), + testInfo); + + final MultiBlockExecutionEnvironment env = builder.runWithBesuNode(runWithBesu).build(); + env.run(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void mixOfEmptyAndNonEmptyBlocks_EEEN(boolean runWithBesu, TestInfo testInfo) { + // Empty block are allowed only after Cancun + if (isPostCancun(fork)) { + + MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = + builderFromBlockTypeList( + List.of( + BlockType.EMPTY_BLOCK, + BlockType.EMPTY_BLOCK, + BlockType.EMPTY_BLOCK, + BlockType.MONO_TRANSACTION_BLOCK___STORING), + testInfo); + + final MultiBlockExecutionEnvironment env = builder.runWithBesuNode(runWithBesu).build(); + env.run(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void mixOfEmptyAndNonEmptyBlocks_E(boolean runWithBesu, TestInfo testInfo) { + // Empty blocks are allowed only after Cancun + if (isPostCancun(fork)) { + + MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = + builderFromBlockTypeList(List.of(BlockType.EMPTY_BLOCK), testInfo); + + final MultiBlockExecutionEnvironment env = builder.runWithBesuNode(runWithBesu).build(); + env.run(); + if (!runWithBesu) { + checkArgument( + env.getHub().txStack().transactions().isEmpty(), + "There should be no transaction in the state"); + } + } + } + + private MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder + builderFromBlockTypeList(List blockTypes, TestInfo testInfo) { + + final MultiBlockExecutionEnvironment.MultiBlockExecutionEnvironmentBuilder builder = + MultiBlockExecutionEnvironment.builder(chainConfig, testInfo) + .accounts(List.of(senderAccount, storingNumber, logging, receivingAccount)) + .runWithBesuNode(true); + + for (BlockType blockType : blockTypes) { + switch (blockType) { + case EMPTY_BLOCK -> builder.addBlock(List.of()); + case MONO_TRANSACTION_BLOCK___STORING -> builder.addBlock(List.of(storing)); + case MONO_TRANSACTION_BLOCK___READING -> builder.addBlock(List.of(reading)); + } + } + + return builder; + } } diff --git a/testing/src/main/java/net/consensys/linea/testing/BesuExecutionTools.java b/testing/src/main/java/net/consensys/linea/testing/BesuExecutionTools.java index a07a68f705..2e649e2fce 100644 --- a/testing/src/main/java/net/consensys/linea/testing/BesuExecutionTools.java +++ b/testing/src/main/java/net/consensys/linea/testing/BesuExecutionTools.java @@ -14,6 +14,8 @@ */ package net.consensys.linea.testing; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; import static java.lang.Long.parseLong; import static net.consensys.linea.testing.ShomeiNode.MerkelProofResponse; import static net.consensys.linea.zktracer.Fork.OSAKA; @@ -175,6 +177,10 @@ public void executeTest() { new NetConditions(new NetTransactions()), new ThreadBesuNodeRunner()); try { + + checkArgument( + !transactions.isEmpty(), "At least one transaction (including null) is required"); + shomeiThread.start(); besuCluster.start(besuNode); @@ -187,14 +193,25 @@ public void executeTest() { Iterator txs = transactions.iterator(); Boolean txHasNext = txs.hasNext(); + int numberOfLeadingEmptyBlocks = 0; + for (Transaction tx : transactions) { + if (tx != null) { + break; + } + numberOfLeadingEmptyBlocks++; + } + boolean allTransactionsAreNull = (numberOfLeadingEmptyBlocks == transactions.size()); + while (txHasNext) { // Send transaction to the transaction pool with eth_sendRawTransaction // If oneTxPerBlock is true, we send one transaction per block if (oneTxPerBlock) { - String txHash = - besuNode.execute( - ethTransactions.sendRawTransaction(txs.next().encoded().toHexString())); - txHashes.add(txHash); + final Transaction tx = txs.next(); + if (tx != null) { + String txHash = + besuNode.execute(ethTransactions.sendRawTransaction(tx.encoded().toHexString())); + txHashes.add(txHash); + } txHasNext = txs.hasNext(); } else { // Send all transactions in the same block @@ -220,10 +237,11 @@ public void executeTest() { currentFork = nextFork; // We trace the conflation - assertThat(blockNumbers).isNotEmpty(); - long startBlockNumber = Collections.min(blockNumbers); - long endBlockNumber = Collections.max(blockNumbers); - TraceFile traceFile = traceAndCheckTracer(startBlockNumber, endBlockNumber, currentFork); + checkState(blockNumbers.isEmpty() == allTransactionsAreNull); + long firstBlockNumber = blockNumbers.isEmpty() ? 1 : Collections.min(blockNumbers); + long finalBlockNumber = + blockNumbers.isEmpty() ? transactions.size() : Collections.max(blockNumbers); + TraceFile traceFile = traceAndCheckTracer(firstBlockNumber, finalBlockNumber, currentFork); Path traceFilePath = Path.of(traceFile.conflatedTracesFileName()); // Clean up for next transaction @@ -235,8 +253,8 @@ public void executeTest() { requestAndStoreExecutionProof( besuNode, ethTransactions, - startBlockNumber, - endBlockNumber, + firstBlockNumber, + finalBlockNumber, traceFile, traceFilePath, testDataDir); diff --git a/testing/src/main/java/net/consensys/linea/testing/MultiBlockExecutionEnvironment.java b/testing/src/main/java/net/consensys/linea/testing/MultiBlockExecutionEnvironment.java index 89bbc00287..b9bbe575c2 100644 --- a/testing/src/main/java/net/consensys/linea/testing/MultiBlockExecutionEnvironment.java +++ b/testing/src/main/java/net/consensys/linea/testing/MultiBlockExecutionEnvironment.java @@ -52,6 +52,7 @@ public class MultiBlockExecutionEnvironment { @Builder.Default private final long startingBlockNumber = DEFAULT_BLOCK_NUMBER; @Builder.Default private final boolean systemContractDeployedPriorToConflation = true; + @Builder.Default private final Boolean runWithBesuNode = false; /** * A transaction validator of each transaction; by default, it asserts that the transaction was @@ -118,18 +119,46 @@ public MultiBlockExecutionEnvironmentBuilder addBlock( } public void run() { - final ConflationSnapshot conflationSnapshot = buildConflationSnapshot(); - final Map historicalBlockhashes = conflationSnapshot.historicalBlockHashes(); - // Remove the last block number as it's not part of the historical blockhashes - historicalBlockhashes.remove(conflationSnapshot.lastBlockNumber()); - tracer = new ZkTracer(chainConfig, historicalBlockhashes); - ReplayExecutionEnvironment.builder() - .zkTracer(tracer) - .useCoinbaseAddressFromBlockHeader(true) - .transactionProcessingResultValidator(transactionProcessingResultValidator) - .systemContractDeployedPriorToConflation(systemContractDeployedPriorToConflation) - .build() - .replay(testsChain, testInfo, conflationSnapshot); + if (runWithBesuNode || System.getenv().containsKey("RUN_WITH_BESU_NODE")) { + // When runnning with a Besu node, the list of transactions present in blocks is not followed + // as set originally. + // With the below, it collects all the transactions in all blocks, and dispatches them one per + // block + List transactionsIncludingNullTransactionsForEmptyBlocks = new ArrayList<>(); + for (BlockSnapshot block : blocks) { + if (block.txs().isEmpty()) { + // Add a null transaction to represent an empty block + transactionsIncludingNullTransactionsForEmptyBlocks.add(null); + } else { + for (TransactionSnapshot txSnapshot : block.txs()) { + transactionsIncludingNullTransactionsForEmptyBlocks.add(txSnapshot.toTransaction()); + } + } + } + BesuExecutionTools besuExecTools = + new BesuExecutionTools( + Optional.of(testInfo), + chainConfig, + ToyExecutionEnvironmentV2.DEFAULT_COINBASE_ADDRESS, + accounts, + transactionsIncludingNullTransactionsForEmptyBlocks, + true, + null); + besuExecTools.executeTest(); + } else { + final ConflationSnapshot conflationSnapshot = buildConflationSnapshot(); + final Map historicalBlockhashes = conflationSnapshot.historicalBlockHashes(); + // Remove the last block number as it's not part of the historical blockhashes + historicalBlockhashes.remove(conflationSnapshot.lastBlockNumber()); + tracer = new ZkTracer(chainConfig, historicalBlockhashes); + ReplayExecutionEnvironment.builder() + .zkTracer(tracer) + .useCoinbaseAddressFromBlockHeader(true) + .transactionProcessingResultValidator(transactionProcessingResultValidator) + .systemContractDeployedPriorToConflation(systemContractDeployedPriorToConflation) + .build() + .replay(testsChain, testInfo, conflationSnapshot); + } } public Hub getHub() {