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
- Rebuild the off-chain database (delete it and let the node re-index from on-chain data).
- Ensure all nodes reading/writing the database run the same fuel-core version.
- 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
- Never mix fuel-core versions on one database directory.
- Rebuild the off-chain DB (it is derivable) after upgrades that touch index codecs.
- Report the key bytes and node version when filing issues about this error.
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
- Unable to decode bytes
- Failed to get registry root: {}
- Failed to get registry root: {}
- off-chain database doesn't have height
- off-chain database height({off_chain_height}) is less than t
AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16).
Data as JSON: /api/errors/1ae600ae9b8a0d20.
Report an issue: GitHub.