{"record":{"id":"3915c541c71bbde2","repo":"linera-io/linera-protocol","slug":"bundle-cannot-be-skipped-it-must-be-received","errorCode":null,"errorMessage":"{bundle:?} cannot be skipped: it must be received before the next messages from the same origin","messagePattern":"(.+?) cannot be skipped: it must be received before the next messages from the same origin","errorType":"validation","errorClass":"InboxError","httpStatus":null,"severity":"error","filePath":"linera-chain/src/inbox.rs","lineNumber":235,"sourceCode":"        if cursor < *self.restored_cursor.get() {\n            // Bundle's effects are already in the restored execution state; treat the\n            // consumption as a no-op without touching `removed_bundles` so the queue\n            // doesn't fill with bundles a sender will never push again.\n            return Ok(true);\n        }\n        ensure!(\n            cursor >= *self.next_cursor_to_remove.get(),\n            InboxError::IncorrectOrder {\n                bundle: bundle.clone(),\n                next_cursor: *self.next_cursor_to_remove.get(),\n            }\n        );\n        // Discard added bundles with lower cursors (if any).\n        while let Some(previous_bundle) = self.added_bundles.front().await? {\n            if previous_bundle.cursor() >= cursor {\n                break;\n            }\n            ensure!(\n                previous_bundle.is_skippable(),\n                InboxError::UnskippableBundle {\n                    bundle: previous_bundle\n                }\n            );\n            self.added_bundles.delete_front();\n            tracing::trace!(\"Skipping previously received bundle {:?}\", previous_bundle);\n        }\n        // Reconcile the bundle with the next added bundle, or mark it as removed.\n        let already_known = match self.added_bundles.front().await? {\n            Some(previous_bundle) => {\n                // Rationale: If the two cursors are equal, then the bundles should match.\n                // Otherwise, at this point we know that `self.next_cursor_to_add >\n                // previous_bundle.cursor() > cursor`. Notably, `bundle` will never be\n                // added in the future. Therefore, we should fail instead of adding\n                // it to `self.removed_bundles`.\n                ensure!(\n                    bundle == &previous_bundle,","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/inbox.rs#L217-L253","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n// receiving only the newest bundle from origin O\ninbox.remove_bundle(&newer_bundle).await?; // Err: older unskippable bundle in front\n\n// after\n// drain the front bundle first by receiving/rejecting its messages in the block\nfor bundle in front_bundles_in_order {\n    if !bundle.is_skippable() {\n        block.receive_messages_of(&bundle); // or reject them\n    }\n    inbox.remove_bundle(&bundle).await?;\n}\ninbox.remove_bundle(&newer_bundle).await?;","handlingStrategy":"validation","validationCode":"// receive or reject pending unskippable bundles from the same origin first\nwhile let Some(front) = inbox.added_bundles.front().await? {\n    if front.cursor() >= bundle.cursor() { break; }\n    ensure!(front.is_skippable(), \"must receive bundle {:?} first\", front);\n}","typeGuard":null,"tryCatchPattern":"match inbox.remove_bundle(&bundle).await {\n    Err(InboxError::UnskippableBundle { bundle }) => {\n        // schedule the pending bundle's messages for receive/reject in this block, then retry\n        Err(anyhow::anyhow!(\"block must handle pending bundle {:?} first\", bundle))\n    }\n    other => other,\n}","preventionTips":["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."],"tags":["inbox","cross-chain-messages","ordering","skipping"],"backgroundTag":"unskippable-message-blocking-inbox","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}