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.

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.

Appendix: 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)