Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
87c80a4
feat(metrics): add Prometheus metrics for Engine API and payload builder
panos-xyz Apr 14, 2026
6aa4607
chore: fix rustfmt and remove stale cargo-deny advisory
panos-xyz Apr 14, 2026
f6082e2
chore: restore RUSTSEC-2026-0002 and add RUSTSEC-2026-0097 to cargo-d…
panos-xyz Apr 14, 2026
199dca9
fix: remove trailing comma in morph-engine dashboard JSON
panos-xyz Apr 14, 2026
f3d531a
chore: fix CodeRabbit review comments
panos-xyz Apr 14, 2026
60354c0
fix: address high/medium/low review issues
panos-xyz Apr 14, 2026
03d8054
fix: unify morph-reth Grafana dashboard layout
panos-xyz Apr 14, 2026
13fb789
feat(metrics): add unified Grafana dashboard and local devnet test infra
panos-xyz Apr 14, 2026
e4a314b
refactor(metrics): simplify Grafana dashboard, make morph-reth.json s…
panos-xyz Apr 15, 2026
f03aad3
refactor(payload-builder): drop skip counters, replace with logs
panos-xyz Apr 15, 2026
b24f3c5
refactor(dashboard): polish Overview row, drop broken panels, fix emp…
panos-xyz Apr 15, 2026
810789c
fix(dashboard): guard DB Average Commit Time against divide-by-zero NaN
panos-xyz Apr 15, 2026
ba53802
fix(dashboard): Connected peers align with Storage row + visible bars
panos-xyz Apr 15, 2026
5d2479e
refactor(dashboard): layout overhaul, chain/role filters, idle-state …
panos-xyz Apr 15, 2026
d56ff00
chore: stop tracking local-test/ (local-only test harness, kept out o…
panos-xyz Apr 15, 2026
8341ab6
revert(local-test): restore main state
panos-xyz Apr 15, 2026
5671fbc
Clean up .gitignore by removing specific paths
panos-xyz Apr 15, 2026
235af70
Update .gitignore
panos-xyz Apr 15, 2026
11cc60f
chore: ignore RUSTSEC-2026-0098 and RUSTSEC-2026-0099 (rustls-webpki)
panos-xyz Apr 15, 2026
1a6ff69
chore: resolve merge conflict with main (RUSTSEC-2026-0097 comment)
panos-xyz Apr 16, 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
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@
/docs

CLAUDE.md

.worktrees/
.claude/
.omc/
.worktrees/
4 changes: 4 additions & 0 deletions crates/engine-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ morph-payload-types.workspace = true
morph-primitives = { workspace = true, features = ["reth-codec"] }

# reth
reth-metrics.workspace = true
reth-node-api.workspace = true
reth-payload-builder.workspace = true
reth-payload-primitives.workspace = true
Expand All @@ -30,6 +31,9 @@ alloy-eips.workspace = true
alloy-primitives.workspace = true
alloy-rpc-types-engine.workspace = true

# metrics
metrics.workspace = true

# misc
async-trait.workspace = true
auto_impl.workspace = true
Expand Down
109 changes: 104 additions & 5 deletions crates/engine-api/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
//!
//! This module provides the concrete Morph L2 Engine API implementation and supporting helpers.

use crate::{EngineApiResult, MorphEngineApiError, MorphL2EngineApi};
use crate::{
EngineApiResult, MorphEngineApiError, MorphL2EngineApi, metrics::MorphEngineApiMetrics,
};
use alloy_consensus::{
BlockHeader, EMPTY_OMMER_ROOT_HASH, Header, proofs::calculate_transaction_root,
};
Expand Down Expand Up @@ -49,6 +51,9 @@ pub struct RealMorphL2EngineApi<Provider> {
/// Engine-state tracker updated from consensus engine events (authoritative) and local FCU
/// success hints (fast path).
engine_state_tracker: Arc<EngineStateTracker>,

/// Prometheus metrics for custom Morph L2 Engine API endpoints and chain head health.
metrics: MorphEngineApiMetrics,
}

#[derive(Debug, Clone, Copy, PartialEq)]
Expand Down Expand Up @@ -151,9 +156,21 @@ impl<Provider> RealMorphL2EngineApi<Provider> {
chain_spec,
engine_handle,
engine_state_tracker,
metrics: MorphEngineApiMetrics::default(),
}
}

/// Updates `head_block_timegap_seconds` gauge after a successful block import.
fn record_head_metrics(&self, block_timestamp: u64) {
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.metrics
.head_block_timegap_seconds
.set(now_secs.saturating_sub(block_timestamp) as f64);
}

/// Returns a reference to the provider.
pub fn provider(&self) -> &Provider {
&self.provider
Expand Down Expand Up @@ -186,7 +203,15 @@ where
&self,
params: AssembleL2BlockParams,
) -> EngineApiResult<ExecutableL2Data> {
let built_payload = self.build_l2_payload(params, None, None).await?;
let started = Instant::now();
let result = self.build_l2_payload(params, None, None).await;
self.metrics
.assemble_l2_block_duration_seconds
.record(started.elapsed());

let built_payload = result.inspect_err(|_| {
self.metrics.assemble_l2_block_failures_total.increment(1);
})?;
let executable_data = built_payload.executable_data;

tracing::debug!(
Expand Down Expand Up @@ -220,6 +245,10 @@ where
actual = data.number,
"cannot validate block with discontinuous block number"
);
self.metrics.validate_l2_block_failures_total.increment(1);
self.metrics
.validate_l2_block_duration_seconds
.record(validate_started.elapsed());
return Err(MorphEngineApiError::DiscontinuousBlockNumber {
expected: current_head.number + 1,
actual: data.number,
Expand All @@ -233,6 +262,10 @@ where
actual = %data.parent_hash,
"parent hash mismatch"
);
self.metrics.validate_l2_block_failures_total.increment(1);
self.metrics
.validate_l2_block_duration_seconds
.record(validate_started.elapsed());
return Err(MorphEngineApiError::WrongParentHash {
expected: current_head.hash,
actual: data.parent_hash,
Expand All @@ -250,6 +283,10 @@ where
error = %err,
"failed to convert executable data for validation"
);
self.metrics.validate_l2_block_failures_total.increment(1);
self.metrics
.validate_l2_block_duration_seconds
.record(validate_started.elapsed());
return Ok(GenericResponse { success: false });
}
};
Expand All @@ -265,6 +302,10 @@ where
error = %err,
"engine new_payload failed during validate_l2_block"
);
self.metrics.validate_l2_block_failures_total.increment(1);
self.metrics
.validate_l2_block_duration_seconds
.record(validate_started.elapsed());
return Ok(GenericResponse { success: false });
}
};
Expand Down Expand Up @@ -294,10 +335,18 @@ where
"validate_l2_block timing"
);

self.metrics
.validate_l2_block_duration_seconds
.record(validate_started.elapsed());
if !success {
self.metrics.validate_l2_block_failures_total.increment(1);
}

Ok(GenericResponse { success })
}

async fn new_l2_block(&self, data: ExecutableL2Data) -> EngineApiResult<()> {
let started = Instant::now();
tracing::debug!(
target: "morph::engine",
block_number = data.number,
Expand All @@ -321,6 +370,9 @@ where
current_number = current_number,
"ignoring past block number"
);
self.metrics
.new_l2_block_duration_seconds
.record(started.elapsed());
return Ok(());
}
// Discontinuous block number
Expand All @@ -330,6 +382,10 @@ where
actual_number = data.number,
"cannot new block with discontinuous block number"
);
self.metrics.new_l2_block_failures_total.increment(1);
self.metrics
.new_l2_block_duration_seconds
.record(started.elapsed());
return Err(MorphEngineApiError::DiscontinuousBlockNumber {
expected: expected_number,
actual: data.number,
Expand All @@ -344,6 +400,10 @@ where
actual = %data.parent_hash,
"wrong parent hash"
);
self.metrics.new_l2_block_failures_total.increment(1);
self.metrics
.new_l2_block_duration_seconds
.record(started.elapsed());
return Err(MorphEngineApiError::WrongParentHash {
expected: current_head.hash,
actual: data.parent_hash,
Expand All @@ -352,7 +412,20 @@ where

let block_hash = data.hash;
let block_number = data.number;
self.import_l2_block_via_engine(data).await?;
let block_timestamp = data.timestamp;
self.import_l2_block_via_engine(data)
.await
.inspect_err(|_| {
self.metrics.new_l2_block_failures_total.increment(1);
self.metrics
.new_l2_block_duration_seconds
.record(started.elapsed());
})?;

self.metrics
.new_l2_block_duration_seconds
.record(started.elapsed());
self.record_head_metrics(block_timestamp);

tracing::debug!(
target: "morph::engine",
Expand All @@ -365,6 +438,7 @@ where
}

async fn new_safe_l2_block(&self, mut data: SafeL2Data) -> EngineApiResult<MorphHeader> {
let started = Instant::now();
tracing::debug!(
target: "morph::engine",
block_number = data.number,
Expand All @@ -375,12 +449,18 @@ where
let latest_number = self.current_head()?.number;

if data.number != latest_number + 1 {
self.metrics.new_safe_l2_block_failures_total.increment(1);
self.metrics
.new_safe_l2_block_duration_seconds
.record(started.elapsed());
return Err(MorphEngineApiError::DiscontinuousBlockNumber {
expected: latest_number + 1,
actual: data.number,
});
}

let block_timestamp = data.timestamp;

// 2. Assemble the block from SafeL2Data inputs.
let assemble_params = AssembleL2BlockParams {
number: data.number,
Expand All @@ -391,14 +471,33 @@ where

let built_payload = self
.build_l2_payload(assemble_params, Some(data.gas_limit), data.base_fee_per_gas)
.await?;
.await
.inspect_err(|_| {
self.metrics.new_safe_l2_block_failures_total.increment(1);
self.metrics
.new_safe_l2_block_duration_seconds
.record(started.elapsed());
})?;
let executable_data = built_payload.executable_data;
// Save hash before moving executable_data into the import call.
let block_hash = executable_data.hash;

// 3. Import the block through reth engine tree and return the in-path header
// (do not rely on immediate DB visibility after FCU).
let header = self.import_l2_block_via_engine(executable_data).await?;
let header = self
.import_l2_block_via_engine(executable_data)
.await
.inspect_err(|_| {
self.metrics.new_safe_l2_block_failures_total.increment(1);
self.metrics
.new_safe_l2_block_duration_seconds
.record(started.elapsed());
})?;

self.metrics
.new_safe_l2_block_duration_seconds
.record(started.elapsed());
self.record_head_metrics(block_timestamp);

// Update safe block tag and seed finalized for memory cleanup.
//
Expand Down
1 change: 1 addition & 0 deletions crates/engine-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
mod api;
mod builder;
mod error;
mod metrics;
mod rpc;
mod validator;

Expand Down
65 changes: 65 additions & 0 deletions crates/engine-api/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Metrics for the Morph L2 Engine API.
//!
//! Tracks per-method latency and failure counts for custom Morph Engine API
//! endpoints, plus a chain head health gauge analogous to geth's
//! `chain/head/timegap`.

use reth_metrics::{
Metrics,
metrics::{Counter, Gauge, Histogram},
};

/// Metrics for the custom Morph L2 Engine API endpoints.
///
/// Each method tracks:
/// - A latency histogram (`*_duration_seconds`)
/// - A failure counter (`*_failures_total`)
///
/// Additionally, a chain-head gauge is updated after each successful block
/// import (equivalent to geth's `chain/head/timegap`):
/// - `head_block_timegap_seconds`
#[derive(Metrics, Clone)]
#[metrics(scope = "morph.engine")]
pub(crate) struct MorphEngineApiMetrics {
// -------------------------------------------------------------------------
// assembleL2Block
// -------------------------------------------------------------------------
/// Latency for `engine_assembleL2Block` calls.
pub(crate) assemble_l2_block_duration_seconds: Histogram,
/// Number of `engine_assembleL2Block` calls that returned an error.
pub(crate) assemble_l2_block_failures_total: Counter,

// -------------------------------------------------------------------------
// newL2Block
// -------------------------------------------------------------------------
/// Latency for `engine_newL2Block` calls.
pub(crate) new_l2_block_duration_seconds: Histogram,
/// Number of `engine_newL2Block` calls that returned an error.
pub(crate) new_l2_block_failures_total: Counter,

// -------------------------------------------------------------------------
// validateL2Block
// -------------------------------------------------------------------------
/// Latency for `engine_validateL2Block` calls.
pub(crate) validate_l2_block_duration_seconds: Histogram,
/// Number of `engine_validateL2Block` calls that returned `success: false`.
pub(crate) validate_l2_block_failures_total: Counter,

// -------------------------------------------------------------------------
// newSafeL2Block
// -------------------------------------------------------------------------
/// Latency for `engine_newSafeL2Block` calls.
pub(crate) new_safe_l2_block_duration_seconds: Histogram,
/// Number of `engine_newSafeL2Block` calls that returned an error.
pub(crate) new_safe_l2_block_failures_total: Counter,

// -------------------------------------------------------------------------
// Chain head health
// -------------------------------------------------------------------------
/// Seconds elapsed since the latest imported block's timestamp.
///
/// A large value indicates the node is behind the chain tip.
/// Updated on every successful block import.
/// Analogous to geth's `chain/head/timegap` gauge.
pub(crate) head_block_timegap_seconds: Gauge,
}
4 changes: 4 additions & 0 deletions crates/payload/builder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ alloy-rlp.workspace = true
# Revm
revm.workspace = true

# Metrics
metrics.workspace = true
reth-metrics.workspace = true

# Utils
tracing.workspace = true
thiserror.workspace = true
Expand Down
Loading
Loading