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

  1. Put the checkpoint transaction first — and per the docs, alone — in its own block
  2. If other transactions must run, confirm them in a separate block before the checkpoint block
  3. 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

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


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/8a4130ce4c2f85a1. Report an issue: GitHub.