Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,39 @@ clean-cache: ## Remove snapshot cache
@rm -rf $(SNAPSHOT_CACHE_DIR)
@echo "Snapshot cache cleared."

# =============================================================================
# Benchmark targets (GasUsed header exposed for comparison testing)
# =============================================================================

# Benchmark flags for comparison testing (no JWT auth, GasUsed header exposed)
BENCHMARK_ARGS := --Arbitrum.ExposeMetadataHeaders=true --JsonRpc.UnsecureDevNoRpcAuthentication=true

run-mainnet-benchmark: ## Run Mainnet with benchmark mode (GasUsed header exposed)
$(call restore-snapshot,arbitrum-mainnet,mainnet)
@echo "Starting Nethermind (Mainnet BENCHMARK MODE - GasUsed header enabled)..."
@echo " HTTP responses will include X-Arb-Gas-Used header"
$(call run-nethermind,arbitrum-mainnet) $(BENCHMARK_ARGS)

run-mainnet-archive-benchmark: ## Run Mainnet Archive with benchmark mode
$(call restore-snapshot,arbitrum-mainnet-archive,mainnet-archive)
@echo "Starting Nethermind (Mainnet Archive BENCHMARK MODE - GasUsed header enabled)..."
@echo " HTTP responses will include X-Arb-Gas-Used header"
$(call run-nethermind,arbitrum-mainnet-archive) $(BENCHMARK_ARGS)

run-sepolia-benchmark: ## Run Sepolia with benchmark mode (GasUsed header exposed)
@echo "Starting Nethermind (Sepolia BENCHMARK MODE - GasUsed header enabled)..."
@echo " HTTP responses will include X-Arb-Gas-Used header"
$(call run-nethermind,arbitrum-sepolia) $(BENCHMARK_ARGS)

run-sepolia-archive-benchmark: ## Run Sepolia Archive with benchmark mode
@echo "Starting Nethermind (Sepolia Archive BENCHMARK MODE - GasUsed header enabled)..."
@echo " HTTP responses will include X-Arb-Gas-Used header"
$(call run-nethermind,arbitrum-sepolia-archive) $(BENCHMARK_ARGS)

clean-run-mainnet-benchmark: clean-mainnet run-mainnet-benchmark ## Clean and run Mainnet benchmark

clean-run-sepolia-benchmark: clean-sepolia run-sepolia-benchmark ## Clean and run Sepolia benchmark

# =============================================================================
# Local / System test targets
# =============================================================================
Expand Down Expand Up @@ -164,6 +197,15 @@ clean-system-test: ## Clean system test data

clean-run-system-test: clean-system-test run-system-test ## Clean and run system test

run-system-test-benchmark: generate-system-test-config ## Run system test with benchmark mode
@echo "Starting Nethermind (System Test BENCHMARK MODE - GasUsed header enabled)..."
@echo " HTTP responses will include X-Arb-Gas-Used header"
cd $(BUILD_OUTPUT_DIR) && dotnet nethermind.dll -c $(CONFIG_NAME) \
--data-dir $(ROOT_DIR)/.data --JsonRpc.UnsecureDevNoRpcAuthentication=true --log debug \
$(BENCHMARK_ARGS)

clean-run-system-test-benchmark: clean-system-test run-system-test-benchmark ## Clean and run system test benchmark

nethermind-help: ## Show Nethermind help
cd $(BUILD_OUTPUT_DIR) && dotnet nethermind.dll -h

Expand Down
2 changes: 1 addition & 1 deletion src/Nethermind
Submodule Nethermind updated 199 files
79 changes: 79 additions & 0 deletions src/Nethermind.Arbitrum.Test/Execution/MetadataHeadersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: https://github.com/NethermindEth/nethermind-arbitrum/blob/main/LICENSE.md

using Nethermind.Arbitrum.Config;
using Nethermind.Arbitrum.Execution;
using Nethermind.Core;
using Nethermind.Core.Test.Builders;
using Nethermind.JsonRpc;
using Nethermind.JsonRpc.Modules;

namespace Nethermind.Arbitrum.Test.Execution;

[TestFixture]
public class MetadataHeadersTests
{
[TearDown]
public void TearDown()
{
// Clean up JsonRpcContext
JsonRpcContext.Current.Value?.Dispose();
JsonRpcContext.Current.Value = null;
}

[Test]
public void SetResponseHeader_WhenConfigEnabled_SetsGasUsedHeader()
{
using JsonRpcContext context = new(RpcEndpoint.Http);
ArbitrumConfig config = new() { ExposeMetadataHeaders = true };
Block block = Build.A.Block.WithGasUsed(12345L).TestObject;

if (config.ExposeMetadataHeaders)
JsonRpcContext.SetResponseHeader(ArbitrumExecutionEngine.HeaderGasUsed, block.Header.GasUsed.ToString());

Assert.That(context.ResponseHeaders, Is.Not.Null);
Assert.That(context.ResponseHeaders![ArbitrumExecutionEngine.HeaderGasUsed], Is.EqualTo("12345"));
}

[Test]
public void SetResponseHeader_WhenConfigDisabled_DoesNotSetHeader()
{
using JsonRpcContext context = new(RpcEndpoint.Http);
ArbitrumConfig config = new() { ExposeMetadataHeaders = true };
Block block = Build.A.Block.WithGasUsed(12345L).TestObject;

if (config.ExposeMetadataHeaders)
JsonRpcContext.SetResponseHeader(ArbitrumExecutionEngine.HeaderGasUsed, block.Header.GasUsed.ToString());
Assert.That(context.ResponseHeaders, Is.Null);
}

[Test]
public void HeaderGasUsed_ConstantValue_IsCorrect()
{
// Verify the constant is correctly defined
Assert.That(ArbitrumExecutionEngine.HeaderGasUsed, Is.EqualTo("X-Arb-Gas-Used"));
}

[Test]
public void SetResponseHeader_WithZeroGasUsed_SetsZeroValue()
{
using JsonRpcContext context = new(RpcEndpoint.Http);
Block block = Build.A.Block.WithGasUsed(0L).TestObject;

JsonRpcContext.SetResponseHeader(ArbitrumExecutionEngine.HeaderGasUsed, block.Header.GasUsed.ToString());

Assert.That(context.ResponseHeaders![ArbitrumExecutionEngine.HeaderGasUsed], Is.EqualTo("0"));
}

[Test]
public void SetResponseHeader_WithLargeGasUsed_SetsCorrectValue()
{
using JsonRpcContext context = new(RpcEndpoint.Http);
const long largeGas = 30_000_000L; // 30M gas - typical block limit
Block block = Build.A.Block.WithGasUsed(largeGas).TestObject;

JsonRpcContext.SetResponseHeader(ArbitrumExecutionEngine.HeaderGasUsed, block.Header.GasUsed.ToString());

Assert.That(context.ResponseHeaders![ArbitrumExecutionEngine.HeaderGasUsed], Is.EqualTo("30000000"));
}
}
1 change: 1 addition & 0 deletions src/Nethermind.Arbitrum/Config/ArbitrumConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class ArbitrumConfig : IArbitrumConfig
public WasmRebuildMode RebuildLocalWasm { get; set; } = WasmRebuildMode.Auto;
public int MessageLagMs { get; set; } = 1000;
public bool ExposeMultiGas { get; set; } = false;
public bool ExposeMetadataHeaders { get; set; } = false;
}

public static class ArbitrumConfigExtensions
Expand Down
3 changes: 3 additions & 0 deletions src/Nethermind.Arbitrum/Config/IArbitrumConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ public interface IArbitrumConfig : IConfig

[ConfigItem(Description = "Experimental: Expose multi-dimensional gas in transaction receipts", DefaultValue = "false")]
bool ExposeMultiGas { get; set; }

[ConfigItem(Description = "Expose metadata via X-Arb-* HTTP headers", DefaultValue = "false")]
bool ExposeMetadataHeaders { get; set; }
}
52 changes: 42 additions & 10 deletions src/Nethermind.Arbitrum/Execution/ArbitrumExecutionEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public sealed class ArbitrumExecutionEngine(
IBlocksConfig blocksConfig)
: IArbitrumExecutionEngine
{
/// <summary>
/// HTTP header name for exposing block gas usage in benchmark comparison mode.
/// </summary>
public const string HeaderGasUsed = "X-Arb-Gas-Used";

private readonly ILogger _logger = logManager.GetClassLogger<ArbitrumExecutionEngine>();

public IBlockTree BlockTree { get; } = blockTree;
Expand All @@ -47,6 +52,24 @@ public sealed class ArbitrumExecutionEngine(
private readonly ConcurrentDictionary<Hash256, TaskCompletionSource<Block>> _newBestSuggestedBlockEvents = new();
private readonly ConcurrentDictionary<Hash256, TaskCompletionSource<BlockRemovedEventArgs>> _blockRemovedEvents = new();

/// <summary>
/// Sets metadata headers (X-Arb-*) if header exposure is enabled.
/// Currently, exposes: X-Arb-Gas-Used. More fields will be added for benchmarking.
/// </summary>
private void SetMetadataHeaders(Block block) => SetMetadataHeaders(block.Header);

/// <summary>
/// Sets metadata headers from a block header.
/// </summary>
private void SetMetadataHeaders(BlockHeader header)
{
if (!arbitrumConfig.ExposeMetadataHeaders)
return;

// Add all benchmark metadata headers here
JsonRpcContext.SetResponseHeader(HeaderGasUsed, header.GasUsed.ToString());
}

public Task<bool> TryAcquireSemaphoreAsync(int millisecondsTimeout = 0)
=> _createBlocksSemaphore.WaitAsync(millisecondsTimeout);

Expand Down Expand Up @@ -212,6 +235,10 @@ public Task<ResultWrapper<MessageResult>> ResultAtMessageIndexAsync(ulong messag
_logger.Trace($"Found block header for block {blockNumberResult.Data}: hash={blockHeader.Hash}");

ArbitrumBlockHeaderInfo headerInfo = ArbitrumBlockHeaderInfo.Deserialize(blockHeader, _logger);

// Set metadata headers for benchmark comparison
SetMetadataHeaders(blockHeader);

return Task.FromResult(ResultWrapper<MessageResult>.Success(new MessageResult
{
BlockHash = blockHeader.Hash ?? Hash256.Zero,
Expand Down Expand Up @@ -451,11 +478,7 @@ await Task.WhenAll(newBestBlockTcs.Task, blockRemovedTcs.Task)
if (resultArgs.ProcessingResult != ProcessingResult.Exception)
return resultArgs.ProcessingResult switch
{
ProcessingResult.Success => ResultWrapper<MessageResult>.Success(new MessageResult
{
BlockHash = block.Hash!,
SendRoot = GetSendRootFromBlock(block)
}),
ProcessingResult.Success => SuccessWithMetadata(block),
ProcessingResult.ProcessingError => ResultWrapper<MessageResult>.Fail(resultArgs.Message ?? "Block processing failed.",
ErrorCodes.InternalError),
_ => ResultWrapper<MessageResult>.Fail($"Block processing ended in an unhandled state: {resultArgs.ProcessingResult}",
Expand Down Expand Up @@ -500,11 +523,7 @@ public async Task<ResultWrapper<MessageResult>> ProduceBlockWithoutWaitingOnProc
if (block?.Hash is null)
return ResultWrapper<MessageResult>.Fail("Failed to build block or block has no hash.", ErrorCodes.InternalError);

return ResultWrapper<MessageResult>.Success(new MessageResult
{
BlockHash = block.Hash!,
SendRoot = GetSendRootFromBlock(block)
});
return SuccessWithMetadata(block);
}
catch (TimeoutException)
{
Expand All @@ -523,6 +542,19 @@ private Hash256 GetSendRootFromBlock(Block block)
return headerInfo.SendRoot;
}

/// <summary>
/// Creates a successful MessageResult for a block, setting metadata headers if enabled.
/// </summary>
private ResultWrapper<MessageResult> SuccessWithMetadata(Block block)
{
SetMetadataHeaders(block);
return ResultWrapper<MessageResult>.Success(new MessageResult
{
BlockHash = block.Hash!,
SendRoot = GetSendRootFromBlock(block)
});
}

private bool TryDeserializeChainConfig(ReadOnlySpan<byte> bytes, [NotNullWhen(true)] out ChainConfig? chainConfig)
{
try
Expand Down
Loading