Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 9 additions & 17 deletions ansible/roles/ethlambda/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,32 +50,24 @@
ethlambda_is_aggregator: "{{ 'true' if (ethlambda_node_config.results[3].stdout | default('') | trim) == 'true' else 'false' }}"
when: ethlambda_node_config is defined

# Compute the full set of subnet ids in the network so aggregators can subscribe
# to attestations from every subnet (not just the one their validators live in).
# Required in multi-subnet deployments for cross-subnet attestation aggregation.
- name: Compute aggregate subnet ids from attestation_committee_count
# Compute this host's attestation committee subnet id (for diagnostics /
# optional --aggregate-subnet-ids when the CSV lists multiple subnets).
# blockblaz/zeam#863 follow-up — see ansible/roles/zeam/tasks/main.yml.
# Per-host (NOT run_once).
- name: Compute aggregate subnet id for {{ node_name }}
shell: |
set -e
ac=$(yq eval '.config.attestation_committee_count // 1' "{{ local_validator_config_path }}")
ac=$(echo "$ac" | tr -d '\r\n' | head -1)
case "$ac" in ''|*[!0-9]*) ac=1;; esac
if [ "$ac" -lt 1 ]; then ac=1; fi
out="0"
i=1
while [ "$i" -lt "$ac" ]; do
out="$out,$i"
i=$((i + 1))
done
echo "$out"
project_root="$(cd '{{ playbook_dir }}/../..' && pwd)"
"$project_root/compute-aggregate-subnet-ids.sh" "{{ local_validator_config_path }}" "{{ node_name }}"
register: ethlambda_all_subnets_raw
changed_when: false
delegate_to: localhost
run_once: true
when: node_name is defined

- name: Set aggregate subnet ids csv
set_fact:
ethlambda_aggregate_subnet_ids: "{{ ethlambda_all_subnets_raw.stdout | trim }}"
run_once: true
when: ethlambda_all_subnets_raw is defined and ethlambda_all_subnets_raw.stdout is defined

- name: Ensure node key file exists
stat:
Expand Down
34 changes: 17 additions & 17 deletions ansible/roles/zeam/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,32 +56,32 @@
zeam_is_aggregator: "{{ 'true' if (node_config.results[3].stdout | default('') | trim) == 'true' else 'false' }}"
when: node_config is defined

# Compute the full set of subnet ids in the network so aggregators can subscribe
# to attestations from every subnet (not just the one their validators live in).
# Required in multi-subnet deployments for cross-subnet attestation aggregation.
- name: Compute aggregate subnet ids from attestation_committee_count
# Compute this host's attestation committee subnet id (for diagnostics /
# optional --aggregate-subnet-ids when the CSV lists multiple subnets).
#
# blockblaz/zeam#863 follow-up: pre-fix every aggregator subscribed to
# every subnet, which under multi-subnet load fans every gossip attestation
# N-ways into zeam's libxev thread. The helper now emits a single id (own
# subnet only); lean-quickstart selects one aggregator per subnet so
# validator-derived gossip subscriptions suffice without a neighbor subnet.
#
# Per-host (NOT run_once): each value depends on which validator that host
# is running, so this task runs in the per-host fact-set context after
# node_config has resolved {{ node_name }}.
- name: Compute aggregate subnet id for {{ node_name }}
shell: |
set -e
ac=$(yq eval '.config.attestation_committee_count // 1' "{{ local_validator_config_path }}")
ac=$(echo "$ac" | tr -d '\r\n' | head -1)
case "$ac" in ''|*[!0-9]*) ac=1;; esac
if [ "$ac" -lt 1 ]; then ac=1; fi
out="0"
i=1
while [ "$i" -lt "$ac" ]; do
out="$out,$i"
i=$((i + 1))
done
echo "$out"
project_root="$(cd '{{ playbook_dir }}/../..' && pwd)"
"$project_root/compute-aggregate-subnet-ids.sh" "{{ local_validator_config_path }}" "{{ node_name }}"
register: zeam_all_subnets_raw
changed_when: false
delegate_to: localhost
run_once: true
when: node_name is defined

- name: Set aggregate subnet ids csv
set_fact:
zeam_aggregate_subnet_ids: "{{ zeam_all_subnets_raw.stdout | trim }}"
run_once: true
when: zeam_all_subnets_raw is defined and zeam_all_subnets_raw.stdout is defined

- name: Ensure node key file exists
stat:
Expand Down
9 changes: 5 additions & 4 deletions client-cmds/ethlambda-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ if [ "$isAggregator" == "true" ]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# Zeam and other clients derive attestation gossip from local validator placement,
# so --aggregate-subnet-ids is only passed when the CSV explicitly lists multiple
# ids (comma). Background: blockblaz/zeam#863.
aggregate_subnet_ids_flag=""
if [ "$isAggregator" == "true" ] && [ -n "${aggregateSubnetIds:-}" ] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
9 changes: 5 additions & 4 deletions client-cmds/gean-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ if [ "$isAggregator" == "true" ]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# Zeam and other clients derive attestation gossip from local validator placement,
# so --aggregate-subnet-ids is only passed when the CSV explicitly lists multiple
# ids (comma). Background: blockblaz/zeam#863.
aggregate_subnet_ids_flag=""
if [ "$isAggregator" == "true" ] && [ -n "${aggregateSubnetIds:-}" ] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
9 changes: 5 additions & 4 deletions client-cmds/grandine-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ if [ "$isAggregator" == "true" ]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# Zeam and other clients derive attestation gossip from local validator placement,
# so --aggregate-subnet-ids is only passed when the CSV explicitly lists multiple
# ids (comma). Background: blockblaz/zeam#863.
aggregate_subnet_ids_flag=""
if [ "$isAggregator" == "true" ] && [ -n "${aggregateSubnetIds:-}" ] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
9 changes: 5 additions & 4 deletions client-cmds/lantern-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ if [ "$isAggregator" == "true" ]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# Zeam and other clients derive attestation gossip from local validator placement,
# so --aggregate-subnet-ids is only passed when the CSV explicitly lists multiple
# ids (comma). Background: blockblaz/zeam#863.
aggregate_subnet_ids_flag=""
if [ "$isAggregator" == "true" ] && [ -n "${aggregateSubnetIds:-}" ] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
9 changes: 5 additions & 4 deletions client-cmds/lighthouse-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ if [ "$isAggregator" == "true" ]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# Zeam and other clients derive attestation gossip from local validator placement,
# so --aggregate-subnet-ids is only passed when the CSV explicitly lists multiple
# ids (comma). Background: blockblaz/zeam#863.
aggregate_subnet_ids_flag=""
if [ "$isAggregator" == "true" ] && [ -n "${aggregateSubnetIds:-}" ] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
9 changes: 5 additions & 4 deletions client-cmds/nlean-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ if [[ "${isAggregator:-false}" == "true" ]]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# Zeam and other clients derive attestation gossip from local validator placement,
# so --aggregate-subnet-ids is only passed when the CSV explicitly lists multiple
# ids (comma). Background: blockblaz/zeam#863.
aggregate_subnet_ids_flag=""
if [[ "${isAggregator:-false}" == "true" ]] && [[ -n "${aggregateSubnetIds:-}" ]] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
15 changes: 8 additions & 7 deletions client-cmds/peam-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ if [ "$isAggregator" == "true" ]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network. Note: peam already subscribes to all
# subnets in [0, committee_count) via allowed_topics above; this flag exists
# for contract parity with other clients and is a no-op unless the binary
# recognises it.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# clients derive attestation gossip from local validator placement, so
# --aggregate-subnet-ids is only passed when the CSV lists multiple ids (comma).
# Background: blockblaz/zeam#863.
# Note: peam already subscribes to all subnets in [0, committee_count)
# via allowed_topics above; this flag exists for contract parity with
# other clients and is a no-op unless the binary recognises it.
aggregate_subnet_ids_flag=""
if [ "$isAggregator" == "true" ] && [ -n "${aggregateSubnetIds:-}" ] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
9 changes: 5 additions & 4 deletions client-cmds/qlean-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ if [ "$isAggregator" == "true" ]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# Zeam and other clients derive attestation gossip from local validator placement,
# so --aggregate-subnet-ids is only passed when the CSV explicitly lists multiple
# ids (comma). Background: blockblaz/zeam#863.
aggregate_subnet_ids_flag=""
if [ "$isAggregator" == "true" ] && [ -n "${aggregateSubnetIds:-}" ] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
9 changes: 5 additions & 4 deletions client-cmds/ream-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ if [ "$isAggregator" == "true" ]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# Zeam and other clients derive attestation gossip from local validator placement,
# so --aggregate-subnet-ids is only passed when the CSV explicitly lists multiple
# ids (comma). Background: blockblaz/zeam#863.
aggregate_subnet_ids_flag=""
if [ "$isAggregator" == "true" ] && [ -n "${aggregateSubnetIds:-}" ] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
9 changes: 5 additions & 4 deletions client-cmds/zeam-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ if [ "$isAggregator" == "true" ]; then
aggregator_flag="--is-aggregator"
fi

# In multi-subnet deployments, an aggregator must subscribe to every subnet's
# attestation topics so it can aggregate votes from all committees. The caller
# (spin-node.sh / ansible roles) exports aggregateSubnetIds as a CSV of the
# full subnet id set for the network.
# Multi-subnet: lean-quickstart picks one aggregator per subnet (spin-node.sh).
# compute-aggregate-subnet-ids.sh reports the node's own committee subnet id;
# Zeam and other clients derive attestation gossip from local validator placement,
# so --aggregate-subnet-ids is only passed when the CSV explicitly lists multiple
# ids (comma). Background: blockblaz/zeam#863.
aggregate_subnet_ids_flag=""
if [ "$isAggregator" == "true" ] && [ -n "${aggregateSubnetIds:-}" ] && [[ "$aggregateSubnetIds" == *,* ]]; then
aggregate_subnet_ids_flag="--aggregate-subnet-ids $aggregateSubnetIds"
Expand Down
102 changes: 102 additions & 0 deletions compute-aggregate-subnet-ids.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/bin/bash
#
# compute-aggregate-subnet-ids.sh
#
# Print the comma-separated subnet ids that a given aggregator should
# subscribe to.
#
# Why this exists (blockblaz/zeam#863 follow-up):
# The pre-fix shape exported a CSV of EVERY attestation subnet for
# every aggregator. With attestation_committee_count=N the libxev
# thread on each aggregator received N copies of every gossip
# attestation (one per subscribed topic), and on a 4-subnet devnet-4
# aggregator that 4× fan-in was a primary contributor to slot-driver
# starvation (~74% of received attestations referenced unimported
# heads, each spawned a BlocksByRoot, the storm fed itself).
#
# We now output only this node's OWN attestation subnet id. Zeam
# already subscribes aggregators to attestation topics implied by
# local validator ids; lean-quickstart uses one aggregator per subnet
# (spin-node.sh), so an extra neighbor subnet on every aggregator is
# redundant and doubles gossip load for N=2.
#
# The output is always a single id (no comma). Callers such as
# client-cmds/zeam-cmd.sh only pass `--aggregate-subnet-ids` when the
# CSV contains a comma, so in normal layouts the flag is omitted and
# subscription comes from validator placement alone.
#
# Usage:
# compute-aggregate-subnet-ids.sh <validator-config.yaml> <node-name>
#
# Output:
# `<own>` (single subnet id, no comma)
#
# Subnet selection priority for `<own>`:
# 1. Per-row `subnet:` field if present (matches the source of truth
# that `_node_subnet` in spin-node.sh consults — generated by
# generate-subnet-config.py / hand-edited devnet layouts).
# 2. Fallback: `validator_index % attestation_committee_count`
# where validator_index is the cumulative sum of the prior rows'
# `count` fields (matches how clients themselves choose subnets).
#
# Exits 0 with the CSV on stdout. Errors go to stderr and exit non-zero.

set -euo pipefail

if [ "$#" -ne 2 ]; then
echo "Usage: $0 <validator-config.yaml> <node-name>" >&2
exit 2
fi

cfg="$1"
node="$2"

if [ ! -f "$cfg" ]; then
echo "Error: validator config not found: $cfg" >&2
exit 1
fi

if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required (brew install yq / apt install yq)" >&2
exit 1
fi

# attestation_committee_count, sanitised to a positive integer (default 1).
ac=$(yq eval '.config.attestation_committee_count // 1' "$cfg" | tr -d '\r\n' | head -1)
case "$ac" in ''|*[!0-9]*) ac=1;; esac
if [ "$ac" -lt 1 ] 2>/dev/null; then ac=1; fi

# Own subnet: prefer the explicit `subnet:` field on the validator row.
own=$(yq eval ".validators[] | select(.name == \"$node\") | .subnet // \"\"" "$cfg" | tr -d '\r\n' | head -1)

if [ -z "$own" ]; then
# Fallback path mirrors `_node_subnet` in spin-node.sh: cumulative
# `count` sum of the rows preceding this node, modulo `ac`. yq emits
# rows as `<name> <count>`; we walk them in declaration order.
vi=0
while IFS=' ' read -r row_name row_count; do
if [ "$row_name" = "$node" ]; then
own=$(( vi % ac ))
break
fi
# Default count to 1 when the row omits the field — matches
# generate-subnet-config.py's implicit assumption.
case "$row_count" in ''|null|*[!0-9]*) row_count=1;; esac
vi=$(( vi + row_count ))
done < <(yq eval '.validators[] | .name + " " + ((.count // 1) | tostring)' "$cfg")

if [ -z "$own" ]; then
echo "Error: node '$node' not found in $cfg" >&2
exit 1
fi
fi

# Sanity-bound `own` so a stray YAML value (e.g. subnet: 99 with
# attestation_committee_count: 4) doesn't silently produce a CSV
# referring to non-existent subnets — clients reject those at startup.
if [ "$own" -ge "$ac" ] 2>/dev/null; then
echo "Error: node '$node' subnet=$own is out of range for attestation_committee_count=$ac" >&2
exit 1
fi

printf '%s\n' "$own"
Loading
Loading