From 39f4c5f4036bda524d2933c7a28248b526ebaf52 Mon Sep 17 00:00:00 2001 From: Caleb Date: Wed, 26 Nov 2025 13:13:13 +0000 Subject: [PATCH 1/5] track ptc assignment --- .../consensus_object_pools/spec_cache.nim | 15 +++++ beacon_chain/spec/beaconstate.nim | 2 +- beacon_chain/validators/action_tracker.nim | 43 +++++++++++++ beacon_chain/validators/beacon_validators.nim | 18 +++++- tests/test_action_tracker.nim | 60 +++++++++++++++++++ 5 files changed, 136 insertions(+), 2 deletions(-) diff --git a/beacon_chain/consensus_object_pools/spec_cache.nim b/beacon_chain/consensus_object_pools/spec_cache.nim index 9640238aa7..58668c1cd4 100644 --- a/beacon_chain/consensus_object_pools/spec_cache.nim +++ b/beacon_chain/consensus_object_pools/spec_cache.nim @@ -325,3 +325,18 @@ func is_aggregator*(shufflingRef: ShufflingRef, slot: Slot, let committee_len = get_beacon_committee_len(shufflingRef, slot, index) return is_aggregator(committee_len, slot_signature) + +# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/gloas/validator.md#payload-timeliness-committee +iterator get_ptc_assignment*( + state: gloas.BeaconState, epoch: Epoch, + validator_indices: HashSet[ValidatorIndex]): + tuple[slot: Slot, validator_index: ValidatorIndex] = + let next_epoch = state.get_current_epoch + 1 + doAssert epoch <= next_epoch + + var cache = StateCache() + + for slot in epoch.slots(): + for validator_index in get_ptc(state, slot, cache): + if validator_index in validator_indices: + yield(slot, validator_index) diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 6fd0f00f8f..b91edccd11 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -2763,7 +2763,7 @@ func can_advance_slots*( withState(state): forkyState.can_advance_slots(block_root, target_slot) # https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/gloas/beacon-chain.md#new-get_ptc -iterator get_ptc(state: gloas.BeaconState, slot: Slot, cache: var StateCache): +iterator get_ptc*(state: gloas.BeaconState, slot: Slot, cache: var StateCache): ValidatorIndex = ## Get the payload timeliness committee for the given ``slot`` let epoch = slot.epoch() diff --git a/beacon_chain/validators/action_tracker.nim b/beacon_chain/validators/action_tracker.nim index 28c59006c5..e6149bb738 100644 --- a/beacon_chain/validators/action_tracker.nim +++ b/beacon_chain/validators/action_tracker.nim @@ -38,6 +38,10 @@ type subnet_id*: SubnetId slot*: Slot + PTCDuty* = object + slot*: Slot + validator_index: ValidatorIndex + ActionTracker* = object nodeId: UInt256 @@ -73,6 +77,9 @@ type lastSyncUpdate*: Opt[SyncCommitteePeriod] syncDuties*: Table[ValidatorPubKey, Epoch] + ptcSlots: array[2, uint32] + ptcDuties*: HashSet[PTCDuty] + func hash*(x: AggregatorDuty): Hash = hashAllFields(x) @@ -116,6 +123,34 @@ func hasSyncDuty*( tracker: ActionTracker, pubkey: ValidatorPubKey, epoch: Epoch): bool = epoch < tracker.syncDuties.getOrDefault(pubkey, GENESIS_EPOCH) +proc registerPTCDuty*( + tracker: var ActionTracker, slot: Slot, vidx: ValidatorIndex) = + if slot < tracker.currentSlot or + slot + (SLOTS_PER_EPOCH + 2) <= tracker.currentSlot: + debug "Irrelevant PTC duty", slot, vidx + return + + tracker.knownValidators[vidx] = slot + + let newDuty = PTCDuty(slot: slot, validator_index: vidx) + + if newDuty in tracker.ptcDuties: + return + + debug "Registering PTC duty", slot, vidx + tracker.ptcDuties.incl(newDuty) + +func hasPTCDuty*(tracker: ActionTracker, slot: Slot): bool = + let epoch = slot.epoch + if epoch > tracker.lastCalculatedEpoch + 1: + return false + ((tracker.ptcSlots[epoch mod 2] and (1'u32 shl (slot mod SLOTS_PER_EPOCH))) != 0) + +func getPTCDuties*(tracker: ActionTracker, slot: Slot): seq[ValidatorIndex] = + for duty in tracker.ptcDuties: + if duty.slot == slot: + result.add(duty.validator_index) + func aggregateSubnets*(tracker: ActionTracker, wallSlot: Slot): AttnetBits = var res: AttnetBits # Subscribe to subnets for upcoming duties @@ -146,6 +181,7 @@ proc updateSlot*(tracker: var ActionTracker, wallSlot: Slot) = # are only so many slot/subnet combos - prune both internal and API-supplied # duties at the same time tracker.duties.keepItIf(it.slot >= wallSlot) + tracker.duties.keepItIf(it.slot >= wallSlot) block: var dels: seq[ValidatorPubKey] @@ -232,6 +268,7 @@ func updateActions*( tracker.proposingSlots[epoch mod 2] or (1'u32 shl i) tracker.attestingSlots[epoch mod 2] = 0 + tracker.ptcSlots[epoch mod 2] = 0 # The relevant bitmaps are 32 bits each. static: doAssert SLOTS_PER_EPOCH <= 32 @@ -257,6 +294,12 @@ func updateActions*( tracker.attestingSlots[epoch mod 2] or (1'u32 shl (slot mod SLOTS_PER_EPOCH)) + for duty in tracker.ptcDuties: + if duty.slot.epoch == epoch: + tracker.ptcSlots[epoch mod 2] = + tracker.ptcSlots[epoch mod 2] or + (1'u32 shl (duty.slot mod SLOTS_PER_EPOCH)) + func init*( T: type ActionTracker, nodeId: UInt256, subscribeAllAttnets: bool): T = T( diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index 639498e529..711ad9f619 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -43,7 +43,7 @@ import validator_pool, ] -from std/sequtils import mapIt +from std/sequtils import mapIt, toSeq from eth/async_utils import awaitWithTimeout from ./message_router_mev import unblindAndRouteBlockMEV @@ -1327,3 +1327,19 @@ proc registerDuties*(node: BeaconNode, wallSlot: Slot) {.async: (raises: [Cancel node.consensusManager[].actionTracker.registerDuty( slot, subnet_id, validator_index, isAggregator) + + if wallSlot == wallSlot.epoch.start_slot(): + let nextEpoch = wallSlot.epoch + 1 + + if node.dag.cfg.consensusForkAtEpoch(nextEpoch) >= ConsensusFork.Gloas: + let validatorIndices = toHashSet(toSeq(node.attachedValidators[].indices())) + + withState(node.dag.headState): + when consensusFork >= ConsensusFork.Gloas: + for (slot, validator_index) in get_ptc_assignment( + forkyState.data, nextEpoch, validatorIndices): + node.consensusManager[].actionTracker.registerPTCDuty( + slot, validator_index) + + debug "PTC duty registered", + slot = slot, epoch = nextEpoch diff --git a/tests/test_action_tracker.nim b/tests/test_action_tracker.nim index 5ff4f3e0c3..9cab41dcc3 100644 --- a/tests/test_action_tracker.nim +++ b/tests/test_action_tracker.nim @@ -12,6 +12,9 @@ import unittest2, ../beacon_chain/validators/action_tracker +from ../beacon_chain/consensus_object_pools/block_pools_types import + ShufflingRef + suite "subnet tracker": test "should register stability subnets on attester duties": var tracker = ActionTracker.init(default(UInt256), false) @@ -98,3 +101,60 @@ suite "subnet tracker": check: tracker.stabilitySubnets(Slot(0)).countOnes() == 64 # All 64 subnets tracker.aggregateSubnets(Slot(0)).countOnes() == 0 + + test "should register and prune PTC duties": + var tracker = ActionTracker.init(default(UInt256), false) + tracker.updateSlot(Slot(100)) + + check: + not tracker.hasPTCDuty(Slot(100)) + + # Register past duty + tracker.registerPTCDuty(Slot(99), ValidatorIndex(0)) + check not tracker.hasPTCDuty(Slot(99)) + + # Register duty too far in future + tracker.registerPTCDuty(Slot(100 + SLOTS_PER_EPOCH * 2), ValidatorIndex(0)) + check not tracker.hasPTCDuty(Slot(100 + SLOTS_PER_EPOCH * 2)) + + tracker.registerPTCDuty(Slot(105), ValidatorIndex(100)) + tracker.registerPTCDuty(Slot(105), ValidatorIndex(101)) + tracker.registerPTCDuty(Slot(110), ValidatorIndex(102)) + + check: + tracker.hasPTCDuty(Slot(105)) + tracker.hasPTCDuty(Slot(110)) + not tracker.hasPTCDuty(Slot(107)) + tracker.knownValidators.len() == 3 + + # Update slot to prune old duties + tracker.updateSlot(Slot(107)) + check: + not tracker.hasPTCDuty(Slot(105)) + tracker.hasPTCDuty(Slot(110)) + + # Validator decays after a long time + tracker.updateSlot(Slot(110 + KNOWN_VALIDATOR_DECAY + 1)) + check tracker.knownValidators.len() == 0 + + test "should track PTC duties in slot bitmaps": + var + tracker = ActionTracker.init(default(UInt256), false) + shufflingRef = ShufflingRef( + epoch: Epoch(1), + attester_dependent_root: ZERO_HASH, + shuffled_active_validator_indices: @[] + ) + beaconProposers: array[SLOTS_PER_EPOCH, Opt[ValidatorIndex]] + + tracker.registerPTCDuty(Slot(32), ValidatorIndex(0)) # First slot of epoch + tracker.registerPTCDuty(Slot(47), ValidatorIndex(1)) # Mid epoch + tracker.registerPTCDuty(Slot(63), ValidatorIndex(2)) # Last slot of epoch + + # Update actions to populate bitmaps + tracker.updateActions(shufflingRef, beaconProposers) + + check: + (tracker.ptcSlots[1] and (1'u32 shl 0)) != 0 # Slot 32 + (tracker.ptcSlots[1] and (1'u32 shl 15)) != 0 # Slot 47 + (tracker.ptcSlots[1] and (1'u32 shl 31)) != 0 # Slot 63 From 9b72a84eb1ac932ad1a01438062e0174c674100d Mon Sep 17 00:00:00 2001 From: Caleb Date: Wed, 26 Nov 2025 14:23:42 +0000 Subject: [PATCH 2/5] update All-Tests-mainnet.md --- AllTests-mainnet.md | 2 ++ beacon_chain/validators/action_tracker.nim | 15 ++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index 21e172a70b..3892861ac8 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -1154,9 +1154,11 @@ AllTests-mainnet ``` ## subnet tracker ```diff ++ should register and prune PTC duties OK + should register stability subnets on attester duties OK + should register sync committee duties OK + should subscribe to all subnets when flag is enabled OK ++ should track PTC duties in slot bitmaps OK ``` ## test_fixture_ssz_generic_types.nim ```diff diff --git a/beacon_chain/validators/action_tracker.nim b/beacon_chain/validators/action_tracker.nim index e6149bb738..cbcb8f3792 100644 --- a/beacon_chain/validators/action_tracker.nim +++ b/beacon_chain/validators/action_tracker.nim @@ -77,7 +77,7 @@ type lastSyncUpdate*: Opt[SyncCommitteePeriod] syncDuties*: Table[ValidatorPubKey, Epoch] - ptcSlots: array[2, uint32] + ptcSlots*: array[2, uint32] ptcDuties*: HashSet[PTCDuty] func hash*(x: AggregatorDuty): Hash = @@ -126,7 +126,7 @@ func hasSyncDuty*( proc registerPTCDuty*( tracker: var ActionTracker, slot: Slot, vidx: ValidatorIndex) = if slot < tracker.currentSlot or - slot + (SLOTS_PER_EPOCH + 2) <= tracker.currentSlot: + slot >= Slot(uint64(tracker.currentSlot) + (SLOTS_PER_EPOCH * 2)): debug "Irrelevant PTC duty", slot, vidx return @@ -140,11 +140,10 @@ proc registerPTCDuty*( debug "Registering PTC duty", slot, vidx tracker.ptcDuties.incl(newDuty) +from std/sequtils import toSeq, anyIt + func hasPTCDuty*(tracker: ActionTracker, slot: Slot): bool = - let epoch = slot.epoch - if epoch > tracker.lastCalculatedEpoch + 1: - return false - ((tracker.ptcSlots[epoch mod 2] and (1'u32 shl (slot mod SLOTS_PER_EPOCH))) != 0) + tracker.ptcDuties.anyIt(it.slot == slot) func getPTCDuties*(tracker: ActionTracker, slot: Slot): seq[ValidatorIndex] = for duty in tracker.ptcDuties: @@ -181,7 +180,7 @@ proc updateSlot*(tracker: var ActionTracker, wallSlot: Slot) = # are only so many slot/subnet combos - prune both internal and API-supplied # duties at the same time tracker.duties.keepItIf(it.slot >= wallSlot) - tracker.duties.keepItIf(it.slot >= wallSlot) + tracker.ptcDuties.keepItIf(it.slot >= wallSlot) block: var dels: seq[ValidatorPubKey] @@ -247,8 +246,6 @@ func needsUpdate*( tracker.attesterDepRoot != state.dependent_root(if epoch > Epoch(0): epoch - 1 else: epoch) -from std/sequtils import toSeq - func updateActions*( tracker: var ActionTracker, shufflingRef: ShufflingRef, beaconProposers: openArray[Opt[ValidatorIndex]]) = From 8f921608395a1576e256cca19f45439d62fadd0a Mon Sep 17 00:00:00 2001 From: Caleb Date: Wed, 26 Nov 2025 17:56:44 +0000 Subject: [PATCH 3/5] reduce registerDuties stack usage --- beacon_chain/validators/beacon_validators.nim | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index 711ad9f619..1965642098 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -1282,6 +1282,24 @@ proc handleValidatorDuties*(node: BeaconNode, lastSlot, slot: Slot) {.async: (ra sendAggregatedAttestations(node, head, slot) sendSyncCommitteeContributions(node, head, slot) +proc registerPTCDuties(node: BeaconNode, epoch: Epoch) = + if node.dag.cfg.consensusForkAtEpoch(epoch) < ConsensusFork.Gloas: + return + + let validatorIndices = toHashSet(toSeq(node.attachedValidators[].indices())) + + withState(node.dag.headState): + when consensusFork >= ConsensusFork.Gloas: + for (slot, validator_index) in get_ptc_assignment( + forkyState.data, epoch, validatorIndices): + + node.consensusManager[].actionTracker.registerPTCDuty( + slot, validator_index) + + debug "PTC duty registered", + slot = slot, + epoch = epoch + proc registerDuties*(node: BeaconNode, wallSlot: Slot) {.async: (raises: [CancelledError]).} = ## Register upcoming duties of attached validators with the duty tracker @@ -1329,17 +1347,4 @@ proc registerDuties*(node: BeaconNode, wallSlot: Slot) {.async: (raises: [Cancel slot, subnet_id, validator_index, isAggregator) if wallSlot == wallSlot.epoch.start_slot(): - let nextEpoch = wallSlot.epoch + 1 - - if node.dag.cfg.consensusForkAtEpoch(nextEpoch) >= ConsensusFork.Gloas: - let validatorIndices = toHashSet(toSeq(node.attachedValidators[].indices())) - - withState(node.dag.headState): - when consensusFork >= ConsensusFork.Gloas: - for (slot, validator_index) in get_ptc_assignment( - forkyState.data, nextEpoch, validatorIndices): - node.consensusManager[].actionTracker.registerPTCDuty( - slot, validator_index) - - debug "PTC duty registered", - slot = slot, epoch = nextEpoch + node.registerPTCDuties(wallSlot.epoch + 1) From 687b7045aca64233f928a05881ac8cb795e85c7c Mon Sep 17 00:00:00 2001 From: Caleb Date: Thu, 27 Nov 2025 08:53:16 +0000 Subject: [PATCH 4/5] fix stackoverflow in PTCDuties registration by using closure iterators --- .../consensus_object_pools/spec_cache.nim | 15 -------------- beacon_chain/spec/beaconstate.nim | 2 +- beacon_chain/validators/beacon_validators.nim | 20 ++++++++++--------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/beacon_chain/consensus_object_pools/spec_cache.nim b/beacon_chain/consensus_object_pools/spec_cache.nim index 58668c1cd4..9640238aa7 100644 --- a/beacon_chain/consensus_object_pools/spec_cache.nim +++ b/beacon_chain/consensus_object_pools/spec_cache.nim @@ -325,18 +325,3 @@ func is_aggregator*(shufflingRef: ShufflingRef, slot: Slot, let committee_len = get_beacon_committee_len(shufflingRef, slot, index) return is_aggregator(committee_len, slot_signature) - -# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/gloas/validator.md#payload-timeliness-committee -iterator get_ptc_assignment*( - state: gloas.BeaconState, epoch: Epoch, - validator_indices: HashSet[ValidatorIndex]): - tuple[slot: Slot, validator_index: ValidatorIndex] = - let next_epoch = state.get_current_epoch + 1 - doAssert epoch <= next_epoch - - var cache = StateCache() - - for slot in epoch.slots(): - for validator_index in get_ptc(state, slot, cache): - if validator_index in validator_indices: - yield(slot, validator_index) diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index b91edccd11..b35451f615 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -2764,7 +2764,7 @@ func can_advance_slots*( # https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/gloas/beacon-chain.md#new-get_ptc iterator get_ptc*(state: gloas.BeaconState, slot: Slot, cache: var StateCache): - ValidatorIndex = + ValidatorIndex {.closure.} = ## Get the payload timeliness committee for the given ``slot`` let epoch = slot.epoch() var buffer {.noinit.}: array[40, byte] diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index 1965642098..885525156e 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -1290,15 +1290,17 @@ proc registerPTCDuties(node: BeaconNode, epoch: Epoch) = withState(node.dag.headState): when consensusFork >= ConsensusFork.Gloas: - for (slot, validator_index) in get_ptc_assignment( - forkyState.data, epoch, validatorIndices): - - node.consensusManager[].actionTracker.registerPTCDuty( - slot, validator_index) - - debug "PTC duty registered", - slot = slot, - epoch = epoch + var cache = new StateCache + + for slot in epoch.slots(): + for validator_index in get_ptc(forkyState.data, slot, cache[]): + if validator_index in validatorIndices: + node.consensusManager[].actionTracker.registerPTCDuty( + slot, validator_index) + + debug "PTC duty registered", + slot = slot, + epoch = epoch proc registerDuties*(node: BeaconNode, wallSlot: Slot) {.async: (raises: [CancelledError]).} = ## Register upcoming duties of attached validators with the duty tracker From c985abdfdc98ac4daf5ca55bc82f984693d6a80b Mon Sep 17 00:00:00 2001 From: Caleb Date: Sun, 30 Nov 2025 21:03:05 +0000 Subject: [PATCH 5/5] fix: address reviews --- beacon_chain/validators/action_tracker.nim | 9 ++++----- beacon_chain/validators/beacon_validators.nim | 10 +++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/beacon_chain/validators/action_tracker.nim b/beacon_chain/validators/action_tracker.nim index cbcb8f3792..1b8a875672 100644 --- a/beacon_chain/validators/action_tracker.nim +++ b/beacon_chain/validators/action_tracker.nim @@ -134,21 +134,20 @@ proc registerPTCDuty*( let newDuty = PTCDuty(slot: slot, validator_index: vidx) - if newDuty in tracker.ptcDuties: - return - debug "Registering PTC duty", slot, vidx tracker.ptcDuties.incl(newDuty) -from std/sequtils import toSeq, anyIt +from std/sequtils import anyIt, toSeq func hasPTCDuty*(tracker: ActionTracker, slot: Slot): bool = tracker.ptcDuties.anyIt(it.slot == slot) func getPTCDuties*(tracker: ActionTracker, slot: Slot): seq[ValidatorIndex] = + var duties: seq[ValidatorIndex] for duty in tracker.ptcDuties: if duty.slot == slot: - result.add(duty.validator_index) + duties.add(duty.validator_index) + duties func aggregateSubnets*(tracker: ActionTracker, wallSlot: Slot): AttnetBits = var res: AttnetBits diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index 885525156e..47814619c4 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -1286,14 +1286,18 @@ proc registerPTCDuties(node: BeaconNode, epoch: Epoch) = if node.dag.cfg.consensusForkAtEpoch(epoch) < ConsensusFork.Gloas: return - let validatorIndices = toHashSet(toSeq(node.attachedValidators[].indices())) + let validatorIndices = block: + var res: HashSet[ValidatorIndex] + for idx in node.attachedValidators[].indices(): + res.incl(idx) + res withState(node.dag.headState): when consensusFork >= ConsensusFork.Gloas: - var cache = new StateCache + var cache: StateCache for slot in epoch.slots(): - for validator_index in get_ptc(forkyState.data, slot, cache[]): + for validator_index in get_ptc(forkyState.data, slot, cache): if validator_index in validatorIndices: node.consensusManager[].actionTracker.registerPTCDuty( slot, validator_index)