Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
219f327
Add cross-client execution metrics and slow block logging
macfarla Feb 17, 2026
7a49bc7
read tracking in state layer
macfarla Feb 17, 2026
2059e04
500ms default for testing
macfarla Feb 17, 2026
1fd91f7
use cli option everywhere
macfarla Feb 17, 2026
5f85026
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 17, 2026
d68e23c
truncate decimal values in logs
macfarla Feb 17, 2026
1cc9fee
move persist
macfarla Feb 17, 2026
6cfc70d
state_read_ms
macfarla Feb 17, 2026
a01c659
gas used
macfarla Feb 17, 2026
b1bdbf9
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 17, 2026
db89b64
handle slow tracer background tracer
macfarla Feb 17, 2026
7daa67e
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 17, 2026
04d42b9
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 18, 2026
ef037ad
formatting imports
macfarla Feb 18, 2026
4d6988d
test mock fix
macfarla Feb 18, 2026
ce5da33
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 18, 2026
003365d
config toml fix
macfarla Feb 18, 2026
bb8eebc
fix AT
macfarla Feb 18, 2026
3d88ad5
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 18, 2026
24f967c
fix toml test file
macfarla Feb 18, 2026
70311c0
Merge branch 'main' of github.com:macfarla/besu into feature/executio…
macfarla Feb 20, 2026
5c90a96
remove delegate pattern
macfarla Feb 20, 2026
1bf8a25
Add tracer type to block processing log; generalize EIP-7702 test mes…
macfarla Feb 20, 2026
1ab105f
generalise regex
macfarla Feb 20, 2026
698432f
formatting
macfarla Feb 20, 2026
13458d9
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 20, 2026
d54a0f9
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 23, 2026
ed2b6ce
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 23, 2026
0d40e84
avoid precompile address range in test
macfarla Feb 23, 2026
6c7116c
review comments: update comments, copyright and stream -> loop in par…
macfarla Feb 24, 2026
43f57a4
simplify cli options to single slow-block-threshold
macfarla Feb 24, 2026
e6c9a44
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 24, 2026
b9de3b9
rename and copyright
macfarla Feb 24, 2026
4445bd6
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
/*
* Copyright contributors to Hyperledger Besu.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Copyright contributors to Hyperledger Besu.
* 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.tests.acceptance;

import static org.assertj.core.api.Assertions.assertThat;

import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase;
import org.hyperledger.besu.tests.acceptance.dsl.account.Account;
import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode;
import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.BesuNodeConfigurationBuilder;
import org.hyperledger.besu.tests.acceptance.slowblock.model.ExpectedMetrics;
import org.hyperledger.besu.tests.acceptance.slowblock.model.MetricsTestScenario;
import org.hyperledger.besu.tests.acceptance.slowblock.model.TaggedBlock;
import org.hyperledger.besu.tests.acceptance.slowblock.report.SlowBlockMetricsReportGenerator;
import org.hyperledger.besu.tests.web3j.generated.SimpleStorage;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* End-to-end acceptance test for validating slow block metrics. This test sends various transaction
* types to a local Besu node, captures slow block logs, and validates that all JSON fields are
* correctly populated.
*
* <p>The test uses a QBFT node (BFT consensus) with a threshold of 0ms to ensure ALL blocks are
* logged as slow blocks. QBFT is used instead of dev mode because it automatically produces blocks,
* which is required for contract deployment and transaction execution.
*
* <p><b>Note on EIP-7702:</b> Testing EIP-7702 delegation metrics requires a Prague-enabled genesis
* and the Engine API for block production. See {@code CodeDelegationTransactionAcceptanceTest} for
* comprehensive EIP-7702 testing. The EIP-7702 metrics (eip7702_delegations_set/cleared) are
* validated to be present in the JSON structure but may be 0 in this test as the genesis doesn't
* enable EIP-7702.
*/
public class SlowBlockMetricsValidationTest extends AcceptanceTestBase {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

// Pattern to match slow block JSON in console output
// Matches from the start marker to the final closing braces on the same line,
// without depending on any specific field order.
private static final Pattern SLOW_BLOCK_PATTERN =
Pattern.compile("(\\{\"level\":\"warn\",\"msg\":\"Slow block\"[^\\n]*\\}\\})");

// All expected JSON field paths (38 fields - matching geth + Besu extras)
// Now sourced from ExpectedMetrics for consistency
private static final List<String> REQUIRED_FIELDS = ExpectedMetrics.ALL_METRIC_PATHS;

// Report output path
private static final Path REPORT_OUTPUT_PATH =
Paths.get("build/reports/slow-block-metrics-analysis.md");

private BesuNode devNode;

@BeforeEach
public void setUp() throws Exception {
// Create a QBFT node with:
// - BFT consensus that automatically produces blocks
// - 0ms slow block threshold (log ALL blocks as slow)
// We use a config modifier to add the environment variable for slow block threshold
final UnaryOperator<BesuNodeConfigurationBuilder> configModifier =
builder -> builder.extraCLIOptions(List.of("--slow-block-threshold=0"));

devNode = besu.createQbftNode("qbft-metrics-node", configModifier);

// Start console capture BEFORE starting the node
cluster.startConsoleCapture();
cluster.start(devNode);

// Wait for blockchain to progress (QBFT produces blocks automatically)
cluster.verify(blockchain.reachesHeight(devNode, 1, 30));
}

@Test
public void shouldCaptureSlowBlockMetricsForVariousTransactions() throws Exception {
// 1. Send ETH transfer
final Account sender = accounts.getPrimaryBenefactor();
final Account recipient = accounts.createAccount("recipient");
devNode.execute(accountTransactions.createTransfer(sender, recipient, 1));

// 2. Deploy contract
final SimpleStorage contract =
devNode.execute(contractTransactions.createSmartContract(SimpleStorage.class));

// 3. Call contract to write storage (SSTORE)
contract.set(BigInteger.valueOf(42)).send();

// 4. Call contract again to read/write (SLOAD + SSTORE)
contract.set(BigInteger.valueOf(100)).send();

// 5. Read contract storage (triggers SLOAD without SSTORE)
contract.get().send();

// Wait a moment for blocks to be processed
Thread.sleep(2000);

// Get console output and parse slow block logs
String consoleOutput = cluster.getConsoleContents();
List<JsonNode> slowBlocks = parseSlowBlockLogs(consoleOutput);

// Assertions
assertThat(slowBlocks).as("Should capture at least one slow block log").isNotEmpty();

// Validate fields in the last slow block
JsonNode lastBlock = slowBlocks.get(slowBlocks.size() - 1);

// Check all required fields are present
List<String> missingFields = new ArrayList<>();
for (String fieldPath : REQUIRED_FIELDS) {
String jsonPointerPath = "/" + fieldPath.replace("/", "/");
JsonNode fieldNode = lastBlock.at(jsonPointerPath);
if (fieldNode.isMissingNode()) {
missingFields.add(fieldPath);
}
}

assertThat(missingFields)
.as("All required fields should be present in slow block JSON. Missing: " + missingFields)
.isEmpty();

// Tag blocks with their transaction types and generate comprehensive report
List<TaggedBlock> taggedBlocks = tagBlocksWithMetricsTestScenarios(slowBlocks);
generateComprehensiveReport(taggedBlocks);

// Also print legacy console report for quick verification
printReport(slowBlocks, lastBlock, missingFields);
}

/**
* Tag each captured slow block with its transaction type based on block metrics. Uses heuristics
* based on EVM opcode counts, state read/write patterns, and transaction count to classify each
* block.
*
* <p>Classification priority (first match wins):
*
* <ol>
* <li>Genesis block (block number 0)
* <li>Empty block (no transactions)
* <li>Contract deployment (CREATE opcode or code writes)
* <li>Storage write with read (SLOAD + SSTORE with code interaction)
* <li>Storage write only (SSTORE with code interaction)
* <li>Storage read only (SLOAD without SSTORE)
* <li>ETH transfer (transactions without contract code interaction)
* </ol>
*/
private List<TaggedBlock> tagBlocksWithMetricsTestScenarios(final List<JsonNode> slowBlocks) {
List<TaggedBlock> taggedBlocks = new ArrayList<>();

for (int i = 0; i < slowBlocks.size(); i++) {
JsonNode block = slowBlocks.get(i);
long blockNumber = block.at("/block/number").asLong();
int txCount = block.at("/block/tx_count").asInt();
long codeWrites = block.at("/state_writes/code").asLong();
long codeReads = block.at("/state_reads/code").asLong();
long creates = block.at("/evm/creates").asLong();
long sstore = block.at("/evm/sstore").asLong();
long sload = block.at("/evm/sload").asLong();
long calls = block.at("/evm/calls").asLong();

// Determine transaction type based on block metrics
MetricsTestScenario txType;
if (blockNumber == 0) {
// Genesis block
txType = MetricsTestScenario.GENESIS;
} else if (txCount == 0) {
// Empty consensus block (no user transactions)
txType = MetricsTestScenario.EMPTY_BLOCK;
} else if (creates > 0 || codeWrites > 0) {
// Contract deployment: CREATE/CREATE2 opcode executed or code written to state
txType = MetricsTestScenario.CONTRACT_DEPLOY;
} else if (sload > 0 && sstore > 0 && codeReads > 0) {
// Storage read-modify-write: contract reads then writes storage
txType = MetricsTestScenario.STORAGE_WRITE;
} else if (sstore > 0 && codeReads > 0) {
// Storage write only: contract writes to storage slot
txType = MetricsTestScenario.STORAGE_WRITE;
} else if (sload > 0 && sstore == 0 && codeReads > 0) {
// Storage read only: contract reads storage without writing
txType = MetricsTestScenario.STORAGE_READ;
} else if (calls > 0 && codeReads > 0) {
// Contract call without storage access
txType = MetricsTestScenario.CONTRACT_CALL;
} else if (txCount > 0) {
// Simple ETH transfer: has transactions but no contract code interaction
txType = MetricsTestScenario.ETH_TRANSFER;
} else {
// Fallback for unidentified patterns
txType = MetricsTestScenario.EMPTY_BLOCK;
}

taggedBlocks.add(new TaggedBlock(block, txType));
}

return taggedBlocks;
}

/** Generate a comprehensive markdown report with expected vs actual analysis for all metrics. */
private void generateComprehensiveReport(final List<TaggedBlock> taggedBlocks)
throws IOException {
SlowBlockMetricsReportGenerator generator =
new SlowBlockMetricsReportGenerator(taggedBlocks, "QBFT (BFT Consensus)");

// Generate and write the report
generator.generateReport(REPORT_OUTPUT_PATH);

// Also print to console for immediate visibility in test output
System.out.println("\n" + generator.generateReportString());
}

private List<JsonNode> parseSlowBlockLogs(final String consoleOutput) {
List<JsonNode> allBlocks = new ArrayList<>();
Matcher matcher = SLOW_BLOCK_PATTERN.matcher(consoleOutput);

while (matcher.find()) {
try {
String json = matcher.group();
JsonNode node = OBJECT_MAPPER.readTree(json);
allBlocks.add(node);
} catch (Exception e) {
// Skip malformed JSON
}
}

// Deduplicate by block hash to avoid duplicate entries
// (blocks may be logged multiple times during validation/import)
Set<String> seenHashes = new LinkedHashSet<>();
return allBlocks.stream()
.filter(block -> seenHashes.add(block.at("/block/hash").asText()))
.collect(Collectors.toList());
}

private void printReport(
final List<JsonNode> slowBlocks, final JsonNode lastBlock, final List<String> missingFields) {
StringBuilder report = new StringBuilder();

report.append("\n");
report.append("═══════════════════════════════════════════════════════════════\n");
report.append(" SLOW BLOCK METRICS VALIDATION REPORT\n");
report.append("═══════════════════════════════════════════════════════════════\n");
report.append("\n");

// Summary
report.append("SUMMARY\n");
report.append("-------\n");
report.append(String.format("Blocks Processed: %d%n", slowBlocks.size()));
report.append(String.format("Missing Fields: %d%n", missingFields.size()));
if (!missingFields.isEmpty()) {
report.append(String.format(" -> %s%n", missingFields));
}
report.append("\n");

// Sample metrics from last block
if (lastBlock != null) {
report.append("SAMPLE METRICS (last block)\n");
report.append("---------------------------\n");
report.append(String.format("Block number: %s%n", lastBlock.at("/block/number").asText()));
report.append(String.format("Gas used: %s%n", lastBlock.at("/block/gas_used").asText()));
report.append(String.format("Tx count: %s%n", lastBlock.at("/block/tx_count").asText()));
report.append(
String.format("Execution time: %s ms%n", lastBlock.at("/timing/execution_ms").asText()));
report.append(
String.format("Account reads: %s%n", lastBlock.at("/state_reads/accounts").asText()));
report.append(
String.format("Account writes: %s%n", lastBlock.at("/state_writes/accounts").asText()));
report.append(
String.format(
"Storage reads: %s%n", lastBlock.at("/state_reads/storage_slots").asText()));
report.append(
String.format(
"Storage writes: %s%n", lastBlock.at("/state_writes/storage_slots").asText()));
report.append(String.format("Code reads: %s%n", lastBlock.at("/state_reads/code").asText()));
report.append(
String.format("Code writes: %s%n", lastBlock.at("/state_writes/code").asText()));
report.append(String.format("SLOAD: %s%n", lastBlock.at("/evm/sload").asText()));
report.append(String.format("SSTORE: %s%n", lastBlock.at("/evm/sstore").asText()));
report.append(String.format("CALLS: %s%n", lastBlock.at("/evm/calls").asText()));
report.append(String.format("CREATES: %s%n", lastBlock.at("/evm/creates").asText()));
}

report.append("\n");
report.append("═══════════════════════════════════════════════════════════════\n");

System.out.println(report);
}
}
Loading