Fri, Jan 9, 2026 Latest

Block/column timing

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-09' AND slot_start_date_time < '2026-01-09'::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-09' AND slot_start_date_time < '2026-01-09'::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-09' AND slot_start_date_time < '2026-01-09'::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-09' AND slot_start_date_time < '2026-01-09'::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-09' AND slot_start_date_time < '2026-01-09'::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-09' AND slot_start_date_time < '2026-01-09'::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-09' AND slot_start_date_time < '2026-01-09'::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-09' AND slot_start_date_time < '2026-01-09'::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,305 (87.6%)
  MEV: 5,961 (94.5%)
  Local: 344 (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: -115
  P90:    -66
  P95:    -46
  P99:    14
  Max:    107

Block to column delay over time

How the block-to-column delay varies 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 spread by blob count (MEV vs local)

Does MEV vs local block production affect how columns spread at each blob count?

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 spread (ms)"),
        legend_title="Block type",
        height=450,
    )
    fig.show(config={"responsive": True})

Block-to-column delay by blob count

How much additional delay per blob for column propagation to begin after block arrival?

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})