diff --git a/CHANGELOG.md b/CHANGELOG.md index d9747caeecf..20e8478887f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Description of the upcoming release here. ### Changed - [#1658](https://github.com/FuelLabs/fuel-core/pull/1658): Removed `Receipts` table. Instead, receipts are part of the `TransactionStatuses` table. +- [#1640](https://github.com/FuelLabs/fuel-core/pull/1640): Upgrade to fuel-vm 0.45.0. +- [#1635](https://github.com/FuelLabs/fuel-core/pull/1635): Move updating of the owned messages and coins to off-chain worker. - [#1650](https://github.com/FuelLabs/fuel-core/pull/1650): Add api endpoint for getting estimates for future gas prices - [#1649](https://github.com/FuelLabs/fuel-core/pull/1649): Add api endpoint for getting latest gas price - [#1600](https://github.com/FuelLabs/fuel-core/pull/1640): Upgrade to fuel-vm 0.45.0 diff --git a/crates/chain-config/src/config/coin.rs b/crates/chain-config/src/config/coin.rs index 5ad447f58ed..01bba78be0b 100644 --- a/crates/chain-config/src/config/coin.rs +++ b/crates/chain-config/src/config/coin.rs @@ -27,7 +27,7 @@ use serde_with::{ #[skip_serializing_none] #[serde_as] -#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[derive(Default, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] pub struct CoinConfig { /// auto-generated if None #[serde_as(as = "Option")] diff --git a/crates/chain-config/src/config/state.rs b/crates/chain-config/src/config/state.rs index 6999776a383..731c5b498cf 100644 --- a/crates/chain-config/src/config/state.rs +++ b/crates/chain-config/src/config/state.rs @@ -21,7 +21,7 @@ use super::{ // TODO: do streaming deserialization to handle large state configs #[serde_as] #[skip_serializing_none] -#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)] +#[derive(Default, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] pub struct StateConfig { /// Spendable coins pub coins: Option>, diff --git a/crates/fuel-core/src/coins_query.rs b/crates/fuel-core/src/coins_query.rs index d219b925c46..1f89146941e 100644 --- a/crates/fuel-core/src/coins_query.rs +++ b/crates/fuel-core/src/coins_query.rs @@ -229,7 +229,19 @@ mod tests { SpendQuery, }, combined_database::CombinedDatabase, - fuel_core_graphql_api::api_service::ReadDatabase as ServiceDatabase, + fuel_core_graphql_api::{ + api_service::ReadDatabase as ServiceDatabase, + storage::{ + coins::{ + owner_coin_id_key, + OwnedCoins, + }, + messages::{ + OwnedMessageIds, + OwnedMessageKey, + }, + }, + }, query::asset_query::{ AssetQuery, AssetSpendTarget, @@ -921,6 +933,7 @@ mod tests { coin_result } + // TODO: Should use any mock database instead of the `fuel_core::CombinedDatabase`. pub struct TestDatabase { database: CombinedDatabase, last_coin_index: u64, @@ -961,6 +974,9 @@ mod tests { let db = self.database.on_chain_mut(); StorageMutate::::insert(db, &id, &coin).unwrap(); + let db = self.database.off_chain_mut(); + let coin_by_owner = owner_coin_id_key(&owner, &id); + StorageMutate::::insert(db, &coin_by_owner, &()).unwrap(); coin.uncompress(id) } @@ -981,6 +997,10 @@ mod tests { let db = self.database.on_chain_mut(); StorageMutate::::insert(db, message.id(), &message).unwrap(); + let db = self.database.off_chain_mut(); + let owned_message_key = OwnedMessageKey::new(&owner, &nonce); + StorageMutate::::insert(db, &owned_message_key, &()) + .unwrap(); message } diff --git a/crates/fuel-core/src/combined_database.rs b/crates/fuel-core/src/combined_database.rs index e31f0367e1c..5658d0aaa52 100644 --- a/crates/fuel-core/src/combined_database.rs +++ b/crates/fuel-core/src/combined_database.rs @@ -79,6 +79,11 @@ impl CombinedDatabase { &self.off_chain } + #[cfg(any(feature = "test-helpers", test))] + pub fn off_chain_mut(&mut self) -> &mut Database { + &mut self.off_chain + } + pub fn relayer(&self) -> &Database { &self.relayer } diff --git a/crates/fuel-core/src/database/coin.rs b/crates/fuel-core/src/database/coin.rs index 5215127b90e..bfaaab7c041 100644 --- a/crates/fuel-core/src/database/coin.rs +++ b/crates/fuel-core/src/database/coin.rs @@ -1,26 +1,20 @@ -use crate::database::{ - database_description::on_chain::OnChain, - Database, +use crate::{ + database::{ + database_description::off_chain::OffChain, + Database, + }, + fuel_core_graphql_api::storage::coins::{ + owner_coin_id_key, + OwnedCoins, + }, }; use fuel_core_chain_config::CoinConfig; use fuel_core_storage::{ - blueprint::plain::Plain, - codec::{ - postcard::Postcard, - primitive::utxo_id_to_bytes, - raw::Raw, - }, iter::IterDirection, not_found, - structured_storage::TableWithBlueprint, tables::Coins, - Error as StorageError, - Mappable, Result as StorageResult, - StorageAsMut, StorageAsRef, - StorageInspect, - StorageMutate, }; use fuel_core_txpool::types::TxId; use fuel_core_types::{ @@ -30,78 +24,8 @@ use fuel_core_types::{ UtxoId, }, }; -use std::borrow::Cow; - -// 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 + TxId::LEN + 1]; - default[0..Address::LEN].copy_from_slice(owner.as_ref()); - default[Address::LEN..].copy_from_slice(utxo_id_to_bytes(coin_id).as_ref()); - default -} - -/// The storage table of owned coin ids. Maps addresses to owned coins. -pub struct OwnedCoins; -/// The storage key for owned coins: `Address ++ UtxoId` -pub type OwnedCoinKey = [u8; Address::LEN + TxId::LEN + 1]; - -impl Mappable for OwnedCoins { - type Key = Self::OwnedKey; - type OwnedKey = OwnedCoinKey; - type Value = Self::OwnedValue; - type OwnedValue = (); -} -impl TableWithBlueprint for OwnedCoins { - type Blueprint = Plain; - type Column = fuel_core_storage::column::Column; - - fn column() -> Self::Column { - Self::Column::OwnedCoins - } -} - -impl StorageInspect for Database { - type Error = StorageError; - - fn get(&self, key: &UtxoId) -> Result>, Self::Error> { - self.data.storage::().get(key) - } - - fn contains_key(&self, key: &UtxoId) -> Result { - self.data.storage::().contains_key(key) - } -} - -impl StorageMutate for Database { - fn insert( - &mut self, - key: &UtxoId, - value: &CompressedCoin, - ) -> Result, Self::Error> { - let coin_by_owner = owner_coin_id_key(value.owner(), key); - // insert primary record - let insert = self.data.storage_as_mut::().insert(key, value)?; - // insert secondary index by owner - self.storage_as_mut::() - .insert(&coin_by_owner, &())?; - Ok(insert) - } - - fn remove(&mut self, key: &UtxoId) -> Result, Self::Error> { - let coin = self.data.storage_as_mut::().remove(key)?; - - // cleanup secondary index - if let Some(coin) = &coin { - let key = owner_coin_id_key(coin.owner(), key); - self.storage_as_mut::().remove(&key)?; - } - - Ok(coin) - } -} - -impl Database { +impl Database { pub fn owned_coins_ids( &self, owner: &Address, @@ -158,22 +82,3 @@ impl Database { Ok(Some(configs)) } } - -#[cfg(test)] -mod test { - use super::*; - - fn generate_key(rng: &mut impl rand::Rng) -> ::Key { - let mut bytes = [0u8; 65]; - rng.fill(bytes.as_mut()); - bytes - } - - fuel_core_storage::basic_storage_tests!( - OwnedCoins, - [0u8; 65], - ::Value::default(), - ::Value::default(), - generate_key - ); -} diff --git a/crates/fuel-core/src/database/database_description/on_chain.rs b/crates/fuel-core/src/database/database_description/on_chain.rs index 2eb3f172696..7023232a445 100644 --- a/crates/fuel-core/src/database/database_description/on_chain.rs +++ b/crates/fuel-core/src/database/database_description/on_chain.rs @@ -22,10 +22,7 @@ impl DatabaseDescription for OnChain { fn prefix(column: &Self::Column) -> Option { match column { - Self::Column::OwnedCoins - | Self::Column::OwnedMessageIds - | Self::Column::ContractsAssets - | Self::Column::ContractsState => { + Self::Column::ContractsAssets | Self::Column::ContractsState => { // prefix is address length Some(32) } diff --git a/crates/fuel-core/src/database/message.rs b/crates/fuel-core/src/database/message.rs index 6f1c5a8fe23..c02bf5a296c 100644 --- a/crates/fuel-core/src/database/message.rs +++ b/crates/fuel-core/src/database/message.rs @@ -1,29 +1,22 @@ -use crate::database::{ - database_description::on_chain::OnChain, - Database, +use crate::{ + database::{ + database_description::off_chain::OffChain, + Database, + }, + fuel_core_graphql_api::storage::messages::{ + OwnedMessageIds, + OwnedMessageKey, + }, }; use fuel_core_chain_config::MessageConfig; use fuel_core_storage::{ - blueprint::plain::Plain, - codec::{ - manual::Manual, - postcard::Postcard, - Decode, - Encode, - }, iter::IterDirection, - structured_storage::TableWithBlueprint, tables::{ Messages, SpentMessages, }, Error as StorageError, - Mappable, Result as StorageResult, - StorageAsMut, - StorageAsRef, - StorageInspect, - StorageMutate, }; use fuel_core_types::{ entities::message::Message, @@ -32,86 +25,8 @@ use fuel_core_types::{ Nonce, }, }; -use std::borrow::Cow; - -fuel_core_types::fuel_vm::double_key!(OwnedMessageKey, Address, address, Nonce, nonce); - -/// The table that stores all messages per owner. -pub struct OwnedMessageIds; - -impl Mappable for OwnedMessageIds { - type Key = OwnedMessageKey; - type OwnedKey = Self::Key; - type Value = (); - type OwnedValue = Self::Value; -} - -impl Encode for Manual { - type Encoder<'a> = Cow<'a, [u8]>; - - fn encode(t: &OwnedMessageKey) -> Self::Encoder<'_> { - Cow::Borrowed(t.as_ref()) - } -} -impl Decode for Manual { - fn decode(bytes: &[u8]) -> anyhow::Result { - OwnedMessageKey::from_slice(bytes) - .map_err(|_| anyhow::anyhow!("Unable to decode bytes")) - } -} - -impl TableWithBlueprint for OwnedMessageIds { - type Blueprint = Plain, Postcard>; - type Column = fuel_core_storage::column::Column; - - fn column() -> Self::Column { - Self::Column::OwnedMessageIds - } -} - -impl StorageInspect for Database { - type Error = StorageError; - - fn get(&self, key: &Nonce) -> Result>, Self::Error> { - self.data.storage::().get(key) - } - - fn contains_key(&self, key: &Nonce) -> Result { - self.data.storage::().contains_key(key) - } -} - -impl StorageMutate for Database { - fn insert( - &mut self, - key: &Nonce, - value: &Message, - ) -> Result, Self::Error> { - // insert primary record - let result = self.data.storage_as_mut::().insert(key, value)?; - - // insert secondary record by owner - self.storage_as_mut::() - .insert(&OwnedMessageKey::new(value.recipient(), key), &())?; - - Ok(result) - } - - fn remove(&mut self, key: &Nonce) -> Result, Self::Error> { - let result: Option = - self.data.storage_as_mut::().remove(key)?; - - if let Some(message) = &result { - self.storage_as_mut::() - .remove(&OwnedMessageKey::new(message.recipient(), key))?; - } - - Ok(result) - } -} - -impl Database { +impl Database { pub fn owned_message_ids( &self, owner: &Address, @@ -179,47 +94,3 @@ impl Database { fuel_core_storage::StorageAsRef::storage::(&self).contains_key(id) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn owned_message_ids() { - let mut db = Database::::default(); - let message = Message::default(); - - // insert a message with the first id - let first_id = 1.into(); - let _ = db - .storage_as_mut::() - .insert(&first_id, &message) - .unwrap(); - - // insert a message with the second id with the same Owner - let second_id = 2.into(); - let _ = db - .storage_as_mut::() - .insert(&second_id, &message) - .unwrap(); - - // verify that 2 message IDs are associated with a single Owner/Recipient - let owned_msg_ids = db.owned_message_ids(message.recipient(), None, None); - assert_eq!(owned_msg_ids.count(), 2); - - // remove the first message with its given id - let _ = db.storage_as_mut::().remove(&first_id).unwrap(); - - // verify that only second ID is left - let owned_msg_ids: Vec<_> = db - .owned_message_ids(message.recipient(), None, None) - .collect(); - assert_eq!(owned_msg_ids.first().unwrap().as_ref().unwrap(), &second_id); - assert_eq!(owned_msg_ids.len(), 1); - - // remove the second message with its given id - let _ = db.storage_as_mut::().remove(&second_id).unwrap(); - let owned_msg_ids = db.owned_message_ids(message.recipient(), None, None); - assert_eq!(owned_msg_ids.count(), 0); - } -} diff --git a/crates/fuel-core/src/database/storage.rs b/crates/fuel-core/src/database/storage.rs index 7ff998bb47e..a801aa70bda 100644 --- a/crates/fuel-core/src/database/storage.rs +++ b/crates/fuel-core/src/database/storage.rs @@ -1,14 +1,16 @@ use crate::{ database::{ block::FuelBlockSecondaryKeyBlockHeights, - coin::OwnedCoins, database_description::DatabaseDescription, - message::OwnedMessageIds, Database, }, - fuel_core_graphql_api::storage::transactions::{ - OwnedTransactions, - TransactionStatuses, + fuel_core_graphql_api::storage::{ + coins::OwnedCoins, + messages::OwnedMessageIds, + transactions::{ + OwnedTransactions, + TransactionStatuses, + }, }, state::DataSource, }; @@ -23,11 +25,13 @@ use fuel_core_storage::{ FuelBlockMerkleData, FuelBlockMerkleMetadata, }, + Coins, ContractsAssets, ContractsInfo, ContractsLatestUtxo, ContractsRawCode, ContractsState, + Messages, ProcessedTransactions, SealedBlockConsensus, SpentMessages, @@ -85,7 +89,9 @@ use_structured_implementation!( ContractsStateMerkleData, ContractsAssetsMerkleMetadata, ContractsAssetsMerkleData, + Coins, OwnedCoins, + Messages, OwnedMessageIds, OwnedTransactions, TransactionStatuses, diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index a78fb1f77c7..583970466e2 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -109,6 +109,7 @@ mod tests { block_producer::Components, executor::{ Error as ExecutorError, + Event as ExecutorEvent, ExecutionBlock, ExecutionResult, ExecutionType, @@ -2012,7 +2013,7 @@ mod tests { transactions: vec![tx.into()], }; - let ExecutionResult { block, .. } = executor + let ExecutionResult { block, events, .. } = executor .execute_and_commit( ExecutionBlock::Production(block), ExecutionOptions { @@ -2022,15 +2023,16 @@ mod tests { .unwrap(); // assert the tx coin is spent - let coin = db - .storage::() - .get( - block.transactions()[0].as_script().unwrap().inputs()[0] - .utxo_id() - .unwrap(), - ) + let utxo_id = block.transactions()[0].as_script().unwrap().inputs()[0] + .utxo_id() .unwrap(); + let coin = db.storage::().get(utxo_id).unwrap(); assert!(coin.is_none()); + assert_eq!(events.len(), 2); + assert!( + matches!(events[0], ExecutorEvent::CoinConsumed(spent_coin) if &spent_coin.utxo_id == utxo_id) + ); + assert!(matches!(events[1], ExecutorEvent::CoinCreated(_))); } #[test] @@ -2943,7 +2945,7 @@ mod tests { // When let producer = create_relayer_executor(on_chain_db, relayer_db); - let block = test_block(block_height.into(), block_da_height.into(), 10); + let block = test_block(block_height.into(), block_da_height.into(), 0); let result = producer.execute_and_commit( ExecutionTypes::Production(block.into()), Default::default(), @@ -2951,17 +2953,22 @@ mod tests { // Then let view = producer.database_view_provider.latest_view(); - assert!(result.skipped_transactions.is_empty()); assert_eq!( view.iter_all::(None).count() as u64, block_da_height - genesis_da_height ); + assert_eq!( + result.events.len() as u64, + block_da_height - genesis_da_height + ); let messages = view.iter_all::(None); - for (da_height, message) in - (genesis_da_height + 1..block_da_height).zip(messages) + for ((da_height, message), event) in (genesis_da_height + 1..block_da_height) + .zip(messages) + .zip(result.events.iter()) { let (_, message) = message.unwrap(); assert_eq!(message.da_height(), da_height.into()); + assert!(matches!(event, ExecutorEvent::MessageImported(_))); } Ok(()) } @@ -3040,6 +3047,15 @@ mod tests { assert_eq!(view.iter_all::(None).count() as u64, 0); // Message added during this block immediately became spent. assert_eq!(view.iter_all::(None).count(), 1); + assert_eq!(result.events.len(), 2); + assert!(matches!( + result.events[0], + ExecutorEvent::MessageImported(_) + )); + assert!(matches!( + result.events[1], + ExecutorEvent::MessageConsumed(_) + )); } } } diff --git a/crates/fuel-core/src/graphql_api/database.rs b/crates/fuel-core/src/graphql_api/database.rs index bb94d008827..7477eadb7fc 100644 --- a/crates/fuel-core/src/graphql_api/database.rs +++ b/crates/fuel-core/src/graphql_api/database.rs @@ -186,15 +186,11 @@ impl DatabaseMessageProof for ReadView { } } -impl OnChainDatabase for ReadView { - fn owned_message_ids( - &self, - owner: &Address, - start_message_id: Option, - direction: IterDirection, - ) -> BoxedIter<'_, StorageResult> { - self.on_chain - .owned_message_ids(owner, start_message_id, direction) +impl OnChainDatabase for ReadView {} + +impl OffChainDatabase for ReadView { + fn tx_status(&self, tx_id: &TxId) -> StorageResult { + self.off_chain.tx_status(tx_id) } fn owned_coins_ids( @@ -203,13 +199,17 @@ impl OnChainDatabase for ReadView { start_coin: Option, direction: IterDirection, ) -> BoxedIter<'_, StorageResult> { - self.on_chain.owned_coins_ids(owner, start_coin, direction) + self.off_chain.owned_coins_ids(owner, start_coin, direction) } -} -impl OffChainDatabase for ReadView { - fn tx_status(&self, tx_id: &TxId) -> StorageResult { - self.off_chain.tx_status(tx_id) + fn owned_message_ids( + &self, + owner: &Address, + start_message_id: Option, + direction: IterDirection, + ) -> BoxedIter<'_, StorageResult> { + self.off_chain + .owned_message_ids(owner, start_message_id, direction) } fn owned_transactions_ids( diff --git a/crates/fuel-core/src/graphql_api/ports.rs b/crates/fuel-core/src/graphql_api/ports.rs index 570c71933d3..62dc5124546 100644 --- a/crates/fuel-core/src/graphql_api/ports.rs +++ b/crates/fuel-core/src/graphql_api/ports.rs @@ -61,6 +61,20 @@ use std::sync::Arc; pub trait OffChainDatabase: Send + Sync { fn tx_status(&self, tx_id: &TxId) -> StorageResult; + fn owned_coins_ids( + &self, + owner: &Address, + start_coin: Option, + direction: IterDirection, + ) -> BoxedIter<'_, StorageResult>; + + fn owned_message_ids( + &self, + owner: &Address, + start_message_id: Option, + direction: IterDirection, + ) -> BoxedIter<'_, StorageResult>; + fn owned_transactions_ids( &self, owner: Address, @@ -70,8 +84,6 @@ pub trait OffChainDatabase: Send + Sync { } /// The on chain database port expected by GraphQL API service. -// TODO: Move `owned_message_ids` and `owned_coins_ids`` to `OffChainDatabase` -// https://github.com/FuelLabs/fuel-core/issues/1583 pub trait OnChainDatabase: Send + Sync @@ -83,19 +95,6 @@ pub trait OnChainDatabase: + DatabaseChain + DatabaseMessageProof { - fn owned_message_ids( - &self, - owner: &Address, - start_message_id: Option, - direction: IterDirection, - ) -> BoxedIter<'_, StorageResult>; - - fn owned_coins_ids( - &self, - owner: &Address, - start_coin: Option, - direction: IterDirection, - ) -> BoxedIter<'_, StorageResult>; } /// Trait that specifies all the getters required for blocks. @@ -199,9 +198,15 @@ pub trait P2pPort: Send + Sync { } pub mod worker { - use crate::database::{ - database_description::off_chain::OffChain, - metadata::MetadataTable, + use crate::{ + database::{ + database_description::off_chain::OffChain, + metadata::MetadataTable, + }, + fuel_core_graphql_api::storage::{ + coins::OwnedCoins, + messages::OwnedMessageIds, + }, }; use fuel_core_services::stream::BoxStream; use fuel_core_storage::{ @@ -225,6 +230,8 @@ pub mod worker { pub trait OffChainDatabase: Send + Sync + + StorageMutate + + StorageMutate + StorageMutate, Error = StorageError> + Transactional { diff --git a/crates/fuel-core/src/graphql_api/storage.rs b/crates/fuel-core/src/graphql_api/storage.rs index 86863244675..89b7ec1427c 100644 --- a/crates/fuel-core/src/graphql_api/storage.rs +++ b/crates/fuel-core/src/graphql_api/storage.rs @@ -1,5 +1,7 @@ use fuel_core_storage::kv_store::StorageColumn; +pub mod coins; +pub mod messages; pub mod transactions; /// GraphQL database tables column ids to the corresponding [`fuel_core_storage::Mappable`] table. diff --git a/crates/fuel-core/src/graphql_api/storage/coins.rs b/crates/fuel-core/src/graphql_api/storage/coins.rs new file mode 100644 index 00000000000..f617c27bf37 --- /dev/null +++ b/crates/fuel-core/src/graphql_api/storage/coins.rs @@ -0,0 +1,63 @@ +use fuel_core_storage::{ + blueprint::plain::Plain, + codec::{ + postcard::Postcard, + primitive::utxo_id_to_bytes, + raw::Raw, + }, + structured_storage::TableWithBlueprint, + Mappable, +}; +use fuel_core_txpool::types::TxId; +use fuel_core_types::fuel_tx::{ + Address, + UtxoId, +}; + +// 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 + TxId::LEN + 1]; + default[0..Address::LEN].copy_from_slice(owner.as_ref()); + default[Address::LEN..].copy_from_slice(utxo_id_to_bytes(coin_id).as_ref()); + default +} + +/// The storage table of owned coin ids. Maps addresses to owned coins. +pub struct OwnedCoins; +/// The storage key for owned coins: `Address ++ UtxoId` +pub type OwnedCoinKey = [u8; Address::LEN + TxId::LEN + 1]; + +impl Mappable for OwnedCoins { + type Key = Self::OwnedKey; + type OwnedKey = OwnedCoinKey; + type Value = Self::OwnedValue; + type OwnedValue = (); +} + +impl TableWithBlueprint for OwnedCoins { + type Blueprint = Plain; + type Column = super::Column; + + fn column() -> Self::Column { + Self::Column::OwnedCoins + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn generate_key(rng: &mut impl rand::Rng) -> ::Key { + let mut bytes = [0u8; 65]; + rng.fill(bytes.as_mut()); + bytes + } + + fuel_core_storage::basic_storage_tests!( + OwnedCoins, + [0u8; 65], + ::Value::default(), + ::Value::default(), + generate_key + ); +} diff --git a/crates/fuel-core/src/graphql_api/storage/messages.rs b/crates/fuel-core/src/graphql_api/storage/messages.rs new file mode 100644 index 00000000000..832120eb644 --- /dev/null +++ b/crates/fuel-core/src/graphql_api/storage/messages.rs @@ -0,0 +1,76 @@ +use fuel_core_storage::{ + blueprint::plain::Plain, + codec::{ + manual::Manual, + postcard::Postcard, + Decode, + Encode, + }, + structured_storage::TableWithBlueprint, + Mappable, +}; +use fuel_core_types::fuel_types::{ + Address, + Nonce, +}; +use rand::{ + distributions::{ + Distribution, + Standard, + }, + Rng, +}; +use std::borrow::Cow; + +fuel_core_types::fuel_vm::double_key!(OwnedMessageKey, Address, address, Nonce, nonce); + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> OwnedMessageKey { + let mut bytes = [0u8; 64]; + + rng.fill_bytes(bytes.as_mut()); + + OwnedMessageKey::from_array(bytes) + } +} + +/// The table that stores all messages per owner. +pub struct OwnedMessageIds; + +impl Mappable for OwnedMessageIds { + type Key = OwnedMessageKey; + type OwnedKey = Self::Key; + type Value = (); + type OwnedValue = Self::Value; +} + +impl Encode for Manual { + type Encoder<'a> = Cow<'a, [u8]>; + + fn encode(t: &OwnedMessageKey) -> Self::Encoder<'_> { + Cow::Borrowed(t.as_ref()) + } +} + +impl Decode for Manual { + fn decode(bytes: &[u8]) -> anyhow::Result { + OwnedMessageKey::from_slice(bytes) + .map_err(|_| anyhow::anyhow!("Unable to decode bytes")) + } +} + +impl TableWithBlueprint for OwnedMessageIds { + type Blueprint = Plain, Postcard>; + type Column = super::Column; + + fn column() -> Self::Column { + Self::Column::OwnedMessageIds + } +} + +#[cfg(test)] +fuel_core_storage::basic_storage_tests!( + OwnedMessageIds, + ::Key::default(), + ::Value::default() +); diff --git a/crates/fuel-core/src/graphql_api/worker_service.rs b/crates/fuel-core/src/graphql_api/worker_service.rs index da0ecb228e5..247d16f1afc 100644 --- a/crates/fuel-core/src/graphql_api/worker_service.rs +++ b/crates/fuel-core/src/graphql_api/worker_service.rs @@ -7,7 +7,19 @@ use crate::{ }, metadata::MetadataTable, }, - fuel_core_graphql_api::ports, + fuel_core_graphql_api::{ + ports, + storage::{ + coins::{ + owner_coin_id_key, + OwnedCoins, + }, + messages::{ + OwnedMessageIds, + OwnedMessageKey, + }, + }, + }, }; use fuel_core_metrics::graphql_metrics::graphql_metrics; use fuel_core_services::{ @@ -47,7 +59,10 @@ use fuel_core_types::{ ImportResult, SharedImportResult, }, - executor::TransactionExecutionStatus, + executor::{ + Event, + TransactionExecutionStatus, + }, txpool::from_executor_to_status, }, }; @@ -55,6 +70,10 @@ use futures::{ FutureExt, StreamExt, }; +use std::{ + borrow::Cow, + ops::Deref, +}; /// The off-chain GraphQL API worker task processes the imported blocks /// and actualize the information used by the GraphQL service. @@ -68,8 +87,7 @@ where D: ports::worker::OffChainDatabase, { fn process_block(&mut self, result: SharedImportResult) -> anyhow::Result<()> { - // TODO: Implement the creation of indexes for the messages and coins. - // Implement table `BlockId -> BlockHeight` to get the block height by block id. + // TODO: Implement table `BlockId -> BlockHeight` to get the block height by block id. // https://github.com/FuelLabs/fuel-core/issues/1583 let block = &result.sealed_block.entity; let mut transaction = self.database.transaction(); @@ -83,6 +101,11 @@ where .increase_tx_count(block.transactions().len() as u64) .unwrap_or_default(); + Self::process_executor_events( + result.events.iter().map(Cow::Borrowed), + transaction.as_mut(), + )?; + // TODO: Temporary solution to store the block height in the database manually here. // Later it will be controlled by the `commit_changes` function on the `Database` side. // https://github.com/FuelLabs/fuel-core/issues/1589 @@ -105,6 +128,49 @@ where Ok(()) } + /// Process the executor events and update the indexes for the messages and coins. + pub fn process_executor_events<'a, Iter>( + events: Iter, + block_st_transaction: &mut D, + ) -> anyhow::Result<()> + where + Iter: Iterator>, + { + for event in events { + match event.deref() { + Event::MessageImported(message) => { + block_st_transaction + .storage_as_mut::() + .insert( + &OwnedMessageKey::new(message.recipient(), message.nonce()), + &(), + )?; + } + Event::MessageConsumed(message) => { + block_st_transaction + .storage_as_mut::() + .remove(&OwnedMessageKey::new( + message.recipient(), + message.nonce(), + ))?; + } + Event::CoinCreated(coin) => { + let coin_by_owner = owner_coin_id_key(&coin.owner, &coin.utxo_id); + block_st_transaction + .storage_as_mut::() + .insert(&coin_by_owner, &())?; + } + Event::CoinConsumed(coin) => { + let key = owner_coin_id_key(&coin.owner, &coin.utxo_id); + block_st_transaction + .storage_as_mut::() + .remove(&key)?; + } + } + } + Ok(()) + } + /// Associate all transactions within a block to their respective UTXO owners fn index_tx_owners_for_block( &self, diff --git a/crates/fuel-core/src/p2p_test_helpers.rs b/crates/fuel-core/src/p2p_test_helpers.rs index 007f4c6060f..a93d79a8d40 100644 --- a/crates/fuel-core/src/p2p_test_helpers.rs +++ b/crates/fuel-core/src/p2p_test_helpers.rs @@ -5,7 +5,6 @@ use crate::{ database::Database, p2p::Multiaddr, service::{ - genesis::execute_and_commit_genesis_block, Config, FuelService, ServiceTrait, @@ -399,7 +398,7 @@ pub async fn make_node(node_config: Config, test_txs: Vec) -> Node async fn extract_p2p_config(node_config: &Config) -> fuel_core_p2p::config::Config { let bootstrap_config = node_config.p2p.clone(); let db = Database::in_memory(); - execute_and_commit_genesis_block(node_config, &db) + crate::service::genesis::execute_and_commit_genesis_block(node_config, &db) .await .unwrap(); bootstrap_config diff --git a/crates/fuel-core/src/service.rs b/crates/fuel-core/src/service.rs index 3030fa1cfa7..25cf398abb0 100644 --- a/crates/fuel-core/src/service.rs +++ b/crates/fuel-core/src/service.rs @@ -253,13 +253,20 @@ impl RunnableService for Task { _: &StateWatcher, _: Self::TaskParams, ) -> anyhow::Result { - let view = self.shared.database.on_chain().latest_view(); + let on_view = self.shared.database.on_chain().latest_view(); + let off_view = self.shared.database.off_chain().latest_view(); // check if chain is initialized - if let Err(err) = view.get_genesis() { + if let Err(err) = on_view.get_genesis() { if err.is_not_found() { - let result = execute_genesis_block(&self.shared.config, &view)?; + let result = execute_genesis_block(&self.shared.config, &on_view)?; self.shared.block_importer.commit_result(result).await?; + + let off_chain_db_transaction = genesis::off_chain::execute_genesis_block( + &self.shared.config, + &off_view, + )?; + off_chain_db_transaction.commit()?; } } 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 00ce75838c1..a42afa4a724 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 @@ -27,8 +27,12 @@ use fuel_core_types::{ Address, Bytes32, TxPointer, + UtxoId, + }, + fuel_types::{ + BlockHeight, + Nonce, }, - fuel_types::BlockHeight, services::txpool::TransactionStatus, }; @@ -39,6 +43,28 @@ impl OffChainDatabase for Database { .ok_or(not_found!("TransactionId"))? } + fn owned_coins_ids( + &self, + owner: &Address, + start_coin: Option, + direction: IterDirection, + ) -> BoxedIter<'_, StorageResult> { + self.owned_coins_ids(owner, start_coin, Some(direction)) + .map(|res| res.map_err(StorageError::from)) + .into_boxed() + } + + fn owned_message_ids( + &self, + owner: &Address, + start_message_id: Option, + direction: IterDirection, + ) -> BoxedIter<'_, StorageResult> { + self.owned_message_ids(owner, start_message_id, Some(direction)) + .map(|result| result.map_err(StorageError::from)) + .into_boxed() + } + fn owned_transactions_ids( &self, owner: Address, diff --git a/crates/fuel-core/src/service/adapters/graphql_api/on_chain.rs b/crates/fuel-core/src/service/adapters/graphql_api/on_chain.rs index c0b1416a0d6..7430e1f2ee3 100644 --- a/crates/fuel-core/src/service/adapters/graphql_api/on_chain.rs +++ b/crates/fuel-core/src/service/adapters/graphql_api/on_chain.rs @@ -30,11 +30,7 @@ use fuel_core_types::{ }, }, entities::message::Message, - fuel_tx::{ - Address, - AssetId, - UtxoId, - }, + fuel_tx::AssetId, fuel_types::{ BlockHeight, Nonce, @@ -114,26 +110,4 @@ impl DatabaseChain for Database { } } -impl OnChainDatabase for Database { - fn owned_message_ids( - &self, - owner: &Address, - start_message_id: Option, - direction: IterDirection, - ) -> BoxedIter<'_, StorageResult> { - self.owned_message_ids(owner, start_message_id, Some(direction)) - .map(|result| result.map_err(StorageError::from)) - .into_boxed() - } - - fn owned_coins_ids( - &self, - owner: &Address, - start_coin: Option, - direction: IterDirection, - ) -> BoxedIter<'_, StorageResult> { - self.owned_coins_ids(owner, start_coin, Some(direction)) - .map(|res| res.map_err(StorageError::from)) - .into_boxed() - } -} +impl OnChainDatabase for Database {} diff --git a/crates/fuel-core/src/service/genesis.rs b/crates/fuel-core/src/service/genesis.rs index 6bcbc50b873..de11eaf38e7 100644 --- a/crates/fuel-core/src/service/genesis.rs +++ b/crates/fuel-core/src/service/genesis.rs @@ -4,6 +4,7 @@ use crate::{ }; use anyhow::anyhow; use fuel_core_chain_config::{ + CoinConfig, ContractConfig, GenesisCommitment, StateConfig, @@ -40,15 +41,9 @@ use fuel_core_types::{ SealedBlock, }, entities::{ - coins::coin::{ - CompressedCoin, - CompressedCoinV1, - }, + coins::coin::Coin, contract::ContractUtxoInfo, - message::{ - Message, - MessageV1, - }, + message::Message, }, fuel_merkle::binary, fuel_tx::{ @@ -68,6 +63,8 @@ use fuel_core_types::{ }; use itertools::Itertools; +pub mod off_chain; + /// Performs the importing of the genesis block from the snapshot. pub fn execute_genesis_block( config: &Config, @@ -101,7 +98,7 @@ pub fn execute_genesis_block( }; let result = UncommittedImportResult::new( - ImportResult::new_from_local(block, vec![]), + ImportResult::new_from_local(block, vec![], vec![]), database_transaction, ); Ok(result) @@ -163,42 +160,9 @@ fn init_coin_state( if let Some(state) = &state { if let Some(coins) = &state.coins { for coin in coins { - let utxo_id = UtxoId::new( - // generated transaction id([0..[out_index/255]]) - coin.tx_id.unwrap_or_else(|| { - Bytes32::try_from( - (0..(Bytes32::LEN - WORD_SIZE)) - .map(|_| 0u8) - .chain( - (generated_output_index / 255) - .to_be_bytes() - .into_iter(), - ) - .collect_vec() - .as_slice(), - ) - .expect("Incorrect genesis transaction id byte length") - }), - coin.output_index.unwrap_or_else(|| { - generated_output_index = generated_output_index - .checked_add(1) - .expect("The maximum number of UTXOs supported in the genesis configuration has been exceeded."); - (generated_output_index % 255) as u8 - }), - ); - - let compressed_coin: CompressedCoin = CompressedCoinV1 { - owner: coin.owner, - amount: coin.amount, - asset_id: coin.asset_id, - maturity: coin.maturity.unwrap_or_default(), - tx_pointer: TxPointer::new( - coin.tx_pointer_block_height.unwrap_or_default(), - coin.tx_pointer_tx_idx.unwrap_or_default(), - ), - } - .into(); - + let coin = create_coin_from_config(coin, &mut generated_output_index); + let utxo_id = coin.utxo_id; + let compressed_coin = coin.compress(); // ensure coin can't point to blocks in the future if compressed_coin.tx_pointer().block_height() > state.height.unwrap_or_default() @@ -334,15 +298,7 @@ fn init_da_messages( if let Some(state) = &state { if let Some(message_state) = &state.messages { for msg in message_state { - let message: Message = MessageV1 { - sender: msg.sender, - recipient: msg.recipient, - nonce: msg.nonce, - amount: msg.amount, - data: msg.data.clone(), - da_height: msg.da_height, - } - .into(); + let message: Message = msg.clone().into(); if db .storage::() @@ -371,6 +327,44 @@ fn init_contract_balance( Ok(()) } +// TODO: Remove when re-genesis PRs are merged. Instead we will use `UtxoId` from the `CoinConfig`. +fn create_coin_from_config(coin: &CoinConfig, generated_output_index: &mut u64) -> Coin { + let utxo_id = UtxoId::new( + // generated transaction id([0..[out_index/255]]) + coin.tx_id.unwrap_or_else(|| { + Bytes32::try_from( + (0..(Bytes32::LEN - WORD_SIZE)) + .map(|_| 0u8) + .chain( + (*generated_output_index / 255) + .to_be_bytes(), + ) + .collect_vec() + .as_slice(), + ) + .expect("Incorrect genesis transaction id byte length") + }), + coin.output_index.unwrap_or_else(|| { + *generated_output_index = generated_output_index + .checked_add(1) + .expect("The maximum number of UTXOs supported in the genesis configuration has been exceeded."); + (*generated_output_index % 255) as u8 + }), + ); + + Coin { + utxo_id, + owner: coin.owner, + amount: coin.amount, + asset_id: coin.asset_id, + maturity: coin.maturity.unwrap_or_default(), + tx_pointer: TxPointer::new( + coin.tx_pointer_block_height.unwrap_or_default(), + coin.tx_pointer_tx_idx.unwrap_or_default(), + ), + } +} + #[cfg(test)] mod tests { use super::*; @@ -500,8 +494,8 @@ mod tests { ..Config::local_node() }; - let db = Database::default(); - FuelService::from_database(db.clone(), service_config) + let db = CombinedDatabase::default(); + FuelService::from_combined_database(db.clone(), service_config) .await .unwrap(); @@ -743,11 +737,13 @@ mod tests { assert!(init_result.is_err()) } - fn get_coins(db: &Database, owner: &Address) -> Vec { - db.owned_coins_ids(owner, None, None) + fn get_coins(db: &CombinedDatabase, owner: &Address) -> Vec { + db.off_chain() + .owned_coins_ids(owner, None, None) .map(|r| { let coin_id = r.unwrap(); - db.storage::() + db.on_chain() + .storage::() .get(&coin_id) .map(|v| v.unwrap().into_owned().uncompress(coin_id)) .unwrap() diff --git a/crates/fuel-core/src/service/genesis/off_chain.rs b/crates/fuel-core/src/service/genesis/off_chain.rs new file mode 100644 index 00000000000..90d283d5f8d --- /dev/null +++ b/crates/fuel-core/src/service/genesis/off_chain.rs @@ -0,0 +1,58 @@ +use crate::{ + database::{ + database_description::off_chain::OffChain, + Database, + }, + graphql_api::worker_service, + service::{ + genesis::create_coin_from_config, + Config, + }, +}; +use fuel_core_storage::transactional::{ + StorageTransaction, + Transactional, +}; +use fuel_core_types::{ + entities::message::Message, + services::executor::Event, +}; +use std::borrow::Cow; + +/// Performs the importing of the genesis block from the snapshot. +pub fn execute_genesis_block( + config: &Config, + original_database: &Database, +) -> anyhow::Result>> { + // start a db transaction for bulk-writing + let mut database_transaction = Transactional::transaction(original_database); + + if let Some(state_config) = &config.chain_conf.initial_state { + if let Some(messages) = &state_config.messages { + let messages_events = messages.iter().map(|config| { + let message: Message = config.clone().into(); + Cow::Owned(Event::MessageImported(message)) + }); + + worker_service::Task::process_executor_events( + messages_events, + database_transaction.as_mut(), + )?; + } + + if let Some(coins) = &state_config.coins { + let mut generated_output_index = 0; + let coin_events = coins.iter().map(|config| { + let coin = create_coin_from_config(config, &mut generated_output_index); + Cow::Owned(Event::CoinCreated(coin)) + }); + + worker_service::Task::process_executor_events( + coin_events, + database_transaction.as_mut(), + )?; + } + } + + Ok(database_transaction) +} diff --git a/crates/services/consensus_module/poa/src/service.rs b/crates/services/consensus_module/poa/src/service.rs index 4fd65a220e4..7ca46abc7fb 100644 --- a/crates/services/consensus_module/poa/src/service.rs +++ b/crates/services/consensus_module/poa/src/service.rs @@ -331,6 +331,7 @@ where block, skipped_transactions, tx_status, + events, }, db_transaction, ) = self @@ -358,7 +359,7 @@ where // Import the sealed block self.block_importer .commit_result(Uncommitted::new( - ImportResult::new_from_local(block, tx_status), + ImportResult::new_from_local(block, tx_status, events), db_transaction, )) .await?; diff --git a/crates/services/consensus_module/poa/src/service_test.rs b/crates/services/consensus_module/poa/src/service_test.rs index 44525e3be62..bd10b097e6f 100644 --- a/crates/services/consensus_module/poa/src/service_test.rs +++ b/crates/services/consensus_module/poa/src/service_test.rs @@ -129,6 +129,7 @@ impl TestContextBuilder { block: Default::default(), skipped_transactions: Default::default(), tx_status: Default::default(), + events: Default::default(), }, StorageTransaction::new(EmptyStorage), )) @@ -287,6 +288,7 @@ async fn remove_skipped_transactions() { }) .collect(), tx_status: Default::default(), + events: Default::default(), }, StorageTransaction::new(EmptyStorage), )) diff --git a/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs b/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs index 761d5fcbd99..c08035dfdb7 100644 --- a/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs +++ b/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs @@ -92,6 +92,7 @@ async fn can_manually_produce_block( block, skipped_transactions: Default::default(), tx_status: Default::default(), + events: Default::default(), }, StorageTransaction::new(EmptyStorage), )) diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 3a68b81ee63..92a4cf7f5d6 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -116,6 +116,7 @@ use fuel_core_types::{ block_producer::Components, executor::{ Error as ExecutorError, + Event as ExecutorEvent, ExecutionKind, ExecutionResult, ExecutionType, @@ -262,6 +263,7 @@ pub struct ExecutionData { found_mint: bool, message_ids: Vec, tx_status: Vec, + events: Vec, pub skipped_transactions: Vec<(TxId, ExecutorError)>, } @@ -480,6 +482,7 @@ where message_ids, tx_status, skipped_transactions, + events, .. } = execution_data; @@ -508,6 +511,7 @@ where block, skipped_transactions, tx_status, + events, }; // Get the complete fuel block. @@ -531,6 +535,7 @@ where found_mint: false, message_ids: Vec::new(), tx_status: Vec::new(), + events: Vec::new(), skipped_transactions: Vec::new(), }; let execution_data = &mut data; @@ -543,7 +548,7 @@ where let block_height = *block.header.height(); if self.relayer.enabled() { - self.process_da(block_st_transaction, &block.header)?; + self.process_da(block_st_transaction, &block.header, execution_data)?; } // ALl transactions should be in the `TxSource`. @@ -656,6 +661,7 @@ where &self, block_st_transaction: &mut D, header: &PartialBlockHeader, + execution_data: &mut ExecutionData, ) -> ExecutorResult<()> { let block_height = *header.height(); let prev_block_height = block_height @@ -686,6 +692,9 @@ where block_st_transaction .storage::() .insert(message.nonce(), &message)?; + execution_data + .events + .push(ExecutorEvent::MessageImported(message)); } } } @@ -850,7 +859,7 @@ where self.persist_output_utxos( block_height, - execution_data.tx_count, + execution_data, &coinbase_id, block_st_transaction.as_mut(), inputs.as_slice(), @@ -1024,12 +1033,17 @@ where } // change the spent status of the tx inputs - self.spend_input_utxos(tx.inputs(), tx_st_transaction.as_mut(), reverted)?; + self.spend_input_utxos( + tx.inputs(), + tx_st_transaction.as_mut(), + reverted, + execution_data, + )?; // Persist utxos first and after calculate the not utxo outputs self.persist_output_utxos( *header.height(), - execution_data.tx_count, + execution_data, &tx_id, tx_st_transaction.as_mut(), tx.inputs(), @@ -1189,25 +1203,55 @@ where inputs: &[Input], db: &mut D, reverted: bool, + execution_data: &mut ExecutionData, ) -> ExecutorResult<()> { for input in inputs { match input { - Input::CoinSigned(CoinSigned { utxo_id, .. }) - | Input::CoinPredicate(CoinPredicate { utxo_id, .. }) => { + Input::CoinSigned(CoinSigned { + utxo_id, + owner, + amount, + asset_id, + maturity, + .. + }) + | Input::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + maturity, + .. + }) => { // prune utxo from db - db.storage::().remove(utxo_id)?; + let coin = db + .storage::() + .remove(utxo_id) + .map_err(Into::into) + .transpose() + .unwrap_or_else(|| { + // If the coin is not found in the database, it means that it was + // already spent or `utxo_validation` is `false`. + self.get_coin_or_default( + db, *utxo_id, *owner, *amount, *asset_id, *maturity, + ) + })?; + + execution_data + .events + .push(ExecutorEvent::CoinConsumed(coin.uncompress(*utxo_id))); } - Input::MessageDataSigned(_) - | Input::MessageDataPredicate(_) - if reverted => { + Input::MessageDataSigned(_) | Input::MessageDataPredicate(_) + if reverted => + { // Don't spend the retryable messages if transaction is reverted continue } Input::MessageCoinSigned(MessageCoinSigned { nonce, .. }) | Input::MessageCoinPredicate(MessageCoinPredicate { nonce, .. }) - | Input::MessageDataSigned(MessageDataSigned { nonce, .. }) // Spend only if tx is not reverted - | Input::MessageDataPredicate(MessageDataPredicate { nonce, .. }) // Spend only if tx is not reverted - => { + | Input::MessageDataSigned(MessageDataSigned { nonce, .. }) + | Input::MessageDataPredicate(MessageDataPredicate { nonce, .. }) => { + // `MessageDataSigned` and `MessageDataPredicate` are spent only if tx is not reverted // mark message id as spent let was_already_spent = db.storage::().insert(nonce, &())?; @@ -1216,7 +1260,13 @@ where return Err(ExecutorError::MessageAlreadySpent(*nonce)) } // cleanup message contents - db.storage::().remove(nonce)?; + let message = db + .storage::() + .remove(nonce)? + .ok_or_else(|| ExecutorError::MessageAlreadySpent(*nonce))?; + execution_data + .events + .push(ExecutorEvent::MessageConsumed(message)); } _ => {} } @@ -1511,12 +1561,13 @@ where fn persist_output_utxos( &self, block_height: BlockHeight, - tx_idx: u16, + execution_data: &mut ExecutionData, tx_id: &Bytes32, db: &mut D, inputs: &[Input], outputs: &[Output], ) -> ExecutorResult<()> { + let tx_idx = execution_data.tx_count; for (output_index, output) in outputs.iter().enumerate() { let index = u8::try_from(output_index) .expect("Transaction can have only up to `u8::MAX` outputs"); @@ -1528,7 +1579,7 @@ where to, } => Self::insert_coin( block_height, - tx_idx, + execution_data, utxo_id, amount, asset_id, @@ -1558,7 +1609,7 @@ where amount, } => Self::insert_coin( block_height, - tx_idx, + execution_data, utxo_id, amount, asset_id, @@ -1571,7 +1622,7 @@ where amount, } => Self::insert_coin( block_height, - tx_idx, + execution_data, utxo_id, amount, asset_id, @@ -1594,7 +1645,7 @@ where fn insert_coin( block_height: BlockHeight, - tx_idx: u16, + execution_data: &mut ExecutionData, utxo_id: UtxoId, amount: &Word, asset_id: &AssetId, @@ -1610,13 +1661,16 @@ where amount: *amount, asset_id: *asset_id, maturity: 0u32.into(), - tx_pointer: TxPointer::new(block_height, tx_idx), + tx_pointer: TxPointer::new(block_height, execution_data.tx_count), } .into(); if db.storage::().insert(&utxo_id, &coin)?.is_some() { return Err(ExecutorError::OutputAlreadyExists) } + execution_data + .events + .push(ExecutorEvent::CoinCreated(coin.uncompress(utxo_id))); } Ok(()) diff --git a/crates/services/importer/src/importer.rs b/crates/services/importer/src/importer.rs index 885f59ceb7d..cde1c539ee5 100644 --- a/crates/services/importer/src/importer.rs +++ b/crates/services/importer/src/importer.rs @@ -384,6 +384,7 @@ where block, skipped_transactions, tx_status, + events, }, db_tx, ) = executor @@ -407,7 +408,8 @@ where entity: block, consensus, }; - let import_result = ImportResult::new_from_network(sealed_block, tx_status); + let import_result = + ImportResult::new_from_network(sealed_block, tx_status, events); Ok(Uncommitted::new(import_result, db_tx)) } diff --git a/crates/services/importer/src/importer/test.rs b/crates/services/importer/src/importer/test.rs index 889fe07a220..5e2f64da752 100644 --- a/crates/services/importer/src/importer/test.rs +++ b/crates/services/importer/src/importer/test.rs @@ -186,6 +186,7 @@ where block: mock_result.block.entity, skipped_transactions, tx_status: vec![], + events: vec![], }, StorageTransaction::new(database), )) @@ -361,7 +362,7 @@ async fn commit_result_assert( let expected_to_broadcast = sealed_block.clone(); let importer = Importer::new(Default::default(), underlying_db, (), ()); let uncommitted_result = UncommittedResult::new( - ImportResult::new_from_local(sealed_block, vec![]), + ImportResult::new_from_local(sealed_block, vec![], vec![]), StorageTransaction::new(executor_db), ); diff --git a/crates/services/producer/src/mocks.rs b/crates/services/producer/src/mocks.rs index 4ca899c5ad1..5ae5740db44 100644 --- a/crates/services/producer/src/mocks.rs +++ b/crates/services/producer/src/mocks.rs @@ -151,6 +151,7 @@ impl Executor> for MockExecutor { block, skipped_transactions: vec![], tx_status: vec![], + events: vec![], }, StorageTransaction::new(self.0.clone()), )) @@ -177,6 +178,7 @@ impl Executor> for FailingMockExecutor { block, skipped_transactions: vec![], tx_status: vec![], + events: vec![], }, StorageTransaction::new(MockDb::default()), )) diff --git a/crates/services/txpool/src/service/test_helpers.rs b/crates/services/txpool/src/service/test_helpers.rs index 3aea0044ff2..633c6aa0da5 100644 --- a/crates/services/txpool/src/service/test_helpers.rs +++ b/crates/services/txpool/src/service/test_helpers.rs @@ -119,8 +119,9 @@ impl MockImporter { let stream = fuel_core_services::stream::unfold(blocks, |mut blocks| async { let block = blocks.pop(); if let Some(sealed_block) = block { - let result: SharedImportResult = - Arc::new(ImportResult::new_from_local(sealed_block, vec![])); + let result: SharedImportResult = Arc::new( + ImportResult::new_from_local(sealed_block, vec![], vec![]), + ); Some((result, blocks)) } else { diff --git a/crates/storage/src/column.rs b/crates/storage/src/column.rs index d277ea2a0b4..6aa5d1ddab7 100644 --- a/crates/storage/src/column.rs +++ b/crates/storage/src/column.rs @@ -66,12 +66,6 @@ pub enum Column { FuelBlockSecondaryKeyBlockHeights = 18, /// See [`SealedBlockConsensus`](crate::tables::SealedBlockConsensus) FuelBlockConsensus = 19, - - // Below are not required tables. They are used for API and may be removed or moved to another place in the future. - /// The column of the table that stores `true` if `owner` owns `Coin` with `coin_id` - OwnedCoins = 20, - /// The column of the table that stores `true` if `owner` owns `Message` with `message_id` - OwnedMessageIds = 21, } impl Column { diff --git a/crates/types/src/services/block_importer.rs b/crates/types/src/services/block_importer.rs index 276a305b960..17e96777fd8 100644 --- a/crates/types/src/services/block_importer.rs +++ b/crates/types/src/services/block_importer.rs @@ -6,7 +6,10 @@ use crate::{ SealedBlock, }, services::{ - executor::TransactionExecutionStatus, + executor::{ + Event, + TransactionExecutionStatus, + }, Uncommitted, }, }; @@ -28,6 +31,8 @@ pub struct ImportResult { pub sealed_block: SealedBlock, /// The status of the transactions execution included into the block. pub tx_status: Vec, + /// The events produced during block execution. + pub events: Vec, /// The source producer of the block. pub source: Source, } @@ -55,10 +60,12 @@ impl ImportResult { pub fn new_from_local( sealed_block: SealedBlock, tx_status: Vec, + events: Vec, ) -> Self { Self { sealed_block, tx_status, + events, source: Source::Local, } } @@ -67,10 +74,12 @@ impl ImportResult { pub fn new_from_network( sealed_block: SealedBlock, tx_status: Vec, + events: Vec, ) -> Self { Self { sealed_block, tx_status, + events, source: Source::Network, } } diff --git a/crates/types/src/services/executor.rs b/crates/types/src/services/executor.rs index 381ac719162..c2351476635 100644 --- a/crates/types/src/services/executor.rs +++ b/crates/types/src/services/executor.rs @@ -8,6 +8,10 @@ use crate::{ }, primitives::BlockId, }, + entities::{ + coins::coin::Coin, + message::Message, + }, fuel_tx::{ Receipt, TxId, @@ -45,6 +49,21 @@ pub struct ExecutionResult { pub skipped_transactions: Vec<(TxId, Error)>, /// The status of the transactions execution included into the block. pub tx_status: Vec, + /// The list of all events generated during the execution of the block. + pub events: Vec, +} + +/// The event represents some internal state changes caused by the block execution. +#[derive(Debug, Clone)] +pub enum Event { + /// Imported a new spendable message from the relayer. + MessageImported(Message), + /// The message was consumed by the transaction. + MessageConsumed(Message), + /// Created a new spendable coin, produced by the transaction. + CoinCreated(Coin), + /// The coin was consumed by the transaction. + CoinConsumed(Coin), } /// The status of a transaction after it is executed. diff --git a/tests/tests/coin.rs b/tests/tests/coin.rs index a18339bc003..46514ddb7ff 100644 --- a/tests/tests/coin.rs +++ b/tests/tests/coin.rs @@ -1,4 +1,8 @@ use fuel_core::{ + chain_config::{ + CoinConfig, + StateConfig, + }, database::Database, service::{ Config, @@ -17,25 +21,35 @@ use fuel_core_client::client::{ }, FuelClient, }; -use fuel_core_storage::{ - tables::Coins, - StorageAsMut, -}; -use fuel_core_types::{ - entities::coins::coin::Coin, - fuel_asm::*, -}; +use fuel_core_types::fuel_asm::*; use rstest::rstest; +async fn setup_service(configs: Vec) -> FuelService { + let mut config = Config::local_node(); + config.chain_conf.initial_state = Some(StateConfig { + coins: Some(configs), + ..Default::default() + }); + + FuelService::from_database(Database::default(), config) + .await + .unwrap() +} + #[tokio::test] async fn coin() { // setup test data in the node - let utxo_id = UtxoId::new(Default::default(), 5); + let tx_index = Default::default(); + let output_index = 0; + let utxo_id = UtxoId::new(tx_index, output_index); // setup server & client - let srv = FuelService::from_database(Database::default(), Config::local_node()) - .await - .unwrap(); + let srv = setup_service(vec![CoinConfig { + tx_id: Some(tx_index), + output_index: Some(output_index), + ..Default::default() + }]) + .await; let client = FuelClient::from(srv.bound_address); // run test @@ -53,27 +67,15 @@ async fn first_5_coins( // setup test data in the node let coins: Vec<_> = (1..10usize) - .map(|i| Coin { - utxo_id: UtxoId::new([i as u8; 32].into(), 0), + .map(|i| CoinConfig { owner, amount: i as Word, - asset_id: Default::default(), - maturity: Default::default(), - tx_pointer: Default::default(), + ..Default::default() }) .collect(); - let mut db = Database::default(); - for coin in coins { - db.storage::() - .insert(&coin.utxo_id.clone(), &coin.compress()) - .unwrap(); - } - // setup server & client - let srv = FuelService::from_database(db, Config::local_node()) - .await - .unwrap(); + let srv = setup_service(coins).await; let client = FuelClient::from(srv.bound_address); // run test @@ -100,27 +102,16 @@ async fn only_asset_id_filtered_coins() { // setup test data in the node let coins: Vec<_> = (1..10usize) - .map(|i| Coin { - utxo_id: UtxoId::new([i as u8; 32].into(), 0), + .map(|i| CoinConfig { owner, amount: i as Word, asset_id: if i <= 5 { asset_id } else { Default::default() }, - maturity: Default::default(), - tx_pointer: Default::default(), + ..Default::default() }) .collect(); - let mut db = Database::default(); - for coin in coins { - db.storage::() - .insert(&coin.utxo_id.clone(), &coin.compress()) - .unwrap(); - } - // setup server & client - let srv = FuelService::from_database(db, Config::local_node()) - .await - .unwrap(); + let srv = setup_service(coins).await; let client = FuelClient::from(srv.bound_address); // run test @@ -148,28 +139,17 @@ async fn get_coins_forwards_backwards( #[values(AssetId::from([1u8; 32]), AssetId::from([32u8; 32]))] asset_id: AssetId, ) { // setup test data in the node - let coins: Vec = (1..11usize) - .map(|i| Coin { - utxo_id: UtxoId::new([i as u8; 32].into(), 0), + let coins: Vec<_> = (1..11usize) + .map(|i| CoinConfig { owner, amount: i as Word, asset_id, - maturity: Default::default(), - tx_pointer: Default::default(), + ..Default::default() }) .collect(); - let mut db = Database::default(); - for coin in coins { - db.storage::() - .insert(&coin.utxo_id.clone(), &coin.compress()) - .unwrap(); - } - // setup server & client - let srv = FuelService::from_database(db, Config::local_node()) - .await - .unwrap(); + let srv = setup_service(coins).await; let client = FuelClient::from(srv.bound_address); // run test diff --git a/tests/tests/tx_gossip.rs b/tests/tests/tx_gossip.rs index 5e9f72f4765..acebcf584ce 100644 --- a/tests/tests/tx_gossip.rs +++ b/tests/tests/tx_gossip.rs @@ -6,14 +6,14 @@ use fuel_core::p2p_test_helpers::{ ProducerSetup, ValidatorSetup, }; -use fuel_core_client::client::{ - types::TransactionStatus, - FuelClient, -}; -use fuel_core_poa::ports::BlockImporter; +use fuel_core_client::client::FuelClient; use fuel_core_types::{ fuel_tx::*, fuel_vm::*, + services::{ + block_importer::SharedImportResult, + executor::TransactionExecutionResult, + }, }; use futures::StreamExt; use rand::{ @@ -26,7 +26,6 @@ use std::{ Hash, Hasher, }, - io, time::Duration, }; @@ -102,7 +101,7 @@ const NUMBER_OF_INVALID_TXS: usize = 100; async fn test_tx_gossiping_invalid_txs( bootstrap_type: BootstrapType, -) -> io::Result { +) -> anyhow::Result { // Create a random seed based on the test parameters. let mut hasher = DefaultHasher::new(); let num_txs = 1; @@ -181,7 +180,7 @@ async fn test_tx_gossiping_invalid_txs( // Give some time to receive all invalid transactions. tokio::time::sleep(Duration::from_secs(5)).await; - let mut authority_blocks = authority.node.shared.block_importer.block_stream(); + let mut authority_blocks = authority.node.shared.block_importer.events(); // Submit a valid transaction from banned sentry to an authority node. let valid_transaction = authority.test_transactions()[0].clone(); @@ -190,12 +189,10 @@ async fn test_tx_gossiping_invalid_txs( .submit(valid_transaction.clone()) .await .expect("Transaction is valid"); - let _ = tokio::time::timeout(Duration::from_secs(5), authority_blocks.next()).await; - - let authority_client = FuelClient::from(authority.node.bound_address); - authority_client - .transaction_status(&valid_transaction.id(&Default::default())) - .await + let block = tokio::time::timeout(Duration::from_secs(5), authority_blocks.next()) + .await? + .unwrap(); + Ok(block) } #[tokio::test(flavor = "multi_thread")] @@ -204,11 +201,14 @@ async fn test_tx_gossiping_reserved_nodes_invalid_txs() { // nodes doesn't decrease its reputation. // After gossiping `NUMBER_OF_INVALID_TXS` transactions, // we will gossip one valid transaction, and it should be included. - let status = test_tx_gossiping_invalid_txs(BootstrapType::ReservedNodes).await; + let result = test_tx_gossiping_invalid_txs(BootstrapType::ReservedNodes) + .await + .expect("Should be able to import block"); + assert_eq!(result.tx_status.len(), 2); assert!(matches!( - status, - Ok(fuel_core_client::client::types::TransactionStatus::Success { .. }) + result.tx_status[0].result, + TransactionExecutionResult::Success { .. } )); } @@ -220,7 +220,7 @@ async fn test_tx_gossiping_non_reserved_nodes_invalid_txs() { // // The test sends `NUMBER_OF_INVALID_TXS` invalid transactions, // and verifies that sending a valid one will be ignored. - let status = test_tx_gossiping_invalid_txs(BootstrapType::BootstrapNodes).await; + let result = test_tx_gossiping_invalid_txs(BootstrapType::BootstrapNodes).await; - assert!(status.is_err()); + assert!(result.is_err()); }