Skip to content

aggregate all block signatures as naive list [devnet-1]#67

Merged
unnawut merged 56 commits intoleanEthereum:mainfrom
g11tech:agg-block-sigs
Oct 25, 2025
Merged

aggregate all block signatures as naive list [devnet-1]#67
unnawut merged 56 commits intoleanEthereum:mainfrom
g11tech:agg-block-sigs

Conversation

@g11tech
Copy link
Contributor

@g11tech g11tech commented Sep 25, 2025

one of the important lean-ing step especially for STF proving considerations as well as to lighten the block network payload

Also the proposer vote isn't casted separate (so validator doesn't emit vote for a validator id if its that validator id's turn to vote)
but is directly bundled with the block because of the OTS scheme limitation

@g11tech g11tech changed the title aggregate all block siignatures aggregate all block siignatures [devnet-1] Sep 25, 2025
@g11tech g11tech changed the title aggregate all block siignatures [devnet-1] aggregate all block signatures [devnet-1] Sep 25, 2025
@g11tech g11tech changed the title aggregate all block signatures [devnet-1] aggregate all block signatures as naive list [devnet-1] Oct 13, 2025
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Copy link
Collaborator

@unnawut unnawut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for missing the Monday's call, and also apologies if any of these comments sound stupid, still trying to catch up.

I'm also trying to grasp all the *Attestation containers and AggregatedSignatures so no thoughts on this part yet other than it seems more complex than the beacon chain.

Should we design this at all for devnet1? Even if we do, I'm thinking it could go as a separate PR so at least the individual signatures can be implemented first. Also would make the discussion scope narrower.

Signed-off-by: Chen Kai <281165273grape@gmail.com>
Copy link
Contributor

@jihoonsong jihoonsong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rebase your PR?

# sync parent chain if not available before adding block to forkchoice
assert parent_state is not None, "Parent state not found, sync parent chain first"

valid_signatures = validate_signatures(block.signature)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the reason that we don't do assert validate_signatures(block.signature) here and make state_transition receive valid_signatures?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its fine either way imo as this is spec, clients can choose to optimize/parallelize anywhich way they want but its good to be explicit in the STF that signatures have been validated

## `Vote`
## `AttestationData`

Vote is the attestation data that can be aggregated. Although note there is no aggregation yet in `devnet0`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: there is no Vote anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will look into it if we wanna remove all vote references,

Comment on lines 116 to 117
latest_known_votes: Dict[ValidatorIndex, SignedValidatorAttestation] = field(default_factory=dict)
latest_new_votes: Dict[ValidatorIndex, SignedValidatorAttestation] = field(default_factory=dict)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do these have to have SignedValidatorAttestation instead of Checkpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for block construction purposes, you need signed objects to get signatures you want to aggregate

# ValidatorAttestation created from its ProposerAttestationData.
# till we can actually consume a block signature for re-packing this block's
# proposer vote by some other future proposer
signature: List[Vector[byte, 4000], VALIDATOR_REGISTRY_LIMIT]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably missing something so I'm asking a question here. When we aggregate signatures, do we want to aggregate all different kinds of signatures? In other words, what's the best way to understand why we want to aggregate attestation signatures along with block signature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signatures are heavy now, also with OTS constraint proposer signature which is over the block and vote is sort of anyway not "pure" anymore so to take things to logical conclusion a block (and vote) signature will represent all valid objects in the block (and vote)

```

## `SignedVote`
## `ProposerAttestationData`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I understand the constraints that made us bring up this approach, I believe the problem of signing multiple times in a slot remains. If we want to adopt this PG signature effort into a production-level protocol, it's highly likely that a proposer needs to sign more than one message in a slot. If we pack all those messages into one, a proposer has to broadcast all of their messages at once. In other words, there is only one deadline within a slot for a proposer. This is not flexible so I'm not sure if this would be enough to meet all the needs of the protocol.

IMO, we could do either find a proper solution for the problem or use a stopgap solution like this one that is just enough for the upcoming few devnets. If we were to go to the latter, current approach is one of solutions. But it seems it adds complexity, which is might not an ideal given that it's not a long-term solution. So I'd like to encourage to find a better one. Few things I have in my mind are:

  1. Currently, BlockBody has a list of unsigned votes. We can append proposer's attestation at the end of the list and append proposer's signature at the SignedBlock's signature. It's not very clean as there is an exception that proposer's signature is made over a block while others are over attestation. But this is already what we have if I'm not wrong, and this doesn't require to change original Vote and related classes nor to add ProposerAttestationData.

  2. We can add a rule that proposers don't vote. This would make things much simpler.

Copy link
Contributor Author

@g11tech g11tech Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for

  1. yes we are trying to move closer to aggregation so the list there is just a temporary thing till we have leanVM coming up. also till then its just proposer votes signature, so not even a full block & vote signature since we can't validate such a signature without leanVM

  2. yes can be considered actually, was in my mind, but eventually signatures will be aggregated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

infact the signature part can just be totally replaced by full block validity proof including execution

so yes design space is open and we will see how to simplify it if it turns out convoluted

Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>

block = sample_store.produce_block(slot, validator_idx)

block_and_signatures = sample_store.produce_block(slot, validator_idx)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
block_and_signatures = sample_store.produce_block(slot, validator_idx)
block, signatures = sample_store.produce_block(slot, validator_idx)

You can remove BlockAndSignatures as well.

Signed-off-by: Chen Kai <281165273grape@gmail.com>
"AttestationData",
"Attestation",
"SignedAttestation",
"SignedAggreagtedAttestations",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"SignedAggreagtedAttestations",
"SignedAggregatedAttestations",

also applies to other places

Comment on lines 25 to 43
@property
def slot(self) -> Slot:
"""Return the attested slot."""
return self.data.slot

@property
def head(self) -> Checkpoint:
"""Return the attested head checkpoint."""
return self.data.head

@property
def target(self) -> Checkpoint:
"""Return the attested target checkpoint."""
return self.data.target

@property
def source(self) -> Checkpoint:
"""Return the attested source checkpoint."""
return self.data.source
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we need these accessor functions in the specs, seems more like implementation's decision to make than the specs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this makes the spec heavier and we can always come and get them with self.data.source for example

Comment on lines 58 to 61
@property
def data(self) -> Attestation:
"""Expose the message for backwards compatibility with SignedVote."""
return self.message
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need backward compatibility right now so this can be removed

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

Comment on lines 72 to 73
committee assignments. This structure is defined for future use and is not
currently exercised by devnets.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
committee assignments. This structure is defined for future use and is not
currently exercised by devnets.
committee assignments.

I think it's needed in devnet1 where aggregation_bits will used to map AggregatedSignatures to their validator id.

# Ensure forkchoice is current before processing gossip
time_slots = self.time // INTERVALS_PER_SLOT
assert vote.slot <= time_slots, "Attestation from future slot"
time_slots = self.time // SECONDS_PER_INTERVAL
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to previous comment:

Suggested change
time_slots = self.time // SECONDS_PER_INTERVAL
time_slots = self.time // SLOT_DURATION_MS // 1000

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge issue, it should be INTERVALS_PER_SLOT

# TODO: Integrate actual aggregated signature verification.
return all(self._is_valid_signature(signature) for signature in signatures)

def process_block(self, signed_block_vote: SignedBlockWithAttestation) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the type name changed

Suggested change
def process_block(self, signed_block_vote: SignedBlockWithAttestation) -> None:
def process_block(self, signed_block_with_attestation: SignedBlockWithAttestation) -> None:

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, we should modify the doc string as well if we change this

for validator_id, checkpoint in self.latest_known_votes.items():
new_attestations: list[Attestation] = []
new_signatures: list[Bytes4000] = []
for signed in self.latest_known_votes.values():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for signed in self.latest_known_votes.values():
for signed_attestation in self.latest_known_votes.values():

naming ambiguity

parent_root=head_root,
state_root=Bytes32.zero(), # Will be updated with computed hash
body=BlockBody(attestations=Attestations(data=attestations)),
body=BlockBody(attestations=Attestations(data=list(attestations))), # need copy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the comment means?

Comment on lines 232 to 235
class Bytes4000(BaseBytes):
"""Fixed-size byte array of exactly 4000 bytes."""

LENGTH = 4000
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type is very unusual and will probably be removed in the future.

As this file is about types that should be a bit more stable than the rest of the code, can you put a TODO or something to indicate that this will be removed in the future?

Comment on lines 25 to 43
@property
def slot(self) -> Slot:
"""Return the attested slot."""
return self.data.slot

@property
def head(self) -> Checkpoint:
"""Return the attested head checkpoint."""
return self.data.head

@property
def target(self) -> Checkpoint:
"""Return the attested target checkpoint."""
return self.data.target

@property
def source(self) -> Checkpoint:
"""Return the attested source checkpoint."""
return self.data.source
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this makes the spec heavier and we can always come and get them with self.data.source for example

Comment on lines 42 to 47
@dataclass(slots=True)
class BlockAndSignatures:
"""Internal bundle pairing a block with its attestation signatures."""

block: Block
signatures: list[Bytes4000]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not BlockAndAttestations here to be consistent with the rest?

# TODO: Integrate actual aggregated signature verification.
return all(self._is_valid_signature(signature) for signature in signatures)

def process_block(self, signed_block_vote: SignedBlockWithAttestation) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, we should modify the doc string as well if we change this

validator_idx = ValidatorIndex(2) # Proposer for slot 2

block = sample_store.produce_block(slot, validator_idx)
block_and_signatures = sample_store.produce_block(slot, validator_idx)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
block_and_signatures = sample_store.produce_block(slot, validator_idx)
block_and_attestations = sample_store.produce_block(slot, validator_idx)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jihoonsong suggest removing this type and just return multi values, is that preference

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the type is just for the return value and is not necessary. Clients can choose to use such intermediary types, if they prefer.

GrapeBaBa and others added 10 commits October 24, 2025 18:12
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Co-authored-by: Thomas Coratger <60488569+tcoratger@users.noreply.github.com>
Co-authored-by: Thomas Coratger <60488569+tcoratger@users.noreply.github.com>
Co-authored-by: Thomas Coratger <60488569+tcoratger@users.noreply.github.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
@GrapeBaBa
Copy link
Contributor

@unnawut @tcoratger fix almost comments

Co-authored-by: JihoonSong <jihoonsong@users.noreply.github.com>
Copy link
Contributor

@jihoonsong jihoonsong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is exactly 1 month old. This can go forever. Let's merge this and iterate fast.

@unnawut unnawut merged commit ac563e7 into leanEthereum:main Oct 25, 2025
9 checks passed
@unnawut unnawut added the specs Scope: Changes to the specifications label Oct 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

specs Scope: Changes to the specifications

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants