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

Missing utxo_id on contract input

Error message

Missing utxo_id on contract input

What it means

contract_input_from_proto requires the utxo_id sub-message on every ProtoContractInput; prost decodes absent optional message fields as None, and this code maps that to Error::Serialization immediately. Any transaction containing a contract input without utxo_id fails to decode, because fuel_tx::input::contract::Contract always carries a UtxoId.

Source

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

        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| {
        Error::Serialization(anyhow!("Could not convert state_root to Bytes32: {}", e))
    })?;
    let tx_pointer_proto = proto.tx_pointer.as_ref().ok_or_else(|| {
        Error::Serialization(anyhow!("Missing tx_pointer on contract input"))
    })?;
    let tx_pointer = tx_pointer_from_proto(tx_pointer_proto)?;
    let contract_id =
        fuel_core_types::fuel_types::ContractId::try_from(proto.contract_id.as_slice())
            .map_err(|e| Error::Serialization(anyhow!(e)))?;

    Ok(fuel_core_types::fuel_tx::input::contract::Contract {
        utxo_id,

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Debug-print the failing ProtoContractInput to see exactly which sub-messages are None.
  2. Fix the producer to always set utxo_id (and tx_pointer) on contract inputs, matching fuel-core's Contract input type.
  3. Pre-validate presence of utxo_id and tx_pointer before conversion and reject with a clearer, positional error.
  4. Re-encode the block from canonical on-chain data instead of patching the proto by hand.

Example fix

// before
let utxo_proto = proto.utxo_id.as_ref().ok_or_else(|| {
    Error::Serialization(anyhow!("Missing utxo_id on contract input"))
})?;

// after: caller-side presence check with tx context
let input_proto = inputs.get(i).expect("indexed input");
if input_proto.utxo_id.is_none() || input_proto.tx_pointer.is_none() {
    return Err(Error::Serialization(anyhow::anyhow!(
        "input[{i}] contract input missing utxo_id/tx_pointer; rejecting tx"
    )));
}
Defensive patterns

Strategy: type-guard

Validate before calling

fn contract_input_has_utxo(p: &ProtoContractInput) -> bool {
    p.utxo_id.is_some()
}

Type guard

fn contract_input_complete(p: &ProtoContractInput) -> bool {
    p.utxo_id.is_some() && p.tx_pointer.is_some()
}

Try / catch

match contract_input_from_proto(p) {
    Ok(input) => Some(input),
    Err(Error::Serialization(ctx)) if ctx.to_string().contains("utxo_id") => {
        tracing::warn!(?p, "rejecting contract input without utxo_id");
        None // or fail the whole block decode
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Decoding a proto block/transaction where a contract input was encoded without its utxo_id field - an encoder that treats utxo_id as optional metadata, a non-fuel-core producer, or test fixtures building ProtoContractInput via Default::default().

Common situations: Version skew between fuel-core versions with different field-presence semantics; hand-constructed proto in tests or migration scripts; partial serialization bugs that skip optional sub-messages.

Related errors


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