static T8nResult runTest(
      final Long chainId,
      final String fork,
      final String rewardString,
      final ObjectMapper objectMapper,
      final ReferenceTestEnv referenceTestEnv,
      final ReferenceTestWorldState initialWorldState,
      final List<Transaction> transactions,
      final List<RejectedTransaction> rejections,
      final TracerManager tracerManager) {

    final ReferenceTestProtocolSchedules referenceTestProtocolSchedules =
        ReferenceTestProtocolSchedules.create(
            new StubGenesisConfigOptions().chainId(BigInteger.valueOf(chainId)));

    final BonsaiReferenceTestWorldState worldState =
        (BonsaiReferenceTestWorldState) initialWorldState.copy();

    final ProtocolSchedule protocolSchedule = referenceTestProtocolSchedules.getByName(fork);
    if (protocolSchedule == null) {
      throw new UnsupportedForkException(fork);
    }

    ProtocolSpec protocolSpec = protocolSchedule.getByBlockHeader(referenceTestEnv);
    Blockchain blockchain = new T8nBlockchain(referenceTestEnv, protocolSpec);
    final BlockHeader blockHeader = referenceTestEnv.parentBlockHeader(protocolSpec);
    final MainnetTransactionProcessor processor = protocolSpec.getTransactionProcessor();
    final BlobGas excessBlobGas =
        Optional.ofNullable(referenceTestEnv.getParentExcessBlobGas())
            .map(
                __ -> calculateExcessBlobGasForParent(protocolSpec, blockHeader)) // blockchain-test
            .orElse(blockHeader.getExcessBlobGas().orElse(BlobGas.ZERO)); // state-test
    final Wei blobGasPrice = protocolSpec.getFeeMarket().blobGasPricePerGas(excessBlobGas);
    long blobGasLimit = protocolSpec.getGasLimitCalculator().currentBlobGasLimit();
    BlockProcessingContext blockProcessingContext =
        new BlockProcessingContext(
            referenceTestEnv,
            worldState,
            protocolSpec,
            protocolSpec
                .getBlockHashProcessor()
                .createBlockHashLookup(blockchain, referenceTestEnv),
            OperationTracer.NO_TRACING);

    if (!referenceTestEnv.isStateTest()) {
      protocolSpec.getBlockHashProcessor().process(blockProcessingContext);
    }

    final WorldUpdater rootWorldStateUpdater = worldState.updater();
    List<TransactionReceipt> receipts = new ArrayList<>();
    List<RejectedTransaction> invalidTransactions = new ArrayList<>(rejections);
    List<Transaction> validTransactions = new ArrayList<>();
    ArrayNode receiptsArray = objectMapper.createArrayNode();
    long gasUsed = 0;
    long blobGasUsed = 0;
    final WorldUpdater worldStateUpdater = rootWorldStateUpdater.updater();
    for (int transactionIndex = 0; transactionIndex < transactions.size(); transactionIndex++) {
      worldStateUpdater.markTransactionBoundary();
      Transaction transaction = transactions.get(transactionIndex);
      final Stopwatch timer = Stopwatch.createStarted();

      GasCalculator gasCalculator = protocolSpec.getGasCalculator();
      int blobCount = transaction.getBlobCount();
      blobGasUsed += gasCalculator.blobGasCost(blobCount);
      if (blobGasUsed > blobGasLimit) {
        invalidTransactions.add(
            new RejectedTransaction(
                transactionIndex,
                String.format(
                    "blob gas (%d) would exceed block maximum %d", blobGasUsed, blobGasLimit)));
        continue;
      }
      final OperationTracer tracer; // You should have picked Mercy.

      final TransactionProcessingResult result;
      try {
        tracer = tracerManager.getManagedTracer(transactionIndex, transaction.getHash());
        tracer.tracePrepareTransaction(worldStateUpdater, transaction);
        tracer.traceStartTransaction(worldStateUpdater, transaction);
        BlockHashLookup blockHashLookup =
            protocolSpec.getBlockHashProcessor().createBlockHashLookup(blockchain, blockHeader);
        if (blockHashLookup instanceof BlockchainBasedBlockHashLookup) {
          // basically t8n test cases for blockhash are broken and one cannot create a blockchain
          // from them so need to
          // add in a manual BlockHashLookup
          blockHashLookup =
              (__, blockNumber) ->
                  referenceTestEnv.getBlockhashByNumber(blockNumber).orElse(Hash.ZERO);
        }
        result =
            processor.processTransaction(
                worldStateUpdater,
                blockHeader,
                transaction,
                blockHeader.getCoinbase(),
                tracer,
                blockHashLookup,
                TransactionValidationParams.processingBlock(),
                blobGasPrice);
        tracerManager.disposeTracer(tracer);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
      timer.stop();

      if (shouldClearEmptyAccounts(fork)) {
        var entries = new ArrayList<>(worldState.getAccumulator().getAccountsToUpdate().entrySet());
        for (var entry : entries) {
          PathBasedAccount updated = entry.getValue().getUpdated();
          if (updated != null && updated.isEmpty()) {
            worldState.getAccumulator().deleteAccount(entry.getKey());
          }
        }
      }
      if (result.isInvalid()) {
        invalidTransactions.add(
            new RejectedTransaction(
                transactionIndex, result.getValidationResult().getErrorMessage()));
        continue;
      }
      validTransactions.add(transaction);

      long transactionGasUsed = transaction.getGasLimit() - result.getGasRemaining();

      gasUsed += transactionGasUsed;
      long intrinsicGas = gasCalculator.transactionIntrinsicGasCost(transaction, 0);
      TransactionReceipt receipt =
          protocolSpec
              .getTransactionReceiptFactory()
              .create(transaction.getType(), result, worldState, gasUsed);
      tracer.traceEndTransaction(
          worldStateUpdater,
          transaction,
          result.isSuccessful(),
          result.getOutput(),
          result.getLogs(),
          gasUsed - intrinsicGas,
          EMPTY_ADDRESS_SET,
          timer.elapsed(TimeUnit.NANOSECONDS));
      Bytes gasUsedInTransaction = Bytes.ofUnsignedLong(transactionGasUsed);
      receipts.add(receipt);
      ObjectNode receiptObject = receiptsArray.addObject();
      receiptObject.put(
          "root", receipt.getStateRoot() == null ? "0x" : receipt.getStateRoot().toHexString());
      int status = receipt.getStatus();
      receiptObject.put("status", "0x" + Math.max(status, 0));
      receiptObject.put("cumulativeGasUsed", Bytes.ofUnsignedLong(gasUsed).toQuantityHexString());
      receiptObject.put("logsBloom", receipt.getBloomFilter().toHexString());
      if (result.getLogs().isEmpty()) {
        receiptObject.putNull("logs");
      } else {
        ArrayNode logsArray = receiptObject.putArray("logs");
        List<Log> logs = result.getLogs();
        for (int logIndex = 0; logIndex < logs.size(); logIndex++) {
          Log log = logs.get(logIndex);
          var obj = logsArray.addObject();
          obj.put("address", log.getLogger().toHexString());
          var topics = obj.putArray("topics");
          log.getTopics().forEach(topic -> topics.add(topic.toHexString()));
          obj.put("data", log.getData().toHexString());
          obj.put("blockNumber", blockHeader.getNumber());
          obj.put("transactionHash", transaction.getHash().toHexString());
          obj.put("transactionIndex", String.format("0x%x", transactionIndex));
          obj.put("blockHash", blockHeader.getHash().toHexString());
          obj.put("logIndex", String.format("0x%x", logIndex));
          obj.put("removed", "false");
        }
      }
      receiptObject.put("transactionHash", transaction.getHash().toHexString());
      receiptObject.put(
          "contractAddress", transaction.contractAddress().orElse(Address.ZERO).toHexString());
      receiptObject.put("gasUsed", gasUsedInTransaction.toQuantityHexString());
      receiptObject.put("blockHash", Hash.ZERO.toHexString());
      receiptObject.put(
          "transactionIndex", Bytes.ofUnsignedLong(transactionIndex).toQuantityHexString());
      worldStateUpdater.commit();
    }

    final ObjectNode resultObject = objectMapper.createObjectNode();

    // block reward
    // The max production reward was 5 Eth, longs can hold over 18 Eth.
    if (!referenceTestEnv.isStateTest()
        && !validTransactions.isEmpty()
        && (rewardString == null || Long.decode(rewardString) > 0)) {
      Wei reward =
          (rewardString == null)
              ? protocolSpec.getBlockReward()
              : Wei.of(Long.decode(rewardString));
      rootWorldStateUpdater
          .getOrCreateSenderAccount(blockHeader.getCoinbase())
          .incrementBalance(reward);
    }

    rootWorldStateUpdater.commit();

    if (referenceTestEnv.isStateTest()) {
      if (!referenceTestEnv.getWithdrawals().isEmpty()) {
        resultObject.put("exception", "withdrawals are not supported in state tests");
      }
    } else {
      // Invoke the withdrawal processor to handle CL withdrawals.
      if (!referenceTestEnv.getWithdrawals().isEmpty()) {
        try {
          protocolSpec
              .getWithdrawalsProcessor()
              .ifPresent(
                  p ->
                      p.processWithdrawals(
                          referenceTestEnv.getWithdrawals(), worldState.updater()));
        } catch (RuntimeException re) {
          resultObject.put("exception", re.getMessage());
        }
      }
    }

    var requestProcessorCoordinator = protocolSpec.getRequestProcessorCoordinator();
    if (requestProcessorCoordinator.isPresent()) {
      var rpc = requestProcessorCoordinator.get();

      RequestProcessingContext requestContext =
          new RequestProcessingContext(blockProcessingContext, receipts);
      Optional<List<Request>> maybeRequests = Optional.of(rpc.process(requestContext));
      Hash requestsHash = BodyValidation.requestsHash(maybeRequests.orElse(List.of()));

      resultObject.put("requestsHash", requestsHash.toHexString());
      ArrayNode requests = resultObject.putArray("requests");
      maybeRequests
          .orElseGet(List::of)
          .forEach(
              request -> {
                if (!request.data().isEmpty()) {
                  requests.add(request.getEncodedRequest().toHexString());
                }
              });
    }

    worldState.persist(blockHeader);

    resultObject.put("stateRoot", worldState.rootHash().toHexString());
    resultObject.put("txRoot", BodyValidation.transactionsRoot(validTransactions).toHexString());
    resultObject.put("receiptsRoot", BodyValidation.receiptsRoot(receipts).toHexString());
    resultObject.put(
        "logsHash",
        Hash.hash(
                RLP.encode(
                    out ->
                        out.writeList(
                            receipts.stream().flatMap(r -> r.getLogsList().stream()).toList(),
                            Log::writeTo)))
            .toHexString());
    resultObject.put("logsBloom", BodyValidation.logsBloom(receipts).toHexString());
    resultObject.set("receipts", receiptsArray);
    if (!invalidTransactions.isEmpty()) {
      resultObject.putPOJO("rejected", invalidTransactions);
    }

    resultObject.put(
        "currentDifficulty",
        !blockHeader.getDifficultyBytes().trimLeadingZeros().isEmpty()
            ? blockHeader.getDifficultyBytes().toShortHexString()
            : null);
    resultObject.put("gasUsed", Bytes.ofUnsignedLong(gasUsed).toQuantityHexString());
    blockHeader
        .getBaseFee()
        .ifPresent(bf -> resultObject.put("currentBaseFee", bf.toQuantityHexString()));
    blockHeader
        .getWithdrawalsRoot()
        .ifPresent(wr -> resultObject.put("withdrawalsRoot", wr.toHexString()));
    var maybeExcessBlobGas = blockHeader.getExcessBlobGas();
    if (maybeExcessBlobGas.isPresent()) {
      resultObject.put(
          "currentExcessBlobGas",
          calculateExcessBlobGasForParent(protocolSpec, blockHeader)
              .toBytes()
              .toQuantityHexString());
      resultObject.put("blobGasUsed", Bytes.ofUnsignedLong(blobGasUsed).toQuantityHexString());
    }

    ObjectNode allocObject = objectMapper.createObjectNode();
    worldState
        .streamAccounts(Bytes32.ZERO, Integer.MAX_VALUE)
        .sorted(Comparator.comparing(o -> o.getAddress().get().toHexString()))
        .forEach(
            a -> {
              Account account = worldState.get(a.getAddress().get());
              ObjectNode accountObject = allocObject.putObject(account.getAddress().toHexString());
              if (account.getCode() != null && !account.getCode().isEmpty()) {
                accountObject.put("code", account.getCode().toHexString());
              }
              List<Entry<UInt256, UInt256>> storageEntries =
                  account.storageEntriesFrom(Bytes32.ZERO, Integer.MAX_VALUE).values().stream()
                      .map(
                          e ->
                              Map.entry(
                                  e.getKey().get(),
                                  account.getStorageValue(UInt256.fromBytes(e.getKey().get()))))
                      .filter(e -> !e.getValue().isZero())
                      .sorted(Map.Entry.comparingByKey())
                      .toList();
              if (!storageEntries.isEmpty()) {
                ObjectNode storageObject = accountObject.putObject("storage");
                storageEntries.forEach(
                    accountStorageEntry ->
                        storageObject.put(
                            accountStorageEntry.getKey().toHexString(),
                            accountStorageEntry.getValue().toHexString()));
              }
              accountObject.put("balance", account.getBalance().toShortHexString());
              if (account.getNonce() != 0) {
                accountObject.put(
                    "nonce", Bytes.ofUnsignedLong(account.getNonce()).toShortHexString());
              }
            });

    BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
    rlpOut.writeList(transactions, Transaction::writeTo);
    TextNode bodyBytes = TextNode.valueOf(rlpOut.encoded().toHexString());
    return new T8nResult(allocObject, bodyBytes, resultObject);
  }
Extracts transactions from a given JSON iterator and adds them to the provided transactions
list. If a transaction cannot be parsed or is invalid, it is added to the rejections list with
its index and error message.

@param out PrintWriter used for outputting information or errors.
@param it Iterator over JSON nodes, each representing a transaction.
@param transactions List of transactions to which parsed transactions are added.
@param rejections List of RejectedTransaction records to which rejected transactions are added.
@return The updated list of transactions after parsing and validation.
ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/T8nExecutor.java