linera-io/linera-protocol · error · ChainError

Empty blocks are not allowed

Error message

Empty blocks are not allowed

What it means

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'.

Source

Thrown at linera-chain/src/chain.rs:1130

                        Self::discard_remaining_bundles(block, i, None);
                        Self::discard_remaining_stream_updates(block, i);
                    } else {
                        info!(
                            %error,
                            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)

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Retry later: the discarded bundles are re-queued on the sender side, so a subsequent proposal after conditions change can succeed
  2. Include at least one always-succeeding operation (e.g., a minimal transfer) in the block so it can never end up empty
  3. Ask the sender to split oversized bundles, or (if you govern the chain) adjust resource limits in the policy

Example fix

// before: proposing a block of only risky incoming bundles
let txs = bundles.into_iter().map(Transaction::ReceiveMessages).collect();
let block = ProposedBlock { transactions: txs, .. };

// after: anchor the block with one guaranteed operation
let mut txs = vec![Transaction::ExecuteOperation(Operation::system(
    SystemOperation::Transfer { .. } // minimal, always admissible
))];
txs.extend(bundles.into_iter().map(Transaction::ReceiveMessages));
let block = ProposedBlock { transactions: txs, .. };
Defensive patterns

Strategy: fallback

Validate before calling

// Before proposing, detect the risky shape: a block made only of incoming bundles
// that resource limits might discard:
let only_bundles = !block.transactions.is_empty()
    && block.transactions.iter().all(|t| matches!(t, Transaction::ReceiveMessages(_)));
if only_bundles {
    // add a fallback transaction so the block can never end up empty
}

Try / catch

match result {
    Err(ChainError::EmptyBlock) => {
        // every bundle was discarded for limits: retry later (bundles stay queued
        // on the sender) or add a minimal always-valid operation and re-propose
    }
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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