Skip to content
Merged
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
9 changes: 6 additions & 3 deletions src/lean_spec/subspecs/validator/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class ValidatorManifestEntry(BaseModel):
"""XMSS public key for signing attestations."""

proposal_pubkey_hex: Bytes52
"""XMSS public key for signing proposer attestations in blocks."""
"""XMSS public key for signing block proposals."""

attestation_privkey_file: str
"""Filename of the attestation private key file."""
Expand Down Expand Up @@ -159,7 +159,7 @@ class ValidatorEntry:
"""Secret key for signing attestations."""

proposal_secret_key: SecretKey
"""Secret key for signing proposer attestations in blocks."""
"""Secret key for signing block proposals."""


@dataclass(slots=True)
Expand All @@ -176,7 +176,10 @@ class ValidatorRegistry:

def add(self, entry: ValidatorEntry) -> None:
"""
Add a validator entry to the registry.
Add or replace a validator entry in the registry.

Replaces any existing entry with the same index.
Used to persist updated key state after signing.

Args:
entry: Validator entry to add.
Expand Down
18 changes: 13 additions & 5 deletions src/lean_spec/subspecs/validator/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,8 @@ async def _maybe_produce_block(self, slot: Slot) -> None:
Checks the proposer schedule against our validator registry.
If one of our validators should propose, produces and emits the block.

The proposer's attestation is bundled into the block rather than
broadcast separately at interval 1. This ensures the proposer's vote
is included without network round-trip delays.
The proposer signs the block root with the proposal key.
Attestation happens separately at interval 1 using the attestation key.

Args:
slot: Current slot number.
Expand All @@ -243,6 +242,10 @@ async def _maybe_produce_block(self, slot: Slot) -> None:
return

num_validators = Uint64(len(head_state.validators))
if num_validators == Uint64(0):
logger.debug("Block production: no validators in state for slot %d", slot)
return

my_indices = list(self.registry.indices())
expected_proposer = int(slot) % int(num_validators)
logger.debug(
Expand Down Expand Up @@ -362,7 +365,12 @@ async def _produce_attestations(self, slot: Slot) -> None:
)
except Exception:
# Best-effort: the attestation always goes via gossip regardless.
pass
logger.debug(
"on_gossip_attestation failed for validator %d at slot %d",
validator_index,
slot,
exc_info=True,
)

# Emit the attestation for network propagation.
await self.on_attestation(signed_attestation)
Expand Down Expand Up @@ -417,7 +425,7 @@ def _sign_attestation(
"""
Sign an attestation for publishing.

Uses XMSS signature scheme with the validator's secret key.
Signs the attestation data root with the validator's attestation key.

Args:
attestation_data: The attestation data to sign.
Expand Down
Loading
Loading