Consensus

Head slot tracking, justification, finalization, and fork choice analysis for PQ Devnet clients.

This notebook examines:

  • Head slot vs current slot (how far behind each client is)
  • Justified and finalized slot progression
  • Head-to-justified, justified-to-finalized, and head-to-finalized distances
  • Fork choice reorgs
Show code
import json
from pathlib import Path

import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from IPython.display import display

# Set default renderer for static HTML output
import plotly.io as pio
pio.renderers.default = "notebook"
Show code
# Resolve devnet_id
DATA_DIR = Path("../data")

if devnet_id is None:
    devnets_path = DATA_DIR / "devnets.json"
    if devnets_path.exists():
        with open(devnets_path) as f:
            devnets = json.load(f).get("devnets", [])
        if devnets:
            devnet_id = devnets[-1]["id"]
            print(f"Using latest devnet: {devnet_id}")
    else:
        raise ValueError("No devnets.json found. Run 'just detect-devnets' first.")

DEVNET_DIR = DATA_DIR / devnet_id
print(f"Loading data from: {DEVNET_DIR}")
Loading data from: ../data/pqdevnet-20260629T1346Z
Show code
# Load devnet metadata
with open(DATA_DIR / "devnets.json") as f:
    devnets_data = json.load(f)
    devnet_info = next((d for d in devnets_data["devnets"] if d["id"] == devnet_id), None)

if devnet_info:
    print(f"Devnet: {devnet_info['id']}")
    print(f"Duration: {devnet_info['duration_hours']:.1f} hours")
    print(f"Time: {devnet_info['start_time']} to {devnet_info['end_time']}")
    print(f"Slots: {devnet_info['start_slot']} \u2192 {devnet_info['end_slot']}")
    print(f"Clients: {', '.join(devnet_info['clients'])}")
Devnet: pqdevnet-20260629T1346Z
Duration: 0.9 hours
Time: 2026-06-29T13:46:23+00:00 to 2026-06-29T14:38:47+00:00
Slots: 0 β†’ 716
Clients: buildx_buildkit_multiarch0, zeam_0, zeam_1, zeam_10, zeam_11, zeam_12, zeam_13, zeam_14, zeam_15, zeam_16, zeam_17, zeam_18, zeam_19, zeam_2, zeam_20, zeam_21, zeam_22, zeam_23, zeam_24, zeam_25, zeam_26, zeam_27, zeam_28, zeam_29, zeam_3, zeam_30, zeam_31, zeam_4, zeam_5, zeam_6, zeam_7, zeam_8, zeam_9

Load DataΒΆ

Show code
# Unified client list from devnet metadata (includes all containers via cAdvisor)
all_clients = sorted(devnet_info["clients"])
n_cols = min(len(all_clients), 2)
n_rows = -(-len(all_clients) // n_cols)

# Load head slot data
head_df = pd.read_parquet(DEVNET_DIR / "head_slot.parquet")
head_df = head_df.groupby(["client", "metric", "timestamp"], as_index=False)["value"].max()
print(f"Head slot: {len(head_df)} records, clients: {sorted(head_df['client'].unique())}")

# Load finality data
finality_df = pd.read_parquet(DEVNET_DIR / "finality_metrics.parquet")
finality_df = finality_df.groupby(["client", "metric", "timestamp"], as_index=False)["value"].max()
print(f"Finality: {len(finality_df)} records, clients: {sorted(finality_df['client'].unique())}")
print(f"Finality metrics: {sorted(finality_df['metric'].unique())}")

# Load fork choice reorgs
reorgs_df = pd.read_parquet(DEVNET_DIR / "fork_choice_reorgs.parquet")
reorgs_df = reorgs_df.groupby(["client", "timestamp"], as_index=False)["value"].max()
print(f"Reorgs: {len(reorgs_df)} records, clients: {sorted(reorgs_df['client'].unique())}")

print(f"\nAll clients ({len(all_clients)}): {all_clients}")
Head slot: 3298 records, clients: ['zeam_0', 'zeam_1', 'zeam_10', 'zeam_11', 'zeam_12', 'zeam_13', 'zeam_14', 'zeam_15', 'zeam_16', 'zeam_17', 'zeam_18', 'zeam_19', 'zeam_2', 'zeam_20', 'zeam_21', 'zeam_22', 'zeam_23', 'zeam_24', 'zeam_25', 'zeam_26', 'zeam_27', 'zeam_28', 'zeam_29', 'zeam_3', 'zeam_30', 'zeam_31', 'zeam_4', 'zeam_5', 'zeam_6', 'zeam_7', 'zeam_8', 'zeam_9']
Finality: 3298 records, clients: ['zeam_0', 'zeam_1', 'zeam_10', 'zeam_11', 'zeam_12', 'zeam_13', 'zeam_14', 'zeam_15', 'zeam_16', 'zeam_17', 'zeam_18', 'zeam_19', 'zeam_2', 'zeam_20', 'zeam_21', 'zeam_22', 'zeam_23', 'zeam_24', 'zeam_25', 'zeam_26', 'zeam_27', 'zeam_28', 'zeam_29', 'zeam_3', 'zeam_30', 'zeam_31', 'zeam_4', 'zeam_5', 'zeam_6', 'zeam_7', 'zeam_8', 'zeam_9']
Finality metrics: ['lean_latest_finalized_slot', 'lean_latest_justified_slot']
Reorgs: 1649 records, clients: ['zeam_0', 'zeam_1', 'zeam_10', 'zeam_11', 'zeam_12', 'zeam_13', 'zeam_14', 'zeam_15', 'zeam_16', 'zeam_17', 'zeam_18', 'zeam_19', 'zeam_2', 'zeam_20', 'zeam_21', 'zeam_22', 'zeam_23', 'zeam_24', 'zeam_25', 'zeam_26', 'zeam_27', 'zeam_28', 'zeam_29', 'zeam_3', 'zeam_30', 'zeam_31', 'zeam_4', 'zeam_5', 'zeam_6', 'zeam_7', 'zeam_8', 'zeam_9']

All clients (33): ['buildx_buildkit_multiarch0', 'zeam_0', 'zeam_1', 'zeam_10', 'zeam_11', 'zeam_12', 'zeam_13', 'zeam_14', 'zeam_15', 'zeam_16', 'zeam_17', 'zeam_18', 'zeam_19', 'zeam_2', 'zeam_20', 'zeam_21', 'zeam_22', 'zeam_23', 'zeam_24', 'zeam_25', 'zeam_26', 'zeam_27', 'zeam_28', 'zeam_29', 'zeam_3', 'zeam_30', 'zeam_31', 'zeam_4', 'zeam_5', 'zeam_6', 'zeam_7', 'zeam_8', 'zeam_9']

Head Slot vs Current SlotΒΆ

Comparing each client's head slot (lean_head_slot) against the expected current slot (lean_current_slot). A gap indicates the client is falling behind.

Show code
fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

colors = {"lean_head_slot": "#636EFA", "lean_current_slot": "#EF553B"}
labels = {"lean_head_slot": "head_slot", "lean_current_slot": "current_slot"}
legend_added = set()

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = head_df[head_df["client"] == client]
    has_data = False
    for metric in ["lean_current_slot", "lean_head_slot"]:
        mdf = cdf[cdf["metric"] == metric].sort_values("timestamp")
        if mdf.empty:
            continue
        has_data = True
        label = labels[metric]
        show_legend = metric not in legend_added
        legend_added.add(metric)
        fig.add_trace(
            go.Scatter(
                x=mdf["timestamp"], y=mdf["value"],
                name=label, legendgroup=metric,
                showlegend=show_legend,
                line=dict(color=colors[metric]),
            ),
            row=row, col=col,
        )
    if not has_data:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slot", row=row, col=col)

fig.update_layout(
    title="Head Slot vs Current Slot by Client",
    height=270 * n_rows,
)
fig.show()

Head, Justified & Finalized SlotsΒΆ

Progression of justified and finalized slots over time. With 3SF, both should track closely behind the head slot.

Show code
jf_metrics = ["lean_latest_justified_slot", "lean_latest_finalized_slot"]
jf_df = finality_df[finality_df["metric"].isin(jf_metrics)].copy()

head_only_all = head_df[head_df["metric"] == "lean_head_slot"].copy()
head_only_all["metric"] = "lean_head_slot"

combined = pd.concat([jf_df, head_only_all], ignore_index=True)

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

colors = {
    "lean_head_slot": "#636EFA",
    "lean_latest_justified_slot": "#00CC96",
    "lean_latest_finalized_slot": "#EF553B",
}
labels = {
    "lean_head_slot": "head",
    "lean_latest_justified_slot": "justified",
    "lean_latest_finalized_slot": "finalized",
}
legend_added = set()

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = combined[combined["client"] == client]
    has_data = False
    for metric in ["lean_head_slot", "lean_latest_justified_slot", "lean_latest_finalized_slot"]:
        mdf = cdf[cdf["metric"] == metric].sort_values("timestamp")
        if mdf.empty:
            continue
        has_data = True
        show_legend = metric not in legend_added
        legend_added.add(metric)
        fig.add_trace(
            go.Scatter(
                x=mdf["timestamp"], y=mdf["value"],
                name=labels[metric], legendgroup=metric,
                showlegend=show_legend,
                line=dict(color=colors[metric]),
            ),
            row=row, col=col,
        )
    if not has_data:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slot", row=row, col=col)

fig.update_layout(
    title="Head, Justified & Finalized Slot by Client",
    height=270 * n_rows,
)
fig.show()

Head-to-Finalized DistanceΒΆ

Total distance between head slot and finalized slot. This is the combined gap across justification and finalization, showing the overall finality lag.

finalized
justified
head
current
Show code
head_ts_hf = head_df[head_df["metric"] == "lean_head_slot"][["client", "timestamp", "value"]].rename(columns={"value": "head_slot"})
fin_ts_hf = finality_df[finality_df["metric"] == "lean_latest_finalized_slot"][["client", "timestamp", "value"]].rename(columns={"value": "finalized_slot"})
head_fin_lag = head_ts_hf.merge(fin_ts_hf, on=["client", "timestamp"], how="inner")
head_fin_lag["lag"] = head_fin_lag["head_slot"] - head_fin_lag["finalized_slot"]

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = head_fin_lag[head_fin_lag["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["lag"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slots", row=row, col=col)

fig.update_layout(
    title="Head-to-Finalized Distance (head_slot - finalized_slot)",
    height=270 * n_rows,
)
fig.show()

Current-to-Head DistanceΒΆ

Difference between current slot and head slot. A value of 0 means the client is fully synced; higher values indicate falling behind.

finalized
justified
head
current
Show code
current_df = head_df[head_df["metric"] == "lean_current_slot"][["client", "timestamp", "value"]].rename(columns={"value": "current_slot"})
head_only = head_df[head_df["metric"] == "lean_head_slot"][["client", "timestamp", "value"]].rename(columns={"value": "head_slot"})
lag_df = current_df.merge(head_only, on=["client", "timestamp"], how="inner")
lag_df["lag"] = lag_df["current_slot"] - lag_df["head_slot"]

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = lag_df[lag_df["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["lag"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slots behind", row=row, col=col)

fig.update_layout(
    title="Current-to-Head Distance (current_slot - head_slot)",
    height=270 * n_rows,
)
fig.show()

Head-to-Justified DistanceΒΆ

Gap between head slot and justified slot. A growing gap means the client's head is advancing but justification is not keeping up.

finalized
justified
head
current
Show code
head_ts = head_df[head_df["metric"] == "lean_head_slot"][["client", "timestamp", "value"]].rename(columns={"value": "head_slot"})
just_ts = finality_df[finality_df["metric"] == "lean_latest_justified_slot"][["client", "timestamp", "value"]].rename(columns={"value": "justified_slot"})
justification_lag = head_ts.merge(just_ts, on=["client", "timestamp"], how="inner")
justification_lag["lag"] = justification_lag["head_slot"] - justification_lag["justified_slot"]

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = justification_lag[justification_lag["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["lag"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slots", row=row, col=col)

fig.update_layout(
    title="Head-to-Justified Distance (head_slot - justified_slot)",
    height=270 * n_rows,
)
fig.show()

Justified-to-Finalized DistanceΒΆ

Gap between justified slot and finalized slot. A growing gap means justification is advancing but finalization is stalling.

finalized
justified
head
current
Show code
fin_ts = finality_df[finality_df["metric"] == "lean_latest_finalized_slot"][["client", "timestamp", "value"]].rename(columns={"value": "finalized_slot"})
finality_lag = just_ts.merge(fin_ts, on=["client", "timestamp"], how="inner")
finality_lag["lag"] = finality_lag["justified_slot"] - finality_lag["finalized_slot"]

fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = finality_lag[finality_lag["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["lag"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Slots", row=row, col=col)

fig.update_layout(
    title="Justified-to-Finalized Distance (justified_slot - finalized_slot)",
    height=270 * n_rows,
)
fig.show()

Fork Choice ReorgsΒΆ

Cumulative chain reorgs per client. Reorgs occur when the fork choice rule switches to a different chain head, often caused by late-arriving blocks or attestations.

Show code
fig = make_subplots(
    rows=n_rows, cols=n_cols,
    subplot_titles=all_clients,
    vertical_spacing=0.12 / max(n_rows - 1, 1) * 2,
    horizontal_spacing=0.08,
)

for i, client in enumerate(all_clients):
    row = i // n_cols + 1
    col = i % n_cols + 1
    cdf = reorgs_df[reorgs_df["client"] == client].sort_values("timestamp")
    if not cdf.empty:
        fig.add_trace(
            go.Scatter(
                x=cdf["timestamp"], y=cdf["value"],
                name=client, showlegend=False,
                line=dict(color="#636EFA"),
            ),
            row=row, col=col,
        )
    else:
        fig.add_trace(
            go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo='skip'),
            row=row, col=col,
        )
        _n = (row - 1) * n_cols + col
        _s = "" if _n == 1 else str(_n)
        fig.add_annotation(
            text="No data available",
            xref=f"x{_s} domain", yref=f"y{_s} domain",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=12, color="#999"),
        )
    fig.update_yaxes(title_text="Cumulative reorgs", row=row, col=col)

fig.update_layout(
    title="Fork Choice Reorgs by Client",
    height=270 * n_rows,
)
fig.show()

SummaryΒΆ

Show code
summary_rows = []

for client in all_clients:
    row = {"Client": client}

    # Current-to-head distance
    client_lag = lag_df[lag_df["client"] == client]["lag"]
    if not client_lag.empty:
        row["Avg C-H Dist."] = f"{client_lag.mean():.1f}"
        row["Max C-H Dist."] = f"{client_lag.max():.0f}"

    # Head-to-justified distance
    client_just = justification_lag[justification_lag["client"] == client]["lag"]
    if not client_just.empty:
        row["Avg H-J Dist."] = f"{client_just.mean():.1f}"
        row["Max H-J Dist."] = f"{client_just.max():.0f}"

    # Justified-to-finalized distance
    client_fin = finality_lag[finality_lag["client"] == client]["lag"]
    if not client_fin.empty:
        row["Avg J-F Dist."] = f"{client_fin.mean():.1f}"
        row["Max J-F Dist."] = f"{client_fin.max():.0f}"

    # Head-to-finalized distance
    client_hf = head_fin_lag[head_fin_lag["client"] == client]["lag"]
    if not client_hf.empty:
        row["Avg H-F Dist."] = f"{client_hf.mean():.1f}"
        row["Max H-F Dist."] = f"{client_hf.max():.0f}"

    # Reorgs
    client_reorgs = reorgs_df[reorgs_df["client"] == client]["value"]
    if not client_reorgs.empty:
        row["Reorgs"] = f"{client_reorgs.max():.0f}"

    # Final head slot
    client_head = head_df[(head_df["client"] == client) & (head_df["metric"] == "lean_head_slot")]
    if not client_head.empty:
        row["Final Head Slot"] = f"{client_head['value'].max():.0f}"

    # Final finalized slot
    client_finalized = finality_df[(finality_df["client"] == client) & (finality_df["metric"] == "lean_latest_finalized_slot")]
    if not client_finalized.empty:
        row["Final Finalized Slot"] = f"{client_finalized['value'].max():.0f}"

    summary_rows.append(row)

if summary_rows:
    summary_df = pd.DataFrame(summary_rows).set_index("Client").fillna("-")
    display(summary_df)

print(f"\nDevnet: {devnet_id}")
if devnet_info:
    print(f"Duration: {devnet_info['duration_hours']:.1f} hours")
Avg C-H Dist. Max C-H Dist. Avg H-J Dist. Max H-J Dist. Avg J-F Dist. Max J-F Dist. Avg H-F Dist. Max H-F Dist. Reorgs Final Head Slot Final Finalized Slot
Client
buildx_buildkit_multiarch0 - - - - - - - - - - -
zeam_0 5.5 42 78.3 318 74.4 182 152.7 439 54 736 297
zeam_1 4.1 24 81.0 321 71.5 121 152.5 442 71 739 297
zeam_10 2.1 6 81.4 319 73.7 182 155.1 440 46 737 297
zeam_11 2.6 7 81.2 321 74.4 182 155.6 442 61 739 297
zeam_12 2.6 8 82.4 321 74.9 196 157.4 442 52 739 297
zeam_13 8.7 69 83.5 299 67.5 196 151.0 420 65 717 297
zeam_14 2.5 7 81.2 319 74.4 182 155.6 440 45 737 297
zeam_15 2.1 6 105.9 321 87.0 182 192.9 442 59 739 297
zeam_16 6.0 67 79.0 302 74.8 196 153.8 423 58 720 297
zeam_17 2.9 8 82.2 321 74.7 196 156.8 442 65 739 297
zeam_18 2.5 8 82.4 321 74.4 196 156.8 442 41 739 297
zeam_19 3.6 57 80.8 321 74.7 182 155.5 442 56 739 297
zeam_2 2.5 7 81.3 321 74.4 182 155.7 442 43 739 297
zeam_20 5.6 73 83.4 321 70.2 182 153.5 442 38 739 297
zeam_21 3.1 53 80.3 319 73.7 182 154.0 440 64 737 297
zeam_22 9.3 335 75.7 308 74.1 196 149.7 429 44 726 297
zeam_23 2.6 8 82.2 321 74.9 196 157.1 442 60 739 297
zeam_24 7.7 100 76.4 312 74.9 196 151.3 433 56 730 297
zeam_25 4.0 31 83.3 321 72.3 121 155.6 442 34 739 297
zeam_26 10.2 142 74.0 248 74.8 196 148.9 369 37 666 297
zeam_27 8.4 30 90.5 353 59.3 182 149.8 434 36 731 297
zeam_28 5.9 79 77.8 314 74.5 182 152.3 435 52 732 297
zeam_29 5.2 40 79.8 315 74.9 196 154.8 436 82 733 297
zeam_3 2.5 7 81.2 321 74.4 182 155.7 442 57 739 297
zeam_30 3.1 21 81.4 316 74.7 182 156.1 437 44 734 297
zeam_31 7.0 41 79.8 285 71.2 196 150.9 406 58 703 297
zeam_4 2.1 6 52.7 322 57.7 182 110.4 443 65 740 297
zeam_5 6.0 52 78.5 291 73.7 182 152.2 412 66 709 297
zeam_6 6.3 103 78.1 292 74.7 182 152.8 413 45 710 297
zeam_7 2.5 8 82.5 321 74.8 196 157.3 442 63 739 297
zeam_8 2.2 9 81.3 319 73.7 182 155.0 440 65 737 297
zeam_9 5.4 188 80.1 321 72.7 182 152.8 442 60 739 297
Devnet: pqdevnet-20260629T1346Z
Duration: 0.9 hours