{"record":{"id":"c3cc5a07f289dbb8","repo":"FuelLabs/fuel-core","slug":"missing-utxo-id-on-contract-input","errorCode":null,"errorMessage":"Missing utxo_id on contract input","messagePattern":"Missing utxo_id on contract input","errorType":"validation","errorClass":"Error::Serialization","httpStatus":null,"severity":"error","filePath":"crates/services/block_aggregator_api/src/blocks/old_block_source/convertor_adapter/proto_to_fuel_conversions.rs","lineNumber":110,"sourceCode":"        Error::Serialization(anyhow!(\n            \"Could not convert storage slot key to Bytes32: {}\",\n            e\n        ))\n    })?;\n    let value = Bytes32::try_from(proto.value.as_slice()).map_err(|e| {\n        Error::Serialization(anyhow!(\n            \"Could not convert storage slot value to Bytes32: {}\",\n            e\n        ))\n    })?;\n    Ok(StorageSlot::new(key, value))\n}\n\nfn contract_input_from_proto(\n    proto: &ProtoContractInput,\n) -> crate::result::Result<fuel_core_types::fuel_tx::input::contract::Contract> {\n    let utxo_proto = proto.utxo_id.as_ref().ok_or_else(|| {\n        Error::Serialization(anyhow!(\"Missing utxo_id on contract input\"))\n    })?;\n    let utxo_id = utxo_id_from_proto(utxo_proto)?;\n    let balance_root = Bytes32::try_from(proto.balance_root.as_slice()).map_err(|e| {\n        Error::Serialization(anyhow!(\"Could not convert balance_root to Bytes32: {}\", e))\n    })?;\n    let state_root = Bytes32::try_from(proto.state_root.as_slice()).map_err(|e| {\n        Error::Serialization(anyhow!(\"Could not convert state_root to Bytes32: {}\", e))\n    })?;\n    let tx_pointer_proto = proto.tx_pointer.as_ref().ok_or_else(|| {\n        Error::Serialization(anyhow!(\"Missing tx_pointer on contract input\"))\n    })?;\n    let tx_pointer = tx_pointer_from_proto(tx_pointer_proto)?;\n    let contract_id =\n        fuel_core_types::fuel_types::ContractId::try_from(proto.contract_id.as_slice())\n            .map_err(|e| Error::Serialization(anyhow!(e)))?;\n\n    Ok(fuel_core_types::fuel_tx::input::contract::Contract {\n        utxo_id,","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/FuelLabs/fuel-core/blob/b9d4d170da3a31c9ace5f963d633b326348e0d42/crates/services/block_aggregator_api/src/blocks/old_block_source/convertor_adapter/proto_to_fuel_conversions.rs#L92-L128","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","solutions":["Debug-print the failing ProtoContractInput to see exactly which sub-messages are None.","Fix the producer to always set utxo_id (and tx_pointer) on contract inputs, matching fuel-core's Contract input type.","Pre-validate presence of utxo_id and tx_pointer before conversion and reject with a clearer, positional error.","Re-encode the block from canonical on-chain data instead of patching the proto by hand."],"exampleFix":"// before\nlet utxo_proto = proto.utxo_id.as_ref().ok_or_else(|| {\n    Error::Serialization(anyhow!(\"Missing utxo_id on contract input\"))\n})?;\n\n// after: caller-side presence check with tx context\nlet input_proto = inputs.get(i).expect(\"indexed input\");\nif input_proto.utxo_id.is_none() || input_proto.tx_pointer.is_none() {\n    return Err(Error::Serialization(anyhow::anyhow!(\n        \"input[{i}] contract input missing utxo_id/tx_pointer; rejecting tx\"\n    )));\n}","handlingStrategy":"type-guard","validationCode":"fn contract_input_has_utxo(p: &ProtoContractInput) -> bool {\n    p.utxo_id.is_some()\n}","typeGuard":"fn contract_input_complete(p: &ProtoContractInput) -> bool {\n    p.utxo_id.is_some() && p.tx_pointer.is_some()\n}","tryCatchPattern":"match contract_input_from_proto(p) {\n    Ok(input) => Some(input),\n    Err(Error::Serialization(ctx)) if ctx.to_string().contains(\"utxo_id\") => {\n        tracing::warn!(?p, \"rejecting contract input without utxo_id\");\n        None // or fail the whole block decode\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Ensure encoders always populate utxo_id and tx_pointer on contract inputs (fuel-core's own converter does).","Pre-validate sub-message presence after ProtoBlock::decode and before type conversion.","Reject blocks from peers with version mismatch rather than attempting lenient decode."],"tags":["rust","fuel-core","protobuf","deserialization","contract-input","missing-field"],"backgroundTag":null,"analyzedSha":"b9d4d170da3a31c9ace5f963d633b326348e0d42","analyzedAt":"2026-08-16T08:56:42.692Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}