use super::{
events::*,
reactor::{bs_insert_height, bs_insert_str, bs_insert_u64},
reorder::BufferedBlockBody,
sequencer::*,
state::*,
work_queue::WorkQueue,
*,
};
pub(super) fn shed_top_until_available(
budget: &mut ByteBudget,
work: &WorkQueue,
sequencer: &mut Sequencer,
target_available: u64,
) -> bool {
let mut shed_any = false;
while budget.available() < target_available {
let lowest_needed = match (work.min_pending(), work.min_in_flight()) {
(Some(pending), Some(in_flight)) => pending.min(in_flight),
(Some(pending), None) => pending,
(None, Some(in_flight)) => in_flight,
(None, None) => break,
};
let Some(top) = sequencer.reorder_max_height() else {
break;
};
if lowest_needed >= top {
break;
}
let freed = sequencer.drop_reorder_from(top);
if freed == 0 {
break;
}
let released = work.release_and_return_items([top]);
debug_assert!(
released == 0 || released == freed,
"shed reorder release must match the per-height budget ledger when present"
);
budget.release(if released == 0 { freed } else { released });
shed_any = true;
}
shed_any
}
pub(super) fn shed_top_for_floor_starvation(
budget: &mut ByteBudget,
work: &WorkQueue,
sequencer: &mut Sequencer,
) -> bool {
shed_top_until_available(
budget,
work,
sequencer,
super::config::BS_PER_BLOCK_WORST_CASE_BYTES,
)
}
#[derive(Clone, Debug)]
pub(super) struct SequencedBody {
pub(super) height: block::Height,
pub(super) hash: block::Hash,
pub(super) body: BufferedBlockBody,
pub(super) bytes: u64,
pub(super) peer: ZakuraPeerId,
pub(super) received_at: Instant,
}
#[derive(Debug)]
pub(super) enum SequencerControlInput {
FrontierAdvance {
frontiers: BlockSyncFrontiers,
release_applied: bool,
},
FrontierReset {
frontiers: BlockSyncFrontiers,
preserve_active_successors: bool,
peer_has_successor_after: bool,
peer_outstanding_conflicts_at_tip: bool,
},
ApplyFinished {
token: BlockApplyToken,
height: block::Height,
hash: block::Hash,
result: BlockApplyResult,
local_frontier: Option<BlockSyncFrontiers>,
},
FundFloorReservation {
needed_bytes: u64,
reply: oneshot::Sender<bool>,
},
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub(super) struct SequencerView {
pub(super) verified_tip: block::Height,
pub(super) verified_hash: block::Hash,
pub(super) download_floor: block::Height,
pub(super) finalized: block::Height,
pub(super) reset_epoch: u64,
pub(super) reaction_epoch: u64,
pub(super) reorder_len: u64,
pub(super) applying_len: u64,
pub(super) reorder_buffered_bytes: u64,
pub(super) applying_buffered_bytes: u64,
pub(super) unsubmitted_applying_count: u64,
pub(super) submitted_applying_count: u64,
pub(super) submitted_applying_bytes: u64,
pub(super) committed_bytes_per_sec: u64,
pub(super) committed_blocks_per_sec: u64,
}
pub(super) fn initial_view(frontiers: BlockSyncFrontiers) -> SequencerView {
SequencerView {
verified_tip: frontiers.verified_block_tip,
verified_hash: frontiers.verified_block_hash,
download_floor: frontiers.verified_block_tip,
finalized: frontiers.finalized_height,
reset_epoch: 0,
reaction_epoch: 0,
reorder_len: 0,
applying_len: 0,
reorder_buffered_bytes: 0,
applying_buffered_bytes: 0,
unsubmitted_applying_count: 0,
submitted_applying_count: 0,
submitted_applying_bytes: 0,
committed_bytes_per_sec: 0,
committed_blocks_per_sec: 0,
}
}
pub(super) struct SequencerTask {
sequencer: Sequencer,
budget: ByteBudget,
work: Arc<WorkQueue>,
actions: mpsc::Sender<BlockSyncAction>,
committed_throughput: ThroughputMeter,
finalized_height: block::Height,
verified_block_hash: block::Hash,
reset_epoch: u64,
reaction_epoch: u64,
body_input_rx: mpsc::Receiver<SequencedBody>,
control_input_rx: mpsc::UnboundedReceiver<SequencerControlInput>,
body_input_bytes: Arc<std::sync::atomic::AtomicU64>,
view_tx: watch::Sender<SequencerView>,
action_send_timeout: Duration,
trace: ZakuraTrace,
}
impl SequencerTask {
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
sequencer: Sequencer,
budget: ByteBudget,
work: Arc<WorkQueue>,
actions: mpsc::Sender<BlockSyncAction>,
committed_throughput: ThroughputMeter,
frontiers: BlockSyncFrontiers,
body_input_rx: mpsc::Receiver<SequencedBody>,
control_input_rx: mpsc::UnboundedReceiver<SequencerControlInput>,
body_input_bytes: Arc<std::sync::atomic::AtomicU64>,
view_tx: watch::Sender<SequencerView>,
action_send_timeout: Duration,
trace: ZakuraTrace,
) -> Self {
Self {
sequencer,
budget,
work,
actions,
committed_throughput,
finalized_height: frontiers.finalized_height,
verified_block_hash: frontiers.verified_block_hash,
reset_epoch: 0,
reaction_epoch: 0,
body_input_rx,
control_input_rx,
body_input_bytes,
view_tx,
action_send_timeout,
trace,
}
}
pub(super) async fn run(mut self) {
let mut control_open = true;
let mut body_open = true;
loop {
if !control_open && !body_open {
break;
}
tokio::select! {
biased;
input = self.control_input_rx.recv(), if control_open => {
match input {
Some(input) => {
let needs_reaction = self.handle_control_input(input).await;
if needs_reaction {
self.reaction_epoch = self.reaction_epoch.saturating_add(1);
}
self.publish_view();
}
None => control_open = false,
}
}
body = self.body_input_rx.recv(), if body_open => {
match body {
Some(body) => {
self.release_body_input_bytes(body.bytes);
self.handle_accept_body(body).await;
shed_top_for_floor_starvation(
&mut self.budget,
&self.work,
&mut self.sequencer,
);
self.publish_view();
}
None => body_open = false,
}
}
}
}
}
async fn handle_control_input(&mut self, input: SequencerControlInput) -> bool {
match input {
SequencerControlInput::FrontierAdvance {
frontiers,
release_applied,
} => {
self.handle_frontier_advance(frontiers, release_applied)
.await;
true
}
SequencerControlInput::FrontierReset {
frontiers,
preserve_active_successors,
peer_has_successor_after,
peer_outstanding_conflicts_at_tip,
} => {
self.handle_frontier_reset(
frontiers,
preserve_active_successors,
peer_has_successor_after,
peer_outstanding_conflicts_at_tip,
)
.await;
true
}
SequencerControlInput::ApplyFinished {
token,
height,
hash,
result,
local_frontier,
} => {
self.handle_apply_finished(token, height, hash, result, local_frontier)
.await
}
SequencerControlInput::FundFloorReservation {
needed_bytes,
reply,
} => {
let shed = shed_top_until_available(
&mut self.budget,
&self.work,
&mut self.sequencer,
needed_bytes,
);
let _ = reply.send(self.budget.available() >= needed_bytes);
shed
}
}
}
fn release_body_input_bytes(&self, bytes: u64) {
let mut current = self
.body_input_bytes
.load(std::sync::atomic::Ordering::Relaxed);
loop {
let next = current.saturating_sub(bytes);
match self.body_input_bytes.compare_exchange_weak(
current,
next,
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
) {
Ok(_) => break,
Err(observed) => current = observed,
}
}
}
async fn handle_accept_body(&mut self, body: SequencedBody) {
let queued_elapsed = body.received_at.elapsed();
let outcome = match self.sequencer.accept_buffered_body(
body.height,
body.hash,
body.body,
body.bytes,
body.peer,
) {
AcceptOutcome::Buffered { .. } => "buffered",
AcceptOutcome::Redundant { release_bytes } => {
self.budget.release(release_bytes);
"redundant"
}
};
self.trace_body_accepted(body.height, queued_elapsed, outcome);
self.release_contiguous_blocks().await;
}
async fn handle_frontier_advance(
&mut self,
frontiers: BlockSyncFrontiers,
release_applied: bool,
) {
self.finalized_height = self.finalized_height.max(frontiers.finalized_height);
if frontiers.verified_block_tip < self.sequencer.verified_tip() {
return;
}
self.verified_block_hash = frontiers.verified_block_hash;
let advance = self
.sequencer
.advance_verified_tip(frontiers.verified_block_tip, release_applied);
self.budget.release(advance.release_bytes);
if advance.changed {
let released = self.work.advance_floor(frontiers.verified_block_tip);
self.budget.release(released);
self.release_contiguous_blocks().await;
}
}
async fn handle_frontier_reset(
&mut self,
frontiers: BlockSyncFrontiers,
preserve_active_successors: bool,
peer_has_successor_after: bool,
peer_outstanding_conflicts_at_tip: bool,
) {
let reset_tip_matches_local_work = !self.reset_tip_conflicts_with_local_work(
&frontiers,
frontiers.verified_block_tip <= self.sequencer.floor(),
peer_outstanding_conflicts_at_tip,
);
if frontiers.verified_block_tip > self.sequencer.verified_tip()
&& (frontiers.verified_block_tip <= self.sequencer.floor()
|| self.has_active_successor_after(
frontiers.verified_block_tip,
peer_has_successor_after,
))
&& reset_tip_matches_local_work
{
self.trace_frontier_reset_classified(
"growth",
&frontiers,
preserve_active_successors,
peer_has_successor_after,
peer_outstanding_conflicts_at_tip,
reset_tip_matches_local_work,
);
self.handle_frontier_advance(frontiers, true).await;
return;
}
metrics::counter!("sync.block.reorg.reset").increment(1);
if preserve_active_successors
&& frontiers.verified_block_tip < self.sequencer.floor()
&& reset_tip_matches_local_work
&& self
.has_active_successor_after(frontiers.verified_block_tip, peer_has_successor_after)
&& self.active_successor_links_to_anchor(
frontiers.verified_block_tip,
frontiers.verified_block_hash,
)
{
self.trace_frontier_reset_classified(
"preserved_stale",
&frontiers,
preserve_active_successors,
peer_has_successor_after,
peer_outstanding_conflicts_at_tip,
reset_tip_matches_local_work,
);
self.handle_frontier_advance(frontiers, true).await;
return;
}
self.trace_frontier_reset_classified(
"destructive",
&frontiers,
preserve_active_successors,
peer_has_successor_after,
peer_outstanding_conflicts_at_tip,
reset_tip_matches_local_work,
);
let remember_released_applies = frontiers.verified_block_tip > frontiers.finalized_height
&& frontiers.verified_block_tip <= self.sequencer.floor();
self.finalized_height = frontiers.finalized_height;
self.verified_block_hash = frontiers.verified_block_hash;
let released = self
.sequencer
.reset_to(frontiers.verified_block_tip, remember_released_applies);
self.budget.release(released);
let released = self.work.reset_above(self.sequencer.floor());
self.budget.release(released);
self.reset_epoch = self.reset_epoch.saturating_add(1);
}
async fn handle_apply_finished(
&mut self,
token: BlockApplyToken,
height: block::Height,
hash: block::Hash,
result: BlockApplyResult,
local_frontier: Option<BlockSyncFrontiers>,
) -> bool {
let Some((applying_token, applying_hash)) = self.sequencer.applying_token_hash(height)
else {
self.sequencer.decrement_submitted_apply(height, hash);
return false;
};
if applying_hash != hash || applying_token != token {
self.sequencer.decrement_submitted_apply(height, hash);
return false;
}
let accepted_local_frontier = if let Some(frontiers) = local_frontier {
if frontiers.verified_block_tip < self.sequencer.verified_tip() {
None
} else {
self.handle_frontier_advance(frontiers, false).await;
Some(frontiers)
}
} else {
None
};
if matches!(result, BlockApplyResult::Duplicate) && self.sequencer.verified_tip() < height {
return accepted_local_frontier.is_some();
}
let applying = self
.sequencer
.remove_applying(height)
.expect("applying entry exists because it was just checked");
self.budget.release(applying.bytes);
if matches!(result, BlockApplyResult::Committed) {
self.committed_throughput.record(applying.bytes);
}
self.sequencer.decrement_submitted_apply(height, hash);
match result {
BlockApplyResult::Committed | BlockApplyResult::Duplicate => {}
BlockApplyResult::Rejected | BlockApplyResult::TimedOut
if height > self.sequencer.verified_tip() =>
{
let released = self.sequencer.release_applying_blocks_from(height);
self.budget.release(released);
self.sequencer.reset_floor_below(height);
let released = self.work.reset_above(self.sequencer.floor());
self.budget.release(released);
let dropped = self.sequencer.drop_reorder_from(height);
self.budget.release(dropped);
if matches!(result, BlockApplyResult::Rejected) {
self.send_action(BlockSyncAction::Misbehavior {
peer: applying.source_peer.clone(),
reason: BlockSyncMisbehavior::InvalidBlock,
})
.await;
}
}
BlockApplyResult::Rejected | BlockApplyResult::TimedOut => {}
}
if let Some(frontiers) = accepted_local_frontier {
let released = self
.sequencer
.release_applied_through(frontiers.verified_block_tip);
self.budget.release(released);
}
self.release_contiguous_blocks().await;
true
}
async fn release_contiguous_blocks(&mut self) {
let _ = self.sequencer.drain_ready_into_applying();
self.submit_pending_blocks().await;
}
async fn submit_pending_blocks(&mut self) {
for height in self.sequencer.submittable_heights() {
let Some(item) = self.sequencer.prepare_submit(height) else {
continue;
};
metrics::counter!("sync.block.submit.sent").increment(1);
if !self
.send_action(BlockSyncAction::SubmitBlock {
token: item.token,
block: item.block,
})
.await
{
self.sequencer.unsubmit(item.height, item.token);
return;
}
self.sequencer
.record_submitted_apply(item.height, item.hash);
self.trace_body_submitted(item.height, item.token);
}
}
fn trace_body_submitted(&self, height: block::Height, token: BlockApplyToken) {
self.trace.emit_with(BLOCK_SYNC_TABLE, |row| {
row.insert(
bs_trace::EVENT.to_string(),
serde_json::Value::String(bs_trace::BLOCK_BODY_SUBMITTED.to_string()),
);
bs_insert_height(row, bs_trace::HEIGHT, height);
bs_insert_u64(row, bs_trace::APPLY_TOKEN, token);
});
}
fn trace_body_accepted(&self, height: block::Height, queued_elapsed: Duration, outcome: &str) {
self.trace.emit_with(BLOCK_SYNC_TABLE, |row| {
row.insert(
bs_trace::EVENT.to_string(),
serde_json::Value::String(bs_trace::BLOCK_BODY_ACCEPTED.to_string()),
);
bs_insert_height(row, bs_trace::HEIGHT, height);
bs_insert_u64(
row,
"sequencer_queue_elapsed_us",
u64::try_from(queued_elapsed.as_micros()).unwrap_or(u64::MAX),
);
row.insert(
bs_trace::RESULT.to_string(),
serde_json::Value::String(outcome.to_string()),
);
});
}
#[allow(clippy::too_many_arguments)]
fn trace_frontier_reset_classified(
&self,
classification: &'static str,
frontiers: &BlockSyncFrontiers,
preserve_active_successors: bool,
peer_has_successor_after: bool,
peer_outstanding_conflicts_at_tip: bool,
reset_tip_matches_local_work: bool,
) {
self.trace.emit_with(BLOCK_SYNC_TABLE, |row| {
bs_insert_str(
row,
bs_trace::EVENT,
bs_trace::BLOCK_FRONTIER_RESET_CLASSIFIED,
);
bs_insert_str(row, bs_trace::RESULT, classification);
bs_insert_height(
row,
bs_trace::VERIFIED_BLOCK_TIP,
frontiers.verified_block_tip,
);
bs_insert_height(row, "previous_verified_tip", self.sequencer.verified_tip());
bs_insert_height(row, "previous_download_floor", self.sequencer.floor());
bs_insert_u64(
row,
"preserve_active_successors",
u64::from(preserve_active_successors),
);
bs_insert_u64(
row,
"peer_has_successor_after",
u64::from(peer_has_successor_after),
);
bs_insert_u64(
row,
"peer_outstanding_conflicts_at_tip",
u64::from(peer_outstanding_conflicts_at_tip),
);
bs_insert_u64(
row,
"reset_tip_matches_local_work",
u64::from(reset_tip_matches_local_work),
);
bs_insert_u64(
row,
"has_local_successor_after",
u64::from(
next_height(frontiers.verified_block_tip)
.is_some_and(|next| self.sequencer.has_buffered_at_or_above(next)),
),
);
});
}
fn reset_tip_conflicts_with_local_work(
&self,
frontiers: &BlockSyncFrontiers,
ignore_non_material_conflicts: bool,
peer_outstanding_conflicts_at_tip: bool,
) -> bool {
let height = frontiers.verified_block_tip;
let hash = frontiers.verified_block_hash;
if self
.sequencer
.reorder_hash(height)
.is_some_and(|buffered_hash| buffered_hash != hash)
{
return true;
}
if self
.sequencer
.applying_hash(height)
.is_some_and(|applying_hash| applying_hash != hash)
{
return true;
}
if !ignore_non_material_conflicts
&& self.sequencer.submitted_has_only_other_hashes(height, hash)
{
return true;
}
if !ignore_non_material_conflicts && peer_outstanding_conflicts_at_tip {
return true;
}
false
}
fn has_active_successor_after(
&self,
height: block::Height,
peer_has_successor_after: bool,
) -> bool {
let Some(next) = next_height(height) else {
return false;
};
self.sequencer.has_buffered_at_or_above(next) || peer_has_successor_after
}
fn active_successor_links_to_anchor(
&self,
height: block::Height,
anchor_hash: block::Hash,
) -> bool {
let Some(next) = next_height(height) else {
return true;
};
self.sequencer
.applying_previous_block_hash(next)
.map(|previous_block_hash| previous_block_hash == anchor_hash)
.unwrap_or(true)
}
async fn send_action(&self, action: BlockSyncAction) -> bool {
match time::timeout(self.action_send_timeout, self.actions.send(action)).await {
Ok(Ok(())) => true,
Ok(Err(_)) => false,
Err(_) => {
metrics::counter!("sync.block.action.send_timeout").increment(1);
false
}
}
}
fn publish_view(&mut self) {
self.committed_throughput.sample(Instant::now());
let reorder_buffered_bytes = self.sequencer.reorder_buffered_bytes();
let applying_buffered_bytes = self.sequencer.applying_buffered_bytes();
let body_input_bytes = self
.body_input_bytes
.load(std::sync::atomic::Ordering::Relaxed);
let expected_budget = self
.work
.reserved_bytes()
.saturating_add(reorder_buffered_bytes)
.saturating_add(applying_buffered_bytes)
.saturating_add(body_input_bytes);
self.budget
.audit(expected_budget, "block-sync sequencer view");
let next = SequencerView {
verified_tip: self.sequencer.verified_tip(),
verified_hash: self.verified_block_hash,
download_floor: self.sequencer.floor(),
finalized: self.finalized_height,
reset_epoch: self.reset_epoch,
reaction_epoch: self.reaction_epoch,
reorder_len: self.sequencer.reorder_len() as u64,
applying_len: self.sequencer.applying_len() as u64,
reorder_buffered_bytes,
applying_buffered_bytes,
unsubmitted_applying_count: self.sequencer.unsubmitted_applying_count() as u64,
submitted_applying_count: self.sequencer.submitted_applying_count() as u64,
submitted_applying_bytes: self.sequencer.submitted_applying_bytes(),
committed_bytes_per_sec: self.committed_throughput.bytes_per_sec(),
committed_blocks_per_sec: self.committed_throughput.blocks_per_sec(),
};
publish_sequencer_view(&self.view_tx, next);
}
}
fn publish_sequencer_view(view_tx: &watch::Sender<SequencerView>, next: SequencerView) {
view_tx.send_if_modified(|current| {
let schedulable_changed = view_schedulable_ne(current, &next);
*current = next;
schedulable_changed
});
}
fn view_schedulable_ne(a: &SequencerView, b: &SequencerView) -> bool {
let strip_rates = |v: &SequencerView| {
let mut v = *v;
v.committed_bytes_per_sec = 0;
v.committed_blocks_per_sec = 0;
v
};
strip_rates(a) != strip_rates(b)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_view() -> SequencerView {
SequencerView {
verified_tip: block::Height(1),
verified_hash: block::Hash([1; 32]),
download_floor: block::Height(1),
finalized: block::Height(1),
reset_epoch: 0,
reaction_epoch: 0,
reorder_len: 0,
applying_len: 0,
reorder_buffered_bytes: 0,
applying_buffered_bytes: 0,
unsubmitted_applying_count: 0,
submitted_applying_count: 0,
submitted_applying_bytes: 0,
committed_bytes_per_sec: 0,
committed_blocks_per_sec: 0,
}
}
#[tokio::test(start_paused = true)]
async fn sequencer_view_rate_refresh_does_not_wake_watchers() {
let initial = test_view();
let (view_tx, mut view_rx) = watch::channel(initial);
let rate_only = SequencerView {
committed_bytes_per_sec: 1024,
committed_blocks_per_sec: 3,
..initial
};
publish_sequencer_view(&view_tx, rate_only);
assert_eq!(*view_rx.borrow(), rate_only);
assert!(
time::timeout(Duration::from_millis(1), view_rx.changed())
.await
.is_err(),
"throughput-only view refresh must not wake watchers"
);
let schedulable = SequencerView {
reaction_epoch: 1,
..rate_only
};
publish_sequencer_view(&view_tx, schedulable);
view_rx
.changed()
.await
.expect("sequencer view sender is still live");
assert_eq!(*view_rx.borrow(), schedulable);
}
}