Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1af3fa5
feat(ethexe-consensus): mini-announces for instant injected TX promises
ukint-vs Apr 8, 2026
b45f07a
Merge remote-tracking branch 'origin/master' into vs/mini-announces
ukint-vs Apr 8, 2026
f3c2e76
fix(ethexe-consensus): drop VR for non-validators in ReadyForMoreAnno…
ukint-vs Apr 8, 2026
cd4d8e3
chore(ethexe-consensus): add TODO for batch commitment during mini-an…
ukint-vs Apr 8, 2026
6c02fd7
fix(ethexe-compute): skip canonical events for same-block mini-announces
ukint-vs Apr 8, 2026
72c90a8
fix(ethexe-consensus): allow dead_code for wait_for_state test helper
ukint-vs Apr 8, 2026
cd54848
fix(ethexe-service): default continuous_block_generation to true in t…
ukint-vs Apr 8, 2026
2dee90e
revert: default continuous_block_generation back to false in tests
ukint-vs Apr 9, 2026
71a9eb3
fix(ethexe-service): guard AggregateBatchCommitment from new heads
ukint-vs Apr 9, 2026
db75857
fix(ethexe-consensus): replay pending events oldest-first in subordinate
ukint-vs Apr 9, 2026
9b2125c
fix(ethexe-service): default continuous_block_generation to true in t…
ukint-vs Apr 9, 2026
af65fee
fix(ethexe-consensus): add batch timer to ReadyForMiniAnnounce
ukint-vs Apr 9, 2026
102613a
fix(ethexe-consensus): buffer next_block in Coordinator for threshold>1
ukint-vs Apr 9, 2026
d0a58bc
chore: fix test by removing validator restarting, detect where conse…
ecol-master Apr 13, 2026
94d1051
Merge branch 'master' into vs/mini-announces
ecol-master Apr 13, 2026
39c75fb
refactor(ethexe-consensus): timer-based polling for mini-announces
ukint-vs Apr 14, 2026
57d0257
fix(ethexe-consensus): timeout Coordinator on second new head
ukint-vs Apr 14, 2026
3cd275d
fix(ethexe-consensus): block-aware CDL for mini-announces correctness
ukint-vs Apr 14, 2026
fc0ea47
fix(ethexe-consensus): validate mini-announce parent and defer VR on …
ukint-vs Apr 15, 2026
63c9791
refactor(ethexe-consensus): simplify subordinate for mini-announces
ukint-vs Apr 16, 2026
bfad58c
fix(ethexe-consensus): CDL off-by-one in block-aware counting
ukint-vs Apr 16, 2026
db0c977
fix(ethexe-consensus): cap pending_events during steady-state deferral
ukint-vs Apr 16, 2026
e67cedc
merge: resolve conflicts with origin/master
ukint-vs Apr 16, 2026
00be1c5
docs(ethexe-consensus): update edge cases with verified constraints
ukint-vs Apr 16, 2026
c20b446
Merge branch 'master' into vs/mini-announces
ukint-vs Apr 16, 2026
d805093
refactor(ethexe-consensus): reduce mini-announces complexity
ukint-vs Apr 16, 2026
d13f0ba
chore: remove .claude/worktrees from tracking
ukint-vs Apr 16, 2026
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
22 changes: 15 additions & 7 deletions ethexe/compute/src/compute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,21 @@ pub(crate) mod utils {
) -> Result<ExecutableData> {
let block_hash = announce.block_hash;

let matured_events =
find_canonical_events_post_quarantine(db, block_hash, canonical_quarantine)?;

let events = matured_events
.into_iter()
.filter_map(|event| event.to_request())
.collect();
// Check if parent announce is for the same block (mini-announce case).
// If so, canonical events were already processed by the parent — skip them.
let is_same_block = db
.announce(announce.parent)
.is_some_and(|parent| parent.block_hash == block_hash);
let events = if is_same_block {
vec![]
} else {
let matured_events =
find_canonical_events_post_quarantine(db, block_hash, canonical_quarantine)?;
matured_events
.into_iter()
.filter_map(|event| event.to_request())
.collect()
};

Ok(ExecutableData {
block: SimpleBlockData {
Expand Down
361 changes: 327 additions & 34 deletions ethexe/consensus/src/announces.rs

Large diffs are not rendered by default.

31 changes: 22 additions & 9 deletions ethexe/consensus/src/validator/batch/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,21 +204,21 @@ pub fn calculate_batch_expiry<DB: BlockMetaStorageRO + OnChainStorageRO + Announ
)
})?;

// Amount of announces which we should check to determine if there are not-base announces in the commitment.
let Some(announces_to_check_amount) = commitment_delay_limit.checked_sub(head_delay) else {
// Number of blocks worth of announces to check for not-base announces in the commitment.
let Some(blocks_to_check) = commitment_delay_limit.checked_sub(head_delay) else {
// No need to set expiry - head announce is old enough, so cannot contain any not-base announces.
return Ok(None);
};

if announces_to_check_amount == 0 {
if blocks_to_check == 0 {
// No need to set expiry - head announce is old enough, so cannot contain any not-base announces.
return Ok(None);
}

let mut oldest_not_base_announce_depth = (!head_announce.is_base()).then_some(0);
let mut oldest_not_base_announce_depth = (!head_announce.is_base()).then_some(0u32);
let mut current_announce_hash = head_announce.parent;

if announces_to_check_amount == 1 {
if blocks_to_check == 1 {
// If head announce is not base and older than commitment delay limit - 1, then expiry is only 1.
return Ok(oldest_not_base_announce_depth.map(|_| 1));
}
Expand All @@ -228,8 +228,12 @@ pub fn calculate_batch_expiry<DB: BlockMetaStorageRO + OnChainStorageRO + Announ
.last_committed_announce
.ok_or_else(|| anyhow!("last committed announce not found for block {}", block.hash))?;

// from 1 because we have already checked head announce (note announces_to_check_amount > 1)
for i in 1..announces_to_check_amount {
// Walk backwards counting block transitions (not announce hops) because
// mini-announces chain within the same block. CDL is defined in blocks per S1.
let mut blocks_seen = 1u32; // already counted head announce's block
let mut prev_block_hash = Some(head_announce.block_hash);

loop {
if current_announce_hash == last_committed_announce {
break;
}
Expand All @@ -238,15 +242,24 @@ pub fn calculate_batch_expiry<DB: BlockMetaStorageRO + OnChainStorageRO + Announ
.announce(current_announce_hash)
.ok_or_else(|| anyhow!("Cannot get announce by {current_announce_hash}",))?;

let is_new_block = prev_block_hash != Some(current_announce.block_hash);
if is_new_block {
blocks_seen += 1;
prev_block_hash = Some(current_announce.block_hash);
if blocks_seen >= blocks_to_check {
break;
}
}

if !current_announce.is_base() {
oldest_not_base_announce_depth = Some(i);
oldest_not_base_announce_depth = Some(blocks_seen);
}

current_announce_hash = current_announce.parent;
}

Ok(oldest_not_base_announce_depth
.map(|depth| announces_to_check_amount - depth)
.map(|depth| blocks_to_check - depth)
.map(TryInto::try_into)
.transpose()?)
}
Expand Down
32 changes: 27 additions & 5 deletions ethexe/consensus/src/validator/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub struct Coordinator {
ctx: ValidatorContext,
validators: BTreeSet<Address>,
multisigned_batch: MultisignedBatchCommitment,
/// Buffered next block to process after submission completes.
next_block: Option<SimpleBlockData>,
}

impl StateHandler for Coordinator {
Expand All @@ -55,6 +57,19 @@ impl StateHandler for Coordinator {
self.ctx
}

fn process_new_head(mut self, block: SimpleBlockData) -> Result<ValidatorState> {
if self.next_block.is_some() {
// Second new head while still waiting for signatures — give up on this batch.
// Not enough validators responded within one block window.
tracing::warn!("Coordinator received second new head, abandoning batch");
Initial::create(self.ctx)?.process_new_head(block)
} else {
// Buffer the first new head. Process it after submission.
self.next_block = Some(block);
Ok(self.into())
}
}

fn process_validation_reply(
mut self,
reply: BatchCommitmentValidationReply,
Expand All @@ -72,7 +87,7 @@ impl StateHandler for Coordinator {
}

if self.multisigned_batch.signatures().len() as u64 >= self.ctx.core.signatures_threshold {
Self::submission(self.ctx, self.multisigned_batch)
Self::submission(self.ctx, self.multisigned_batch, self.next_block)
} else {
Ok(self.into())
}
Expand Down Expand Up @@ -110,7 +125,7 @@ impl Coordinator {
.set(block.header.height);

if multisigned_batch.signatures().len() as u64 >= ctx.core.signatures_threshold {
return Self::submission(ctx, multisigned_batch);
return Self::submission(ctx, multisigned_batch, None);
}

let era_index = ctx
Expand All @@ -131,13 +146,15 @@ impl Coordinator {
ctx,
validators: validators.into_iter().collect(),
multisigned_batch,
next_block: None,
}
.into())
}

pub fn submission(
ctx: ValidatorContext,
multisigned_batch: MultisignedBatchCommitment,
next_block: Option<SimpleBlockData>,
) -> Result<ValidatorState> {
let (batch, signatures) = multisigned_batch.into_parts();
let cloned_committer = ctx.core.committer.clone_boxed();
Expand All @@ -150,16 +167,21 @@ impl Coordinator {
block_hash,
batch_digest,
tx,
}.into(),
}
.into(),
Err(err) => ConsensusEvent::Warning(format!(
"Failed to submit commitment for block {block_hash}, digest {batch_digest}: {err}"
))
)),
};
Ok(event)
}
.boxed(),
);
Initial::create(ctx)
let state = Initial::create(ctx)?;
match next_block {
Some(block) => state.process_new_head(block),
None => Ok(state),
}
}
}

Expand Down
6 changes: 4 additions & 2 deletions ethexe/consensus/src/validator/initial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,11 +438,13 @@ mod tests {

let ctx = state.into_context();
assert_eq!(ctx.output, vec![]);
for i in last - 5..last - 5 + ctx.core.commitment_delay_limit as usize {
// With block-aware CDL, non-base announces are eligible for CDL blocks per S1
// (`<= commitment_delay_limit`), so sibling branches survive one block longer.
for i in last - 5..=last - 5 + ctx.core.commitment_delay_limit as usize {
let announces = ctx.core.db.block_meta(chain.blocks[i].hash).announces;
assert_eq!(announces.unwrap().len(), 2);
}
for i in last - 5 + ctx.core.commitment_delay_limit as usize..=last {
for i in last - 5 + ctx.core.commitment_delay_limit as usize + 1..=last {
let announces = ctx.core.db.block_meta(chain.blocks[i].hash).announces;
assert_eq!(announces.unwrap().len(), 1);
}
Expand Down
1 change: 1 addition & 0 deletions ethexe/consensus/src/validator/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ impl ElectionProvider for MockEthereum {
#[async_trait]
pub trait WaitFor {
async fn wait_for_event(self) -> Result<(ValidatorState, ConsensusEvent)>;
#[allow(dead_code)]
async fn wait_for_state<F>(self, f: F) -> Result<ValidatorState>
where
F: Fn(&ValidatorState) -> bool + Unpin + Send;
Expand Down
Loading
Loading