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_upgradeView on GitHub (pinned to b9d4d170da)
Solutions
- Ensure the encoder serializes upgrade_purpose for every Upgrade transaction and re-send the block
- Pin matching fuel-core / proto versions on both ends of the block source
- Ingest with a compatibility layer that maps legacy upgrade encodings to an explicit purpose before conversion
- 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
- Upgrade producers and consumers together when UpgradePurpose variants change
- Reject upgrade protos without purpose in producer-side validation before broadcast
- Keep fixture generators in sync with the latest proto schema
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
- Missing input_contract on mint transaction
- Missing output_contract on mint transaction
- Missing input variant
- Missing utxo_id
- Missing tx_pointer
AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16).
Data as JSON: /api/errors/9e96c95633aac416.
Report an issue: GitHub.