FuelLabs/fuel-core · error · Error::Serialization
Missing panic reason
Error message
Missing panic reason
What it means
receipt_from_proto requires the Panic receipt's reason sub-message; prost yields None when the field was never set, and this code maps it to Error::Serialization. The reason is then fed to panic_instruction_from_proto, which itself tolerates unknown enum values (defaults to Unknown) - but the sub-message itself must exist.
Source
Thrown at crates/services/block_aggregator_api/src/blocks/old_block_source/convertor_adapter/proto_to_fuel_conversions.rs:357
))
})?;
Ok(FuelReceipt::return_data_with_len(
id,
rd.ptr,
rd.len,
digest,
rd.pc,
rd.is,
rd.data.clone(),
))
}
ProtoReceiptVariant::Panic(panic_receipt) => {
let id = ContractId::try_from(panic_receipt.id.as_slice())
.map_err(|e| Error::Serialization(anyhow!(e)))?;
let reason_proto = panic_receipt
.reason
.as_ref()
.ok_or_else(|| Error::Serialization(anyhow!("Missing panic reason")))?;
let reason = panic_instruction_from_proto(reason_proto);
let contract_id = panic_receipt
.contract_id
.as_ref()
.map(|cid| {
ContractId::try_from(cid.as_slice())
.map_err(|e| Error::Serialization(anyhow!(e)))
})
.transpose()?;
Ok(
FuelReceipt::panic(id, reason, panic_receipt.pc, panic_receipt.is)
.with_panic_contract_id(contract_id),
)
}
ProtoReceiptVariant::Revert(revert) => {
let id = ContractId::try_from(revert.id.as_slice())
.map_err(|e| Error::Serialization(anyhow!(e)))?;
Ok(FuelReceipt::revert(id, revert.ra, revert.pc, revert.is))View on GitHub (pinned to b9d4d170da)
Solutions
- Debug-print the failing Panic receipt to confirm reason is None.
- Fix the encoder to always attach a PanicReason (even Unknown) when writing panic receipts.
- Pre-validate presence of reason on Panic receipts and reject with position context.
- Re-fetch the block from a compatible peer.
Example fix
// before
let reason_proto = panic_receipt.reason.as_ref().ok_or_else(|| {
Error::Serialization(anyhow!("Missing panic reason"))
})?;
// after: validate before decoding, keep position context
if let ProtoReceiptVariant::Panic(p) = variant {
if p.reason.is_none() {
return Err(Error::Serialization(anyhow::anyhow!(
"receipt[{i}] Panic receipt missing reason sub-message"
)));
}
} Defensive patterns
Strategy: type-guard
Validate before calling
fn panic_receipt_has_reason(p: &ProtoPanic) -> bool {
p.reason.is_some()
} Type guard
fn panic_receipt_complete(p: &ProtoPanic) -> bool {
p.reason.is_some()
} Try / catch
match receipt_from_proto(r) {
Ok(receipt) => out.push(receipt),
Err(Error::Serialization(ctx)) if ctx.to_string().contains("panic reason") => {
tracing::warn!(?r, "rejecting Panic receipt without reason");
return Err(Error::Serialization(ctx));
}
Err(e) => return Err(e),
} Prevention
- Encoders must attach a reason sub-message to every panic receipt (even ProtoPanicReason::Unknown).
- Pre-validate Panic receipts after decode and before conversion.
- Watch version upgrades that add receipt sub-fields (e.g. panic contract_id) for presence regressions.
When it happens
Trigger: Decoding a proto block where a Panic receipt was encoded without its reason sub-message - an encoder that skips the field, a truncated message, or a fixture built with Default::default().
Common situations: Version skew between fuel-core versions that added or renamed panic-receipt fields; third-party producers; fuzzed input where nested sub-messages are dropped.
Related errors
- Missing receipt variant
- Missing script result payload
- Missing utxo_id on contract input
- Missing tx_pointer on contract input
- Could not convert return data digest to Bytes32: {}
AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16).
Data as JSON: /api/errors/23a5f4909a2ddcbf.
Report an issue: GitHub.