Skip to content

feat: make epoch length dynamic and add protocol param for it#138

Merged
matthias-wright merged 6 commits intomainfrom
m/epocher
Mar 9, 2026
Merged

feat: make epoch length dynamic and add protocol param for it#138
matthias-wright merged 6 commits intomainfrom
m/epocher

Conversation

@matthias-wright
Copy link
Collaborator

This makes the epoch length dynamic and introduces a new protocol param for it.

The epocher trait is fairly generic:

pub trait Epocher:
    fn containing(&self, height: Height) -> Option<EpochInfo>;
    fn first(&self, epoch: Epoch) -> Option<Height>;
    fn last(&self, epoch: Epoch) -> Option<Height>;
}

There are no restrictions or contracts that stop an actor from calling first or last with a future epoch. Similarly, nothing is stopping an actor from calling containing with a future height.
This is tricky, because with the dynamic epocher, the length of future epochs can change if a protocol param change is submitted. We have to avoid a scenario where epocher.first(n) returns different values for the same n at different times.
Therefore, the dynamic epocher keeps track of the current epoch. The finalizer actor updates the epocher after every epoch change.
If one of the methods is called for an epoch larger than current_epoch + 1, the dynamic epocher will return None.
When a protocol param request updates the epoch length, it will be updated starting from current_epoch + 2. This is because the epocher could've already answered a query for current_epoch + 1.

These are all the places where these methods are called:

  Orchestrator (orchestrator/src/actor.rs):
  - Line 218: self.epocher.last(our_epoch) — current epoch

  Syncer (syncer/src/actor.rs):
  - Line 716: self.epocher.containing(height) — height from a peer (could be past)
  - Line 867: epocher_is_last_block_of_epoch(&self.epocher, next_height.get()) — next block being synced (past during
  catch-up)

  Application (application/src/actor.rs):
  - Line 430: self.epocher.last(Epoch::new(aux_data.epoch)) — current epoch
  - Line 561: epocher.last(Epoch::new(aux_data.epoch)) — current epoch

  Finalizer (finalizer/src/actor.rs, via utils.rs helpers):
  - epocher_is_first_block_of_epoch — current height
  - epocher_is_last_block_of_epoch — current height
  - epocher_is_penultimate_block_of_epoch — current height

All of these are for the current epoch (or past epochs during syncing), so the return value will never be None.
Only the application and the orchestrator would panic on a None value, but the queries are always for the current epoch.

Changes:

  • Make actors generic over the Epocher trait
  • Add dynamic epocher
  • Add dynamic epocher to consensus state and implement serialization and deserialization
  • Extend the protocol params e2e test to test the epoch length protocol param request
  • Update tests

@matthias-wright matthias-wright merged commit 36c1c27 into main Mar 9, 2026
4 checks passed
@matthias-wright matthias-wright deleted the m/epocher branch March 9, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant