Sat, Jan 10, 2026

Block/column timing - 2026-01-10

Analysis of block to column propagation timing on Ethereum mainnet.

Show code
display_sql("block_production_timeline", target_date)
View query
WITH
-- Base slots using proposer duty as the source of truth
slots AS (
    SELECT DISTINCT
        slot,
        slot_start_date_time,
        proposer_validator_index
    FROM canonical_beacon_proposer_duty
    WHERE meta_network_name = 'mainnet'
      AND slot_start_date_time >= '2026-01-10' AND slot_start_date_time < '2026-01-10'::date + INTERVAL 1 DAY
),

-- Proposer entity mapping
proposer_entity AS (
    SELECT
        index,
        entity
    FROM ethseer_validator_entity
    WHERE meta_network_name = 'mainnet'
),

-- Blob count per slot
blob_count AS (
    SELECT
        slot,
        uniq(blob_index) AS blob_count
    FROM canonical_beacon_blob_sidecar
    WHERE meta_network_name = 'mainnet'
      AND slot_start_date_time >= '2026-01-10' AND slot_start_date_time < '2026-01-10'::date + INTERVAL 1 DAY
    GROUP BY slot
),

-- Canonical block hash (to verify MEV payload was actually used)
canonical_block AS (
    SELECT
        slot,
        execution_payload_block_hash
    FROM canonical_beacon_block
    WHERE meta_network_name = 'mainnet'
      AND slot_start_date_time >= '2026-01-10' AND slot_start_date_time < '2026-01-10'::date + INTERVAL 1 DAY
),

-- MEV bid timing using timestamp_ms
mev_bids AS (
    SELECT
        slot,
        slot_start_date_time,
        min(timestamp_ms) AS first_bid_timestamp_ms,
        max(timestamp_ms) AS last_bid_timestamp_ms
    FROM mev_relay_bid_trace
    WHERE meta_network_name = 'mainnet'
      AND slot_start_date_time >= '2026-01-10' AND slot_start_date_time < '2026-01-10'::date + INTERVAL 1 DAY
    GROUP BY slot, slot_start_date_time
),

-- MEV payload delivery - join canonical block with delivered payloads
-- Note: Use is_mev flag because ClickHouse LEFT JOIN returns 0 (not NULL) for non-matching rows
-- Get value from proposer_payload_delivered (not bid_trace, which may not have the winning block)
mev_payload AS (
    SELECT
        cb.slot,
        cb.execution_payload_block_hash AS winning_block_hash,
        1 AS is_mev,
        max(pd.value) AS winning_bid_value,
        groupArray(DISTINCT pd.relay_name) AS relay_names,
        any(pd.builder_pubkey) AS winning_builder
    FROM canonical_block cb
    GLOBAL INNER JOIN mev_relay_proposer_payload_delivered pd
        ON cb.slot = pd.slot AND cb.execution_payload_block_hash = pd.block_hash
    WHERE pd.meta_network_name = 'mainnet'
      AND slot_start_date_time >= '2026-01-10' AND slot_start_date_time < '2026-01-10'::date + INTERVAL 1 DAY
    GROUP BY cb.slot, cb.execution_payload_block_hash
),

-- Winning bid timing from bid_trace (may not exist for all MEV blocks)
winning_bid AS (
    SELECT
        bt.slot,
        bt.slot_start_date_time,
        argMin(bt.timestamp_ms, bt.event_date_time) AS winning_bid_timestamp_ms
    FROM mev_relay_bid_trace bt
    GLOBAL INNER JOIN mev_payload mp ON bt.slot = mp.slot AND bt.block_hash = mp.winning_block_hash
    WHERE bt.meta_network_name = 'mainnet'
      AND slot_start_date_time >= '2026-01-10' AND slot_start_date_time < '2026-01-10'::date + INTERVAL 1 DAY
    GROUP BY bt.slot, bt.slot_start_date_time
),

-- Block gossip timing with spread
block_gossip AS (
    SELECT
        slot,
        min(event_date_time) AS block_first_seen,
        max(event_date_time) AS block_last_seen
    FROM libp2p_gossipsub_beacon_block
    WHERE meta_network_name = 'mainnet'
      AND slot_start_date_time >= '2026-01-10' AND slot_start_date_time < '2026-01-10'::date + INTERVAL 1 DAY
    GROUP BY slot
),

-- Column arrival timing: first arrival per column, then min/max of those
column_gossip AS (
    SELECT
        slot,
        min(first_seen) AS first_column_first_seen,
        max(first_seen) AS last_column_first_seen
    FROM (
        SELECT
            slot,
            column_index,
            min(event_date_time) AS first_seen
        FROM libp2p_gossipsub_data_column_sidecar
        WHERE meta_network_name = 'mainnet'
          AND slot_start_date_time >= '2026-01-10' AND slot_start_date_time < '2026-01-10'::date + INTERVAL 1 DAY
          AND event_date_time > '1970-01-01 00:00:01'
        GROUP BY slot, column_index
    )
    GROUP BY slot
)

SELECT
    s.slot AS slot,
    s.slot_start_date_time AS slot_start_date_time,
    pe.entity AS proposer_entity,

    -- Blob count
    coalesce(bc.blob_count, 0) AS blob_count,

    -- MEV bid timing (absolute and relative to slot start)
    fromUnixTimestamp64Milli(mb.first_bid_timestamp_ms) AS first_bid_at,
    mb.first_bid_timestamp_ms - toInt64(toUnixTimestamp(mb.slot_start_date_time)) * 1000 AS first_bid_ms,
    fromUnixTimestamp64Milli(mb.last_bid_timestamp_ms) AS last_bid_at,
    mb.last_bid_timestamp_ms - toInt64(toUnixTimestamp(mb.slot_start_date_time)) * 1000 AS last_bid_ms,

    -- Winning bid timing (from bid_trace, may be NULL if block hash not in bid_trace)
    if(wb.slot != 0, fromUnixTimestamp64Milli(wb.winning_bid_timestamp_ms), NULL) AS winning_bid_at,
    if(wb.slot != 0, wb.winning_bid_timestamp_ms - toInt64(toUnixTimestamp(s.slot_start_date_time)) * 1000, NULL) AS winning_bid_ms,

    -- MEV payload info (from proposer_payload_delivered, always present for MEV blocks)
    if(mp.is_mev = 1, mp.winning_bid_value, NULL) AS winning_bid_value,
    if(mp.is_mev = 1, mp.relay_names, []) AS winning_relays,
    if(mp.is_mev = 1, mp.winning_builder, NULL) AS winning_builder,

    -- Block gossip timing with spread
    bg.block_first_seen,
    dateDiff('millisecond', s.slot_start_date_time, bg.block_first_seen) AS block_first_seen_ms,
    bg.block_last_seen,
    dateDiff('millisecond', s.slot_start_date_time, bg.block_last_seen) AS block_last_seen_ms,
    dateDiff('millisecond', bg.block_first_seen, bg.block_last_seen) AS block_spread_ms,

    -- Column arrival timing (NULL when no blobs)
    if(coalesce(bc.blob_count, 0) = 0, NULL, cg.first_column_first_seen) AS first_column_first_seen,
    if(coalesce(bc.blob_count, 0) = 0, NULL, dateDiff('millisecond', s.slot_start_date_time, cg.first_column_first_seen)) AS first_column_first_seen_ms,
    if(coalesce(bc.blob_count, 0) = 0, NULL, cg.last_column_first_seen) AS last_column_first_seen,
    if(coalesce(bc.blob_count, 0) = 0, NULL, dateDiff('millisecond', s.slot_start_date_time, cg.last_column_first_seen)) AS last_column_first_seen_ms,
    if(coalesce(bc.blob_count, 0) = 0, NULL, dateDiff('millisecond', cg.first_column_first_seen, cg.last_column_first_seen)) AS column_spread_ms

FROM slots s
GLOBAL LEFT JOIN proposer_entity pe ON s.proposer_validator_index = pe.index
GLOBAL LEFT JOIN blob_count bc ON s.slot = bc.slot
GLOBAL LEFT JOIN mev_bids mb ON s.slot = mb.slot
GLOBAL LEFT JOIN mev_payload mp ON s.slot = mp.slot
GLOBAL LEFT JOIN winning_bid wb ON s.slot = wb.slot
GLOBAL LEFT JOIN block_gossip bg ON s.slot = bg.slot
GLOBAL LEFT JOIN column_gossip cg ON s.slot = cg.slot

ORDER BY s.slot DESC
Show code
df = load_parquet("block_production_timeline", target_date)

# Flag MEV vs local blocks
df["has_mev"] = df["winning_bid_value"].notna()
df["block_type"] = df["has_mev"].map({True: "MEV", False: "Local"})

# Filter to slots with blobs
df["has_blobs"] = df["blob_count"] > 0
df_blobs = df[df["has_blobs"]].copy()

# Calculate block to first column delay
df_blobs = df_blobs.dropna(subset=["block_first_seen_ms", "first_column_first_seen_ms"])
df_blobs["block_to_column_ms"] = df_blobs["first_column_first_seen_ms"] - df_blobs["block_first_seen_ms"]

print(f"Total slots: {len(df):,}")
print(f"Slots with blobs: {len(df_blobs):,} ({len(df_blobs)/len(df)*100:.1f}%)")
print(f"  MEV: {df_blobs['has_mev'].sum():,} ({df_blobs['has_mev'].mean()*100:.1f}%)")
print(f"  Local: {(~df_blobs['has_mev']).sum():,} ({(~df_blobs['has_mev']).mean()*100:.1f}%)")
Total slots: 7,200
Slots with blobs: 6,441 (89.5%)
  MEV: 6,086 (94.5%)
  Local: 355 (5.5%)

Block-to-column delayΒΆ

Time from block first seen to first column first seen. Shows how quickly columns start propagating after the block arrives.

Note on negative values: A negative delay means a column was observed before the block. This can happen due to how data propagates through the network: columns may reach certain parts of the network before the block does.

Show code
if len(df_blobs) > 0:
    fig = px.histogram(
        df_blobs,
        x="block_to_column_ms",
        color="block_type",
        category_orders={"block_type": ["MEV", "Local"]},
        nbins=60,
        barmode="overlay",
        opacity=0.7,
        color_discrete_map={"MEV": "#AB63FA", "Local": "#19D3F3"},
    )
    fig.update_layout(
        margin=dict(l=60, r=30, t=30, b=60),
        xaxis=dict(title="Block to first column (ms)"),
        yaxis=dict(title="Slots"),
        legend_title="Block type",
        height=400,
    )
    fig.show(config={"responsive": True})
else:
    print("No block-to-column timing data available.")
Show code
# Summary statistics
if len(df_blobs) > 0:
    stats = df_blobs["block_to_column_ms"].describe(percentiles=[0.5, 0.9, 0.95, 0.99])
    print("Block to first column (ms):")
    print(f"  Median: {stats['50%']:.0f}")
    print(f"  P90:    {stats['90%']:.0f}")
    print(f"  P95:    {stats['95%']:.0f}")
    print(f"  P99:    {stats['99%']:.0f}")
    print(f"  Max:    {stats['max']:.0f}")
Block to first column (ms):
  Median: -112
  P90:    -67
  P95:    -46
  P99:    17
  Max:    62

Block-to-column delay by blob countΒΆ

The distribution of the delay by blob count.

Box: 25th-75th percentile. Line: median. Whiskers: min/max excluding outliers.

Show code
df_delay = df[df["blob_count"] > 0].dropna(subset=["block_first_seen_ms", "first_column_first_seen_ms"])
df_delay["block_to_column_ms"] = df_delay["first_column_first_seen_ms"] - df_delay["block_first_seen_ms"]
if len(df_delay) > 0:
    fig = px.box(
        df_delay,
        x="blob_count",
        y="block_to_column_ms",
        color="block_type",
        category_orders={"block_type": ["MEV", "Local"]},
    )
    fig.update_layout(
        margin=dict(l=60, r=30, t=30, b=60),
        xaxis=dict(title="Blob count", dtick=1),
        yaxis=dict(title="Block to first column (ms)"),
        legend_title="Block type",
        height=450,
    )
    fig.show(config={"responsive": True})

Block-to-column delay over timeΒΆ

How the block-to-column delay varied throughout the day.

Show code
if len(df_blobs) > 0:
    df_plot = df_blobs.copy()
    df_plot["blob_count_f"] = df_plot["blob_count"].astype(float)  # Force continuous color
    max_blobs = df_plot["blob_count"].max()
    
    fig = px.scatter(
        df_plot,
        x="slot_start_date_time",
        y="block_to_column_ms",
        color="blob_count_f",
        color_continuous_scale="Plasma",
        range_color=[0, max_blobs],
        opacity=0.5,
        hover_data={"slot": True, "blob_count": True, "block_to_column_ms": ":.0f", "slot_start_date_time": False, "blob_count_f": False},
    )
    fig.update_layout(
        margin=dict(l=60, r=30, t=30, b=60),
        xaxis=dict(title="Time (UTC)", tickformat="%H:%M"),
        yaxis=dict(title="Block to first column (ms)"),
        coloraxis_colorbar=dict(title="Blobs"),
        height=400,
    )
    fig.show(config={"responsive": True})

Column arrival spread by blob count (MEV vs local)ΒΆ

The difference between the first observations of the first seen column and the last seen column, categorized by MEV vs. local block production.

Box: 25th-75th percentile. Line: median. Whiskers: min/max excluding outliers.

Show code
# Filter to slots with blobs (column_spread only exists for blob slots)
df_col_spread = df[df["blob_count"] > 0].dropna(subset=["column_spread_ms"])
if len(df_col_spread) > 0:
    fig = px.box(
        df_col_spread,
        x="blob_count",
        y="column_spread_ms",
        color="block_type",
        category_orders={"block_type": ["MEV", "Local"]},
    )
    fig.update_layout(
        margin=dict(l=60, r=30, t=30, b=60),
        xaxis=dict(title="Blob count", dtick=1),
        yaxis=dict(title="Column arrival spread (ms)"),
        legend_title="Block type",
        height=450,
    )
    fig.show(config={"responsive": True})