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

  1. Process the pending earlier bundle(s) from the same origin first — receive or explicitly reject their messages — before removing the later bundle.
  2. When building blocks, take bundles from the inbox front in cursor order rather than selecting arbitrary bundles.
  3. If the earlier bundle's messages should never execute, mark them rejected in the block so the bundle becomes skippable.
  4. 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

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


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/3915c541c71bbde2. Report an issue: GitHub.