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

Missing purpose on upgrade transaction

Error message

Missing purpose on upgrade transaction

What it means

Upgrade transactions carry a purpose (consensus parameters change, state transition upgrade, etc.) modeled as a nested proto message. If ProtoTransactionVariant::Upgrade arrives with purpose unset, upgrade_purpose_from_proto cannot run and the convertor raises Error::Serialization immediately.

Source

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

            let mint_asset_id = fuel_core_types::fuel_types::AssetId::try_from(
                proto_mint.mint_asset_id.as_slice(),
            )
            .map_err(|e| Error::Serialization(anyhow!(e)))?;

            let mint_tx = FuelTransaction::mint(
                tx_pointer,
                input_contract,
                output_contract,
                proto_mint.mint_amount,
                mint_asset_id,
                proto_mint.gas_price,
            );

            Ok(FuelTransaction::Mint(mint_tx))
        }
        ProtoTransactionVariant::Upgrade(proto_upgrade) => {
            let purpose_proto = proto_upgrade.purpose.as_ref().ok_or_else(|| {
                Error::Serialization(anyhow!("Missing purpose on upgrade transaction"))
            })?;
            let upgrade_purpose = upgrade_purpose_from_proto(purpose_proto)?;
            let policies = proto_upgrade
                .policies
                .clone()
                .map(|p| policies_from_proto_policies(&p))
                .unwrap_or_default();
            let inputs = proto_upgrade
                .inputs
                .iter()
                .map(input_from_proto_input)
                .collect::<crate::result::Result<Vec<_>>>()?;
            let outputs = proto_upgrade
                .outputs
                .iter()
                .map(output_from_proto_output)
                .collect::<crate::result::Result<Vec<_>>>()?;
            let witnesses = proto_upgrade

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Ensure the encoder serializes upgrade_purpose for every Upgrade transaction and re-send the block
  2. Pin matching fuel-core / proto versions on both ends of the block source
  3. Ingest with a compatibility layer that maps legacy upgrade encodings to an explicit purpose before conversion
  4. Pre-validate: reject Upgrade protos with purpose None and log the offending transaction id for re-encoding

Example fix

// before
let up = ProtoUpgrade { policies: Some(p), ..Default::default() }; // purpose: None

// after
let up = ProtoUpgrade {
    purpose: Some(ProtoUpgradePurpose { variant: Some(purpose_variant) }),
    policies: Some(p),
    ..Default::default()
};
Defensive patterns

Strategy: validation

Validate before calling

fn upgrade_tx_is_complete(u: &ProtoUpgrade) -> bool { u.purpose.is_some() }

Type guard

fn has_upgrade_purpose(u: &ProtoUpgrade) -> bool { u.purpose.as_ref().map(|p| p.variant.is_some()).unwrap_or(false) }

Try / catch

match convert_transaction(&proto_tx) {
    Err(e) if e.to_string().contains("Missing purpose on upgrade") => {
        log::warn!("dropping malformed upgrade tx {:?}", tx_id(&proto_tx)); Ok(None)
    }
    other => other.map(Some),
}

Prevention

When it happens

Trigger: Decoding an Upgrade transaction whose proto encoding predates the purpose field or omitted it. Also triggered by test/hand-built upgrade protos that only set policies/inputs/outputs/witnesses.

Common situations: Schema drift after a fuel-core upgrade that added or reshaped UpgradePurpose variants; blocks relayed from a peer running different fuel-core version; stale cached proto blocks.

Related errors


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