linera-io/linera-protocol · critical · ChainError
Block advances the chain's epoch from {start_epoch} to {end_
Error message
Block advances the chain's epoch from {start_epoch} to {end_epoch}; a block may advance the epoch at most once What it means
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.
Source
Thrown at linera-chain/src/chain.rs:1135
index = i,
"UpdateStream exceeded block limits, discarding for retry"
);
block.transactions.remove(i);
}
// Do not increment i - the next transaction is now at i.
}
(Err(e), _, _) => return Err(e),
};
}
// This can only happen if all transactions were incoming bundles that all got discarded
// due to resource limit errors. This is unlikely in practice but theoretically possible.
ensure!(!block.transactions.is_empty(), ChainError::EmptyBlock);
// A block may advance the epoch at most once, so that consecutive blocks never skip
// an epoch: the child of a block in epoch `e` is at most in epoch `e + 1`.
let end_epoch = *chain.system.epoch.get();
ensure!(
end_epoch.0 <= start_epoch.0.saturating_add(1),
ChainError::MultipleEpochAdvances {
start_epoch,
end_epoch,
}
);
let recipients = block_execution_tracker.recipients();
let non_ack_tx_indices = block_execution_tracker.non_checkpoint_ack_tx_indices();
let mut recipient_heights = Vec::new();
for (recipient, height) in chain
.previous_message_blocks
.multi_get_pairs(recipients)
.await?
{
// Only `CheckpointAck`-only blocks are excluded from the chain-level
// tracking. Otherwise the recipient never acknowledges (a
// `CheckpointAck` doesn't trigger a return `CheckpointAck`), so theView on GitHub (pinned to 6c226ddcb3)
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
Example fix
// before: both epoch changes in one block
let block = ProposedBlock { transactions: vec![
Transaction::ExecuteOperation(SystemOperation::ChangeEpoch(new_epoch_1).into()),
Transaction::ExecuteOperation(SystemOperation::ChangeEpoch(new_epoch_2).into()),
], .. };
// after: one epoch advance per block; wait for confirmation in between
client.submit_block(vec![SystemOperation::ChangeEpoch(new_epoch_1).into()]).await?;
client.wait_for_epoch(new_epoch_1).await?;
client.submit_block(vec![SystemOperation::ChangeEpoch(new_epoch_2).into()]).await?; Defensive patterns
Strategy: validation
Validate before calling
// Before submitting, count epoch-changing operations (heuristic mirror of the
// post-execution rule — at most one per block):
let epoch_changes = block.operations()
.filter(|op| matches!(op, Operation::System(SystemOperation::ChangeEpoch(_))))
.count();
if epoch_changes > 1 {
anyhow::bail!("split epoch changes across blocks: a block may advance the epoch once");
} Try / catch
match result {
Err(ChainError::MultipleEpochAdvances { start_epoch, end_epoch }) => {
// split the block: one ChangeEpoch per block, confirm, then continue
}
other => other?,
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- MetaMask is not connected with the requested owner: ${owner}
- InvalidCommitteeRemoval
- Default wallet directory is not supported in this platform:
- nonexistent chain `{chain_id}`
- keypair not found for chain `{chain_id}`
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/0c11f71b70f87e2c.
Report an issue: GitHub.