{"record":{"id":"1b6961de6890963b","repo":"linera-io/linera-protocol","slug":"empty-blocks-are-not-allowed","errorCode":null,"errorMessage":"Empty blocks are not allowed","messagePattern":"Empty blocks are not allowed","errorType":"validation","errorClass":"ChainError","httpStatus":null,"severity":"error","filePath":"linera-chain/src/chain.rs","lineNumber":1130,"sourceCode":"                        Self::discard_remaining_bundles(block, i, None);\n                        Self::discard_remaining_stream_updates(block, i);\n                    } else {\n                        info!(\n                            %error,\n                            index = i,\n                            \"UpdateStream exceeded block limits, discarding for retry\"\n                        );\n                        block.transactions.remove(i);\n                    }\n                    // Do not increment i - the next transaction is now at i.\n                }\n                (Err(e), _, _) => return Err(e),\n            };\n        }\n\n        // This can only happen if all transactions were incoming bundles that all got discarded\n        // due to resource limit errors. This is unlikely in practice but theoretically possible.\n        ensure!(!block.transactions.is_empty(), ChainError::EmptyBlock);\n\n        // A block may advance the epoch at most once, so that consecutive blocks never skip\n        // an epoch: the child of a block in epoch `e` is at most in epoch `e + 1`.\n        let end_epoch = *chain.system.epoch.get();\n        ensure!(\n            end_epoch.0 <= start_epoch.0.saturating_add(1),\n            ChainError::MultipleEpochAdvances {\n                start_epoch,\n                end_epoch,\n            }\n        );\n\n        let recipients = block_execution_tracker.recipients();\n        let non_ack_tx_indices = block_execution_tracker.non_checkpoint_ack_tx_indices();\n        let mut recipient_heights = Vec::new();\n        for (recipient, height) in chain\n            .previous_message_blocks\n            .multi_get_pairs(recipients)","sourceCodeStart":1112,"sourceCodeEnd":1148,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/chain.rs#L1112-L1148","documentation":"execute_block_inner (linera-chain/src/chain.rs:1130) fires EmptyBlock after the execution loop when every transaction was an incoming bundle that got discarded — not rejected — due to resource limit errors (the discard path at chain.rs:1060-1123 removes them for retry in a later block). If all transactions are removed this way, the block would confirm nothing, which Linera forbids. The code comment calls it 'unlikely in practice but theoretically possible'.","triggerScenarios":"A proposal consisting solely of incoming bundles that all exceed block resource limits (fuel, message size, event counts) and whose senders are protected/never-reject, forcing discard instead of reject; a lowered resource policy making previously-fine bundles oversized.","commonSituations":"Adversarial or buggy senders publishing oversized bundles to a recipient chain; committee policy tightening limits so queued bundles no longer fit; chains whose every pending message comes from one heavy application.","solutions":["Retry later: the discarded bundles are re-queued on the sender side, so a subsequent proposal after conditions change can succeed","Include at least one always-succeeding operation (e.g., a minimal transfer) in the block so it can never end up empty","Ask the sender to split oversized bundles, or (if you govern the chain) adjust resource limits in the policy"],"exampleFix":"// before: proposing a block of only risky incoming bundles\nlet txs = bundles.into_iter().map(Transaction::ReceiveMessages).collect();\nlet block = ProposedBlock { transactions: txs, .. };\n\n// after: anchor the block with one guaranteed operation\nlet mut txs = vec![Transaction::ExecuteOperation(Operation::system(\n    SystemOperation::Transfer { .. } // minimal, always admissible\n))];\ntxs.extend(bundles.into_iter().map(Transaction::ReceiveMessages));\nlet block = ProposedBlock { transactions: txs, .. };","handlingStrategy":"fallback","validationCode":"// Before proposing, detect the risky shape: a block made only of incoming bundles\n// that resource limits might discard:\nlet only_bundles = !block.transactions.is_empty()\n    && block.transactions.iter().all(|t| matches!(t, Transaction::ReceiveMessages(_)));\nif only_bundles {\n    // add a fallback transaction so the block can never end up empty\n}","typeGuard":null,"tryCatchPattern":"match result {\n    Err(ChainError::EmptyBlock) => {\n        // every bundle was discarded for limits: retry later (bundles stay queued\n        // on the sender) or add a minimal always-valid operation and re-propose\n    }\n    other => other?,\n}","preventionTips":["Anchor bundle-only blocks with one trivial operation","Watch resource-limit metrics on senders whose bundles target your chains","Remember discarded (not rejected) bundles are retried in later blocks — this error can clear on its own"],"tags":["linera","block","execution","resource-limits","messages","rust"],"backgroundTag":"empty-block-rejected","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}