diff --git a/crates/fuel-core/src/coins_query.rs b/crates/fuel-core/src/coins_query.rs index 412fb16d8b..3e3adab87f 100644 --- a/crates/fuel-core/src/coins_query.rs +++ b/crates/fuel-core/src/coins_query.rs @@ -1,12 +1,9 @@ use crate::{ - fuel_core_graphql_api::database::ReadView, - graphql_api::{ - ports::CoinsToSpendIndexIter, - storage::coins::{ - CoinsToSpendIndexEntry, - IndexedCoinType, - }, + fuel_core_graphql_api::{ + database::ReadView, + storage::coins::CoinsToSpendIndexKey, }, + graphql_api::ports::CoinsToSpendIndexIter, query::asset_query::{ AssetQuery, AssetSpendTarget, @@ -41,7 +38,6 @@ use rand::prelude::*; use std::{ cmp::Reverse, collections::HashSet, - ops::Deref, }; use thiserror::Error; @@ -289,7 +285,7 @@ pub async fn select_coins_to_spend( asset_id: &AssetId, excluded_ids: &ExcludedCoinIds<'_>, batch_size: usize, -) -> Result, CoinsQueryError> { +) -> Result, CoinsQueryError> { const TOTAL_AMOUNT_ADJUSTMENT_FACTOR: u64 = 2; if total == 0 || max == 0 { return Err(CoinsQueryError::IncorrectQueryParameters { @@ -351,124 +347,65 @@ pub async fn select_coins_to_spend( .collect()) } -// This is the `CoinsToSpendIndexEntry` which is guaranteed to have a key -// which allows to properly decode the amount. -struct CheckedCoinsToSpendIndexEntry { - inner: CoinsToSpendIndexEntry, - amount: u64, -} - -impl TryFrom for CheckedCoinsToSpendIndexEntry { - type Error = CoinsQueryError; - - fn try_from(value: CoinsToSpendIndexEntry) -> Result { - let amount = value - .0 - .amount() - .ok_or(CoinsQueryError::IncorrectCoinsToSpendIndexKey)?; - Ok(Self { - inner: value, - amount, - }) - } -} - -impl From for CoinsToSpendIndexEntry { - fn from(value: CheckedCoinsToSpendIndexEntry) -> Self { - value.inner - } -} - -impl Deref for CheckedCoinsToSpendIndexEntry { - type Target = CoinsToSpendIndexEntry; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - async fn big_coins( - big_coins_stream: impl Stream> + Unpin, + big_coins_stream: impl Stream> + Unpin, total: u64, max: u16, excluded_ids: &ExcludedCoinIds<'_>, -) -> Result<(u64, Vec), CoinsQueryError> { - select_coins_until( - big_coins_stream, - max, - excluded_ids, - |_, total_so_far| total_so_far >= total, - CheckedCoinsToSpendIndexEntry::try_from, - ) +) -> Result<(u64, Vec), CoinsQueryError> { + select_coins_until(big_coins_stream, max, excluded_ids, |_, total_so_far| { + total_so_far >= total + }) .await } async fn dust_coins( - dust_coins_stream: impl Stream> + Unpin, - last_big_coin: &CoinsToSpendIndexEntry, + dust_coins_stream: impl Stream> + Unpin, + last_big_coin: &CoinsToSpendIndexKey, max_dust_count: u16, excluded_ids: &ExcludedCoinIds<'_>, -) -> Result<(u64, Vec), CoinsQueryError> { +) -> Result<(u64, Vec), CoinsQueryError> { select_coins_until( dust_coins_stream, max_dust_count, excluded_ids, |coin, _| coin == last_big_coin, - Ok::, ) .await } -async fn select_coins_until( - mut coins_stream: impl Stream> + Unpin, +async fn select_coins_until( + mut coins_stream: impl Stream> + Unpin, max: u16, excluded_ids: &ExcludedCoinIds<'_>, predicate: Pred, - mapper: Mapper, -) -> Result<(u64, Vec), CoinsQueryError> +) -> Result<(u64, Vec), CoinsQueryError> where - Pred: Fn(&CoinsToSpendIndexEntry, u64) -> bool, - Mapper: Fn(CoinsToSpendIndexEntry) -> Result, - E: From, + Pred: Fn(&CoinsToSpendIndexKey, u64) -> bool, { let mut coins_total_value: u64 = 0; let mut coins = Vec::with_capacity(max as usize); while let Some(coin) = coins_stream.next().await { let coin = coin?; - if !is_excluded(&coin, excluded_ids)? { + if !is_excluded(&coin, excluded_ids) { if coins.len() >= max as usize || predicate(&coin, coins_total_value) { break; } - let amount = coin - .0 - .amount() - .ok_or(CoinsQueryError::IncorrectCoinsToSpendIndexKey)?; + let amount = coin.amount(); coins_total_value = coins_total_value.saturating_add(amount); - coins.push( - mapper(coin) - .map_err(|_| CoinsQueryError::IncorrectCoinsToSpendIndexKey)?, - ); + coins.push(coin); } } Ok((coins_total_value, coins)) } -fn is_excluded( - (key, coin_type): &CoinsToSpendIndexEntry, - excluded_ids: &ExcludedCoinIds, -) -> Result { - match coin_type { - IndexedCoinType::Coin => { - let utxo = key - .try_into() - .map_err(|_| CoinsQueryError::IncorrectCoinForeignKeyInIndex)?; - Ok(excluded_ids.is_coin_excluded(&utxo)) +fn is_excluded(key: &CoinsToSpendIndexKey, excluded_ids: &ExcludedCoinIds) -> bool { + match key { + CoinsToSpendIndexKey::Coin { utxo_id, .. } => { + excluded_ids.is_coin_excluded(utxo_id) } - IndexedCoinType::Message => { - let nonce = key - .try_into() - .map_err(|_| CoinsQueryError::IncorrectMessageForeignKeyInIndex)?; - Ok(excluded_ids.is_message_excluded(&nonce)) + CoinsToSpendIndexKey::Message { nonce, .. } => { + excluded_ids.is_message_excluded(nonce) } } } @@ -479,12 +416,12 @@ fn max_dust_count(max: u16, big_coins_len: u16) -> u16 { } fn skip_big_coins_up_to_amount( - big_coins: impl IntoIterator, + big_coins: impl IntoIterator, skipped_amount: u64, -) -> impl Iterator { +) -> impl Iterator { let mut current_dust_coins_value = skipped_amount; big_coins.into_iter().skip_while(move |item| { - let item_amount = item.amount; + let item_amount = item.amount(); current_dust_coins_value .checked_sub(item_amount) .map(|new_value| { @@ -1150,22 +1087,16 @@ mod tests { select_coins_to_spend, select_coins_until, CoinsQueryError, - CoinsToSpendIndexEntry, + CoinsToSpendIndexKey, ExcludedCoinIds, }, - graphql_api::{ - ports::CoinsToSpendIndexIter, - storage::coins::{ - CoinsToSpendIndexKey, - IndexedCoinType, - }, - }, + graphql_api::ports::CoinsToSpendIndexIter, }; const BATCH_SIZE: usize = 1; struct TestCoinSpec { - index_entry: Result, + index_entry: Result, utxo_id: UtxoId, } @@ -1186,10 +1117,7 @@ mod tests { }; TestCoinSpec { - index_entry: Ok(( - CoinsToSpendIndexKey::from_coin(&coin), - IndexedCoinType::Coin, - )), + index_entry: Ok(CoinsToSpendIndexKey::from_coin(&coin)), utxo_id, } }) @@ -1215,7 +1143,6 @@ mod tests { MAX, &excluded, |_, _| false, - Ok::, ) .await .expect("should select coins"); @@ -1247,7 +1174,6 @@ mod tests { MAX, &excluded, |_, _| false, - Ok::, ) .await .expect("should select coins"); @@ -1271,7 +1197,7 @@ mod tests { let excluded = ExcludedCoinIds::new(std::iter::empty(), std::iter::empty()); - let predicate: fn(&CoinsToSpendIndexEntry, u64) -> bool = + let predicate: fn(&CoinsToSpendIndexKey, u64) -> bool = |_, total| total > TOTAL; // When @@ -1280,7 +1206,6 @@ mod tests { MAX, &excluded, predicate, - Ok::, ) .await .expect("should select coins"); @@ -1329,14 +1254,14 @@ mod tests { let mut results = result .into_iter() - .map(|(key, _)| key.amount()) + .map(|key| key.amount()) .collect::>(); // Then // Because we select a total of 202 (TOTAL * 2), first 3 coins should always selected (100, 100, 4). let expected = vec![100, 100, 4]; - let actual: Vec<_> = results.drain(..3).map(Option::unwrap).collect(); + let actual: Vec<_> = results.drain(..3).collect(); assert_eq!(expected, actual); // The number of dust coins is selected randomly, so we might have: @@ -1349,7 +1274,7 @@ mod tests { let expected_1: Vec = vec![]; let expected_2: Vec = vec![2]; let expected_3: Vec = vec![2, 3]; - let actual: Vec<_> = results.drain(..).map(Option::unwrap).collect(); + let actual: Vec<_> = results; assert!( actual == expected_1 || actual == expected_2 || actual == expected_3, @@ -1390,10 +1315,7 @@ mod tests { .expect("should not error"); // Then - let results: Vec<_> = result - .into_iter() - .map(|(key, _)| key.amount().unwrap()) - .collect(); + let results: Vec<_> = result.into_iter().map(|key| key.amount()).collect(); assert_eq!(results, vec![10, 10]); } diff --git a/crates/fuel-core/src/graphql_api/indexation/coins_to_spend.rs b/crates/fuel-core/src/graphql_api/indexation/coins_to_spend.rs index 869d437452..67a24056e7 100644 --- a/crates/fuel-core/src/graphql_api/indexation/coins_to_spend.rs +++ b/crates/fuel-core/src/graphql_api/indexation/coins_to_spend.rs @@ -14,7 +14,6 @@ use crate::graphql_api::{ storage::coins::{ CoinsToSpendIndex, CoinsToSpendIndexKey, - IndexedCoinType, }, }; @@ -32,7 +31,7 @@ where { let key = CoinsToSpendIndexKey::from_coin(coin); let storage = block_st_transaction.storage::(); - let maybe_old_value = storage.replace(&key, &IndexedCoinType::Coin)?; + let maybe_old_value = storage.replace(&key, &())?; if maybe_old_value.is_some() { return Err(IndexationError::CoinToSpendAlreadyIndexed { owner: coin.owner, @@ -75,7 +74,7 @@ where { let key = CoinsToSpendIndexKey::from_message(message, base_asset_id); let storage = block_st_transaction.storage::(); - let maybe_old_value = storage.replace(&key, &IndexedCoinType::Message)?; + let maybe_old_value = storage.replace(&key, &())?; if maybe_old_value.is_some() { return Err(IndexationError::MessageToSpendAlreadyIndexed { owner: *message.recipient(), @@ -191,10 +190,10 @@ mod tests { .map(|entry| entry.expect("should read entries")) .map(|entry| { ( - entry.key.owner().unwrap(), - entry.key.asset_id().unwrap(), - [entry.key.retryable_flag().unwrap()], - entry.key.amount().unwrap(), + *entry.key.owner(), + *entry.key.asset_id(), + [entry.key.retryable_flag()], + entry.key.amount(), ) }) .collect(); @@ -727,7 +726,7 @@ mod tests { .entries::(None, IterDirection::Forward) .map(|entry| entry.expect("should read entries")) .map(|entry| - entry.key.amount().unwrap(), + entry.key.amount(), ) .collect(); diff --git a/crates/fuel-core/src/graphql_api/ports.rs b/crates/fuel-core/src/graphql_api/ports.rs index 7227e5961a..df355f7cce 100644 --- a/crates/fuel-core/src/graphql_api/ports.rs +++ b/crates/fuel-core/src/graphql_api/ports.rs @@ -1,3 +1,5 @@ +use super::storage::balances::TotalBalanceAmount; +use crate::fuel_core_graphql_api::storage::coins::CoinsToSpendIndexKey; use async_trait::async_trait; use fuel_core_services::stream::BoxStream; use fuel_core_storage::{ @@ -64,14 +66,9 @@ use fuel_core_types::{ }; use std::sync::Arc; -use super::storage::{ - balances::TotalBalanceAmount, - coins::CoinsToSpendIndexEntry, -}; - pub struct CoinsToSpendIndexIter<'a> { - pub big_coins_iter: BoxedIter<'a, Result>, - pub dust_coins_iter: BoxedIter<'a, Result>, + pub big_coins_iter: BoxedIter<'a, Result>, + pub dust_coins_iter: BoxedIter<'a, Result>, } pub trait OffChainDatabase: Send + Sync { diff --git a/crates/fuel-core/src/graphql_api/storage/coins.rs b/crates/fuel-core/src/graphql_api/storage/coins.rs index 67226c4a7a..c963ce5b46 100644 --- a/crates/fuel-core/src/graphql_api/storage/coins.rs +++ b/crates/fuel-core/src/graphql_api/storage/coins.rs @@ -1,6 +1,16 @@ +mod codecs; + +use crate::fuel_core_graphql_api::{ + indexation::coins_to_spend::{ + NON_RETRYABLE_BYTE, + RETRYABLE_BYTE, + }, + storage::coins::codecs::UTXO_ID_SIZE, +}; use fuel_core_storage::{ blueprint::plain::Plain, codec::{ + manual::Manual, postcard::Postcard, primitive::utxo_id_to_bytes, raw::Raw, @@ -14,33 +24,14 @@ use fuel_core_types::{ Message, }, fuel_tx::{ - self, Address, AssetId, TxId, UtxoId, }, - fuel_types::{ - self, - Nonce, - }, -}; - -use crate::graphql_api::indexation; - -use self::indexation::{ - coins_to_spend::{ - NON_RETRYABLE_BYTE, - RETRYABLE_BYTE, - }, - error::IndexationError, + fuel_types::Nonce, }; -const AMOUNT_SIZE: usize = size_of::(); -const UTXO_ID_SIZE: usize = size_of::(); -const RETRYABLE_FLAG_SIZE: usize = size_of::(); - -// TODO: Reuse `fuel_vm::storage::double_key` macro. pub fn owner_coin_id_key(owner: &Address, coin_id: &UtxoId) -> OwnedCoinKey { let mut default = [0u8; Address::LEN + UTXO_ID_SIZE]; default[0..Address::LEN].copy_from_slice(owner.as_ref()); @@ -56,11 +47,11 @@ impl Mappable for CoinsToSpendIndex { type Key = Self::OwnedKey; type OwnedKey = CoinsToSpendIndexKey; type Value = Self::OwnedValue; - type OwnedValue = IndexedCoinType; + type OwnedValue = (); } impl TableWithBlueprint for CoinsToSpendIndex { - type Blueprint = Plain; + type Blueprint = Plain, Postcard>; type Column = super::Column; fn column() -> Self::Column { @@ -68,49 +59,23 @@ impl TableWithBlueprint for CoinsToSpendIndex { } } -// For coins, the foreign key is the UtxoId (34 bytes). -pub(crate) const COIN_FOREIGN_KEY_LEN: usize = UTXO_ID_SIZE; - -// For messages, the foreign key is the nonce (32 bytes). -pub(crate) const MESSAGE_FOREIGN_KEY_LEN: usize = Nonce::LEN; - -#[repr(u8)] -#[derive(Debug, Clone, PartialEq)] -pub enum IndexedCoinType { - Coin, - Message, -} - -impl AsRef<[u8]> for IndexedCoinType { - fn as_ref(&self) -> &[u8] { - match self { - IndexedCoinType::Coin => &[IndexedCoinType::Coin as u8], - IndexedCoinType::Message => &[IndexedCoinType::Message as u8], - } - } -} - -impl TryFrom<&[u8]> for IndexedCoinType { - type Error = IndexationError; - - fn try_from(value: &[u8]) -> Result { - match value { - [0] => Ok(IndexedCoinType::Coin), - [1] => Ok(IndexedCoinType::Message), - [] => Err(IndexationError::InvalidIndexedCoinType { coin_type: None }), - x => Err(IndexationError::InvalidIndexedCoinType { - coin_type: Some(x[0]), - }), - } - } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CoinsToSpendIndexKey { + Coin { + owner: Address, + asset_id: AssetId, + amount: u64, + utxo_id: UtxoId, + }, + Message { + retryable_flag: u8, + owner: Address, + asset_id: AssetId, + amount: u64, + nonce: Nonce, + }, } -pub type CoinsToSpendIndexEntry = (CoinsToSpendIndexKey, IndexedCoinType); - -// TODO: Convert this key from Vec to strongly typed struct: https://github.com/FuelLabs/fuel-core/issues/2498 -#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct CoinsToSpendIndexKey(Vec); - impl core::fmt::Display for CoinsToSpendIndexKey { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!( @@ -124,53 +89,14 @@ impl core::fmt::Display for CoinsToSpendIndexKey { } } -impl TryFrom<&CoinsToSpendIndexKey> for fuel_tx::UtxoId { - type Error = (); - - fn try_from(value: &CoinsToSpendIndexKey) -> Result { - let bytes: [u8; COIN_FOREIGN_KEY_LEN] = value - .foreign_key_bytes() - .ok_or(())? - .try_into() - .map_err(|_| ())?; - let (tx_id_bytes, output_index_bytes) = bytes.split_at(TxId::LEN); - let tx_id = TxId::try_from(tx_id_bytes).map_err(|_| ())?; - let output_index = - u16::from_be_bytes(output_index_bytes.try_into().map_err(|_| ())?); - Ok(fuel_tx::UtxoId::new(tx_id, output_index)) - } -} - -impl TryFrom<&CoinsToSpendIndexKey> for fuel_types::Nonce { - type Error = (); - - fn try_from(value: &CoinsToSpendIndexKey) -> Result { - value - .foreign_key_bytes() - .and_then(|bytes| <[u8; MESSAGE_FOREIGN_KEY_LEN]>::try_from(bytes).ok()) - .map(fuel_types::Nonce::from) - .ok_or(()) - } -} - impl CoinsToSpendIndexKey { pub fn from_coin(coin: &Coin) -> Self { - let retryable_flag_bytes = NON_RETRYABLE_BYTE; - let address_bytes = coin.owner.as_ref(); - let asset_id_bytes = coin.asset_id.as_ref(); - let amount_bytes = coin.amount.to_be_bytes(); - let utxo_id_bytes = utxo_id_to_bytes(&coin.utxo_id); - - Self( - retryable_flag_bytes - .iter() - .chain(address_bytes) - .chain(asset_id_bytes) - .chain(amount_bytes.iter()) - .chain(utxo_id_bytes.iter()) - .copied() - .collect(), - ) + Self::Coin { + owner: coin.owner, + asset_id: coin.asset_id, + amount: coin.amount, + utxo_id: coin.utxo_id, + } } pub fn from_message(message: &Message, base_asset_id: &AssetId) -> Self { @@ -179,74 +105,41 @@ impl CoinsToSpendIndexKey { } else { NON_RETRYABLE_BYTE }; - let address_bytes = message.recipient().as_ref(); - let asset_id_bytes = base_asset_id.as_ref(); - let amount_bytes = message.amount().to_be_bytes(); - let nonce_bytes = message.nonce().as_slice(); - - Self( - retryable_flag_bytes - .iter() - .chain(address_bytes) - .chain(asset_id_bytes) - .chain(amount_bytes.iter()) - .chain(nonce_bytes) - .copied() - .collect(), - ) - } - - fn from_slice(slice: &[u8]) -> Self { - Self(slice.into()) - } - - pub fn owner(&self) -> Option
{ - const ADDRESS_START: usize = RETRYABLE_FLAG_SIZE; - const ADDRESS_END: usize = ADDRESS_START + Address::LEN; - - let bytes = self.0.get(ADDRESS_START..ADDRESS_END)?; - bytes.try_into().ok().map(Address::new) - } - - pub fn asset_id(&self) -> Option { - const OFFSET: usize = RETRYABLE_FLAG_SIZE + Address::LEN; - const ASSET_ID_START: usize = OFFSET; - const ASSET_ID_END: usize = ASSET_ID_START + AssetId::LEN; - - let bytes = self.0.get(ASSET_ID_START..ASSET_ID_END)?; - bytes.try_into().ok().map(AssetId::new) - } - - pub fn retryable_flag(&self) -> Option { - const OFFSET: usize = 0; - self.0.get(OFFSET).copied() + Self::Message { + retryable_flag: retryable_flag_bytes[0], + owner: *message.recipient(), + asset_id: *base_asset_id, + amount: message.amount(), + nonce: *message.nonce(), + } } - pub fn amount(&self) -> Option { - const OFFSET: usize = RETRYABLE_FLAG_SIZE + Address::LEN + AssetId::LEN; - const AMOUNT_START: usize = OFFSET; - const AMOUNT_END: usize = AMOUNT_START + AMOUNT_SIZE; - - let bytes = self.0.get(AMOUNT_START..AMOUNT_END)?; - bytes.try_into().ok().map(u64::from_be_bytes) + pub fn owner(&self) -> &Address { + match self { + CoinsToSpendIndexKey::Coin { owner, .. } => owner, + CoinsToSpendIndexKey::Message { owner, .. } => owner, + } } - pub fn foreign_key_bytes(&self) -> Option<&[u8]> { - const OFFSET: usize = - RETRYABLE_FLAG_SIZE + Address::LEN + AssetId::LEN + AMOUNT_SIZE; - self.0.get(OFFSET..) + pub fn asset_id(&self) -> &AssetId { + match self { + CoinsToSpendIndexKey::Coin { asset_id, .. } => asset_id, + CoinsToSpendIndexKey::Message { asset_id, .. } => asset_id, + } } -} -impl From<&[u8]> for CoinsToSpendIndexKey { - fn from(slice: &[u8]) -> Self { - CoinsToSpendIndexKey::from_slice(slice) + pub fn retryable_flag(&self) -> u8 { + match self { + CoinsToSpendIndexKey::Coin { .. } => NON_RETRYABLE_BYTE[0], + CoinsToSpendIndexKey::Message { retryable_flag, .. } => *retryable_flag, + } } -} -impl AsRef<[u8]> for CoinsToSpendIndexKey { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() + pub fn amount(&self) -> u64 { + match self { + CoinsToSpendIndexKey::Coin { amount, .. } => *amount, + CoinsToSpendIndexKey::Message { amount, .. } => *amount, + } } } @@ -273,6 +166,18 @@ impl TableWithBlueprint for OwnedCoins { #[cfg(test)] mod test { + use crate::{ + fuel_core_graphql_api::storage::coins::codecs::{ + AMOUNT_SIZE, + COIN_TYPE_SIZE, + RETRYABLE_FLAG_SIZE, + }, + graphql_api::storage::coins::codecs::CoinType, + }; + use fuel_core_storage::codec::{ + Encode, + Encoder, + }; use fuel_core_types::{ entities::relayer::message::MessageV1, fuel_types::Nonce, @@ -284,16 +189,22 @@ mod test { for rand::distributions::Standard { fn sample(&self, rng: &mut R) -> CoinsToSpendIndexKey { - let bytes: Vec<_> = if rng.gen() { - (0..COIN_TO_SPEND_COIN_KEY_LEN) - .map(|_| rng.gen::()) - .collect() + if rng.gen() { + CoinsToSpendIndexKey::Coin { + owner: rng.gen(), + asset_id: rng.gen(), + amount: rng.gen(), + utxo_id: rng.gen(), + } } else { - (0..COIN_TO_SPEND_MESSAGE_KEY_LEN) - .map(|_| rng.gen::()) - .collect() - }; - CoinsToSpendIndexKey(bytes) + CoinsToSpendIndexKey::Message { + retryable_flag: rng.gen(), + owner: rng.gen(), + asset_id: rng.gen(), + amount: rng.gen(), + nonce: rng.gen(), + } + } } } @@ -303,11 +214,11 @@ mod test { // Total length of the coins to spend index key for coins. const COIN_TO_SPEND_COIN_KEY_LEN: usize = - COIN_TO_SPEND_BASE_KEY_LEN + COIN_FOREIGN_KEY_LEN; + COIN_TO_SPEND_BASE_KEY_LEN + UTXO_ID_SIZE + COIN_TYPE_SIZE; // Total length of the coins to spend index key for messages. const COIN_TO_SPEND_MESSAGE_KEY_LEN: usize = - COIN_TO_SPEND_BASE_KEY_LEN + MESSAGE_FOREIGN_KEY_LEN; + COIN_TO_SPEND_BASE_KEY_LEN + Nonce::LEN + COIN_TYPE_SIZE; fn generate_key(rng: &mut impl rand::Rng) -> ::Key { let mut bytes = [0u8; 66]; @@ -325,28 +236,19 @@ mod test { fuel_core_storage::basic_storage_tests!( CoinsToSpendIndex, - ::Key::default(), - IndexedCoinType::Coin + CoinsToSpendIndexKey::Coin { + owner: Default::default(), + asset_id: Default::default(), + amount: 0, + utxo_id: Default::default(), + }, + Default::default() ); - fn merge_foreign_key_bytes(a: A, b: B) -> [u8; N] - where - A: AsRef<[u8]>, - B: AsRef<[u8]>, - { - a.as_ref() - .iter() - .copied() - .chain(b.as_ref().iter().copied()) - .collect::>() - .try_into() - .expect("should have correct length") - } - #[test] - fn key_from_coin() { + fn serialized_key_from_coin_is_correct() { // Given - let retryable_flag = NON_RETRYABLE_BYTE; + let retryable_flag = NON_RETRYABLE_BYTE[0]; let owner = Address::new([ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, @@ -385,12 +287,17 @@ mod test { // Then let key_bytes: [u8; COIN_TO_SPEND_COIN_KEY_LEN] = - key.as_ref().try_into().expect("should have correct length"); + Manual::::encode(&key) + .as_bytes() + .as_ref() + .try_into() + .expect("should have correct length"); + #[rustfmt::skip] assert_eq!( key_bytes, [ - 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, + retryable_flag, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, @@ -398,24 +305,15 @@ mod test { 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, - 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0xFE, 0xFF, + 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0xFE, 0xFF, CoinType::Coin as u8, ] ); - - assert_eq!(key.owner().unwrap(), owner); - assert_eq!(key.asset_id().unwrap(), asset_id); - assert_eq!(key.retryable_flag().unwrap(), retryable_flag[0]); - assert_eq!(key.amount().unwrap(), u64::from_be_bytes(amount)); - assert_eq!( - key.foreign_key_bytes().unwrap(), - &merge_foreign_key_bytes::<_, _, COIN_FOREIGN_KEY_LEN>(tx_id, output_index) - ); } #[test] - fn key_from_non_retryable_message() { + fn serialized_key_from_non_retryable_message_is_correct() { // Given - let retryable_flag = NON_RETRYABLE_BYTE; + let retryable_flag = NON_RETRYABLE_BYTE[0]; let owner = Address::new([ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, @@ -452,12 +350,17 @@ mod test { // Then let key_bytes: [u8; COIN_TO_SPEND_MESSAGE_KEY_LEN] = - key.as_ref().try_into().expect("should have correct length"); + Manual::::encode(&key) + .as_bytes() + .as_ref() + .try_into() + .expect("should have correct length"); + #[rustfmt::skip] assert_eq!( key_bytes, [ - 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, + retryable_flag, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, @@ -465,21 +368,15 @@ mod test { 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, - 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, CoinType::Message as u8, ] ); - - assert_eq!(key.owner().unwrap(), owner); - assert_eq!(key.asset_id().unwrap(), base_asset_id); - assert_eq!(key.retryable_flag().unwrap(), retryable_flag[0]); - assert_eq!(key.amount().unwrap(), u64::from_be_bytes(amount)); - assert_eq!(key.foreign_key_bytes().unwrap(), nonce.as_ref()); } #[test] - fn key_from_retryable_message() { + fn serialized_key_from_retryable_message_is_correct() { // Given - let retryable_flag = RETRYABLE_BYTE; + let retryable_flag = RETRYABLE_BYTE[0]; let owner = Address::new([ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, @@ -516,12 +413,17 @@ mod test { // Then let key_bytes: [u8; COIN_TO_SPEND_MESSAGE_KEY_LEN] = - key.as_ref().try_into().expect("should have correct length"); + Manual::::encode(&key) + .as_bytes() + .as_ref() + .try_into() + .expect("should have correct length"); + #[rustfmt::skip] assert_eq!( key_bytes, [ - 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, + retryable_flag, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, @@ -529,14 +431,8 @@ mod test { 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, - 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F + 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, CoinType::Message as u8, ] ); - - assert_eq!(key.owner().unwrap(), owner); - assert_eq!(key.asset_id().unwrap(), base_asset_id); - assert_eq!(key.retryable_flag().unwrap(), retryable_flag[0]); - assert_eq!(key.amount().unwrap(), u64::from_be_bytes(amount)); - assert_eq!(key.foreign_key_bytes().unwrap(), nonce.as_ref()); } } diff --git a/crates/fuel-core/src/graphql_api/storage/coins/codecs.rs b/crates/fuel-core/src/graphql_api/storage/coins/codecs.rs new file mode 100644 index 0000000000..5683e2896b --- /dev/null +++ b/crates/fuel-core/src/graphql_api/storage/coins/codecs.rs @@ -0,0 +1,190 @@ +use crate::fuel_core_graphql_api::{ + indexation::coins_to_spend::NON_RETRYABLE_BYTE, + storage::coins::CoinsToSpendIndexKey, +}; +use fuel_core_storage::codec::{ + manual::Manual, + primitive::utxo_id_to_bytes, + Decode, + Encode, + Encoder, +}; +use fuel_core_types::{ + fuel_tx::{ + Address, + AssetId, + TxId, + UtxoId, + }, + fuel_types::Nonce, +}; +use std::borrow::Cow; + +pub const AMOUNT_SIZE: usize = size_of::(); +pub const UTXO_ID_SIZE: usize = size_of::(); +pub const RETRYABLE_FLAG_SIZE: usize = size_of::(); + +#[repr(u8)] +pub enum CoinType { + Coin, + Message, +} + +pub const COIN_TYPE_SIZE: usize = size_of::(); +pub const COIN_VARIANT_SIZE: usize = + 1 + Address::LEN + AssetId::LEN + AMOUNT_SIZE + UTXO_ID_SIZE + COIN_TYPE_SIZE; +pub const MESSAGE_VARIANT_SIZE: usize = + 1 + Address::LEN + AssetId::LEN + AMOUNT_SIZE + Nonce::LEN + COIN_TYPE_SIZE; + +pub enum SerializedCoinsToSpendIndexKey { + Coin([u8; COIN_VARIANT_SIZE]), + Message([u8; MESSAGE_VARIANT_SIZE]), +} + +impl Encoder for SerializedCoinsToSpendIndexKey { + fn as_bytes(&self) -> Cow<[u8]> { + match self { + SerializedCoinsToSpendIndexKey::Coin(bytes) => Cow::Borrowed(bytes), + SerializedCoinsToSpendIndexKey::Message(bytes) => Cow::Borrowed(bytes), + } + } +} + +impl Encode for Manual { + type Encoder<'a> = SerializedCoinsToSpendIndexKey; + + fn encode(t: &CoinsToSpendIndexKey) -> Self::Encoder<'_> { + match t { + CoinsToSpendIndexKey::Coin { + owner, + asset_id, + amount, + utxo_id, + } => { + let retryable_flag_bytes = NON_RETRYABLE_BYTE; + + // retryable_flag | address | asset_id | amount | utxo_id | coin_type + let mut serialized_coin = [0u8; COIN_VARIANT_SIZE]; + let mut start = 0; + let mut end = RETRYABLE_FLAG_SIZE; + serialized_coin[start] = retryable_flag_bytes[0]; + start = end; + end = end.saturating_add(Address::LEN); + serialized_coin[start..end].copy_from_slice(owner.as_ref()); + start = end; + end = end.saturating_add(AssetId::LEN); + serialized_coin[start..end].copy_from_slice(asset_id.as_ref()); + start = end; + end = end.saturating_add(AMOUNT_SIZE); + serialized_coin[start..end].copy_from_slice(&amount.to_be_bytes()); + start = end; + end = end.saturating_add(UTXO_ID_SIZE); + serialized_coin[start..end].copy_from_slice(&utxo_id_to_bytes(utxo_id)); + start = end; + serialized_coin[start] = CoinType::Coin as u8; + + SerializedCoinsToSpendIndexKey::Coin(serialized_coin) + } + CoinsToSpendIndexKey::Message { + retryable_flag, + owner, + asset_id, + amount, + nonce, + } => { + // retryable_flag | address | asset_id | amount | nonce | coin_type + let mut serialized_coin = [0u8; MESSAGE_VARIANT_SIZE]; + let mut start = 0; + let mut end = RETRYABLE_FLAG_SIZE; + serialized_coin[start] = *retryable_flag; + start = end; + end = end.saturating_add(Address::LEN); + serialized_coin[start..end].copy_from_slice(owner.as_ref()); + start = end; + end = end.saturating_add(AssetId::LEN); + serialized_coin[start..end].copy_from_slice(asset_id.as_ref()); + start = end; + end = end.saturating_add(AMOUNT_SIZE); + 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 for Manual { + fn decode(bytes: &[u8]) -> anyhow::Result { + 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; + // let retryable_flag = bytes[start..end]; + 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); + + let (tx_id_bytes, output_index_bytes) = + bytes[start..end].split_at(TxId::LEN); + let tx_id = TxId::try_from(tx_id_bytes)?; + let output_index = u16::from_be_bytes(output_index_bytes.try_into()?); + let utxo_id = UtxoId::new(tx_id, output_index); + + CoinsToSpendIndexKey::Coin { + owner, + asset_id, + amount, + utxo_id, + } + } + CoinType::Message => { + let bytes: [u8; MESSAGE_VARIANT_SIZE] = bytes.try_into()?; + let mut start = 0; + let mut end = RETRYABLE_FLAG_SIZE; + let retryable_flag = bytes[start..end][0]; + 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(Nonce::LEN); + let nonce = Nonce::try_from(&bytes[start..end])?; + + CoinsToSpendIndexKey::Message { + retryable_flag, + owner, + asset_id, + amount, + nonce, + } + } + }; + + Ok(result) + } +} diff --git a/crates/fuel-core/src/schema/coins.rs b/crates/fuel-core/src/schema/coins.rs index 68893c7ca2..b7a1cc765d 100644 --- a/crates/fuel-core/src/schema/coins.rs +++ b/crates/fuel-core/src/schema/coins.rs @@ -10,15 +10,12 @@ use crate::{ }, fuel_core_graphql_api::{ query_costs, + storage::coins::CoinsToSpendIndexKey, IntoApiResult, }, graphql_api::{ api_service::ConsensusProvider, database::ReadView, - storage::coins::{ - CoinsToSpendIndexEntry, - IndexedCoinType, - }, }, query::asset_query::AssetSpendTarget, schema::{ @@ -412,7 +409,7 @@ async fn coins_to_spend_with_cache( .await?; let mut coins_per_asset = Vec::with_capacity(selected_coins.len()); - for coin_or_message_id in into_coin_id(&selected_coins)? { + for coin_or_message_id in into_coin_id(&selected_coins) { let coin_type = match coin_or_message_id { coins::CoinId::Utxo(utxo_id) => { db.coin(utxo_id).map(|coin| CoinType::Coin(coin.into()))? @@ -432,26 +429,14 @@ async fn coins_to_spend_with_cache( Ok(all_coins) } -fn into_coin_id( - selected: &[CoinsToSpendIndexEntry], -) -> Result, CoinsQueryError> { +fn into_coin_id(selected: &[CoinsToSpendIndexKey]) -> Vec { let mut coins = Vec::with_capacity(selected.len()); - for (key, coin_type) in selected { - let coin = match coin_type { - IndexedCoinType::Coin => { - let utxo = key - .try_into() - .map_err(|_| CoinsQueryError::IncorrectCoinForeignKeyInIndex)?; - CoinId::Utxo(utxo) - } - IndexedCoinType::Message => { - let nonce = key - .try_into() - .map_err(|_| CoinsQueryError::IncorrectMessageForeignKeyInIndex)?; - CoinId::Message(nonce) - } + for coin in selected { + let coin = match coin { + CoinsToSpendIndexKey::Coin { utxo_id, .. } => CoinId::Utxo(*utxo_id), + CoinsToSpendIndexKey::Message { nonce, .. } => CoinId::Message(*nonce), }; coins.push(coin); } - Ok(coins) + coins } diff --git a/crates/fuel-core/src/service/adapters/graphql_api/off_chain.rs b/crates/fuel-core/src/service/adapters/graphql_api/off_chain.rs index 77b931a022..d9bcbe0348 100644 --- a/crates/fuel-core/src/service/adapters/graphql_api/off_chain.rs +++ b/crates/fuel-core/src/service/adapters/graphql_api/off_chain.rs @@ -302,16 +302,22 @@ impl OffChainDatabase for OffChainIterableKeyValueView { .collect(); CoinsToSpendIndexIter { - big_coins_iter: self.iter_all_filtered::( - Some(&prefix), - None, - Some(IterDirection::Reverse), - ), - dust_coins_iter: self.iter_all_filtered::( - Some(&prefix), - None, - Some(IterDirection::Forward), - ), + big_coins_iter: self + .iter_all_filtered::( + Some(&prefix), + None, + Some(IterDirection::Reverse), + ) + .map(|result| result.map(|(key, _)| key)) + .into_boxed(), + dust_coins_iter: self + .iter_all_filtered::( + Some(&prefix), + None, + Some(IterDirection::Forward), + ) + .map(|result| result.map(|(key, _)| key)) + .into_boxed(), } } }