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

Could not convert storage slot value to Bytes32: {}

Error message

Could not convert storage slot value to Bytes32: {}

What it means

Thrown by storage_slot_from_proto when the proto storage slot's value field cannot be converted: Bytes32::try_from only accepts exactly 32 bytes, and proto.value has a different length. It is wrapped as Error::Serialization and fails the slot, the containing Create transaction, and the block decode.

Source

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

    let block_height = proto.block_height.into();
    #[allow(clippy::useless_conversion)]
    let tx_index = proto.tx_index.try_into().map_err(|e| {
        Error::Serialization(anyhow!("Could not convert tx_index to target type: {}", e))
    })?;
    Ok(TxPointer::new(block_height, tx_index))
}

fn storage_slot_from_proto(
    proto: &ProtoStorageSlot,
) -> crate::result::Result<StorageSlot> {
    let key = Bytes32::try_from(proto.key.as_slice()).map_err(|e| {
        Error::Serialization(anyhow!(
            "Could not convert storage slot key to Bytes32: {}",
            e
        ))
    })?;
    let value = Bytes32::try_from(proto.value.as_slice()).map_err(|e| {
        Error::Serialization(anyhow!(
            "Could not convert storage slot value to Bytes32: {}",
            e
        ))
    })?;
    Ok(StorageSlot::new(key, value))
}

fn contract_input_from_proto(
    proto: &ProtoContractInput,
) -> crate::result::Result<fuel_core_types::fuel_tx::input::contract::Contract> {
    let utxo_proto = proto.utxo_id.as_ref().ok_or_else(|| {
        Error::Serialization(anyhow!("Missing utxo_id on contract input"))
    })?;
    let utxo_id = utxo_id_from_proto(utxo_proto)?;
    let balance_root = Bytes32::try_from(proto.balance_root.as_slice()).map_err(|e| {
        Error::Serialization(anyhow!("Could not convert balance_root to Bytes32: {}", e))
    })?;
    let state_root = Bytes32::try_from(proto.state_root.as_slice()).map_err(|e| {

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Log the actual value length of the failing slot to confirm the mismatch.
  2. Validate slot.key and slot.value are both 32 bytes before decoding; reject the block early with slot position in the error.
  3. Align producer and consumer fuel-core versions so storage slot values are always 32 bytes.
  4. Re-fetch the block from a healthy peer or re-encode from canonical on-chain data.

Example fix

// before
let slot = storage_slot_from_proto(proto_slot)?;

// after
if proto_slot.value.len() != 32 {
    return Err(Error::Serialization(anyhow::anyhow!(
        "storage slot value has {} bytes; expected 32",
        proto_slot.value.len()
    )));
}
let slot = storage_slot_from_proto(proto_slot)?;
Defensive patterns

Strategy: validation

Validate before calling

fn storage_slot_value_valid(slot: &ProtoStorageSlot) -> bool {
    slot.value.len() == 32
}

Type guard

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

Try / catch

match storage_slot_from_proto(slot) {
    Ok(s) => slots.push(s),
    Err(Error::Serialization(ctx)) if ctx.to_string().contains("storage slot value") => {
        tracing::warn!(value_len = slot.value.len(), "rejecting malformed storage slot");
        return Err(Error::Serialization(ctx));
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: fuel_block_from_protobuf on a block where some ProtoStorageSlot.value is empty or any non-32 length - corrupted blob, truncated frame, or a producer writing variable-length bytes for a fixed 32-byte field.

Common situations: Version-incompatible encoder; corruption on disk or in transit; test data with arbitrary-length byte vectors.

Related errors


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