linera-io/linera-protocol · warning · ChainError
Checkpoint precondition failed: Checkpoint must be the first
Error message
Checkpoint precondition failed: Checkpoint must be the first transaction in its block
What it means
execute_block enforces the structural checkpoint invariant (linera-chain/src/chain.rs:1343-1352): no transaction after index 0 may be a checkpoint — `Transaction::is_checkpoint()` must be false for all but the first position. Per the block docs (data_types/mod.rs:100-103) a checkpoint must be the first and effectively the only transaction of its block. Violating this rejects the block before execution.
Source
Thrown at linera-chain/src/chain.rs:1343
.map(|blob| blob.id())
.collect::<BTreeSet<_>>(),
ChainError::InternalError("published_blobs mismatch".to_string())
);
if *self.execution_state.system.closed.get() {
ensure!(block.has_only_rejected_messages(), ChainError::ClosedChain);
}
Self::check_app_permissions(
self.execution_state
.system
.application_permissions
.get()
.await?,
&block,
)?;
ensure!(
!block
.transactions
.iter()
.skip(1)
.any(Transaction::is_checkpoint),
ChainError::CheckpointPreconditionFailed(
"Checkpoint must be the first transaction in its block",
)
);
let (origin_cursors, inbox_cursors, outbox_block_hashes) = if block.starts_with_checkpoint()
{
self.check_checkpoint_preconditions().await?;
let origin_cursors = self.collect_inbox_cursors().await?;
let inbox_cursors = self.collect_all_inbox_cursors().await?;
let hashes = self.collect_unfinalized_block_hashes().await?;
(origin_cursors, inbox_cursors, hashes)
} else {
(Vec::new(), Vec::new(), Vec::new())View on GitHub (pinned to 6c226ddcb3)
Solutions
- Put the checkpoint transaction first — and per the docs, alone — in its own block
- If other transactions must run, confirm them in a separate block before the checkpoint block
- Add a pre-submit assertion mirroring the check: no is_checkpoint() among transactions[1..]
Example fix
// before: checkpoint appended after other work
let mut txs = user_transactions;
txs.push(Transaction::ExecuteOperation(Operation::system(SystemOperation::Checkpoint)));
// after: checkpoint leads its own block
let checkpoint_block = ProposedBlock {
transactions: vec![Transaction::ExecuteOperation(Operation::system(SystemOperation::Checkpoint))],
..
};
client.submit_block(user_transactions).await?; // block 1: work
client.submit_block(checkpoint_block).await?; // block 2: checkpoint Defensive patterns
Strategy: validation
Validate before calling
// Mirror the structural rule before submitting:
if block.transactions.iter().skip(1).any(|t| t.is_checkpoint()) {
anyhow::bail!("checkpoint must be the first transaction; restructure the block");
}
// best practice: put the checkpoint alone in its own block Type guard
fn checkpoint_well_placed(block: &ProposedBlock) -> bool {
// no checkpoint after position 0 (execute_block's rule, chain.rs:1343)
!block.transactions.iter().skip(1).any(Transaction::is_checkpoint)
} Try / catch
match result {
Err(ChainError::CheckpointPreconditionFailed(msg)) => {
// structural fix required: move the checkpoint to position 0 / its own block,
// then re-propose
}
other => other?,
} Prevention
- Always place a Checkpoint operation first — ideally alone — in its block
- Never append checkpoint ops to a block that already has transactions
- Automate checkpoints through a dedicated builder that cannot mix them with user transactions
When it happens
Trigger: Composing a block where a SystemOperation::Checkpoint follows other transactions (SDK appending the checkpoint at the end of a batch); scripts that build 'do everything including checkpoint' blocks; deserialized proposals that were reordered.
Common situations: Checkpoint automation naively batched with pending user operations; client code that pushes the checkpoint op onto a non-empty transaction list; migrations constructing blocks field-by-field.
Related errors
- Incoming message bundle in block proposed to {chain_id} has
- Checkpoint precondition failed: chain has consumed system ev
- MetaMask is not connected with the requested owner: ${owner}
- owner should be different from spender
- Invalid address value: {s}
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/8a4130ce4c2f85a1.
Report an issue: GitHub.