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

  1. Log the index of the failing transaction and inspect its variant field.
  2. Fix the encoder to never emit variant-less transactions (skip unknown tx types instead).
  3. Pre-validate every transaction has a variant before calling fuel_block_from_protobuf.
  4. 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

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


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