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

Invalid coin type {:?}

Error message

Invalid coin type {:?}

What it means

The off-chain coins-to-spend index encodes each key with a trailing discriminant byte: 0 for Coin, 1 for Message. The Manual Decode impl inspects bytes.last() and rejects anything other than 0 or 1 with this error. Hitting it means the stored index bytes are not in the expected format — corruption or data written by an incompatible fuel-core version/codec.

Source

Thrown at crates/fuel-core/src/graphql_api/storage/coins/codecs.rs:138

                serialized_coin[start..end].copy_from_slice(&amount.to_be_bytes());
                start = end;
                end = end.saturating_add(Nonce::LEN);
                serialized_coin[start..end].copy_from_slice(nonce.as_ref());
                start = end;
                serialized_coin[start] = CoinType::Message as u8;

                SerializedCoinsToSpendIndexKey::Message(serialized_coin)
            }
        }
    }
}

impl Decode<CoinsToSpendIndexKey> for Manual<CoinsToSpendIndexKey> {
    fn decode(bytes: &[u8]) -> anyhow::Result<CoinsToSpendIndexKey> {
        let coin_type = match bytes.last() {
            Some(0) => CoinType::Coin,
            Some(1) => CoinType::Message,
            _ => return Err(anyhow::anyhow!("Invalid coin type {:?}", bytes.last())),
        };

        let result = match coin_type {
            CoinType::Coin => {
                let bytes: [u8; COIN_VARIANT_SIZE] = bytes.try_into()?;
                let mut start;
                let mut end = RETRYABLE_FLAG_SIZE;
                start = end;
                end = end.saturating_add(Address::LEN);
                let owner = Address::try_from(&bytes[start..end])?;
                start = end;
                end = end.saturating_add(AssetId::LEN);
                let asset_id = AssetId::try_from(&bytes[start..end])?;
                start = end;
                end = end.saturating_add(AMOUNT_SIZE);
                let amount = u64::from_be_bytes(bytes[start..end].try_into()?);
                start = end;
                end = end.saturating_add(UTXO_ID_SIZE);

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Rebuild the off-chain database (delete it and let the node re-index from on-chain data).
  2. Ensure all nodes reading/writing the database run the same fuel-core version.
  3. If it recurs on fresh data, inspect the writer of that column for a codec mismatch and file/report the version pair involved.
Defensive patterns

Strategy: validation

Validate before calling

// Guard before decoding an index key: discriminant byte must be 0 (Coin) or 1 (Message).
fn valid_coin_index_key(bytes: &[u8]) -> bool {
    matches!(bytes.last(), Some(0) | Some(1))
}
if !valid_coin_index_key(&raw_key) {
    anyhow::bail!("foreign/corrupt coins-to-spend index entry; rebuild off-chain DB");
}

Type guard

fn coin_type_of(bytes: &[u8]) -> Option<CoinType> {
    match bytes.last() {
        Some(0) => Some(CoinType::Coin),
        Some(1) => Some(CoinType::Message),
        _ => None,
    }
}

Try / catch

match Manual::<CoinsToSpendIndexKey>::decode(&raw) {
    Ok(k) => Ok(k),
    Err(e) if e.to_string().contains("Invalid coin type") => {
        // storage-format mismatch or corruption: rebuild the off-chain index
        anyhow::bail!("coins index corrupt ({}); wipe and re-index off-chain DB", e)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Iterating the CoinsToSpendIndexKey column and encountering a key whose last byte is neither 0 nor 1 — corrupted storage, a truncated/mangled key, or an on-disk format written by a different codec version.

Common situations: Upgrading fuel-core across off-chain index format changes without re-syncing; bit rot or partial writes; pointing a newer node at an old database directory.

Related errors


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