Network overview - 2026-01-09
Network overview using Xatu sentry node observations on Ethereum mainnet.
Show code
display_sql("xatu_client_connectivity", target_date)
View query
Show code
df = load_parquet("xatu_client_connectivity", target_date)
Total unique peers¶
Number of unique peers observed by Xatu sentry nodes throughout the day.
Show code
df_unique = df.groupby("hour_bucket")["peer_id"].nunique().reset_index()
df_unique.columns = ["hour_bucket", "unique_peers"]
fig = px.line(
df_unique,
x="hour_bucket",
y="unique_peers",
)
fig.update_layout(
xaxis_title=None,
yaxis_title="Unique peers",
height=400,
)
fig.show()
Client distribution¶
Distribution of unique peers by client implementation over time. Each peer is assigned to a single client based on their most recently observed agent string.
Show code
# Filter out empty client names and deduplicate per hour/peer
df_clients = df[df["client_name"].notna() & (df["client_name"] != "")].copy()
df_clients = df_clients.sort_values(["hour_bucket", "peer_id", "client_name"], ascending=[True, True, False])
df_clients = df_clients.drop_duplicates(subset=["hour_bucket", "peer_id"], keep="first")
df_client_counts = df_clients.groupby(["hour_bucket", "client_name"]).size().reset_index(name="peers")
fig = px.area(
df_client_counts,
x="hour_bucket",
y="peers",
color="client_name",
)
fig.update_layout(
xaxis_title=None,
yaxis_title="Peers",
legend_title="Client",
height=500,
)
fig.show()
Connections per Xatu node¶
Number of unique peers connected to each Xatu sentry node over time.
Show code
df_xatu = df.groupby(["hour_bucket", "local_name"])["peer_id"].nunique().reset_index()
df_xatu.columns = ["hour_bucket", "local_name", "peers"]
df_xatu["local_name"] = df_xatu["local_name"].str.replace("ethpandaops/mainnet/", "", regex=False)
fig = px.line(
df_xatu,
x="hour_bucket",
y="peers",
color="local_name",
)
fig.update_layout(
xaxis_title=None,
yaxis_title="Connected peers",
legend=dict(
title="Xatu node",
orientation="h",
yanchor="top",
y=-0.2,
xanchor="center",
x=0.5,
),
height=500,
)
fig.show()
Transport protocol distribution¶
Distribution of connections by IP protocol (IPv4/IPv6) and transport protocol (TCP/QUIC) combinations.
Show code
# Group transports per peer/hour/protocol
df_transport = (
df.groupby(["hour_bucket", "peer_id", "protocol"])["transport_protocol"]
.apply(lambda x: " & ".join(sorted(x.dropna().unique())))
.reset_index()
)
df_transport["protocol_combos"] = df_transport["protocol"] + " + (" + df_transport["transport_protocol"] + ")"
df_proto_counts = df_transport.groupby(["hour_bucket", "protocol_combos"]).size().reset_index(name="peers")
fig = px.line(
df_proto_counts,
x="hour_bucket",
y="peers",
color="protocol_combos",
)
fig.update_layout(
xaxis_title=None,
yaxis_title="Connected peers",
legend_title="Protocol",
height=500,
)
fig.show()
Port popularity¶
Most commonly used ports by connected peers. Standard Ethereum P2P port is 30303.
Show code
# Count unique peers per port
df_ports = df.drop_duplicates(subset=["peer_id", "port"]).groupby("port").size().reset_index(name="peers")
df_ports = df_ports.sort_values("peers", ascending=False).head(20)
df_ports["port"] = df_ports["port"].astype(str)
fig = px.bar(
df_ports,
x="port",
y="peers",
)
fig.update_xaxes(type="category")
fig.update_layout(
xaxis_title="Port",
yaxis_title="Peers",
height=400,
)
fig.show()