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
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ def _build_block_from_spec(
block_spec: BlockSpec,
state: State,
block_registry: dict[str, Block],
*,
is_final_block: bool,
) -> tuple[Block, State | None]:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,21 +161,15 @@ class AggregatedAttestationSpec(AttestationSpec):

aggregation_bits: AggregationBits | None = None
"""
Raw aggregation bits placed into the block body verbatim.
Raw aggregation bits for the block body, overriding index derivation.

When None (default), the bits are derived from the validator indices,
producing the tightest bitfield that covers the highest set index.
Set this to author bit patterns the derivation cannot express:
a zero-length bitfield, all-false bits, or trailing padding past the
validator registry.

Only honored by the state transition format's forced-attestation
path, which bypasses signing. Signed paths derive their bits from
the validator indices so proofs match the claimed participants.
- When unset, bits are derived from the validator indices.
- When set, the bits are used verbatim, even when zero-length or padded.
- Only the unsigned forced-attestation path honors the override.
"""

def resolve_aggregation_bits(self) -> AggregationBits:
"""Return the explicit bits override, or bits derived from the validator indices."""
"""Return the bit override when present, else bits derived from the validator indices."""
if self.aggregation_bits is not None:
return self.aggregation_bits
return AggregationBits.from_indices(self.validator_indices)
Expand Down
3 changes: 3 additions & 0 deletions src/lean_spec/spec/forks/lstar/state_transition.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ def process_attestations(
# A vote from a nonexistent validator has no flag to set,
# so the whole block is invalid.
# Trailing unset bits beyond the registry are harmless padding.
#
# Signature verification normally rejects such a bit first.
# This guards the unsigned path, which has no signature stage.
for validator_index in voting_validator_indices:
if not validator_index.is_within_registry(Uint64(len(state.validators))):
raise SpecRejectionError(
Expand Down
32 changes: 9 additions & 23 deletions tests/consensus/lstar/state_transition/test_aggregation_bits.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def test_aggregation_bit_beyond_validator_registry_rejects_block(
- 4 validators (indices 0-3); the vote tally has one flag per validator.
- the chain:
genesis -> block_1(1) -> block(2)
- block(2) carries an attestation for block_1 whose aggregation bits
name V0 and V1 plus nonexistent validator 4.
- block(2) carries an attestation for block_1 naming V0, V1, and V4.
- index 4 is within the bitfield limit but one past the registry.

When
----
Expand All @@ -37,9 +37,6 @@ def test_aggregation_bit_beyond_validator_registry_rejects_block(
Then
----
- the block is rejected with VALIDATOR_INDEX_OUT_OF_RANGE.
- a client that silently skips the attestation instead would accept a
block the rest of the network refuses: a consensus split on a single
crafted block.
"""
state_transition_test(
blocks=[
Expand All @@ -48,8 +45,6 @@ def test_aggregation_bit_beyond_validator_registry_rejects_block(
slot=Slot(2),
parent_label="block_1",
forced_attestations=[
# Bits 0, 1, and 4 are set.
# Bit 4 points one past the 4-validator registry.
AggregatedAttestationSpec(
validator_indices=[
ValidatorIndex(0),
Expand Down Expand Up @@ -89,9 +84,6 @@ def test_all_false_aggregation_bits_rejects_block(
Then
----
- the block is rejected with EMPTY_AGGREGATION_BITS.
- a client that processes the attestation as a no-op instead leaves an
all-false tally entry in its post-state, diverging from clients that
reject the block.
"""
state_transition_test(
blocks=[
Expand Down Expand Up @@ -128,6 +120,7 @@ def test_zero_length_aggregation_bits_rejects_block(
genesis -> block_1(1) -> block(2)
- block(2) carries an attestation for block_1 whose aggregation bits
hold no bits at all.
- a zero-length bitfield is a distinct SSZ encoding from an all-false one.

When
----
Expand All @@ -136,9 +129,6 @@ def test_zero_length_aggregation_bits_rejects_block(
Then
----
- the block is rejected with EMPTY_AGGREGATION_BITS.
- the zero-length bitfield is a distinct SSZ encoding from an all-false
bitfield of registry size; both name no voter and both must reject
the block identically across clients.
"""
state_transition_test(
blocks=[
Expand Down Expand Up @@ -173,21 +163,19 @@ def test_oversized_aggregation_bits_with_in_range_votes_processes_normally(
- 4 validators; a slot needs 3 votes (2/3) to be justified.
- the chain:
genesis -> block_1(1) -> block(2)
- block(2) carries an attestation for block_1 whose bitfield is 6 bits
long (two bits past the registry) with only V0, V1, and V2 set.
- block(2) carries an attestation for block_1.
- the bitfield is 6 bits long with only V0, V1, V2 set.
- bits 4 and 5 are unset padding past the registry.

When
----
- the chain processes both blocks.

Then
----
- the attestation is processed normally: every set bit addresses a real
validator, and the trailing unset padding is harmless.
- block_1's slot is justified and its pending tally is cleared.
- a client that skips the attestation because of the bitfield length
computes a different post-state for a valid block; the pinned
post-state root catches that divergence directly.
- block_1's slot is justified.
- the pending tally for block_1 is cleared.
- finalization stays at genesis.
"""
state_transition_test(
blocks=[
Expand All @@ -196,8 +184,6 @@ def test_oversized_aggregation_bits_with_in_range_votes_processes_normally(
slot=Slot(2),
parent_label="block_1",
forced_attestations=[
# Bits 0-2 are set; bits 3-5 are unset.
# Bits 4 and 5 pad past the 4-validator registry.
AggregatedAttestationSpec(
validator_indices=[],
aggregation_bits=AggregationBits(
Expand Down
Loading