linera-io/linera-protocol · error · ChainError
Incoming message bundle in block proposed to {chain_id} has
Error message
Incoming message bundle in block proposed to {chain_id} has timestamp {bundle_timestamp:}, which is later than the block timestamp {block_timestamp:}. What it means
remove_bundles_from_inboxes (linera-chain/src/chain.rs:721) verifies that every incoming bundle's timestamp is <= the receiving block's timestamp (chain.rs:730-737). Bundles carry the timestamp of the sender block that produced them, so a block cannot claim to receive messages 'from the future' — its own timestamp must not be earlier than any bundle it includes. The check runs on the proposal path (must_be_present=true) before execution.
Source
Thrown at linera-chain/src/chain.rs:730
/// Removes the incoming message bundles in the block from the inboxes.
///
/// If `must_be_present` is `true`, an error is returned if any of the bundles have not been
/// added to the inbox yet. So this should be `true` if the bundles are in a block _proposal_,
/// and `false` if the block is already confirmed.
#[instrument(skip_all, fields(
chain_id = %self.chain_id(),
))]
pub async fn remove_bundles_from_inboxes(
&mut self,
timestamp: Timestamp,
must_be_present: bool,
incoming_bundles: impl IntoIterator<Item = &IncomingBundle>,
) -> Result<(), ChainError> {
let chain_id = self.chain_id();
let mut bundles_by_origin: BTreeMap<_, Vec<&MessageBundle>> = Default::default();
for IncomingBundle { bundle, origin, .. } in incoming_bundles {
ensure!(
bundle.timestamp <= timestamp,
ChainError::IncorrectBundleTimestamp {
chain_id,
bundle_timestamp: bundle.timestamp,
block_timestamp: timestamp,
}
);
let bundles = bundles_by_origin.entry(*origin).or_default();
bundles.push(bundle);
}
let origins = bundles_by_origin.keys().copied().collect::<Vec<_>>();
let inboxes = self.inboxes.try_load_entries_mut(&origins).await?;
// When the bundles must already be present (block proposals), collect *every* missing
// `(origin, height)` rather than bailing on the first, so the caller can be told the
// full set of cross-chain updates to fetch in a single round-trip.
let mut missing_bundles = Vec::new();
for ((origin, bundles), mut inbox) in bundles_by_origin.into_iter().zip(inboxes) {
tracing::trace!(View on GitHub (pinned to 6c226ddcb3)
Solutions
- Do not set block.timestamp manually — use the client's default derivation, which already accounts for bundle timestamps
- If you must set it, use max(parent_timestamp, max(bundle.timestamp), local_time) or later
- Retry after wall-clock time passes the highest bundle timestamp if timestamps come from local clocks
Example fix
// before: pinning the block timestamp
let block = ProposedBlock { timestamp: Timestamp::from(1_000), incoming_bundles, .. };
// after: at least the newest bundle timestamp, e.g. let the client derive it
let max_bundle_ts = incoming_bundles.iter().map(|b| b.bundle.timestamp).max().unwrap_or_default();
let block = ProposedBlock { timestamp: local_time.max(max_bundle_ts), incoming_bundles, .. }; Defensive patterns
Strategy: validation
Validate before calling
// Before proposing, ensure the block timestamp covers every bundle:
let max_bundle_ts = block
.incoming_bundles()
.map(|b| b.bundle.timestamp)
.max()
.unwrap_or_default();
if block.timestamp < max_bundle_ts {
block.timestamp = max_bundle_ts.max(local_time); // or let the client derive it
} Try / catch
match result {
Err(ChainError::IncorrectBundleTimestamp { bundle_timestamp, block_timestamp, .. }) => {
// bump block.timestamp to >= bundle_timestamp and re-propose
}
other => other?,
} Prevention
- Never pin block timestamps manually; use the client default (max of parent, bundles, local time)
- Keep proposing machines' clocks NTP-synced
- In tests, derive timestamps relative to the chains involved rather than constants
When it happens
Trigger: Manually setting ProposedBlock.timestamp below the timestamp of an included incoming bundle; a client clock behind the sender chain's clock; test code pinning block timestamps to fixed values while bundles from another chain carry later timestamps.
Common situations: Developers overriding block timestamps instead of letting the client derive them (the default takes the max of parent timestamp, incoming bundle timestamps, and local time); clock skew between chains operated by different machines; fixtures with hardcoded timestamps that drift out of validity.
Related errors
- Cannot vote for block proposal of chain {chain_id} because {
- Empty blocks are not allowed
- Block timestamp {new} must not be earlier than the parent bl
- Checkpoint precondition failed: Checkpoint must be the first
- Local error handling validator response: validator still rep
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/59ad817570856710.
Report an issue: GitHub.