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

  1. Debug-print the failing Panic receipt to confirm reason is None.
  2. Fix the encoder to always attach a PanicReason (even Unknown) when writing panic receipts.
  3. Pre-validate presence of reason on Panic receipts and reject with position context.
  4. 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

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


AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16). Data as JSON: /api/errors/23a5f4909a2ddcbf. Report an issue: GitHub.