{"record":{"id":"0c11f71b70f87e2c","repo":"linera-io/linera-protocol","slug":"block-advances-the-chain-s-epoch-from-start-epoch","errorCode":null,"errorMessage":"Block advances the chain's epoch from {start_epoch} to {end_epoch}; a block may advance the epoch at most once","messagePattern":"Block advances the chain's epoch from (.+?) to (.+?); a block may advance the epoch at most once","errorType":"validation","errorClass":"ChainError","httpStatus":null,"severity":"critical","filePath":"linera-chain/src/chain.rs","lineNumber":1135,"sourceCode":"                            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)\n            .await?\n        {\n            // Only `CheckpointAck`-only blocks are excluded from the chain-level\n            // tracking. Otherwise the recipient never acknowledges (a\n            // `CheckpointAck` doesn't trigger a return `CheckpointAck`), so the","sourceCodeStart":1117,"sourceCodeEnd":1153,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/chain.rs#L1117-L1153","documentation":"execute_block_inner enforces that a block advances the chain's epoch at most once: end_epoch must be <= start_epoch + 1 (linera-chain/src/chain.rs:1132-1141), so consecutive blocks never skip an epoch. MultipleEpochAdvances means execution of the block moved the epoch by two or more — the block is invalid per protocol and is rejected before confirmation.","triggerScenarios":"A block containing more than one epoch-changing system operation (e.g., two ChangeEpoch admin operations in one proposal); re-execution with replayed oracle responses that apply an epoch change twice; hand-constructed blocks in tests stacking admin operations.","commonSituations":"Automation scripts batching all pending admin operations into a single block during committee migration; version skew where a client composes epoch operations differently than the node expects.","solutions":["Split the epoch changes across consecutive blocks: at most one ChangeEpoch per block, then propose the next epoch change in the following block","When composing admin batches, sequence them so each block's execution ends at most one epoch ahead of where it started","If this appears during re-execution of an already-confirmed block, suspect corrupted replayed oracle responses and re-sync from peers"],"exampleFix":"// before: both epoch changes in one block\nlet block = ProposedBlock { transactions: vec![\n    Transaction::ExecuteOperation(SystemOperation::ChangeEpoch(new_epoch_1).into()),\n    Transaction::ExecuteOperation(SystemOperation::ChangeEpoch(new_epoch_2).into()),\n], .. };\n\n// after: one epoch advance per block; wait for confirmation in between\nclient.submit_block(vec![SystemOperation::ChangeEpoch(new_epoch_1).into()]).await?;\nclient.wait_for_epoch(new_epoch_1).await?;\nclient.submit_block(vec![SystemOperation::ChangeEpoch(new_epoch_2).into()]).await?;","handlingStrategy":"validation","validationCode":"// Before submitting, count epoch-changing operations (heuristic mirror of the\n// post-execution rule — at most one per block):\nlet epoch_changes = block.operations()\n    .filter(|op| matches!(op, Operation::System(SystemOperation::ChangeEpoch(_))))\n    .count();\nif epoch_changes > 1 {\n    anyhow::bail!(\"split epoch changes across blocks: a block may advance the epoch once\");\n}","typeGuard":null,"tryCatchPattern":"match result {\n    Err(ChainError::MultipleEpochAdvances { start_epoch, end_epoch }) => {\n        // split the block: one ChangeEpoch per block, confirm, then continue\n    }\n    other => other?,\n}","preventionTips":["Sequence committee migrations one epoch per block","Never batch multiple ChangeEpoch admin operations into one proposal","During epoch migration, verify each block's end epoch before proposing the next"],"tags":["linera","epoch","governance","protocol-rules","rust"],"backgroundTag":"multiple-epoch-advance","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}