linera-io/linera-protocol · error · InboxError
{bundle:?} cannot be skipped: it must be received before the
Error message
{bundle:?} cannot be skipped: it must be received before the next messages from the same origin What it means
Raised by InboxStateView::remove_bundle when it must discard a previously received bundle with a lower cursor, but that bundle is not marked skippable (InboxError::UnskippableBundle, surfaced as ChainError::CannotSkipMessage). While consuming a bundle, remove_bundle pops added bundles whose cursor is below the target; skipping is only legal for bundles the protocol allows to be jumped over (e.g. rejected/failed messages). A non-skippable bundle must be received (its messages processed or explicitly rejected) before anything later from the same origin.
Source
Thrown at linera-chain/src/inbox.rs:235
if cursor < *self.restored_cursor.get() {
// Bundle's effects are already in the restored execution state; treat the
// consumption as a no-op without touching `removed_bundles` so the queue
// doesn't fill with bundles a sender will never push again.
return Ok(true);
}
ensure!(
cursor >= *self.next_cursor_to_remove.get(),
InboxError::IncorrectOrder {
bundle: bundle.clone(),
next_cursor: *self.next_cursor_to_remove.get(),
}
);
// Discard added bundles with lower cursors (if any).
while let Some(previous_bundle) = self.added_bundles.front().await? {
if previous_bundle.cursor() >= cursor {
break;
}
ensure!(
previous_bundle.is_skippable(),
InboxError::UnskippableBundle {
bundle: previous_bundle
}
);
self.added_bundles.delete_front();
tracing::trace!("Skipping previously received bundle {:?}", previous_bundle);
}
// Reconcile the bundle with the next added bundle, or mark it as removed.
let already_known = match self.added_bundles.front().await? {
Some(previous_bundle) => {
// Rationale: If the two cursors are equal, then the bundles should match.
// Otherwise, at this point we know that `self.next_cursor_to_add >
// previous_bundle.cursor() > cursor`. Notably, `bundle` will never be
// added in the future. Therefore, we should fail instead of adding
// it to `self.removed_bundles`.
ensure!(
bundle == &previous_bundle,View on GitHub (pinned to 6c226ddcb3)
Solutions
- Process the pending earlier bundle(s) from the same origin first — receive or explicitly reject their messages — before removing the later bundle.
- When building blocks, take bundles from the inbox front in cursor order rather than selecting arbitrary bundles.
- If the earlier bundle's messages should never execute, mark them rejected in the block so the bundle becomes skippable.
- Check bundle.is_skippable() and inbox front ordering before scheduling the removal.
Example fix
// before
// receiving only the newest bundle from origin O
inbox.remove_bundle(&newer_bundle).await?; // Err: older unskippable bundle in front
// after
// drain the front bundle first by receiving/rejecting its messages in the block
for bundle in front_bundles_in_order {
if !bundle.is_skippable() {
block.receive_messages_of(&bundle); // or reject them
}
inbox.remove_bundle(&bundle).await?;
}
inbox.remove_bundle(&newer_bundle).await?; Defensive patterns
Strategy: validation
Validate before calling
// receive or reject pending unskippable bundles from the same origin first
while let Some(front) = inbox.added_bundles.front().await? {
if front.cursor() >= bundle.cursor() { break; }
ensure!(front.is_skippable(), "must receive bundle {:?} first", front);
} Try / catch
match inbox.remove_bundle(&bundle).await {
Err(InboxError::UnskippableBundle { bundle }) => {
// schedule the pending bundle's messages for receive/reject in this block, then retry
Err(anyhow::anyhow!("block must handle pending bundle {:?} first", bundle))
}
other => other,
} Prevention
- Always drain the inbox front in cursor order when building blocks.
- Mark unwanted messages as rejected in the block so their bundle becomes skippable.
- Never select bundles out of order by content filters.
When it happens
Trigger: Calling remove_bundle for a bundle whose cursor is above the front of added_bundles, where that front bundle's is_skippable() returns false — e.g. a block declares it receives message N+1 while message N (containing guaranteed or tracking messages) is still pending in the inbox.
Common situations: Application logic that selects which incoming messages to process per block without respecting origin ordering; a sender publishing multiple bundles to the same origin and the receiver trying to consume the later one first; cross-chain updates applied out of order after a partial sync.
Related errors
- {bundle:?} is out of order. Block and height should be at le
- Cannot reconcile {bundle:?} with {previous_bundle:?}
- InvalidCrossChainRequest
- Cannot vote for block proposal of chain {chain_id} because {
- UnexpectedBlockHeight
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/3915c541c71bbde2.
Report an issue: GitHub.