diff --git a/BAL_TRACING.md b/BAL_TRACING.md new file mode 100644 index 00000000000..f4a82c689e7 --- /dev/null +++ b/BAL_TRACING.md @@ -0,0 +1,170 @@ +# EIP-7928 Block-Level Access Lists OpenTelemetry Tracing + +This document describes the OpenTelemetry tracing implementation for EIP-7928 Block-Level Access Lists (BAL) in Besu. + +## Overview + +The implementation provides comprehensive OpenTelemetry tracing for BAL as specified in the EIP-7928 BAL OTel specification, including: + +- Structured span hierarchy for block processing +- Counter and histogram metrics +- Configurable tracing levels +- Performance-optimized implementation + +## Span Hierarchy + +``` +ethereum.block +├── ethereum.bal.prefetch +│ ├── ethereum.bal.prefetch.account (optional) +│ └── ethereum.bal.prefetch.slot (optional) +├── ethereum.tx.execute (per transaction) +└── ethereum.stateroot +``` + +## Key Components + +### BalOtelTracer +Main tracer class that manages the complete span hierarchy. Integrates with `AbstractBlockProcessor` to provide tracing for block processing. + +### BalMetrics +Implements all counter and histogram metrics as defined in sections 6.2 and 6.3 of the specification: + +**Counters:** +- `ethereum.blocks.total` +- `ethereum.tx.total` +- `ethereum.bal.blocks.total` +- `ethereum.bal.prefetch.accounts` +- `ethereum.bal.prefetch.slots` +- `ethereum.bal.prefetch.cache_hits` +- `ethereum.bal.prefetch.cache_misses` + +**Histograms:** +- `ethereum.block.duration` +- `ethereum.tx.duration` +- `ethereum.stateroot.duration` +- `ethereum.throughput.mgas_per_sec` +- `ethereum.bal.prefetch.duration` +- `ethereum.bal.size` + +### BalPrefetchTracer +Specialized tracer for BAL prefetch operations with optional per-account and per-slot child spans. + +### BalTracingConfig +Configuration class for enabling/disabling BAL tracing and setting OTLP endpoints. + +## Usage + +### Basic Configuration + +```java +// Enable BAL tracing with default settings +BalTracingConfig config = BalTracingConfig.defaultEnabled(); + +// Create tracer +BalOtelTracer balTracer = new BalOtelTracer( + openTelemetryTracer, + metricsSystem, + chainId, + config.isEnabled(), + config.isDetailedTracingEnabled() +); +``` + +### Integration with Block Processing + +The tracing is automatically integrated into `AbstractBlockProcessor`. To enable it, pass a `BalOtelTracer` instance to the constructor: + +```java +AbstractBlockProcessor processor = new MainnetBlockProcessor( + transactionProcessor, + transactionReceiptFactory, + blockReward, + miningBeneficiaryCalculator, + skipZeroBlockRewards, + gasBudgetCalculator, + balTracer // Optional - pass null to disable BAL tracing +); +``` + +### Configuration Options + +```java +// Disabled tracing +BalTracingConfig disabled = BalTracingConfig.disabled(); + +// Detailed tracing with per-account/slot spans +BalTracingConfig detailed = BalTracingConfig.detailedEnabled("production"); + +// Custom configuration +BalTracingConfig custom = new BalTracingConfig( + true, // enabled + false, // detailed tracing + "localhost:4318", // OTLP endpoint + 0.1, // sampling rate (10%) + "staging" // environment +); +``` + +## Performance Characteristics + +The implementation is designed to meet the performance requirements specified in section 7: + +- **Tracing disabled overhead:** < 0.1% (null checks and early returns) +- **Tracing enabled overhead:** < 2% (lazy attribute setting, minimal span creation) +- **Per-span creation:** < 1μs (efficient OpenTelemetry usage) + +## Resource Attributes + +All spans include the following resource attributes as per section 4: + +- `service.name`: "besu" +- `service.version`: Current Besu version +- `deployment.environment`: Configured environment +- `ethereum.chain.id`: Chain ID + +## BAL Data Model + +The implementation includes placeholder BAL data model classes: + +### BlockAccessList +Represents a complete BAL for a block with: +- Block hash +- Map of addresses to access list entries +- Size in bytes +- Utility methods for counts and lookups + +### BlockAccessListEntry +Represents individual entries with: +- Storage slot keys accessed +- Code access flag +- Utility methods for access checks + +## Future Integration + +This tracing implementation is designed to work with the actual EIP-7928 BAL implementation when it becomes available. The `extractBlockAccessList` method in `AbstractBlockProcessor` is a placeholder that should be replaced with actual BAL extraction logic. + +## Testing + +Comprehensive unit tests are provided for all major components: +- `BalOtelTracerTest`: Tests tracer functionality and span management +- `BalMetricsTest`: Tests metrics recording and timers +- `BlockAccessListTest`: Tests BAL data model + +## Configuration Files + +The tracing can be enabled through Besu configuration. Future work should include: +- Command-line options for BAL tracing +- Configuration file settings +- Environment variable support + +## Monitoring and Observability + +When enabled, the tracing provides rich observability into: +- Block processing performance +- Transaction execution timing +- State root calculation performance +- BAL prefetch effectiveness +- Cache hit/miss ratios + +This data can be exported to any OpenTelemetry-compatible observability platform (Jaeger, Zipkin, etc.) for analysis and monitoring. \ No newline at end of file diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/BlockAccessList.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/BlockAccessList.java new file mode 100644 index 00000000000..5914e42dbda --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/BlockAccessList.java @@ -0,0 +1,144 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal; + +import org.hyperledger.besu.ethereum.core.Address; +import org.hyperledger.besu.ethereum.core.Hash; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a Block-level Access List (BAL) as defined in EIP-7928. + * This is a data structure that tracks which accounts, storage slots, and code + * are accessed during block execution to enable prefetching optimizations. + */ +public class BlockAccessList { + + private final Hash blockHash; + private final Map entries; + private final long sizeBytes; + + /** + * Creates a new BlockAccessList. + * + * @param blockHash The hash of the block this BAL belongs to + * @param entries Map of addresses to their access list entries + * @param sizeBytes Total size of the BAL in bytes + */ + public BlockAccessList( + final Hash blockHash, + final Map entries, + final long sizeBytes) { + this.blockHash = Objects.requireNonNull(blockHash, "blockHash cannot be null"); + this.entries = Objects.requireNonNull(entries, "entries cannot be null"); + this.sizeBytes = sizeBytes; + } + + /** @return The hash of the block this BAL belongs to */ + public Hash getBlockHash() { + return blockHash; + } + + /** @return Map of addresses to their access list entries */ + public Map getEntries() { + return entries; + } + + /** @return Total number of accounts in the BAL */ + public int getAccountsCount() { + return entries.size(); + } + + /** @return Total number of storage slots across all accounts in the BAL */ + public int getStorageSlotsCount() { + return entries.values().stream() + .mapToInt(entry -> entry.getStorageKeys().size()) + .sum(); + } + + /** @return Number of accounts that have code access */ + public int getCodeCount() { + return (int) entries.values().stream() + .filter(BlockAccessListEntry::hasCodeAccess) + .count(); + } + + /** @return Total size of the BAL in bytes */ + public long getSizeBytes() { + return sizeBytes; + } + + /** @return List of all addresses in the BAL */ + public List
getAddresses() { + return List.copyOf(entries.keySet()); + } + + /** + * Gets the access list entry for a specific address. + * + * @param address The address to look up + * @return The access list entry for the address, or null if not present + */ + public BlockAccessListEntry getEntry(final Address address) { + return entries.get(address); + } + + /** + * Checks if the BAL contains an entry for the specified address. + * + * @param address The address to check + * @return true if the address is in the BAL, false otherwise + */ + public boolean containsAddress(final Address address) { + return entries.containsKey(address); + } + + /** @return true if the BAL is empty (no entries), false otherwise */ + public boolean isEmpty() { + return entries.isEmpty(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final BlockAccessList that = (BlockAccessList) o; + return sizeBytes == that.sizeBytes + && Objects.equals(blockHash, that.blockHash) + && Objects.equals(entries, that.entries); + } + + @Override + public int hashCode() { + return Objects.hash(blockHash, entries, sizeBytes); + } + + @Override + public String toString() { + return String.format( + "BlockAccessList{blockHash=%s, accountsCount=%d, storageSlotsCount=%d, codeCount=%d, sizeBytes=%d}", + blockHash.toHexString(), + getAccountsCount(), + getStorageSlotsCount(), + getCodeCount(), + sizeBytes); + } +} \ No newline at end of file diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/BlockAccessListEntry.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/BlockAccessListEntry.java new file mode 100644 index 00000000000..9300ab409fe --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/BlockAccessListEntry.java @@ -0,0 +1,121 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal; + +import org.hyperledger.besu.ethereum.core.Hash; + +import java.util.List; +import java.util.Objects; + +/** + * Represents an individual entry in a Block-level Access List (BAL). + * Each entry corresponds to a specific account and tracks which storage slots + * and code were accessed during block execution. + */ +public class BlockAccessListEntry { + + private final List storageKeys; + private final boolean codeAccess; + + /** + * Creates a new BlockAccessListEntry. + * + * @param storageKeys List of storage slot keys (hashes) that were accessed + * @param codeAccess Whether the account's code was accessed + */ + public BlockAccessListEntry(final List storageKeys, final boolean codeAccess) { + this.storageKeys = Objects.requireNonNull(storageKeys, "storageKeys cannot be null"); + this.codeAccess = codeAccess; + } + + /** + * Creates a new BlockAccessListEntry with only code access and no storage access. + * + * @param codeAccess Whether the account's code was accessed + * @return A new entry with code access but no storage keys + */ + public static BlockAccessListEntry codeOnly(final boolean codeAccess) { + return new BlockAccessListEntry(List.of(), codeAccess); + } + + /** + * Creates a new BlockAccessListEntry with only storage access and no code access. + * + * @param storageKeys List of storage slot keys that were accessed + * @return A new entry with storage access but no code access + */ + public static BlockAccessListEntry storageOnly(final List storageKeys) { + return new BlockAccessListEntry(storageKeys, false); + } + + /** @return List of storage slot keys (hashes) that were accessed */ + public List getStorageKeys() { + return storageKeys; + } + + /** @return true if the account's code was accessed, false otherwise */ + public boolean hasCodeAccess() { + return codeAccess; + } + + /** @return true if any storage slots were accessed, false otherwise */ + public boolean hasStorageAccess() { + return !storageKeys.isEmpty(); + } + + /** @return Number of storage slots accessed */ + public int getStorageKeysCount() { + return storageKeys.size(); + } + + /** + * Checks if a specific storage key was accessed. + * + * @param storageKey The storage key to check + * @return true if the storage key was accessed, false otherwise + */ + public boolean hasStorageKey(final Hash storageKey) { + return storageKeys.contains(storageKey); + } + + /** @return true if this entry has any access (code or storage), false otherwise */ + public boolean hasAnyAccess() { + return codeAccess || !storageKeys.isEmpty(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final BlockAccessListEntry that = (BlockAccessListEntry) o; + return codeAccess == that.codeAccess && Objects.equals(storageKeys, that.storageKeys); + } + + @Override + public int hashCode() { + return Objects.hash(storageKeys, codeAccess); + } + + @Override + public String toString() { + return String.format( + "BlockAccessListEntry{storageKeysCount=%d, codeAccess=%s}", + storageKeys.size(), codeAccess); + } +} \ No newline at end of file diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalMetrics.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalMetrics.java new file mode 100644 index 00000000000..188c205b570 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalMetrics.java @@ -0,0 +1,270 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal.tracing; + +import org.hyperledger.besu.metrics.BesuMetricCategory; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer; + +/** + * OpenTelemetry metrics for Block-level Access Lists (BAL) as defined + * in the EIP-7928 BAL OTel specification sections 6.2 and 6.3. + */ +public class BalMetrics { + + // Counter metrics (Section 6.2) + private final Counter blocksTotal; + private final Counter txTotal; + private final Counter balBlocksTotal; + private final Counter balPrefetchAccounts; + private final Counter balPrefetchSlots; + private final Counter balPrefetchCacheHits; + private final Counter balPrefetchCacheMisses; + + // Histogram metrics (Section 6.3) + private final LabelledMetric blockDuration; + private final LabelledMetric txDuration; + private final LabelledMetric stateRootDuration; + private final LabelledMetric throughputMgasPerSec; + private final LabelledMetric balPrefetchDuration; + private final LabelledMetric balSize; + + /** + * Creates a new BalMetrics instance. + * + * @param metricsSystem The metrics system to register metrics with + */ + public BalMetrics(final MetricsSystem metricsSystem) { + // Counter metrics + blocksTotal = + metricsSystem + .createLabelledCounter( + BesuMetricCategory.ETHEREUM, + "blocks_total", + "Total number of blocks processed", + "chain_id") + .labels(""); + + txTotal = + metricsSystem + .createLabelledCounter( + BesuMetricCategory.ETHEREUM, + "tx_total", + "Total number of transactions processed", + "chain_id") + .labels(""); + + balBlocksTotal = + metricsSystem + .createLabelledCounter( + BesuMetricCategory.ETHEREUM, + "bal_blocks_total", + "Total number of BAL-enabled blocks processed", + "chain_id") + .labels(""); + + balPrefetchAccounts = + metricsSystem + .createLabelledCounter( + BesuMetricCategory.ETHEREUM, + "bal_prefetch_accounts", + "Total number of accounts prefetched via BAL", + "chain_id") + .labels(""); + + balPrefetchSlots = + metricsSystem + .createLabelledCounter( + BesuMetricCategory.ETHEREUM, + "bal_prefetch_slots", + "Total number of storage slots prefetched via BAL", + "chain_id") + .labels(""); + + balPrefetchCacheHits = + metricsSystem + .createLabelledCounter( + BesuMetricCategory.ETHEREUM, + "bal_prefetch_cache_hits", + "Total number of BAL prefetch cache hits", + "chain_id") + .labels(""); + + balPrefetchCacheMisses = + metricsSystem + .createLabelledCounter( + BesuMetricCategory.ETHEREUM, + "bal_prefetch_cache_misses", + "Total number of BAL prefetch cache misses", + "chain_id") + .labels(""); + + // Histogram metrics + blockDuration = + metricsSystem.createLabelledTimer( + BesuMetricCategory.ETHEREUM, + "block_duration", + "Block processing time in seconds", + "chain_id"); + + txDuration = + metricsSystem.createLabelledTimer( + BesuMetricCategory.ETHEREUM, + "tx_duration", + "Transaction processing time in seconds", + "chain_id"); + + stateRootDuration = + metricsSystem.createLabelledTimer( + BesuMetricCategory.ETHEREUM, + "stateroot_duration", + "State root calculation time in seconds", + "chain_id"); + + throughputMgasPerSec = + metricsSystem.createLabelledTimer( + BesuMetricCategory.ETHEREUM, + "throughput_mgas_per_sec", + "Gas throughput in millions of gas per second", + "chain_id"); + + balPrefetchDuration = + metricsSystem.createLabelledTimer( + BesuMetricCategory.ETHEREUM, + "bal_prefetch_duration", + "BAL prefetch phase time in seconds", + "chain_id"); + + balSize = + metricsSystem.createLabelledTimer( + BesuMetricCategory.ETHEREUM, + "bal_size", + "BAL size in bytes", + "chain_id"); + } + + /** Increments the total blocks processed counter */ + public void incrementBlocksTotal() { + blocksTotal.inc(); + } + + /** Increments the total transactions processed counter */ + public void incrementTxTotal() { + txTotal.inc(); + } + + /** Increments the total BAL-enabled blocks processed counter */ + public void incrementBalBlocksTotal() { + balBlocksTotal.inc(); + } + + /** + * Increments the BAL prefetch accounts counter. + * + * @param count Number of accounts prefetched + */ + public void incrementBalPrefetchAccounts(final long count) { + balPrefetchAccounts.inc(count); + } + + /** + * Increments the BAL prefetch slots counter. + * + * @param count Number of storage slots prefetched + */ + public void incrementBalPrefetchSlots(final long count) { + balPrefetchSlots.inc(count); + } + + /** + * Increments the BAL prefetch cache hits counter. + * + * @param count Number of cache hits + */ + public void incrementBalPrefetchCacheHits(final long count) { + balPrefetchCacheHits.inc(count); + } + + /** + * Increments the BAL prefetch cache misses counter. + * + * @param count Number of cache misses + */ + public void incrementBalPrefetchCacheMisses(final long count) { + balPrefetchCacheMisses.inc(count); + } + + /** + * Creates a timer for block duration measurement. + * + * @param chainId Chain ID for labeling + * @return Operation timer for block processing + */ + public OperationTimer.TimingContext startBlockTimer(final String chainId) { + return blockDuration.labels(chainId).startTimer(); + } + + /** + * Creates a timer for transaction duration measurement. + * + * @param chainId Chain ID for labeling + * @return Operation timer for transaction processing + */ + public OperationTimer.TimingContext startTxTimer(final String chainId) { + return txDuration.labels(chainId).startTimer(); + } + + /** + * Creates a timer for state root calculation duration measurement. + * + * @param chainId Chain ID for labeling + * @return Operation timer for state root calculation + */ + public OperationTimer.TimingContext startStateRootTimer(final String chainId) { + return stateRootDuration.labels(chainId).startTimer(); + } + + /** + * Records gas throughput in Mgas/sec. + * + * @param chainId Chain ID for labeling + * @param mgasPerSec Throughput in millions of gas per second + */ + public void recordThroughput(final String chainId, final double mgasPerSec) { + throughputMgasPerSec.labels(chainId).observeDuration(mgasPerSec * 1000.0, "ms"); + } + + /** + * Creates a timer for BAL prefetch duration measurement. + * + * @param chainId Chain ID for labeling + * @return Operation timer for BAL prefetch + */ + public OperationTimer.TimingContext startBalPrefetchTimer(final String chainId) { + return balPrefetchDuration.labels(chainId).startTimer(); + } + + /** + * Records BAL size in bytes. + * + * @param chainId Chain ID for labeling + * @param sizeBytes BAL size in bytes + */ + public void recordBalSize(final String chainId, final long sizeBytes) { + balSize.labels(chainId).observeDuration(sizeBytes / 1000.0, "kb"); + } +} \ No newline at end of file diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalOtelTracer.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalOtelTracer.java new file mode 100644 index 00000000000..ba5beafd015 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalOtelTracer.java @@ -0,0 +1,311 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal.tracing; + +import org.hyperledger.besu.ethereum.bal.BlockAccessList; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.Hash; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; + +/** + * Main OpenTelemetry tracer for Block-level Access Lists (BAL) as defined + * in the EIP-7928 BAL OTel specification. + * + *

Manages the complete span hierarchy: + *

+ * ethereum.block
+ * ├── ethereum.bal.prefetch
+ * │   ├── ethereum.bal.prefetch.account (optional)
+ * │   └── ethereum.bal.prefetch.slot (optional)
+ * ├── ethereum.tx.execute (per transaction)
+ * └── ethereum.stateroot
+ * 
+ */ +public class BalOtelTracer { + + private static final String SERVICE_NAME = "besu"; + private static final String SERVICE_VERSION = "21.1.7-SNAPSHOT"; // Besu version from this fork + + private final Tracer tracer; + private final BalMetrics metrics; + private final String chainId; + private final boolean enabled; + private final boolean enableDetailedTracing; + + // Current span state + private Span blockSpan; + private OperationTimer.TimingContext blockTimer; + private BalPrefetchTracer prefetchTracer; + private final AtomicBoolean blockProcessing = new AtomicBoolean(false); + + // Block processing stats + private long blockStartTime; + private long totalGasUsed = 0; + private int transactionCount = 0; + + /** + * Creates a new BalOtelTracer. + * + * @param tracer OpenTelemetry tracer instance + * @param metricsSystem Metrics system for recording metrics + * @param chainId Chain ID for resource attributes and metrics labeling + * @param enabled Whether BAL tracing is enabled + * @param enableDetailedTracing Whether to enable optional per-account/slot tracing + */ + public BalOtelTracer( + final Tracer tracer, + final MetricsSystem metricsSystem, + final String chainId, + final boolean enabled, + final boolean enableDetailedTracing) { + this.tracer = tracer; + this.metrics = new BalMetrics(metricsSystem); + this.chainId = chainId; + this.enabled = enabled; + this.enableDetailedTracing = enableDetailedTracing; + } + + /** + * Starts the root ethereum.block span for block processing. + * + * @param blockHeader The block header + * @param bal The block access list (may be null if not available) + * @return The block span + */ + public Span startBlockProcessing(final BlockHeader blockHeader, final BlockAccessList bal) { + if (!enabled || !blockProcessing.compareAndSet(false, true)) { + return null; + } + + blockStartTime = System.nanoTime(); + totalGasUsed = 0; + transactionCount = 0; + + blockSpan = + tracer + .spanBuilder("ethereum.block") + .setSpanKind(SpanKind.INTERNAL) + .setAttribute(BalSpanAttributes.SERVICE_NAME, SERVICE_NAME) + .setAttribute(BalSpanAttributes.SERVICE_VERSION, SERVICE_VERSION) + .setAttribute(BalSpanAttributes.ETHEREUM_CHAIN_ID, chainId) + .startSpan(); + + // Add BAL-specific attributes if available + if (bal != null) { + blockSpan.setAttribute(BalSpanAttributes.BAL_HASH, bal.getBlockHash().toHexString()); + blockSpan.setAttribute(BalSpanAttributes.BAL_ACCOUNTS_COUNT, bal.getAccountsCount()); + blockSpan.setAttribute( + BalSpanAttributes.BAL_STORAGE_SLOTS_COUNT, bal.getStorageSlotsCount()); + blockSpan.setAttribute(BalSpanAttributes.BAL_CODE_COUNT, bal.getCodeCount()); + blockSpan.setAttribute(BalSpanAttributes.BAL_SIZE_BYTES, bal.getSizeBytes()); + + // Record BAL metrics + metrics.incrementBalBlocksTotal(); + metrics.recordBalSize(chainId, bal.getSizeBytes()); + } + + blockTimer = metrics.startBlockTimer(chainId); + prefetchTracer = + new BalPrefetchTracer(tracer, metrics, chainId, enableDetailedTracing); + + return blockSpan; + } + + /** + * Starts BAL prefetch tracing. + * + * @param bal The block access list to prefetch + * @return The prefetch tracer instance + */ + public BalPrefetchTracer startPrefetch(final BlockAccessList bal) { + if (!enabled || blockSpan == null || bal == null) { + return null; + } + + prefetchTracer.startPrefetch(bal, blockSpan); + return prefetchTracer; + } + + /** + * Starts a transaction execution span. + * + * @param transaction The transaction being executed + * @param transactionIndex The transaction index in the block + * @return The transaction span + */ + public Span startTransactionExecution(final Transaction transaction, final int transactionIndex) { + if (!enabled || blockSpan == null) { + return null; + } + + transactionCount++; + + final Span txSpan = + tracer + .spanBuilder("ethereum.tx.execute") + .setParent(io.opentelemetry.context.Context.current().with(blockSpan)) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute(BalSpanAttributes.TX_INDEX, transactionIndex) + .setAttribute(BalSpanAttributes.TX_HASH, transaction.getHash().toHexString()) + .startSpan(); + + metrics.startTxTimer(chainId); + return txSpan; + } + + /** + * Finishes a transaction execution span. + * + * @param txSpan The transaction span to finish + * @param gasUsed Gas used by the transaction + * @param success Whether the transaction was successful + */ + public void finishTransactionExecution(final Span txSpan, final long gasUsed, final boolean success) { + if (txSpan == null) { + return; + } + + try { + txSpan.setAttribute(BalSpanAttributes.TX_GAS_USED, gasUsed); + if (!success) { + txSpan.setStatus(StatusCode.ERROR, "Transaction execution failed"); + } + + totalGasUsed += gasUsed; + metrics.incrementTxTotal(); + + } finally { + txSpan.end(); + } + } + + /** + * Starts a state root calculation span. + * + * @param accountsUpdated Number of accounts updated + * @param storageSlotsUpdated Number of storage slots updated + * @param parallel Whether state root calculation was done in parallel + * @return The state root span + */ + public Span startStateRootCalculation( + final int accountsUpdated, final int storageSlotsUpdated, final boolean parallel) { + if (!enabled || blockSpan == null) { + return null; + } + + final Span stateRootSpan = + tracer + .spanBuilder("ethereum.stateroot") + .setParent(io.opentelemetry.context.Context.current().with(blockSpan)) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute(BalSpanAttributes.ACCOUNTS_UPDATED, accountsUpdated) + .setAttribute(BalSpanAttributes.STORAGE_SLOTS_UPDATED, storageSlotsUpdated) + .setAttribute(BalSpanAttributes.BAL_PARALLEL, parallel) + .startSpan(); + + metrics.startStateRootTimer(chainId); + return stateRootSpan; + } + + /** + * Finishes a state root calculation span. + * + * @param stateRootSpan The state root span to finish + * @param stateRoot The calculated state root hash + */ + public void finishStateRootCalculation(final Span stateRootSpan, final Hash stateRoot) { + if (stateRootSpan == null) { + return; + } + + try { + stateRootSpan.setAttribute("state.root", stateRoot.toHexString()); + } finally { + stateRootSpan.end(); + } + } + + /** Finishes block processing and records final metrics. */ + public void finishBlockProcessing() { + if (!enabled || blockSpan == null) { + return; + } + + try { + // Finish prefetch tracing if active + if (prefetchTracer != null) { + prefetchTracer.finishPrefetch(); + } + + // Calculate and record throughput + final long blockDurationNanos = System.nanoTime() - blockStartTime; + final double blockDurationSeconds = blockDurationNanos / 1_000_000_000.0; + final double mgasPerSec = blockDurationSeconds > 0 + ? (totalGasUsed / 1_000_000.0) / blockDurationSeconds + : 0; + + metrics.recordThroughput(chainId, mgasPerSec); + metrics.incrementBlocksTotal(); + + } finally { + blockSpan.end(); + if (blockTimer != null) { + blockTimer.stop(); + } + blockProcessing.set(false); + } + } + + /** + * Marks block processing as failed and finishes the span with an error status. + * + * @param reason The failure reason + */ + public void failBlockProcessing(final String reason) { + if (blockSpan != null) { + blockSpan.setStatus(StatusCode.ERROR, reason); + finishBlockProcessing(); + } + } + + /** @return Whether BAL tracing is currently enabled */ + public boolean isEnabled() { + return enabled; + } + + /** @return Whether detailed per-account/slot tracing is enabled */ + public boolean isDetailedTracingEnabled() { + return enableDetailedTracing; + } + + /** @return The current chain ID */ + public String getChainId() { + return chainId; + } + + /** @return Whether a block is currently being processed */ + public boolean isBlockProcessing() { + return blockProcessing.get(); + } +} \ No newline at end of file diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalPrefetchTracer.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalPrefetchTracer.java new file mode 100644 index 00000000000..d807bace09b --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalPrefetchTracer.java @@ -0,0 +1,195 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal.tracing; + +import org.hyperledger.besu.ethereum.bal.BlockAccessList; +import org.hyperledger.besu.ethereum.core.Address; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; + +/** + * Specialized tracer for BAL prefetch operations. + * Handles the ethereum.bal.prefetch span and optional per-account/slot child spans. + */ +public class BalPrefetchTracer { + + private final Tracer tracer; + private final BalMetrics metrics; + private final String chainId; + private final boolean enableDetailedTracing; + + private Span prefetchSpan; + private OperationTimer.TimingContext prefetchTimer; + private long cacheHits = 0; + private long cacheMisses = 0; + private long codeBytes = 0; + + /** + * Creates a new BalPrefetchTracer. + * + * @param tracer OpenTelemetry tracer + * @param metrics BAL metrics instance + * @param chainId Chain ID for metrics labeling + * @param enableDetailedTracing Whether to enable optional per-account/slot tracing + */ + public BalPrefetchTracer( + final Tracer tracer, + final BalMetrics metrics, + final String chainId, + final boolean enableDetailedTracing) { + this.tracer = tracer; + this.metrics = metrics; + this.chainId = chainId; + this.enableDetailedTracing = enableDetailedTracing; + } + + /** + * Starts the BAL prefetch span with the given BAL. + * + * @param bal The block access list being prefetched + * @param parentSpan The parent span (ethereum.block) + * @return The prefetch span + */ + public Span startPrefetch(final BlockAccessList bal, final Span parentSpan) { + prefetchSpan = + tracer + .spanBuilder("ethereum.bal.prefetch") + .setParent(io.opentelemetry.context.Context.current().with(parentSpan)) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute(BalSpanAttributes.ACCOUNTS_COUNT, bal.getAccountsCount()) + .setAttribute(BalSpanAttributes.STORAGE_SLOTS_COUNT, bal.getStorageSlotsCount()) + .setAttribute(BalSpanAttributes.CODE_COUNT, bal.getCodeCount()) + .startSpan(); + + prefetchTimer = metrics.startBalPrefetchTimer(chainId); + + // Update metrics + metrics.incrementBalPrefetchAccounts(bal.getAccountsCount()); + metrics.incrementBalPrefetchSlots(bal.getStorageSlotsCount()); + + return prefetchSpan; + } + + /** + * Records cache hits during prefetch. + * + * @param hits Number of cache hits + */ + public void recordCacheHits(final long hits) { + cacheHits += hits; + } + + /** + * Records cache misses during prefetch. + * + * @param misses Number of cache misses + */ + public void recordCacheMisses(final long misses) { + cacheMisses += misses; + } + + /** + * Records code bytes prefetched. + * + * @param bytes Number of bytes of code prefetched + */ + public void recordCodeBytes(final long bytes) { + codeBytes += bytes; + } + + /** + * Traces prefetching for a specific account (optional detailed tracing). + * Creates a ethereum.bal.prefetch.account child span if detailed tracing is enabled. + * + * @param address The account address being prefetched + * @return The account prefetch span, or null if detailed tracing is disabled + */ + public Span traceAccountPrefetch(final Address address) { + if (!enableDetailedTracing || prefetchSpan == null) { + return null; + } + + return tracer + .spanBuilder("ethereum.bal.prefetch.account") + .setParent(io.opentelemetry.context.Context.current().with(prefetchSpan)) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("account.address", address.toHexString()) + .startSpan(); + } + + /** + * Traces prefetching for a specific storage slot (optional detailed tracing). + * Creates a ethereum.bal.prefetch.slot child span if detailed tracing is enabled. + * + * @param address The account address + * @param storageKey The storage slot key + * @return The slot prefetch span, or null if detailed tracing is disabled + */ + public Span traceSlotPrefetch(final Address address, final String storageKey) { + if (!enableDetailedTracing || prefetchSpan == null) { + return null; + } + + return tracer + .spanBuilder("ethereum.bal.prefetch.slot") + .setParent(io.opentelemetry.context.Context.current().with(prefetchSpan)) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("account.address", address.toHexString()) + .setAttribute("storage.key", storageKey) + .startSpan(); + } + + /** Finishes the prefetch span and records final attributes. */ + public void finishPrefetch() { + if (prefetchSpan == null) { + return; + } + + try { + // Set final attributes + prefetchSpan.setAttribute(BalSpanAttributes.CODE_BYTES, codeBytes); + prefetchSpan.setAttribute(BalSpanAttributes.CACHE_HITS, cacheHits); + prefetchSpan.setAttribute(BalSpanAttributes.CACHE_MISSES, cacheMisses); + + // Update metrics + metrics.incrementBalPrefetchCacheHits(cacheHits); + metrics.incrementBalPrefetchCacheMisses(cacheMisses); + + } finally { + prefetchSpan.end(); + if (prefetchTimer != null) { + prefetchTimer.stop(); + } + } + } + + /** @return Current number of cache hits recorded */ + public long getCacheHits() { + return cacheHits; + } + + /** @return Current number of cache misses recorded */ + public long getCacheMisses() { + return cacheMisses; + } + + /** @return Current number of code bytes recorded */ + public long getCodeBytes() { + return codeBytes; + } +} \ No newline at end of file diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalSpanAttributes.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalSpanAttributes.java new file mode 100644 index 00000000000..c4f5dcf3dba --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalSpanAttributes.java @@ -0,0 +1,94 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal.tracing; + +/** + * Constants for OpenTelemetry span attributes related to Block-level Access Lists (BAL) + * as defined in the EIP-7928 BAL OTel specification. + */ +public final class BalSpanAttributes { + + // Block-level BAL attributes + /** BAL hash attribute */ + public static final String BAL_HASH = "bal.hash"; + + /** Number of accounts in the BAL */ + public static final String BAL_ACCOUNTS_COUNT = "bal.accounts_count"; + + /** Number of storage slots in the BAL */ + public static final String BAL_STORAGE_SLOTS_COUNT = "bal.storage_slots_count"; + + /** Number of code accesses in the BAL */ + public static final String BAL_CODE_COUNT = "bal.code_count"; + + /** Size of the BAL in bytes */ + public static final String BAL_SIZE_BYTES = "bal.size_bytes"; + + // Transaction attributes + /** Transaction index in block */ + public static final String TX_INDEX = "tx.index"; + + /** Transaction hash */ + public static final String TX_HASH = "tx.hash"; + + /** Gas used by transaction */ + public static final String TX_GAS_USED = "tx.gas_used"; + + // State root calculation attributes + /** Number of accounts updated during state root calculation */ + public static final String ACCOUNTS_UPDATED = "accounts_updated"; + + /** Number of storage slots updated during state root calculation */ + public static final String STORAGE_SLOTS_UPDATED = "storage_slots_updated"; + + /** Whether BAL processing was done in parallel */ + public static final String BAL_PARALLEL = "bal.parallel"; + + // BAL prefetch attributes + /** Number of accounts prefetched */ + public static final String ACCOUNTS_COUNT = "accounts_count"; + + /** Number of storage slots prefetched */ + public static final String STORAGE_SLOTS_COUNT = "storage_slots_count"; + + /** Number of code accesses prefetched */ + public static final String CODE_COUNT = "code_count"; + + /** Total bytes of code prefetched */ + public static final String CODE_BYTES = "code_bytes"; + + /** Number of cache hits during prefetch */ + public static final String CACHE_HITS = "cache_hits"; + + /** Number of cache misses during prefetch */ + public static final String CACHE_MISSES = "cache_misses"; + + // Resource attributes (as per Section 4 of the spec) + /** Service name */ + public static final String SERVICE_NAME = "service.name"; + + /** Service version */ + public static final String SERVICE_VERSION = "service.version"; + + /** Deployment environment */ + public static final String DEPLOYMENT_ENVIRONMENT = "deployment.environment"; + + /** Ethereum chain ID */ + public static final String ETHEREUM_CHAIN_ID = "ethereum.chain.id"; + + private BalSpanAttributes() { + // Utility class - prevent instantiation + } +} \ No newline at end of file diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalTracingConfig.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalTracingConfig.java new file mode 100644 index 00000000000..880ce69c029 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bal/tracing/BalTracingConfig.java @@ -0,0 +1,143 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal.tracing; + +/** + * Configuration for Block-level Access List (BAL) OpenTelemetry tracing. + * Defines settings for enabling/disabling tracing and configuring OTLP endpoint. + */ +public class BalTracingConfig { + + /** Default OTLP endpoint as per specification */ + public static final String DEFAULT_OTLP_ENDPOINT = "localhost:4317"; + + /** Default sampling rate (1.0 = sample all) */ + public static final double DEFAULT_SAMPLING_RATE = 1.0; + + private final boolean enabled; + private final boolean detailedTracingEnabled; + private final String otlpEndpoint; + private final double samplingRate; + private final String deploymentEnvironment; + + /** + * Creates a new BalTracingConfig. + * + * @param enabled Whether BAL tracing is enabled + * @param detailedTracingEnabled Whether to enable optional per-account/slot child spans + * @param otlpEndpoint OTLP endpoint for sending traces + * @param samplingRate Sampling rate (0.0 to 1.0) + * @param deploymentEnvironment Deployment environment name (e.g., "production", "dev") + */ + public BalTracingConfig( + final boolean enabled, + final boolean detailedTracingEnabled, + final String otlpEndpoint, + final double samplingRate, + final String deploymentEnvironment) { + this.enabled = enabled; + this.detailedTracingEnabled = detailedTracingEnabled; + this.otlpEndpoint = otlpEndpoint; + this.samplingRate = Math.max(0.0, Math.min(1.0, samplingRate)); // Clamp to [0.0, 1.0] + this.deploymentEnvironment = deploymentEnvironment; + } + + /** Creates a disabled BAL tracing configuration. */ + public static BalTracingConfig disabled() { + return new BalTracingConfig(false, false, DEFAULT_OTLP_ENDPOINT, 0.0, "unknown"); + } + + /** Creates a default enabled BAL tracing configuration. */ + public static BalTracingConfig defaultEnabled() { + return new BalTracingConfig( + true, false, DEFAULT_OTLP_ENDPOINT, DEFAULT_SAMPLING_RATE, "development"); + } + + /** + * Creates a BAL tracing configuration with detailed tracing enabled. + * + * @param deploymentEnvironment Deployment environment name + * @return Configuration with detailed tracing enabled + */ + public static BalTracingConfig detailedEnabled(final String deploymentEnvironment) { + return new BalTracingConfig( + true, true, DEFAULT_OTLP_ENDPOINT, DEFAULT_SAMPLING_RATE, deploymentEnvironment); + } + + /** @return Whether BAL tracing is enabled */ + public boolean isEnabled() { + return enabled; + } + + /** @return Whether detailed per-account/slot tracing is enabled */ + public boolean isDetailedTracingEnabled() { + return detailedTracingEnabled; + } + + /** @return OTLP endpoint for sending traces */ + public String getOtlpEndpoint() { + return otlpEndpoint; + } + + /** @return Sampling rate (0.0 to 1.0) */ + public double getSamplingRate() { + return samplingRate; + } + + /** @return Deployment environment name */ + public String getDeploymentEnvironment() { + return deploymentEnvironment; + } + + /** + * Creates a new configuration with different enabled status. + * + * @param enabled New enabled status + * @return New configuration with updated enabled status + */ + public BalTracingConfig withEnabled(final boolean enabled) { + return new BalTracingConfig( + enabled, detailedTracingEnabled, otlpEndpoint, samplingRate, deploymentEnvironment); + } + + /** + * Creates a new configuration with different OTLP endpoint. + * + * @param otlpEndpoint New OTLP endpoint + * @return New configuration with updated endpoint + */ + public BalTracingConfig withOtlpEndpoint(final String otlpEndpoint) { + return new BalTracingConfig( + enabled, detailedTracingEnabled, otlpEndpoint, samplingRate, deploymentEnvironment); + } + + /** + * Creates a new configuration with different sampling rate. + * + * @param samplingRate New sampling rate (0.0 to 1.0) + * @return New configuration with updated sampling rate + */ + public BalTracingConfig withSamplingRate(final double samplingRate) { + return new BalTracingConfig( + enabled, detailedTracingEnabled, otlpEndpoint, samplingRate, deploymentEnvironment); + } + + @Override + public String toString() { + return String.format( + "BalTracingConfig{enabled=%s, detailedTracing=%s, otlpEndpoint='%s', samplingRate=%.2f, environment='%s'}", + enabled, detailedTracingEnabled, otlpEndpoint, samplingRate, deploymentEnvironment); + } +} \ No newline at end of file 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 1758e1010e5..ff090fd51c8 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 @@ -14,6 +14,8 @@ */ package org.hyperledger.besu.ethereum.mainnet; +import org.hyperledger.besu.ethereum.bal.BlockAccessList; +import org.hyperledger.besu.ethereum.bal.tracing.BalOtelTracer; import org.hyperledger.besu.ethereum.bonsai.BonsaiPersistedWorldState; import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateUpdater; import org.hyperledger.besu.ethereum.chain.Blockchain; @@ -133,6 +135,8 @@ public boolean isSuccessful() { protected final TransactionGasBudgetCalculator gasBudgetCalculator; + protected final BalOtelTracer balTracer; + protected AbstractBlockProcessor( final MainnetTransactionProcessor transactionProcessor, final TransactionReceiptFactory transactionReceiptFactory, @@ -140,12 +144,25 @@ protected AbstractBlockProcessor( final MiningBeneficiaryCalculator miningBeneficiaryCalculator, final boolean skipZeroBlockRewards, final TransactionGasBudgetCalculator gasBudgetCalculator) { + this(transactionProcessor, transactionReceiptFactory, blockReward, miningBeneficiaryCalculator, + skipZeroBlockRewards, gasBudgetCalculator, null); + } + + protected AbstractBlockProcessor( + final MainnetTransactionProcessor transactionProcessor, + final TransactionReceiptFactory transactionReceiptFactory, + final Wei blockReward, + final MiningBeneficiaryCalculator miningBeneficiaryCalculator, + final boolean skipZeroBlockRewards, + final TransactionGasBudgetCalculator gasBudgetCalculator, + final BalOtelTracer balTracer) { this.transactionProcessor = transactionProcessor; this.transactionReceiptFactory = transactionReceiptFactory; this.blockReward = blockReward; this.miningBeneficiaryCalculator = miningBeneficiaryCalculator; this.skipZeroBlockRewards = skipZeroBlockRewards; this.gasBudgetCalculator = gasBudgetCalculator; + this.balTracer = balTracer; } @Override @@ -158,14 +175,33 @@ public AbstractBlockProcessor.Result processBlock( final PrivateMetadataUpdater privateMetadataUpdater) { final Span globalProcessBlock = tracer.spanBuilder("processBlock").setSpanKind(Span.Kind.INTERNAL).startSpan(); + + // Start BAL tracing if enabled + final BlockAccessList bal = extractBlockAccessList(blockHeader, transactions); + final Span balBlockSpan = balTracer != null ? balTracer.startBlockProcessing(blockHeader, bal) : null; + try { + // Start BAL prefetch if available + if (balTracer != null && bal != null) { + balTracer.startPrefetch(bal); + } + final List receipts = new ArrayList<>(); long currentGasUsed = 0; + int transactionIndex = 0; for (final Transaction transaction : transactions) { if (!hasAvailableBlockBudget(blockHeader, transaction, currentGasUsed)) { + if (balTracer != null) { + balTracer.failBlockProcessing("Insufficient block gas budget"); + } return AbstractBlockProcessor.Result.failed(); } + // Start transaction tracing + final Span txSpan = balTracer != null + ? balTracer.startTransactionExecution(transaction, transactionIndex) + : null; + final WorldUpdater worldStateUpdater = worldState.updater(); final BlockHashLookup blockHashLookup = new BlockHashLookup(blockHeader, blockchain); final Address miningBeneficiary = @@ -183,26 +219,45 @@ public AbstractBlockProcessor.Result processBlock( true, TransactionValidationParams.processingBlock(), privateMetadataUpdater); + final long gasUsed = transaction.getGasLimit() - result.getGasRemaining(); + if (result.isInvalid()) { LOG.info( "Block processing error: transaction invalid '{}'. Block {} Transaction {}", result.getValidationResult().getInvalidReason(), blockHeader.getHash().toHexString(), transaction.getHash().toHexString()); + + // Finish transaction span with error + if (balTracer != null && txSpan != null) { + balTracer.finishTransactionExecution(txSpan, gasUsed, false); + } + if (worldState instanceof BonsaiPersistedWorldState) { ((BonsaiWorldStateUpdater) worldStateUpdater).reset(); } + + if (balTracer != null) { + balTracer.failBlockProcessing("Transaction validation failed: " + + result.getValidationResult().getInvalidReason()); + } return AbstractBlockProcessor.Result.failed(); } worldStateUpdater.commit(); + currentGasUsed += gasUsed; - currentGasUsed += transaction.getGasLimit() - result.getGasRemaining(); + // Finish transaction span successfully + if (balTracer != null && txSpan != null) { + balTracer.finishTransactionExecution(txSpan, gasUsed, true); + } final TransactionReceipt transactionReceipt = transactionReceiptFactory.create( transaction.getType(), result, worldState, currentGasUsed); receipts.add(transactionReceipt); + + transactionIndex++; } if (!rewardCoinbase(worldState, blockHeader, ommers, skipZeroBlockRewards)) { @@ -210,10 +265,33 @@ public AbstractBlockProcessor.Result processBlock( if (worldState instanceof BonsaiPersistedWorldState) { ((BonsaiWorldStateUpdater) worldState.updater()).reset(); } + + if (balTracer != null) { + balTracer.failBlockProcessing("Coinbase reward calculation failed"); + } return AbstractBlockProcessor.Result.failed(); } + // Start state root calculation tracing + final Span stateRootSpan = balTracer != null + ? balTracer.startStateRootCalculation( + getAccountsUpdatedCount(), + getStorageSlotsUpdatedCount(), + false) // parallel processing not implemented in this version + : null; + worldState.persist(blockHeader); + + // Finish state root calculation + if (balTracer != null && stateRootSpan != null) { + balTracer.finishStateRootCalculation(stateRootSpan, blockHeader.getStateRoot()); + } + + // Finish block processing + if (balTracer != null) { + balTracer.finishBlockProcessing(); + } + return AbstractBlockProcessor.Result.successful(receipts); } finally { globalProcessBlock.end(); @@ -247,4 +325,42 @@ abstract boolean rewardCoinbase( final BlockHeader header, final List ommers, final boolean skipZeroBlockRewards); + + /** + * Extracts or creates a BlockAccessList for the given block. + * Since EIP-7928 BAL is not yet implemented, this returns null. + * This method should be overridden when BAL implementation is added. + * + * @param blockHeader The block header + * @param transactions List of transactions in the block + * @return BlockAccessList if available, null otherwise + */ + protected BlockAccessList extractBlockAccessList( + final BlockHeader blockHeader, final List transactions) { + // TODO: Implement actual BAL extraction when EIP-7928 is implemented + // For now, return null since the BAL data structure is not populated + return null; + } + + /** + * Gets the number of accounts updated during block processing. + * This is a placeholder implementation for state root calculation tracing. + * + * @return Number of accounts updated (placeholder value) + */ + protected int getAccountsUpdatedCount() { + // TODO: Implement actual account tracking when needed + return 0; // Placeholder + } + + /** + * Gets the number of storage slots updated during block processing. + * This is a placeholder implementation for state root calculation tracing. + * + * @return Number of storage slots updated (placeholder value) + */ + protected int getStorageSlotsUpdatedCount() { + // TODO: Implement actual storage slot tracking when needed + return 0; // Placeholder + } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bal/BlockAccessListTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bal/BlockAccessListTest.java new file mode 100644 index 00000000000..ba5bcc010c4 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bal/BlockAccessListTest.java @@ -0,0 +1,155 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.hyperledger.besu.ethereum.core.Address; +import org.hyperledger.besu.ethereum.core.Hash; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +public class BlockAccessListTest { + + private static final Hash BLOCK_HASH = Hash.fromHexString("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); + private static final Address ADDRESS_1 = Address.fromHexString("0x1234567890123456789012345678901234567890"); + private static final Address ADDRESS_2 = Address.fromHexString("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef"); + private static final Hash STORAGE_KEY_1 = Hash.fromHexString("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + private static final Hash STORAGE_KEY_2 = Hash.fromHexString("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + @Test + public void shouldCreateBlockAccessListWithEntries() { + final Map entries = Map.of( + ADDRESS_1, BlockAccessListEntry.codeOnly(true), + ADDRESS_2, BlockAccessListEntry.storageOnly(List.of(STORAGE_KEY_1, STORAGE_KEY_2)) + ); + + final BlockAccessList bal = new BlockAccessList(BLOCK_HASH, entries, 1024L); + + assertThat(bal.getBlockHash()).isEqualTo(BLOCK_HASH); + assertThat(bal.getEntries()).isEqualTo(entries); + assertThat(bal.getSizeBytes()).isEqualTo(1024L); + } + + @Test + public void shouldCalculateCorrectCounts() { + final Map entries = Map.of( + ADDRESS_1, BlockAccessListEntry.codeOnly(true), + ADDRESS_2, BlockAccessListEntry.storageOnly(List.of(STORAGE_KEY_1, STORAGE_KEY_2)) + ); + + final BlockAccessList bal = new BlockAccessList(BLOCK_HASH, entries, 1024L); + + assertThat(bal.getAccountsCount()).isEqualTo(2); + assertThat(bal.getStorageSlotsCount()).isEqualTo(2); // Only ADDRESS_2 has storage + assertThat(bal.getCodeCount()).isEqualTo(1); // Only ADDRESS_1 has code access + } + + @Test + public void shouldReturnCorrectAddressList() { + final Map entries = Map.of( + ADDRESS_1, BlockAccessListEntry.codeOnly(true), + ADDRESS_2, BlockAccessListEntry.storageOnly(List.of(STORAGE_KEY_1)) + ); + + final BlockAccessList bal = new BlockAccessList(BLOCK_HASH, entries, 1024L); + + assertThat(bal.getAddresses()).containsExactlyInAnyOrder(ADDRESS_1, ADDRESS_2); + } + + @Test + public void shouldGetEntryForAddress() { + final BlockAccessListEntry entry1 = BlockAccessListEntry.codeOnly(true); + final Map entries = Map.of(ADDRESS_1, entry1); + + final BlockAccessList bal = new BlockAccessList(BLOCK_HASH, entries, 1024L); + + assertThat(bal.getEntry(ADDRESS_1)).isEqualTo(entry1); + assertThat(bal.getEntry(ADDRESS_2)).isNull(); + } + + @Test + public void shouldCheckIfContainsAddress() { + final Map entries = Map.of( + ADDRESS_1, BlockAccessListEntry.codeOnly(true) + ); + + final BlockAccessList bal = new BlockAccessList(BLOCK_HASH, entries, 1024L); + + assertThat(bal.containsAddress(ADDRESS_1)).isTrue(); + assertThat(bal.containsAddress(ADDRESS_2)).isFalse(); + } + + @Test + public void shouldDetectEmptyBAL() { + final BlockAccessList emptyBal = new BlockAccessList(BLOCK_HASH, Map.of(), 0L); + final BlockAccessList nonEmptyBal = new BlockAccessList(BLOCK_HASH, + Map.of(ADDRESS_1, BlockAccessListEntry.codeOnly(true)), 1024L); + + assertThat(emptyBal.isEmpty()).isTrue(); + assertThat(nonEmptyBal.isEmpty()).isFalse(); + } + + @Test + public void shouldThrowWhenNullBlockHash() { + assertThatThrownBy(() -> new BlockAccessList(null, Map.of(), 0L)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("blockHash cannot be null"); + } + + @Test + public void shouldThrowWhenNullEntries() { + assertThatThrownBy(() -> new BlockAccessList(BLOCK_HASH, null, 0L)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("entries cannot be null"); + } + + @Test + public void shouldHaveCorrectEqualsAndHashCode() { + final Map entries = Map.of( + ADDRESS_1, BlockAccessListEntry.codeOnly(true) + ); + + final BlockAccessList bal1 = new BlockAccessList(BLOCK_HASH, entries, 1024L); + final BlockAccessList bal2 = new BlockAccessList(BLOCK_HASH, entries, 1024L); + final BlockAccessList bal3 = new BlockAccessList(BLOCK_HASH, entries, 2048L); // Different size + + assertThat(bal1).isEqualTo(bal2); + assertThat(bal1.hashCode()).isEqualTo(bal2.hashCode()); + assertThat(bal1).isNotEqualTo(bal3); + } + + @Test + public void shouldHaveCorrectToString() { + final Map entries = Map.of( + ADDRESS_1, BlockAccessListEntry.codeOnly(true), + ADDRESS_2, BlockAccessListEntry.storageOnly(List.of(STORAGE_KEY_1)) + ); + + final BlockAccessList bal = new BlockAccessList(BLOCK_HASH, entries, 1024L); + final String toString = bal.toString(); + + assertThat(toString).contains("BlockAccessList"); + assertThat(toString).contains("accountsCount=2"); + assertThat(toString).contains("storageSlotsCount=1"); + assertThat(toString).contains("codeCount=1"); + assertThat(toString).contains("sizeBytes=1024"); + assertThat(toString).contains(BLOCK_HASH.toHexString()); + } +} \ No newline at end of file diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bal/tracing/BalMetricsTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bal/tracing/BalMetricsTest.java new file mode 100644 index 00000000000..2ac8a527c16 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bal/tracing/BalMetricsTest.java @@ -0,0 +1,147 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal.tracing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BalMetricsTest { + + @Mock private MetricsSystem mockMetricsSystem; + @Mock private LabelledMetric mockCounterMetric; + @Mock private LabelledMetric mockTimerMetric; + @Mock private Counter mockCounter; + @Mock private OperationTimer mockTimer; + @Mock private OperationTimer.TimingContext mockTimingContext; + + private BalMetrics balMetrics; + private static final String CHAIN_ID = "1337"; + + @Before + public void setUp() { + when(mockMetricsSystem.createLabelledCounter(any(), anyString(), anyString(), anyString())) + .thenReturn(mockCounterMetric); + when(mockMetricsSystem.createLabelledTimer(any(), anyString(), anyString(), anyString())) + .thenReturn(mockTimerMetric); + when(mockCounterMetric.labels(anyString())).thenReturn(mockCounter); + when(mockTimerMetric.labels(anyString())).thenReturn(mockTimer); + when(mockTimer.startTimer()).thenReturn(mockTimingContext); + + balMetrics = new BalMetrics(mockMetricsSystem); + } + + @Test + public void shouldIncrementBlocksTotal() { + balMetrics.incrementBlocksTotal(); + verify(mockCounter).inc(); + } + + @Test + public void shouldIncrementTxTotal() { + balMetrics.incrementTxTotal(); + verify(mockCounter).inc(); + } + + @Test + public void shouldIncrementBalBlocksTotal() { + balMetrics.incrementBalBlocksTotal(); + verify(mockCounter).inc(); + } + + @Test + public void shouldIncrementBalPrefetchAccounts() { + balMetrics.incrementBalPrefetchAccounts(5L); + verify(mockCounter).inc(5L); + } + + @Test + public void shouldIncrementBalPrefetchSlots() { + balMetrics.incrementBalPrefetchSlots(10L); + verify(mockCounter).inc(10L); + } + + @Test + public void shouldIncrementBalPrefetchCacheHits() { + balMetrics.incrementBalPrefetchCacheHits(3L); + verify(mockCounter).inc(3L); + } + + @Test + public void shouldIncrementBalPrefetchCacheMisses() { + balMetrics.incrementBalPrefetchCacheMisses(2L); + verify(mockCounter).inc(2L); + } + + @Test + public void shouldStartBlockTimer() { + final OperationTimer.TimingContext context = balMetrics.startBlockTimer(CHAIN_ID); + + assertThat(context).isEqualTo(mockTimingContext); + verify(mockTimer).startTimer(); + } + + @Test + public void shouldStartTxTimer() { + final OperationTimer.TimingContext context = balMetrics.startTxTimer(CHAIN_ID); + + assertThat(context).isEqualTo(mockTimingContext); + verify(mockTimer).startTimer(); + } + + @Test + public void shouldStartStateRootTimer() { + final OperationTimer.TimingContext context = balMetrics.startStateRootTimer(CHAIN_ID); + + assertThat(context).isEqualTo(mockTimingContext); + verify(mockTimer).startTimer(); + } + + @Test + public void shouldRecordThroughput() { + balMetrics.recordThroughput(CHAIN_ID, 2.5); + + verify(mockTimer).observeDuration(2500.0, "ms"); + } + + @Test + public void shouldStartBalPrefetchTimer() { + final OperationTimer.TimingContext context = balMetrics.startBalPrefetchTimer(CHAIN_ID); + + assertThat(context).isEqualTo(mockTimingContext); + verify(mockTimer).startTimer(); + } + + @Test + public void shouldRecordBalSize() { + balMetrics.recordBalSize(CHAIN_ID, 2048L); + + verify(mockTimer).observeDuration(2.048, "kb"); + } +} \ No newline at end of file diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bal/tracing/BalOtelTracerTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bal/tracing/BalOtelTracerTest.java new file mode 100644 index 00000000000..3a553456f5e --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bal/tracing/BalOtelTracerTest.java @@ -0,0 +1,220 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.bal.tracing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.bal.BlockAccessList; +import org.hyperledger.besu.ethereum.bal.BlockAccessListEntry; +import org.hyperledger.besu.ethereum.core.Address; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.core.Hash; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer; + +import java.util.List; +import java.util.Map; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BalOtelTracerTest { + + @Mock private Tracer mockTracer; + @Mock private MetricsSystem mockMetricsSystem; + @Mock private LabelledMetric mockCounterMetric; + @Mock private LabelledMetric mockTimerMetric; + @Mock private Counter mockCounter; + @Mock private OperationTimer mockTimer; + @Mock private OperationTimer.TimingContext mockTimingContext; + @Mock private SpanBuilder mockSpanBuilder; + @Mock private Span mockSpan; + + private BalOtelTracer balTracer; + private BlockHeader blockHeader; + private BlockAccessList blockAccessList; + private Transaction transaction; + + private static final String CHAIN_ID = "1337"; + + @Before + public void setUp() { + // Setup mock metrics + when(mockMetricsSystem.createLabelledCounter(any(), anyString(), anyString(), anyString())) + .thenReturn(mockCounterMetric); + when(mockMetricsSystem.createLabelledTimer(any(), anyString(), anyString(), anyString())) + .thenReturn(mockTimerMetric); + when(mockCounterMetric.labels(anyString())).thenReturn(mockCounter); + when(mockTimerMetric.labels(anyString())).thenReturn(mockTimer); + when(mockTimer.startTimer()).thenReturn(mockTimingContext); + + // Setup mock tracer + when(mockTracer.spanBuilder(anyString())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setSpanKind(any())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setParent(any())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setAttribute(anyString(), anyString())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setAttribute(anyString(), any(Long.class))).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setAttribute(anyString(), any(Integer.class))).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setAttribute(anyString(), any(Boolean.class))).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.startSpan()).thenReturn(mockSpan); + + balTracer = new BalOtelTracer(mockTracer, mockMetricsSystem, CHAIN_ID, true, false); + + // Setup test data + blockHeader = new BlockHeaderTestFixture().number(1).buildHeader(); + + final Map balEntries = Map.of( + Address.fromHexString("0x1234567890123456789012345678901234567890"), + BlockAccessListEntry.codeOnly(true), + Address.fromHexString("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef"), + BlockAccessListEntry.storageOnly(List.of(Hash.ZERO)) + ); + + blockAccessList = new BlockAccessList(blockHeader.getHash(), balEntries, 1024); + + transaction = new TransactionTestFixture().createTransaction(); + } + + @Test + public void shouldStartBlockProcessingWhenEnabled() { + final Span span = balTracer.startBlockProcessing(blockHeader, blockAccessList); + + assertThat(span).isEqualTo(mockSpan); + assertThat(balTracer.isBlockProcessing()).isTrue(); + verify(mockTracer).spanBuilder("ethereum.block"); + } + + @Test + public void shouldNotStartBlockProcessingWhenDisabled() { + final BalOtelTracer disabledTracer = new BalOtelTracer(mockTracer, mockMetricsSystem, CHAIN_ID, false, false); + + final Span span = disabledTracer.startBlockProcessing(blockHeader, blockAccessList); + + assertThat(span).isNull(); + assertThat(disabledTracer.isBlockProcessing()).isFalse(); + verify(mockTracer, never()).spanBuilder(anyString()); + } + + @Test + public void shouldStartTransactionExecution() { + balTracer.startBlockProcessing(blockHeader, blockAccessList); + + final Span txSpan = balTracer.startTransactionExecution(transaction, 0); + + assertThat(txSpan).isEqualTo(mockSpan); + verify(mockTracer).spanBuilder("ethereum.tx.execute"); + } + + @Test + public void shouldFinishTransactionExecutionSuccessfully() { + balTracer.startBlockProcessing(blockHeader, blockAccessList); + final Span txSpan = balTracer.startTransactionExecution(transaction, 0); + + balTracer.finishTransactionExecution(txSpan, 21000L, true); + + verify(mockSpan).setAttribute("tx.gas_used", 21000L); + verify(mockSpan, never()).setStatus(any(), anyString()); + verify(mockSpan).end(); + } + + @Test + public void shouldFinishTransactionExecutionWithError() { + balTracer.startBlockProcessing(blockHeader, blockAccessList); + final Span txSpan = balTracer.startTransactionExecution(transaction, 0); + + balTracer.finishTransactionExecution(txSpan, 21000L, false); + + verify(mockSpan).setAttribute("tx.gas_used", 21000L); + verify(mockSpan).setStatus(any(), anyString()); + verify(mockSpan).end(); + } + + @Test + public void shouldStartStateRootCalculation() { + balTracer.startBlockProcessing(blockHeader, blockAccessList); + + final Span stateRootSpan = balTracer.startStateRootCalculation(5, 10, false); + + assertThat(stateRootSpan).isEqualTo(mockSpan); + verify(mockTracer).spanBuilder("ethereum.stateroot"); + } + + @Test + public void shouldFinishStateRootCalculation() { + balTracer.startBlockProcessing(blockHeader, blockAccessList); + final Span stateRootSpan = balTracer.startStateRootCalculation(5, 10, false); + + balTracer.finishStateRootCalculation(stateRootSpan, blockHeader.getStateRoot()); + + verify(mockSpan).setAttribute("state.root", blockHeader.getStateRoot().toHexString()); + verify(mockSpan).end(); + } + + @Test + public void shouldFinishBlockProcessing() { + balTracer.startBlockProcessing(blockHeader, blockAccessList); + + balTracer.finishBlockProcessing(); + + assertThat(balTracer.isBlockProcessing()).isFalse(); + verify(mockSpan).end(); + verify(mockTimingContext).stop(); + } + + @Test + public void shouldFailBlockProcessing() { + balTracer.startBlockProcessing(blockHeader, blockAccessList); + + balTracer.failBlockProcessing("Test failure"); + + assertThat(balTracer.isBlockProcessing()).isFalse(); + verify(mockSpan).setStatus(any(), "Test failure"); + verify(mockSpan).end(); + } + + @Test + public void shouldReturnCorrectConfiguration() { + assertThat(balTracer.isEnabled()).isTrue(); + assertThat(balTracer.isDetailedTracingEnabled()).isFalse(); + assertThat(balTracer.getChainId()).isEqualTo(CHAIN_ID); + } + + @Test + public void shouldNotProcessWhenAlreadyProcessing() { + balTracer.startBlockProcessing(blockHeader, blockAccessList); + + final Span secondSpan = balTracer.startBlockProcessing(blockHeader, blockAccessList); + + assertThat(secondSpan).isNull(); + } +} \ No newline at end of file