From 1cf4c692746fa08d516b2566e9c1254c2e6f22b9 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Thu, 26 Mar 2026 08:25:55 -0500 Subject: [PATCH 1/4] Pre-fetch effective balances in `compute_balance_weighted_selection` Move validator balance lookups out of the rejection sampling loop in `compute_balance_weighted_selection` into a one-time pre-fetch. This avoids repeated SSZ Merkle tree traversals during each acceptance check, reducing `compute_ptc` time on mainnet from ~30s to ~9s per epoch (3.3x improvement). The signature of `compute_balance_weighted_acceptance` changes from `(state, index, seed, i)` to `(effective_balance, seed, i)`. --- specs/gloas/beacon-chain.md | 12 +++++------- .../test_process_payload_attestation.py | 3 ++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 3c6d0404064..b8dbe4c01b5 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -581,6 +581,7 @@ def compute_balance_weighted_selection( """ total = uint64(len(indices)) assert total > 0 + effective_balances = [state.validators[index].effective_balance for index in indices] selected: List[ValidatorIndex] = [] i = uint64(0) while len(selected) < size: @@ -588,7 +589,7 @@ def compute_balance_weighted_selection( if shuffle_indices: next_index = compute_shuffled_index(next_index, total, seed) candidate_index = indices[next_index] - if compute_balance_weighted_acceptance(state, candidate_index, seed, i): + if compute_balance_weighted_acceptance(effective_balances[next_index], seed, i): selected.append(candidate_index) i += 1 return selected @@ -597,18 +598,15 @@ def compute_balance_weighted_selection( #### New `compute_balance_weighted_acceptance` ```python -def compute_balance_weighted_acceptance( - state: BeaconState, index: ValidatorIndex, seed: Bytes32, i: uint64 -) -> bool: +def compute_balance_weighted_acceptance(effective_balance: Gwei, seed: Bytes32, i: uint64) -> bool: """ - Return whether to accept the selection of the validator ``index``, with probability - proportional to its ``effective_balance``, and randomness given by ``seed`` and ``i``. + Return whether to accept the selection of a validator with the given ``effective_balance``, + with probability proportional to its balance, and randomness given by ``seed`` and ``i``. """ MAX_RANDOM_VALUE = 2**16 - 1 random_bytes = hash(seed + uint_to_bytes(i // 16)) offset = i % 16 * 2 random_value = bytes_to_uint64(random_bytes[offset : offset + 2]) - effective_balance = state.validators[index].effective_balance return effective_balance * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_ELECTRA * random_value ``` diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_payload_attestation.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_payload_attestation.py index eef256e5081..a6988731d23 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_payload_attestation.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_payload_attestation.py @@ -126,7 +126,8 @@ def _compute_selection_with_acceptance_iterations(spec, state, indices, seed, si i = 0 while len(selected) < size: candidate_index = indices[i % total] - if spec.compute_balance_weighted_acceptance(state, candidate_index, seed, spec.uint64(i)): + effective_balance = state.validators[candidate_index].effective_balance + if spec.compute_balance_weighted_acceptance(effective_balance, seed, spec.uint64(i)): selected.append(candidate_index) accepted_at.append(i) i += 1 From 9190a038f0bbde7ef087b83181f82ac7075277a3 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Thu, 26 Mar 2026 09:06:23 -0500 Subject: [PATCH 2/4] Add caching for expensive balance-weighted selection functions Cache three functions that call compute_balance_weighted_selection, which performs costly SSZ tree traversals for validator balances during rejection sampling: - altair: get_next_sync_committee_indices (called at sync committee boundaries) - fulu: compute_proposer_indices (called during process_proposer_lookahead) - gloas: compute_ptc (called during process_ptc_window) --- pysetup/spec_builders/altair.py | 8 +++++++- pysetup/spec_builders/fulu.py | 6 ++++++ pysetup/spec_builders/gloas.py | 6 ++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pysetup/spec_builders/altair.py b/pysetup/spec_builders/altair.py index aa668e96fb5..21ff63ef38f 100644 --- a/pysetup/spec_builders/altair.py +++ b/pysetup/spec_builders/altair.py @@ -34,7 +34,13 @@ def get_generalized_index(ssz_class: Any, *path: PyUnion[int, SSZVariableName]) def compute_merkle_proof(object: SSZObject, index: GeneralizedIndex) -> list[Bytes32]: - return build_proof(object.get_backing(), index)""" + return build_proof(object.get_backing(), index) + + +_get_next_sync_committee_indices = get_next_sync_committee_indices +get_next_sync_committee_indices = cache_this( + lambda state: (state.validators.hash_tree_root(), get_current_epoch(state)), + _get_next_sync_committee_indices, lru_size=2)""" @classmethod def hardcoded_ssz_dep_constants(cls) -> dict[str, str]: diff --git a/pysetup/spec_builders/fulu.py b/pysetup/spec_builders/fulu.py index df4158783c2..6b35e4b9157 100644 --- a/pysetup/spec_builders/fulu.py +++ b/pysetup/spec_builders/fulu.py @@ -46,6 +46,12 @@ def sundry_functions(cls) -> str: def retrieve_column_sidecars(beacon_block_root: Root) -> Sequence[DataColumnSidecar]: # pylint: disable=unused-argument return [] + + +_compute_proposer_indices = compute_proposer_indices +compute_proposer_indices = cache_this( + lambda state, epoch, seed, indices: (state.validators.hash_tree_root(), epoch, seed), + _compute_proposer_indices, lru_size=SLOTS_PER_EPOCH * 2) """ @classmethod diff --git a/pysetup/spec_builders/gloas.py b/pysetup/spec_builders/gloas.py index 0cc1f599464..68031d4c4ae 100644 --- a/pysetup/spec_builders/gloas.py +++ b/pysetup/spec_builders/gloas.py @@ -36,4 +36,10 @@ def retrieve_column_sidecars_and_kzg_commitments( ) -> tuple[Sequence[DataColumnSidecar], Sequence[KZGCommitment]]: # pylint: disable=unused-argument return [], [] + + +_compute_ptc = compute_ptc +compute_ptc = cache_this( + lambda state, slot: (state.validators.hash_tree_root(), state.randao_mixes.hash_tree_root(), slot), + _compute_ptc, lru_size=SLOTS_PER_EPOCH * 4) """ From 56f6134dfed85c0ba1f3e340b90e612c7c32601a Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Thu, 26 Mar 2026 09:53:28 -0500 Subject: [PATCH 3/4] Revert non-Gloas specific changes --- pysetup/spec_builders/altair.py | 8 +------- pysetup/spec_builders/fulu.py | 6 ------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/pysetup/spec_builders/altair.py b/pysetup/spec_builders/altair.py index 21ff63ef38f..aa668e96fb5 100644 --- a/pysetup/spec_builders/altair.py +++ b/pysetup/spec_builders/altair.py @@ -34,13 +34,7 @@ def get_generalized_index(ssz_class: Any, *path: PyUnion[int, SSZVariableName]) def compute_merkle_proof(object: SSZObject, index: GeneralizedIndex) -> list[Bytes32]: - return build_proof(object.get_backing(), index) - - -_get_next_sync_committee_indices = get_next_sync_committee_indices -get_next_sync_committee_indices = cache_this( - lambda state: (state.validators.hash_tree_root(), get_current_epoch(state)), - _get_next_sync_committee_indices, lru_size=2)""" + return build_proof(object.get_backing(), index)""" @classmethod def hardcoded_ssz_dep_constants(cls) -> dict[str, str]: diff --git a/pysetup/spec_builders/fulu.py b/pysetup/spec_builders/fulu.py index 6b35e4b9157..df4158783c2 100644 --- a/pysetup/spec_builders/fulu.py +++ b/pysetup/spec_builders/fulu.py @@ -46,12 +46,6 @@ def sundry_functions(cls) -> str: def retrieve_column_sidecars(beacon_block_root: Root) -> Sequence[DataColumnSidecar]: # pylint: disable=unused-argument return [] - - -_compute_proposer_indices = compute_proposer_indices -compute_proposer_indices = cache_this( - lambda state, epoch, seed, indices: (state.validators.hash_tree_root(), epoch, seed), - _compute_proposer_indices, lru_size=SLOTS_PER_EPOCH * 2) """ @classmethod From c6ef15d03788727a4bd951b5520acd64ed1fe4a7 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Thu, 26 Mar 2026 13:29:20 -0500 Subject: [PATCH 4/4] Remove compute_ptc cache --- pysetup/spec_builders/gloas.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pysetup/spec_builders/gloas.py b/pysetup/spec_builders/gloas.py index 68031d4c4ae..0cc1f599464 100644 --- a/pysetup/spec_builders/gloas.py +++ b/pysetup/spec_builders/gloas.py @@ -36,10 +36,4 @@ def retrieve_column_sidecars_and_kzg_commitments( ) -> tuple[Sequence[DataColumnSidecar], Sequence[KZGCommitment]]: # pylint: disable=unused-argument return [], [] - - -_compute_ptc = compute_ptc -compute_ptc = cache_this( - lambda state, slot: (state.validators.hash_tree_root(), state.randao_mixes.hash_tree_root(), slot), - _compute_ptc, lru_size=SLOTS_PER_EPOCH * 4) """