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

Could not convert state_root to Bytes32: {}

Error message

Could not convert state_root to Bytes32: {}

What it means

Thrown while decoding a proto contract input: the state_root bytes field must be exactly 32 bytes to construct fuel's Bytes32; try_from fails on any other length and is wrapped as Error::Serialization. The failed input propagates the error up through tx_from_proto_tx and fuel_block_from_protobuf.

Source

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

            "Could not convert storage slot value to Bytes32: {}",
            e
        ))
    })?;
    Ok(StorageSlot::new(key, value))
}

fn contract_input_from_proto(
    proto: &ProtoContractInput,
) -> crate::result::Result<fuel_core_types::fuel_tx::input::contract::Contract> {
    let utxo_proto = proto.utxo_id.as_ref().ok_or_else(|| {
        Error::Serialization(anyhow!("Missing utxo_id on contract input"))
    })?;
    let utxo_id = utxo_id_from_proto(utxo_proto)?;
    let balance_root = Bytes32::try_from(proto.balance_root.as_slice()).map_err(|e| {
        Error::Serialization(anyhow!("Could not convert balance_root to Bytes32: {}", e))
    })?;
    let state_root = Bytes32::try_from(proto.state_root.as_slice()).map_err(|e| {
        Error::Serialization(anyhow!("Could not convert state_root to Bytes32: {}", e))
    })?;
    let tx_pointer_proto = proto.tx_pointer.as_ref().ok_or_else(|| {
        Error::Serialization(anyhow!("Missing tx_pointer on contract input"))
    })?;
    let tx_pointer = tx_pointer_from_proto(tx_pointer_proto)?;
    let contract_id =
        fuel_core_types::fuel_types::ContractId::try_from(proto.contract_id.as_slice())
            .map_err(|e| Error::Serialization(anyhow!(e)))?;

    Ok(fuel_core_types::fuel_tx::input::contract::Contract {
        utxo_id,
        balance_root,
        state_root,
        tx_pointer,
        contract_id,
    })
}

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Log the state_root length of the failing input to confirm the mismatch.
  2. Pre-validate all 32-byte fields (balance_root, state_root) on contract inputs before conversion.
  3. Align producer and consumer fuel-core versions.
  4. Re-fetch the block from a known-good source.

Example fix

// before
let state_root = Bytes32::try_from(proto.state_root.as_slice()).map_err(|e| {
    Error::Serialization(anyhow!("Could not convert state_root to Bytes32: {}", e))
})?;

// after: caller pre-check
if input_proto.state_root.len() != 32 {
    return Err(Error::Serialization(anyhow::anyhow!(
        "state_root length {} != 32", input_proto.state_root.len()
    )));
}
Defensive patterns

Strategy: validation

Validate before calling

fn contract_input_state_root_valid(p: &ProtoContractInput) -> bool {
    p.state_root.len() == 32
}

Type guard

fn is_bytes32(b: &[u8]) -> bool { b.len() == 32 }

Try / catch

match contract_input_from_proto(p) {
    Ok(input) => inputs.push(input),
    Err(Error::Serialization(ctx)) if ctx.to_string().contains("state_root") => {
        tracing::warn!(len = p.state_root.len(), "rejecting input with bad state_root");
        return Err(Error::Serialization(ctx));
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Decoding a proto block where a ProtoContractInput.state_root has a length other than 32 - truncated or corrupted blob, or an encoder that does not enforce the fixed size.

Common situations: Producer/consumer version mismatch; data corruption; test fixtures with arbitrary byte vectors.

Related errors


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