FuelLabs/fuel-core · error · Error::Serialization

Could not convert return data digest to Bytes32: {}

Error message

Could not convert return data digest to Bytes32: {}

What it means

Thrown while decoding a ReturnData receipt: the digest bytes field must be exactly 32 bytes to build fuel's Bytes32; any other length fails Bytes32::try_from and is wrapped as Error::Serialization, failing the receipt, its transaction, and the block.

Source

Thrown at crates/services/block_aggregator_api/src/blocks/old_block_source/convertor_adapter/proto_to_fuel_conversions.rs:336

                call.amount,
                asset_id,
                call.gas,
                call.param1,
                call.param2,
                call.pc,
                call.is,
            ))
        }
        ProtoReceiptVariant::ReturnReceipt(ret) => {
            let id = ContractId::try_from(ret.id.as_slice())
                .map_err(|e| Error::Serialization(anyhow!(e)))?;
            Ok(FuelReceipt::ret(id, ret.val, ret.pc, ret.is))
        }
        ProtoReceiptVariant::ReturnData(rd) => {
            let id = ContractId::try_from(rd.id.as_slice())
                .map_err(|e| Error::Serialization(anyhow!(e)))?;
            let digest = Bytes32::try_from(rd.digest.as_slice()).map_err(|e| {
                Error::Serialization(anyhow!(
                    "Could not convert return data digest to Bytes32: {}",
                    e
                ))
            })?;
            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

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Log the digest length of the failing ReturnData receipt.
  2. Pre-validate len == 32 for digest fields on ReturnData/LogData/MessageOut receipts before decoding.
  3. Align producer and consumer versions so digests are always 32 bytes.
  4. Re-fetch the block from a healthy source.

Example fix

// before
let digest = Bytes32::try_from(rd.digest.as_slice()).map_err(|e| {
    Error::Serialization(anyhow!("Could not convert return data digest to Bytes32: {}", e))
})?;

// after
if rd.digest.len() != 32 {
    return Err(Error::Serialization(anyhow::anyhow!(
        "ReturnData digest length {} != 32", rd.digest.len()
    )));
}
Defensive patterns

Strategy: validation

Validate before calling

fn return_data_digest_valid(rd: &ProtoReturnData) -> bool {
    rd.digest.len() == 32
}

Type guard

fn is_bytes32(b: &[u8]) -> bool { b.len() == 32 }

Try / catch

match receipt_from_proto(r) {
    Ok(receipt) => out.push(receipt),
    Err(Error::Serialization(ctx)) if ctx.to_string().contains("return data digest") => {
        tracing::warn!(len = rd.digest.len(), "rejecting ReturnData with bad digest");
        return Err(Error::Serialization(ctx));
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: fuel_block_from_protobuf on a block where a ReturnData receipt's digest field is empty or not 32 bytes - corrupted blob, truncated frame, or an encoder that writes unbounded bytes for the digest.

Common situations: Version-incompatible producer; data corruption in transit/storage; test fixtures with arbitrary digest lengths.

Related errors


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