Consumer context: gas-killer/roadmap#18. Service re-pins its rev in gas-killer/service#375.
Problem
A skip certificate never reaches the application. Submitter::handle_certified (router/src/submitter.rs:161-165) notifies the sequencer with ResolutionKind::Skipped and returns without touching the handler:
if digest == skip_digest(&self.namespace, height) {
info!(height, "skip certificate observed; nothing to submit");
self.notify(height, ResolutionKind::Skipped);
return;
}
That is correct — there is nothing to submit. But it means the application has no callback on the one resolution path that does not run its handler, so it cannot settle the task that was skipped.
Today the Gas Killer router works around this with a single-slot heuristic: "if the in-flight slot is still occupied when the next task is dequeued, the previous height must have been skipped." That heuristic is precisely what breaks with a window (see gas-killer/service#364 — it can mark a healthy in-flight task as failed and then attribute its payload to a different task).
The alternative for the consumer is a timeout-based sweeper, which works but is coarse: it cannot distinguish "skipped" from "slow" without waiting out (W+1) * ROUND_TIMEOUT.
Scope
Add a default-bodied hook to the TaskSource trait:
#[async_trait::async_trait]
pub trait TaskSource<T: TaskData>: Send {
async fn next_task(&mut self) -> Option<SequencedTask<T>>;
/// Called once when a height this source's task was assigned to resolves.
/// Lets the application settle its own bookkeeping — in particular for
/// `Skipped`, which never reaches the verification handler.
async fn on_resolution(&self, _sequenced: &SequencedTask<T>, _kind: ResolutionKind) {}
}
Called from the sequencer's resolution handling, passing the SequencedTask the sequencer already holds for that height, so the application gets its own task back without any identifier needing to traverse SequencedTask/Assignment/Submitter.
- Default empty body → zero breakage for any other implementor
- Call it for every
ResolutionKind, not just Skipped; a consumer that wants only skips can match on the kind. Uniform is easier to reason about than special-cased.
- Document that it is called at most once per (task, height) pair, and that a task re-assigned after
Foreign/Superseded will see the hook once per height it occupied
Acceptance
~10 lines. Small, but it is what lets the consumer settle skipped tasks promptly and exactly instead of inferring them from a timer.
Consumer context: gas-killer/roadmap#18. Service re-pins its
revin gas-killer/service#375.Problem
A skip certificate never reaches the application.
Submitter::handle_certified(router/src/submitter.rs:161-165) notifies the sequencer withResolutionKind::Skippedand returns without touching the handler:That is correct — there is nothing to submit. But it means the application has no callback on the one resolution path that does not run its handler, so it cannot settle the task that was skipped.
Today the Gas Killer router works around this with a single-slot heuristic: "if the in-flight slot is still occupied when the next task is dequeued, the previous height must have been skipped." That heuristic is precisely what breaks with a window (see gas-killer/service#364 — it can mark a healthy in-flight task as failed and then attribute its payload to a different task).
The alternative for the consumer is a timeout-based sweeper, which works but is coarse: it cannot distinguish "skipped" from "slow" without waiting out
(W+1) * ROUND_TIMEOUT.Scope
Add a default-bodied hook to the
TaskSourcetrait:Called from the sequencer's resolution handling, passing the
SequencedTaskthe sequencer already holds for that height, so the application gets its own task back without any identifier needing to traverseSequencedTask/Assignment/Submitter.ResolutionKind, not justSkipped; a consumer that wants only skips can match on the kind. Uniform is easier to reason about than special-cased.Foreign/Supersededwill see the hook once per height it occupiedAcceptance
Skippedexactly onceForeignresolution followed by re-assignment and success invokes the hook twice with the right kinds~10 lines. Small, but it is what lets the consumer settle skipped tasks promptly and exactly instead of inferring them from a timer.