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

  1. Do not set block.timestamp manually — use the client's default derivation, which already accounts for bundle timestamps
  2. If you must set it, use max(parent_timestamp, max(bundle.timestamp), local_time) or later
  3. 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

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


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