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

Could not convert receipts_root to Bytes32: {}

Error message

Could not convert receipts_root to Bytes32: {}

What it means

While decoding a Script transaction, the receipts_root bytes field must convert to Bytes32, which requires exactly 32 bytes; any other length fails try_from and is wrapped as Error::Serialization, failing the transaction and block. Note the setter path: the decoded Script tx is built first and receipts_root_mut() is overwritten from the proto field.

Source

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

            let witnesses = proto_script
                .witnesses
                .iter()
                .map(|w| Ok(Witness::from(w.clone())))
                .collect::<crate::result::Result<Vec<_>>>()?;
            let mut script_tx = FuelTransaction::script(
                proto_script.script_gas_limit,
                proto_script.script.clone(),
                proto_script.script_data.clone(),
                policies,
                inputs,
                outputs,
                witnesses,
            );
            *script_tx.receipts_root_mut() = Bytes32::try_from(
                proto_script.receipts_root.as_slice(),
            )
            .map_err(|e| {
                Error::Serialization(anyhow!(
                    "Could not convert receipts_root to Bytes32: {}",
                    e
                ))
            })?;

            Ok(FuelTransaction::Script(script_tx))
        }
        ProtoTransactionVariant::Create(proto_create) => {
            let policies = proto_create
                .policies
                .clone()
                .map(|p| policies_from_proto_policies(&p))
                .unwrap_or_default();
            let inputs = proto_create
                .inputs
                .iter()
                .map(input_from_proto_input)
                .collect::<crate::result::Result<Vec<_>>>()?;

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Log the receipts_root length of the failing Script tx.
  2. Pre-validate len == 32 before decoding the block.
  3. Pin matching fuel-core versions on both ends so receipts_root is always 32 bytes.
  4. Re-fetch the block from a known-good peer.

Example fix

// before
*script_tx.receipts_root_mut() =
    Bytes32::try_from(proto_script.receipts_root.as_slice()).map_err(|e| {
        Error::Serialization(anyhow!("Could not convert receipts_root to Bytes32: {}", e))
    })?;

// after: caller-side pre-check
if proto_script.receipts_root.len() != 32 {
    return Err(Error::Serialization(anyhow::anyhow!(
        "receipts_root length {} != 32", proto_script.receipts_root.len()
    )));
}
Defensive patterns

Strategy: validation

Validate before calling

fn script_receipts_root_valid(script: &ProtoScript) -> bool {
    script.receipts_root.len() == 32
}

Type guard

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

Try / catch

match tx_from_proto_tx(t) {
    Ok(tx) => txs.push(tx),
    Err(Error::Serialization(ctx)) if ctx.to_string().contains("receipts_root") => {
        tracing::warn!(len = script.receipts_root.len(), "rejecting Script tx with bad receipts_root");
        return Err(Error::Serialization(ctx));
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: fuel_block_from_protobuf on a block where a Script transaction's receipts_root is empty or not 32 bytes - corruption, truncation, or a producer that does not fix the field size.

Common situations: Producer/consumer version mismatch; corrupted storage or transfer; test fixtures with default empty byte fields.

Related errors


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