linera-io/linera-protocol · warning · WorkerError
UnexpectedBlob
UnexpectedBlob
Error message
Blob was not required by any pending block
What it means
Validators only store blobs that a pending block actually requires. handle_pending_blob inserts into pending_validated_blobs and every pending_proposed_blobs entry via maybe_insert, which returns true only when the blob id was already announced by a validated block or by a proposal's expected blob ids. If no pending block lists the blob, the request fails with UnexpectedBlob, so validators cannot be used as arbitrary blob storage.
Source
Thrown at linera-core/src/chain_worker/state.rs:2288
.pending_proposed_blobs
.try_load_all_entries_mut()
.await?
{
if !pending_blobs.validated.get() {
let (_, committee) = self.chain.current_committee().await?;
let policy = committee.policy();
policy
.check_blob_size(blob.content())
.with_execution_context(ChainExecutionContext::Block)?;
ensure!(
u64::try_from(pending_blobs.pending_blobs.iterative_count().await?)
.is_ok_and(|count| count < policy.maximum_published_blobs),
WorkerError::TooManyPublishedBlobs(policy.maximum_published_blobs)
);
}
was_expected = was_expected || pending_blobs.maybe_insert(&blob).await?;
}
ensure!(was_expected, WorkerError::UnexpectedBlob);
self.save().await?;
self.chain_info_response().await
}
/// Returns a stored [`Certificate`] for the chain's block at the requested [`BlockHeight`].
///
/// Does not need `&mut self` because the chain is eagerly initialized when the
/// chain handle is created.
#[cfg(with_testing)]
#[instrument(skip_all, fields(
chain_id = %self.chain_id(),
height = %height
))]
pub(crate) async fn read_certificate(
&self,
height: BlockHeight,
) -> Result<Option<CacheArc<ConfirmedBlockCertificate>>, WorkerError> {
let certificate_hash = match self.chain.block_hashes.get(&height).await? {View on GitHub (pinned to 6c226ddcb3)
Solutions
- Send the block proposal (or certificate) first; only then upload the blobs it announces.
- Filter uploads against the proposal's expected_blob_ids.
- On retries after a round change, resend the updated proposal before resending blobs.
Example fix
// before: pushing blobs before anything references them
for blob in blobs {
client.upload_blob(chain_id, blob).await?;
}
client.submit_proposal(proposal).await?;
// after: the proposal announces the blob ids, then the blobs follow
client.submit_proposal(proposal).await?;
for blob in blobs {
client.upload_blob(chain_id, blob).await?;
} Defensive patterns
Strategy: validation
Validate before calling
// Only upload blobs the pending proposal actually requires.
let expected: HashSet<BlobId> = proposal.expected_blob_ids().iter().cloned().collect();
for blob in &blobs {
ensure!(expected.contains(&blob.id()), WorkerError::UnexpectedBlob);
} Try / catch
match node.handle_pending_blob(blob).await {
Err(NodeError::WorkerError(err)) if matches!(*err, WorkerError::UnexpectedBlob) => {
// Not required (yet): submit the proposal first, then retry the upload.
}
result => result,
} Prevention
- Order requests: proposal or certificate first, blobs second.
- Derive the upload list from expected_blob_ids, never from local state alone.
- After any round change, resend the proposal before resending blobs.
- Do not use validators as blob caches: unrequired blobs are rejected by design.
When it happens
Trigger: Calling handle_pending_blob (directly, via send_pending_blobs, or with a handle_message carrying a blob) before the proposal or certificate that references the blob has arrived; uploading a blob the proposal does not list in its expected blob ids; resending blobs after a round change cleared the pending set.
Common situations: Custom clients that pre-upload blobs eagerly; ordering bugs where the blob upload races the block proposal; retries after round changes that reset pending_blobs.
Related errors
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/819859b42fb42383.
Report an issue: GitHub.