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'.
Solutions
- Retry later: the discarded bundles are re-queued on the sender side, so a subsequent proposal after conditions change can succeed
- Include at least one always-succeeding operation (e.g., a minimal transfer) in the block so it can never end up empty
- 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
- Anchor bundle-only blocks with one trivial operation
- Watch resource-limit metrics on senders whose bundles target your chains
- Remember discarded (not rejected) bundles are retried in later blocks — this error can clear on its own
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
- Block timestamp must not be earlier than the parent block's…
- Cannot vote for block proposal of chain
- Chain is expecting a next block at height
- Incoming message bundle in block proposed to
- MaximumFuelExceeded
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/1b6961de6890963b.
Report an issue: GitHub.
Appendix: 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)