FuelLabs/fuel-core · error · Error::Serialization
Missing transaction variant
Error message
Missing transaction variant
What it means
tx_from_proto_tx matches on the Transaction.variant oneof; prost leaves the oneof as None when the proto Transaction message has no variant set, and the code maps that to Error::Serialization. A variant-less transaction cannot map to any FuelTransaction variant, so the block decode fails.
Source
Thrown at crates/services/block_aggregator_api/src/blocks/old_block_source/convertor_adapter/proto_to_fuel_conversions.rs:576
pub fn partial_header_from_proto_header(
proto_header: &ProtoHeader,
) -> crate::result::Result<(PartialBlockHeader, Bytes32)> {
let partial_header = PartialBlockHeader {
consensus: proto_header_to_empty_consensus_header(proto_header)?,
application: proto_header_to_empty_application_header(proto_header)?,
};
let event_inbox_root = proto_header_to_event_inbox_root(proto_header)?;
Ok((partial_header, event_inbox_root))
}
pub fn tx_from_proto_tx(
proto_tx: &ProtoTransaction,
) -> crate::result::Result<FuelTransaction> {
let variant = proto_tx
.variant
.as_ref()
.ok_or_else(|| Error::Serialization(anyhow!("Missing transaction variant")))?;
match variant {
ProtoTransactionVariant::Script(proto_script) => {
let policies = proto_script
.policies
.clone()
.map(|p| policies_from_proto_policies(&p))
.unwrap_or_default();
let inputs = proto_script
.inputs
.iter()
.map(input_from_proto_input)
.collect::<crate::result::Result<Vec<_>>>()?;
let outputs = proto_script
.outputs
.iter()
.map(output_from_proto_output)
.collect::<crate::result::Result<Vec<_>>>()?;View on GitHub (pinned to b9d4d170da)
Solutions
- Log the index of the failing transaction and inspect its variant field.
- Fix the encoder to never emit variant-less transactions (skip unknown tx types instead).
- Pre-validate every transaction has a variant before calling fuel_block_from_protobuf.
- Reject the block and re-fetch from a compatible peer.
Example fix
// before
let txs = v1_inner.transactions.iter().map(tx_from_proto_tx)
.collect::<Result<Vec<_>>>()?;
// after: validate variant presence with position context
for (i, tx) in v1_inner.transactions.iter().enumerate() {
if tx.variant.is_none() {
return Err(Error::Serialization(anyhow::anyhow!(
"transaction[{i}] has no variant"
)));
}
}
let txs = v1_inner.transactions.iter().map(tx_from_proto_tx)
.collect::<Result<Vec<_>>>()?; Defensive patterns
Strategy: type-guard
Validate before calling
fn all_txs_have_variants(txs: &[ProtoTransaction]) -> bool {
txs.iter().all(|t| t.variant.is_some())
} Type guard
fn tx_has_variant(t: &ProtoTransaction) -> bool {
t.variant.is_some()
} Try / catch
match tx_from_proto_tx(t) {
Ok(tx) => txs.push(tx),
Err(Error::Serialization(ctx)) if ctx.to_string().contains("transaction variant") => {
tracing::warn!(?t, "rejecting transaction without variant");
return Err(Error::Serialization(ctx));
}
Err(e) => return Err(e),
} Prevention
- Encoders must never emit variant-less transactions; skip unknown tx types instead.
- Validate every transaction has a variant right after ProtoBlock::decode.
- Log transaction indices during decode for fast location of bad entries.
When it happens
Trigger: Decoding a proto block containing an empty Transaction message (no Script/Create/Mint/... variant) - placeholder entries from a faulty encoder, truncated data, or fuzzed input.
Common situations: Cross-version block exchange with schema drift; producers emitting default transactions for unmappable types; fuzz/corpus testing.
Related errors
- Missing utxo_id on contract input
- Missing tx_pointer on contract input
- Missing receipt variant
- Missing panic reason
- Missing script result payload
AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16).
Data as JSON: /api/errors/03cf3bc4c7fb0665.
Report an issue: GitHub.