diff --git a/src/lean_spec/subspecs/validator/service.py b/src/lean_spec/subspecs/validator/service.py index 591e97cf..d074068b 100644 --- a/src/lean_spec/subspecs/validator/service.py +++ b/src/lean_spec/subspecs/validator/service.py @@ -110,12 +110,6 @@ class ValidatorService: _attested_slots: set[Slot] = field(default_factory=set, repr=False) """Slots for which we've already produced attestations (prevents duplicates).""" - _blocks_skipped_lag: int = field(default=0, repr=False) - """Block proposals skipped because the local view was too stale.""" - - _attestations_skipped_lag: int = field(default=0, repr=False) - """Attestations skipped because the local view was too stale.""" - _duty_gate_closed: bool = field(default=False, repr=False) """Hysteresis flag. True while signing is silenced.""" @@ -178,8 +172,6 @@ async def run(self) -> None: logger.debug("ValidatorService: checking block production for slot %d", slot) if self._is_synced_for_duties(slot, "block"): await self._maybe_produce_block(slot) - else: - self._blocks_skipped_lag += 1 logger.debug("ValidatorService: done block production check for slot %d", slot) # Re-fetch interval after block production. @@ -235,8 +227,6 @@ async def run(self) -> None: # Older slots are no longer attestable. prune_threshold = Slot(max(0, int(slot) - 4)) self._attested_slots = {s for s in self._attested_slots if s >= prune_threshold} - else: - self._attestations_skipped_lag += 1 # Intervals 2-4 have no additional validator duties. diff --git a/src/lean_spec/subspecs/xmss/containers.py b/src/lean_spec/subspecs/xmss/containers.py index 02ec1f85..2057bc6c 100644 --- a/src/lean_spec/subspecs/xmss/containers.py +++ b/src/lean_spec/subspecs/xmss/containers.py @@ -159,7 +159,7 @@ def _decode_secret_key(cls, value: object) -> object: @field_serializer("public_key", "secret_key", when_used="json") def _encode_hex(self, value: PublicKey | SecretKey) -> str: """Emit each half as plain hex in JSON mode only.""" - return value.to_hex() + return value.encode_bytes().hex() class ValidatorKeyPair(StrictBaseModel): diff --git a/src/lean_spec/types/container.py b/src/lean_spec/types/container.py index 3f0718ea..a34dd4b4 100644 --- a/src/lean_spec/types/container.py +++ b/src/lean_spec/types/container.py @@ -240,7 +240,3 @@ def deserialize(cls, stream: IO[bytes], scope: int) -> Self: def from_hex(cls, value: str) -> Self: """Decode from a hex string with an optional "0x" prefix.""" return cls.decode_bytes(bytes.fromhex(value.removeprefix("0x"))) - - def to_hex(self) -> str: - """Encode as a plain hex string (no "0x" prefix).""" - return self.encode_bytes().hex() diff --git a/tests/lean_spec/subspecs/validator/test_service.py b/tests/lean_spec/subspecs/validator/test_service.py index 458ef18d..f1bed1e7 100644 --- a/tests/lean_spec/subspecs/validator/test_service.py +++ b/tests/lean_spec/subspecs/validator/test_service.py @@ -1446,25 +1446,10 @@ def test_hysteresis_prevents_flap(self, sync_service: SyncService) -> None: _add_block_at_slot(sync_service, Slot(20)) assert service._is_synced_for_duties(Slot(15), "block") - def test_counters_split_block_and_attestation(self, sync_service: SyncService) -> None: - """Counters live on the loop, not on the gate.""" - - # Head 0, wall clock 20, fresh block at 20: gate closes. - _replace_head_at_slot(sync_service, Slot(0)) - _add_block_at_slot(sync_service, Slot(20)) - service = _build_gate_service(sync_service) - - # Invariant: the gate never moves counters. Attribution belongs - # to the run loop. Querying the gate must leave them at zero. - assert not service._is_synced_for_duties(Slot(20), "block") - assert service._blocks_skipped_lag == 0 - assert service._attestations_skipped_lag == 0 - assert service._duty_gate_closed is True - async def test_run_loop_skips_block_production_when_gated( self, sync_service: SyncService, key_manager: XmssKeyManager ) -> None: - """Closed gate at interval 0 skips block production and ticks only the block counter.""" + """Closed gate at interval 0 skips block production.""" # Wall clock at slot 10 interval 0, head stuck at slot 0. # Fresh local block at slot 10 makes the lag local, not network-wide. @@ -1491,10 +1476,8 @@ async def stop_on_sleep(_d: float) -> None: ): await service.run() - # Block path bypassed, attestation counter untouched. + # Block path bypassed. assert block_calls == [] - assert service._blocks_skipped_lag >= 1 - assert service._attestations_skipped_lag == 0 async def test_run_loop_skips_attestation_when_gated( self, sync_service: SyncService, key_manager: XmssKeyManager @@ -1532,11 +1515,9 @@ async def stop_on_sleep(_d: float) -> None: ): await service.run() - # Attestation skipped, slot retryable, block counter untouched. + # Attestation skipped, slot retryable. assert attest_calls == [] assert Slot(10) not in service._attested_slots - assert service._attestations_skipped_lag >= 1 - assert service._blocks_skipped_lag == 0 def test_gate_logs_only_on_transition( self, sync_service: SyncService, caplog: pytest.LogCaptureFixture diff --git a/tests/lean_spec/subspecs/xmss/test_containers.py b/tests/lean_spec/subspecs/xmss/test_containers.py index 5a624a64..c4c981df 100644 --- a/tests/lean_spec/subspecs/xmss/test_containers.py +++ b/tests/lean_spec/subspecs/xmss/test_containers.py @@ -27,12 +27,12 @@ def hex_dict(keypair_a: KeyPair, keypair_b: KeyPair) -> dict[str, dict[str, str] """Nested-hex JSON mapping that mirrors the on-disk format.""" return { "attestation_keypair": { - "public_key": keypair_a.public_key.to_hex(), - "secret_key": keypair_a.secret_key.to_hex(), + "public_key": keypair_a.public_key.encode_bytes().hex(), + "secret_key": keypair_a.secret_key.encode_bytes().hex(), }, "proposal_keypair": { - "public_key": keypair_b.public_key.to_hex(), - "secret_key": keypair_b.secret_key.to_hex(), + "public_key": keypair_b.public_key.encode_bytes().hex(), + "secret_key": keypair_b.secret_key.encode_bytes().hex(), }, } @@ -76,8 +76,8 @@ def test_validator_accepts_mixed_inputs(keypair_a: KeyPair, keypair_b: KeyPair) data = { "attestation_keypair": keypair_a, "proposal_keypair": { - "public_key": keypair_b.public_key.to_hex(), - "secret_key": keypair_b.secret_key.to_hex(), + "public_key": keypair_b.public_key.encode_bytes().hex(), + "secret_key": keypair_b.secret_key.encode_bytes().hex(), }, } vkp = ValidatorKeyPair.model_validate(data) @@ -137,7 +137,7 @@ def test_rejects_missing_public_key( """A role mapping without the public half fails to decode.""" # KeyError on value["public_key"] surfaces as ValidationError. data = { - "attestation_keypair": {"secret_key": keypair_a.secret_key.to_hex()}, + "attestation_keypair": {"secret_key": keypair_a.secret_key.encode_bytes().hex()}, "proposal_keypair": hex_dict["proposal_keypair"], } with pytest.raises(ValidationError): @@ -150,7 +150,7 @@ def test_rejects_missing_secret_key( """A role mapping without the secret half fails to decode.""" # KeyError on value["secret_key"] surfaces as ValidationError. data = { - "attestation_keypair": {"public_key": keypair_a.public_key.to_hex()}, + "attestation_keypair": {"public_key": keypair_a.public_key.encode_bytes().hex()}, "proposal_keypair": hex_dict["proposal_keypair"], } with pytest.raises(ValidationError): @@ -163,7 +163,10 @@ def test_rejects_non_string_hex_value( """Hex fields must be strings; an integer is rejected before SSZ decoding.""" # AttributeError on int.removeprefix surfaces as ValidationError. data = { - "attestation_keypair": {"public_key": 12345, "secret_key": keypair_a.secret_key.to_hex()}, + "attestation_keypair": { + "public_key": 12345, + "secret_key": keypair_a.secret_key.encode_bytes().hex(), + }, "proposal_keypair": hex_dict["proposal_keypair"], } with pytest.raises(ValidationError): @@ -178,7 +181,7 @@ def test_rejects_invalid_hex_characters( data = { "attestation_keypair": { "public_key": "zz" * 26, - "secret_key": keypair_a.secret_key.to_hex(), + "secret_key": keypair_a.secret_key.encode_bytes().hex(), }, "proposal_keypair": hex_dict["proposal_keypair"], } @@ -192,7 +195,7 @@ def test_rejects_wrong_length_hex(keypair_a: KeyPair, hex_dict: dict[str, dict[s data = { "attestation_keypair": { "public_key": "deadbeef", - "secret_key": keypair_a.secret_key.to_hex(), + "secret_key": keypair_a.secret_key.encode_bytes().hex(), }, "proposal_keypair": hex_dict["proposal_keypair"], }