From ed92738280e068475682e220b93921bbbfebfdd7 Mon Sep 17 00:00:00 2001 From: Green Baneling Date: Wed, 21 Feb 2024 19:33:58 -0500 Subject: [PATCH] Extraction of the `FuelBlockSecondaryKeyBlockHeights` table to an off-chain worker (#1671) Closes https://github.com/FuelLabs/fuel-core/issues/1583 The change moves the `FuelBlockSecondaryKeyBlockHeights` table and related logic to the off-chain worker. Along with this, the change adds a new `Merklelized` blueprint that maintains the binary Merkle tree over the storage data. It supports only the insertion of the objects without removing them. This blueprint replaces the logic that previously was defined in the `Database`. Some side changes caused by the main change: - Now, each blueprint provides its own set of tests related to the logic of this blueprint. - The `TransactionStatus` uses `block_height` inside instead of the `block_id`. - The key for the dense merkle tree looks like: ```rust pub enum DenseMetadataKey { /// The primary key of the `DenseMerkleMetadata`. Primary(PrimaryKey), #[default] /// The latest `DenseMerkleMetadata` of the table. Latest, } ``` --------- Co-authored-by: Voxelot --- CHANGELOG.md | 6 + Cargo.lock | 1 + crates/client/assets/schema.sdl | 1 + crates/client/src/client.rs | 7 +- crates/client/src/client/schema/block.rs | 17 +- ...ue_transaction_by_id_query_gql_output.snap | 4 +- ...sts__transactions_by_owner_gql_output.snap | 4 +- ...nsactions_connection_query_gql_output.snap | 4 +- ...nt_transaction_by_id_query_gql_output.snap | 4 +- crates/client/src/client/schema/tx.rs | 6 +- crates/client/src/client/types.rs | 13 +- crates/client/src/client/types/gas_price.rs | 5 +- crates/fuel-core/src/database/block.rs | 194 +------ crates/fuel-core/src/database/storage.rs | 4 +- crates/fuel-core/src/graphql_api/database.rs | 8 +- crates/fuel-core/src/graphql_api/ports.rs | 6 +- crates/fuel-core/src/graphql_api/storage.rs | 3 + .../src/graphql_api/storage/blocks.rs | 42 ++ .../src/graphql_api/worker_service.rs | 11 +- crates/fuel-core/src/query/block.rs | 8 - crates/fuel-core/src/query/message.rs | 10 +- crates/fuel-core/src/query/message/test.rs | 31 +- .../fuel-core/src/query/subscriptions/test.rs | 4 +- crates/fuel-core/src/schema/block.rs | 10 +- crates/fuel-core/src/schema/message.rs | 2 +- crates/fuel-core/src/schema/tx/types.rs | 28 +- .../service/adapters/graphql_api/off_chain.rs | 6 + .../service/adapters/graphql_api/on_chain.rs | 10 +- .../service/update_sender/tests/test_e2e.rs | 3 +- .../update_sender/tests/test_sending.rs | 4 +- .../src/service/update_sender/tests/utils.rs | 4 +- crates/storage/Cargo.toml | 1 + crates/storage/src/blueprint.rs | 13 + crates/storage/src/blueprint/merklized.rs | 543 ++++++++++++++++++ crates/storage/src/blueprint/plain.rs | 211 +++++++ crates/storage/src/blueprint/sparse.rs | 262 ++++++++- crates/storage/src/column.rs | 4 +- crates/storage/src/structured_storage.rs | 460 +-------------- .../storage/src/structured_storage/blocks.rs | 141 ++++- .../src/structured_storage/merkle_data.rs | 3 +- crates/storage/src/tables.rs | 27 +- crates/types/src/blockchain/header.rs | 8 + crates/types/src/services/txpool.rs | 24 +- tests/tests/blocks.rs | 20 +- tests/tests/poa.rs | 26 +- tests/tests/tx/txpool.rs | 2 +- tests/tests/tx/utxo_validation.rs | 7 +- 47 files changed, 1423 insertions(+), 789 deletions(-) create mode 100644 crates/fuel-core/src/graphql_api/storage/blocks.rs create mode 100644 crates/storage/src/blueprint/merklized.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 48706e23a1..a875ec88c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Description of the upcoming release here. +### Added + +- [#1671](https://github.com/FuelLabs/fuel-core/pull/1671): Added a new `Merklized` blueprint that maintains the binary Merkle tree over the storage data. It supports only the insertion of the objects without removing them. + ### Changed +- [#1671](https://github.com/FuelLabs/fuel-core/pull/1671): The logic related to the `FuelBlockIdsToHeights` is moved to the off-chain worker. - [#1663](https://github.com/FuelLabs/fuel-core/pull/1663): Reduce the punishment criteria for mempool gossipping. - [#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. @@ -33,6 +38,7 @@ Description of the upcoming release here. - [#1636](https://github.com/FuelLabs/fuel-core/pull/1636): Add more docs to GraphQL DAP API. #### Breaking +- [#1671](https://github.com/FuelLabs/fuel-core/pull/1671): The GraphQL API uses block height instead of the block id where it is possible. The transaction status contains `block_height` instead of the `block_id`. - [#1675](https://github.com/FuelLabs/fuel-core/pull/1675): Simplify GQL schema by disabling contract resolvers in most cases, and just return a ContractId scalar instead. - [#1658](https://github.com/FuelLabs/fuel-core/pull/1658): Receipts are part of the transaction status. Removed `reason` from the `TransactionExecutionResult::Failed`. It can be calculated based on the program state and receipts. diff --git a/Cargo.lock b/Cargo.lock index 00f95fee3c..f8a0b33440 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3041,6 +3041,7 @@ dependencies = [ "serde", "strum 0.25.0", "strum_macros 0.25.3", + "test-case", ] [[package]] diff --git a/crates/client/assets/schema.sdl b/crates/client/assets/schema.sdl index f2b5ea5474..a760d45c8e 100644 --- a/crates/client/assets/schema.sdl +++ b/crates/client/assets/schema.sdl @@ -46,6 +46,7 @@ input BalanceFilterInput { type Block { id: BlockId! + height: U32! header: Header! consensus: Consensus! transactions: [Transaction!]! diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 03bb0b7c06..b0df84c7c9 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -745,9 +745,12 @@ impl FuelClient { Ok(block) } - pub async fn block_by_height(&self, height: u32) -> io::Result> { + pub async fn block_by_height( + &self, + height: BlockHeight, + ) -> io::Result> { let query = schema::block::BlockByHeightQuery::build(BlockByHeightArgs { - height: Some(U32(height)), + height: Some(U32(height.into())), }); let block = self.query(query).await?.block.map(Into::into); diff --git a/crates/client/src/client/schema/block.rs b/crates/client/src/client/schema/block.rs index 54bee13243..5c1ae9f3bd 100644 --- a/crates/client/src/client/schema/block.rs +++ b/crates/client/src/client/schema/block.rs @@ -8,7 +8,10 @@ use crate::client::schema::{ U32, U64, }; -use fuel_core_types::fuel_crypto; +use fuel_core_types::{ + fuel_crypto, + fuel_types::BlockHeight, +}; use super::{ tx::TransactionIdFragment, @@ -87,6 +90,12 @@ pub struct BlockIdFragment { pub id: BlockId, } +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema_path = "./assets/schema.sdl", graphql_type = "Block")] +pub struct BlockHeightFragment { + pub height: U32, +} + #[derive(cynic::QueryVariables, Debug)] pub struct ProduceBlockArgs { pub start_timestamp: Option, @@ -159,6 +168,12 @@ impl Block { } } +impl From for BlockHeight { + fn from(fragment: BlockHeightFragment) -> Self { + BlockHeight::new(fragment.height.into()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__opaque_transaction_by_id_query_gql_output.snap b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__opaque_transaction_by_id_query_gql_output.snap index 815f7e8353..3fd81a8cc2 100644 --- a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__opaque_transaction_by_id_query_gql_output.snap +++ b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__opaque_transaction_by_id_query_gql_output.snap @@ -13,7 +13,7 @@ query($id: TransactionId!) { ... on SuccessStatus { transactionId block { - id + height } time programState { @@ -57,7 +57,7 @@ query($id: TransactionId!) { ... on FailureStatus { transactionId block { - id + height } time reason diff --git a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transactions_by_owner_gql_output.snap b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transactions_by_owner_gql_output.snap index 904701e375..1832b7afa6 100644 --- a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transactions_by_owner_gql_output.snap +++ b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transactions_by_owner_gql_output.snap @@ -16,7 +16,7 @@ query($owner: Address!, $after: String, $before: String, $first: Int, $last: Int ... on SuccessStatus { transactionId block { - id + height } time programState { @@ -60,7 +60,7 @@ query($owner: Address!, $after: String, $before: String, $first: Int, $last: Int ... on FailureStatus { transactionId block { - id + height } time reason diff --git a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transactions_connection_query_gql_output.snap b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transactions_connection_query_gql_output.snap index 6b7d459777..62b69418a2 100644 --- a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transactions_connection_query_gql_output.snap +++ b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transactions_connection_query_gql_output.snap @@ -16,7 +16,7 @@ query($after: String, $before: String, $first: Int, $last: Int) { ... on SuccessStatus { transactionId block { - id + height } time programState { @@ -60,7 +60,7 @@ query($after: String, $before: String, $first: Int, $last: Int) { ... on FailureStatus { transactionId block { - id + height } time reason diff --git a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transparent_transaction_by_id_query_gql_output.snap b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transparent_transaction_by_id_query_gql_output.snap index 05ad55b65c..5f5e7b0143 100644 --- a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transparent_transaction_by_id_query_gql_output.snap +++ b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__tx__tests__transparent_transaction_by_id_query_gql_output.snap @@ -95,7 +95,7 @@ query($id: TransactionId!) { ... on SuccessStatus { transactionId block { - id + height } time programState { @@ -139,7 +139,7 @@ query($id: TransactionId!) { ... on FailureStatus { transactionId block { - id + height } time reason diff --git a/crates/client/src/client/schema/tx.rs b/crates/client/src/client/schema/tx.rs index 803a185d80..dc4d3f0420 100644 --- a/crates/client/src/client/schema/tx.rs +++ b/crates/client/src/client/schema/tx.rs @@ -1,4 +1,4 @@ -use super::block::BlockIdFragment; +use super::block::BlockHeightFragment; use crate::client::{ schema::{ schema, @@ -178,7 +178,7 @@ pub struct SubmittedStatus { #[cynic(schema_path = "./assets/schema.sdl")] pub struct SuccessStatus { pub transaction_id: TransactionId, - pub block: BlockIdFragment, + pub block: BlockHeightFragment, pub time: Tai64Timestamp, pub program_state: Option, pub receipts: Vec, @@ -188,7 +188,7 @@ pub struct SuccessStatus { #[cynic(schema_path = "./assets/schema.sdl")] pub struct FailureStatus { pub transaction_id: TransactionId, - pub block: BlockIdFragment, + pub block: BlockHeightFragment, pub time: Tai64Timestamp, pub reason: String, pub program_state: Option, diff --git a/crates/client/src/client/types.rs b/crates/client/src/client/types.rs index 5cbab01afc..f0db1e76aa 100644 --- a/crates/client/src/client/types.rs +++ b/crates/client/src/client/types.rs @@ -48,7 +48,10 @@ use fuel_core_types::{ Receipt, Transaction, }, - fuel_types::canonical::Deserialize, + fuel_types::{ + canonical::Deserialize, + BlockHeight, + }, fuel_vm::ProgramState, }; use tai64::Tai64; @@ -92,7 +95,7 @@ pub enum TransactionStatus { submitted_at: Tai64, }, Success { - block_id: String, + block_height: BlockHeight, time: Tai64, program_state: Option, receipts: Vec, @@ -101,7 +104,7 @@ pub enum TransactionStatus { reason: String, }, Failure { - block_id: String, + block_height: BlockHeight, time: Tai64, reason: String, program_state: Option, @@ -118,7 +121,7 @@ impl TryFrom for TransactionStatus { submitted_at: s.time.0, }, SchemaTxStatus::SuccessStatus(s) => TransactionStatus::Success { - block_id: s.block.id.0.to_string(), + block_height: s.block.height.into(), time: s.time.0, program_state: s.program_state.map(TryInto::try_into).transpose()?, receipts: s @@ -128,7 +131,7 @@ impl TryFrom for TransactionStatus { .collect::, _>>()?, }, SchemaTxStatus::FailureStatus(s) => TransactionStatus::Failure { - block_id: s.block.id.0.to_string(), + block_height: s.block.height.into(), time: s.time.0, reason: s.reason, program_state: s.program_state.map(TryInto::try_into).transpose()?, diff --git a/crates/client/src/client/types/gas_price.rs b/crates/client/src/client/types/gas_price.rs index 33b81787b6..bc1f63905d 100644 --- a/crates/client/src/client/types/gas_price.rs +++ b/crates/client/src/client/types/gas_price.rs @@ -1,8 +1,9 @@ use crate::client::schema; +use fuel_core_types::fuel_types::BlockHeight; pub struct LatestGasPrice { pub gas_price: u64, - pub block_height: u32, + pub block_height: BlockHeight, } // GraphQL Translation @@ -10,7 +11,7 @@ impl From for LatestGasPrice { fn from(value: schema::gas_price::LatestGasPrice) -> Self { Self { gas_price: value.gas_price.into(), - block_height: value.block_height.into(), + block_height: BlockHeight::new(value.block_height.into()), } } } diff --git a/crates/fuel-core/src/database/block.rs b/crates/fuel-core/src/database/block.rs index a7189e8c5a..9927af0851 100644 --- a/crates/fuel-core/src/database/block.rs +++ b/crates/fuel-core/src/database/block.rs @@ -1,24 +1,22 @@ -use crate::database::{ - database_description::{ - on_chain::OnChain, - DatabaseDescription, - DatabaseMetadata, +use crate::{ + database::{ + database_description::{ + off_chain::OffChain, + on_chain::OnChain, + DatabaseDescription, + DatabaseMetadata, + }, + metadata::MetadataTable, + Database, }, - metadata::MetadataTable, - Database, + fuel_core_graphql_api::storage::blocks::FuelBlockIdsToHeights, }; use fuel_core_storage::{ - blueprint::plain::Plain, - codec::{ - primitive::Primitive, - raw::Raw, - }, iter::IterDirection, not_found, - structured_storage::TableWithBlueprint, tables::{ merkle::{ - DenseMerkleMetadata, + DenseMetadataKey, FuelBlockMerkleData, FuelBlockMerkleMetadata, }, @@ -47,39 +45,7 @@ use fuel_core_types::{ fuel_types::BlockHeight, }; use itertools::Itertools; -use std::borrow::{ - BorrowMut, - Cow, -}; - -/// The table of fuel block's secondary key - `BlockId`. -/// It links the `BlockId` to corresponding `BlockHeight`. -pub struct FuelBlockSecondaryKeyBlockHeights; - -impl Mappable for FuelBlockSecondaryKeyBlockHeights { - /// Primary key - `BlockId`. - type Key = BlockId; - type OwnedKey = Self::Key; - /// Secondary key - `BlockHeight`. - type Value = BlockHeight; - type OwnedValue = Self::Value; -} - -impl TableWithBlueprint for FuelBlockSecondaryKeyBlockHeights { - type Blueprint = Plain>; - type Column = fuel_core_storage::column::Column; - - fn column() -> Self::Column { - Self::Column::FuelBlockSecondaryKeyBlockHeights - } -} - -#[cfg(test)] -fuel_core_storage::basic_storage_tests!( - FuelBlockSecondaryKeyBlockHeights, - ::Key::default(), - ::Value::default() -); +use std::borrow::Cow; impl StorageInspect for Database { type Error = StorageError; @@ -110,32 +76,6 @@ impl StorageMutate for Database { .storage_as_mut::() .insert(key, value)?; - let height = value.header().height(); - let block_id = value.id(); - self.storage::() - .insert(&block_id, key)?; - - // Get latest metadata entry - let prev_metadata = self - .iter_all::(Some(IterDirection::Reverse)) - .next() - .transpose()? - .map(|(_, metadata)| metadata) - .unwrap_or_default(); - - let storage = self.borrow_mut(); - let mut tree: MerkleTree = - MerkleTree::load(storage, prev_metadata.version()) - .map_err(|err| StorageError::Other(anyhow::anyhow!(err)))?; - tree.push(block_id.as_slice())?; - - // Generate new metadata for the updated tree - let root = tree.root(); - let version = tree.leaves_count(); - let metadata = DenseMerkleMetadata::new(root, version); - self.storage::() - .insert(height, &metadata)?; - // 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 @@ -143,7 +83,7 @@ impl StorageMutate for Database { &(), &DatabaseMetadata::V1 { version: OnChain::version(), - height: *height, + height: *key, }, )?; @@ -154,21 +94,15 @@ impl StorageMutate for Database { &mut self, key: &::Key, ) -> Result::OwnedValue>, Self::Error> { - let prev: Option = - self.data.storage_as_mut::().remove(key)?; - - if let Some(block) = &prev { - let height = block.header().height(); - let _ = self - .storage::() - .remove(&block.id()); - // We can't clean up `MerkleTree`. - // But if we plan to insert a new block, it will override old values in the - // `FuelBlockMerkleData` table. - let _ = self.storage::().remove(height); - } + self.data.storage_as_mut::().remove(key) + } +} - Ok(prev) +impl Database { + pub fn get_block_height(&self, id: &BlockId) -> StorageResult> { + self.storage::() + .get(id) + .map(|v| v.map(|v| v.into_owned())) } } @@ -187,12 +121,6 @@ impl Database { self.latest_compressed_block() } - pub fn get_block_height(&self, id: &BlockId) -> StorageResult> { - self.storage::() - .get(id) - .map(|v| v.map(|v| v.into_owned())) - } - /// Retrieve the full block and all associated transactions pub(crate) fn get_full_block( &self, @@ -224,11 +152,7 @@ impl MerkleRootStorage for Database { &self, key: &BlockHeight, ) -> Result { - let metadata = self - .storage::() - .get(key)? - .ok_or(not_found!(FuelBlocks))?; - Ok(*metadata.root()) + self.data.storage_as_ref::().root(key) } } @@ -246,12 +170,12 @@ impl Database { let message_merkle_metadata = self .storage::() - .get(message_block_height)? + .get(&DenseMetadataKey::Primary(*message_block_height))? .ok_or(not_found!(FuelBlockMerkleMetadata))?; let commit_merkle_metadata = self .storage::() - .get(commit_block_height)? + .get(&DenseMetadataKey::Primary(*commit_block_height))? .ok_or(not_found!(FuelBlockMerkleMetadata))?; let storage = self; @@ -288,77 +212,9 @@ mod tests { primitives::Empty, }, fuel_types::ChainId, - fuel_vm::crypto::ephemeral_merkle_root, }; use test_case::test_case; - #[test_case(&[0]; "initial block with height 0")] - #[test_case(&[1337]; "initial block with arbitrary height")] - #[test_case(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; "ten sequential blocks starting from height 0")] - #[test_case(&[100, 101, 102, 103, 104, 105]; "five sequential blocks starting from height 100")] - #[test_case(&[0, 2, 5, 7, 11]; "five non-sequential blocks starting from height 0")] - #[test_case(&[100, 102, 105, 107, 111]; "five non-sequential blocks starting from height 100")] - fn can_get_merkle_root_of_inserted_blocks(heights: &[u32]) { - let mut database = Database::default(); - let blocks = heights - .iter() - .copied() - .map(|height| { - let header = PartialBlockHeader { - application: Default::default(), - consensus: ConsensusHeader:: { - height: height.into(), - ..Default::default() - }, - }; - let block = PartialFuelBlock::new(header, vec![]); - block.generate(&[]) - }) - .collect::>(); - - // Insert the blocks. Each insertion creates a new version of Block - // metadata, including a new root. - for block in &blocks { - StorageMutate::::insert( - &mut database, - block.header().height(), - &block.compress(&ChainId::default()), - ) - .unwrap(); - } - - // Check each version - for version in 1..=blocks.len() { - // Generate the expected root for the version - let blocks = blocks.iter().take(version).collect::>(); - let block_ids = blocks.iter().map(|block| block.id()); - let expected_root = ephemeral_merkle_root(block_ids); - - // Check that root for the version is present - let last_block = blocks.last().unwrap(); - let actual_root = database - .storage::() - .root(last_block.header().height()) - .expect("root to exist") - .into(); - - assert_eq!(expected_root, actual_root); - } - } - - #[test] - fn get_merkle_root_with_no_blocks_returns_not_found_error() { - let database = Database::default(); - - // check that root is not present - let err = database - .storage::() - .root(&0u32.into()) - .expect_err("expected error getting invalid Block Merkle root"); - - assert!(matches!(err, fuel_core_storage::Error::NotFound(_, _))); - } - const TEST_BLOCKS_COUNT: u32 = 10; fn insert_test_ascending_blocks( diff --git a/crates/fuel-core/src/database/storage.rs b/crates/fuel-core/src/database/storage.rs index a801aa70bd..9be0a63893 100644 --- a/crates/fuel-core/src/database/storage.rs +++ b/crates/fuel-core/src/database/storage.rs @@ -1,10 +1,10 @@ use crate::{ database::{ - block::FuelBlockSecondaryKeyBlockHeights, database_description::DatabaseDescription, Database, }, fuel_core_graphql_api::storage::{ + blocks::FuelBlockIdsToHeights, coins::OwnedCoins, messages::OwnedMessageIds, transactions::{ @@ -95,7 +95,7 @@ use_structured_implementation!( OwnedMessageIds, OwnedTransactions, TransactionStatuses, - FuelBlockSecondaryKeyBlockHeights, + FuelBlockIdsToHeights, FuelBlockMerkleData, FuelBlockMerkleMetadata ); diff --git a/crates/fuel-core/src/graphql_api/database.rs b/crates/fuel-core/src/graphql_api/database.rs index 7477eadb7f..5ab0e09ead 100644 --- a/crates/fuel-core/src/graphql_api/database.rs +++ b/crates/fuel-core/src/graphql_api/database.rs @@ -106,10 +106,6 @@ pub struct ReadView { } impl DatabaseBlocks for ReadView { - fn block_height(&self, block_id: &BlockId) -> StorageResult { - self.on_chain.block_height(block_id) - } - fn blocks( &self, height: Option, @@ -189,6 +185,10 @@ impl DatabaseMessageProof for ReadView { impl OnChainDatabase for ReadView {} impl OffChainDatabase for ReadView { + fn block_height(&self, block_id: &BlockId) -> StorageResult { + self.off_chain.block_height(block_id) + } + fn tx_status(&self, tx_id: &TxId) -> StorageResult { self.off_chain.tx_status(tx_id) } diff --git a/crates/fuel-core/src/graphql_api/ports.rs b/crates/fuel-core/src/graphql_api/ports.rs index 62dc512454..720431c396 100644 --- a/crates/fuel-core/src/graphql_api/ports.rs +++ b/crates/fuel-core/src/graphql_api/ports.rs @@ -59,6 +59,8 @@ use fuel_core_types::{ use std::sync::Arc; pub trait OffChainDatabase: Send + Sync { + fn block_height(&self, block_id: &BlockId) -> StorageResult; + fn tx_status(&self, tx_id: &TxId) -> StorageResult; fn owned_coins_ids( @@ -102,8 +104,6 @@ pub trait DatabaseBlocks: StorageInspect + StorageInspect { - fn block_height(&self, block_id: &BlockId) -> StorageResult; - fn blocks( &self, height: Option, @@ -198,6 +198,7 @@ pub trait P2pPort: Send + Sync { } pub mod worker { + use super::super::storage::blocks::FuelBlockIdsToHeights; use crate::{ database::{ database_description::off_chain::OffChain, @@ -233,6 +234,7 @@ pub mod worker { + StorageMutate + StorageMutate + StorageMutate, Error = StorageError> + + StorageMutate + Transactional { fn record_tx_id_owner( diff --git a/crates/fuel-core/src/graphql_api/storage.rs b/crates/fuel-core/src/graphql_api/storage.rs index 89b7ec1427..d06df8bfed 100644 --- a/crates/fuel-core/src/graphql_api/storage.rs +++ b/crates/fuel-core/src/graphql_api/storage.rs @@ -1,5 +1,6 @@ use fuel_core_storage::kv_store::StorageColumn; +pub mod blocks; pub mod coins; pub mod messages; pub mod transactions; @@ -30,6 +31,8 @@ pub enum Column { OwnedMessageIds = 4, /// The column of the table that stores statistic about the blockchain. Statistic = 5, + /// See [`blocks::FuelBlockIdsToHeights`] + FuelBlockIdsToHeights = 6, } impl Column { diff --git a/crates/fuel-core/src/graphql_api/storage/blocks.rs b/crates/fuel-core/src/graphql_api/storage/blocks.rs new file mode 100644 index 0000000000..f2652d3129 --- /dev/null +++ b/crates/fuel-core/src/graphql_api/storage/blocks.rs @@ -0,0 +1,42 @@ +use fuel_core_storage::{ + blueprint::plain::Plain, + codec::{ + primitive::Primitive, + raw::Raw, + }, + structured_storage::TableWithBlueprint, + Mappable, +}; +use fuel_core_types::{ + blockchain::primitives::BlockId, + fuel_types::BlockHeight, +}; + +/// The table of fuel block's secondary key - `BlockId`. +/// It links the `BlockId` to corresponding `BlockHeight`. +pub struct FuelBlockIdsToHeights; + +impl Mappable for FuelBlockIdsToHeights { + /// Primary key - `BlockId`. + type Key = BlockId; + type OwnedKey = Self::Key; + /// Secondary key - `BlockHeight`. + type Value = BlockHeight; + type OwnedValue = Self::Value; +} + +impl TableWithBlueprint for FuelBlockIdsToHeights { + type Blueprint = Plain>; + type Column = super::Column; + + fn column() -> Self::Column { + Self::Column::FuelBlockIdsToHeights + } +} + +#[cfg(test)] +fuel_core_storage::basic_storage_tests!( + FuelBlockIdsToHeights, + ::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 247d16f1af..0fcd0dc627 100644 --- a/crates/fuel-core/src/graphql_api/worker_service.rs +++ b/crates/fuel-core/src/graphql_api/worker_service.rs @@ -10,6 +10,7 @@ use crate::{ fuel_core_graphql_api::{ ports, storage::{ + blocks::FuelBlockIdsToHeights, coins::{ owner_coin_id_key, OwnedCoins, @@ -87,8 +88,6 @@ where D: ports::worker::OffChainDatabase, { fn process_block(&mut self, result: SharedImportResult) -> anyhow::Result<()> { - // 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(); // save the status for every transaction using the finalized block id @@ -96,6 +95,14 @@ where // save the associated owner for each transaction in the block self.index_tx_owners_for_block(block, transaction.as_mut())?; + + let height = block.header().height(); + let block_id = block.id(); + transaction + .as_mut() + .storage::() + .insert(&block_id, height)?; + let total_tx_count = transaction .as_mut() .increase_tx_count(block.transactions().len() as u64) diff --git a/crates/fuel-core/src/query/block.rs b/crates/fuel-core/src/query/block.rs index 2d7edbd0b3..0367c79c1e 100644 --- a/crates/fuel-core/src/query/block.rs +++ b/crates/fuel-core/src/query/block.rs @@ -16,15 +16,12 @@ use fuel_core_types::{ blockchain::{ block::CompressedBlock, consensus::Consensus, - primitives::BlockId, }, fuel_types::BlockHeight, }; pub trait SimpleBlockData: Send + Sync { fn block(&self, id: &BlockHeight) -> StorageResult; - - fn block_by_id(&self, id: &BlockId) -> StorageResult; } impl SimpleBlockData for D { @@ -37,11 +34,6 @@ impl SimpleBlockData for D { Ok(block) } - - fn block_by_id(&self, id: &BlockId) -> StorageResult { - let height = self.block_height(id)?; - self.block(&height) - } } pub trait BlockQueryData: Send + Sync + SimpleBlockData { diff --git a/crates/fuel-core/src/query/message.rs b/crates/fuel-core/src/query/message.rs index 93b96e4738..8820a1521e 100644 --- a/crates/fuel-core/src/query/message.rs +++ b/crates/fuel-core/src/query/message.rs @@ -173,17 +173,17 @@ pub fn message_proof( data.ok_or(anyhow::anyhow!("Output message doesn't contain any `data`"))?; // Get the block id from the transaction status if it's ready. - let message_block_id = match database + let message_block_height = match database .transaction_status(&transaction_id) - .into_api_result::()? - { - Some(TransactionStatus::Success { block_id, .. }) => block_id, + .into_api_result::( + )? { + Some(TransactionStatus::Success { block_height, .. }) => block_height, _ => return Ok(None), }; // Get the message fuel block header. let (message_block_header, message_block_txs) = match database - .block_by_id(&message_block_id) + .block(&message_block_height) .into_api_result::()? { Some(t) => t.into_inner(), diff --git a/crates/fuel-core/src/query/message/test.rs b/crates/fuel-core/src/query/message/test.rs index b7a8800f98..2c40700fdf 100644 --- a/crates/fuel-core/src/query/message/test.rs +++ b/crates/fuel-core/src/query/message/test.rs @@ -1,13 +1,10 @@ use std::ops::Deref; use fuel_core_types::{ - blockchain::{ - header::{ - ApplicationHeader, - ConsensusHeader, - PartialBlockHeader, - }, - primitives::BlockId, + blockchain::header::{ + ApplicationHeader, + ConsensusHeader, + PartialBlockHeader, }, entities::message::MerkleProof, fuel_tx::{ @@ -63,7 +60,6 @@ mockall::mock! { pub ProofDataStorage {} impl SimpleBlockData for ProofDataStorage { fn block(&self, height: &BlockHeight) -> StorageResult; - fn block_by_id(&self, id: &BlockId) -> StorageResult; } impl DatabaseMessageProof for ProofDataStorage { @@ -175,34 +171,25 @@ async fn can_build_message_proof() { move |_, _| Ok(block_proof.clone()) }); - let message_block_id = message_block.id(); + let message_block_height = *message_block.header().height(); data.expect_transaction_status() .with(eq(transaction_id)) .returning(move |_| { Ok(TransactionStatus::Success { - block_id: message_block_id, + block_height: message_block_height, time: Tai64::UNIX_EPOCH, result: None, receipts: vec![], }) }); - data.expect_block().times(1).returning({ + data.expect_block().times(2).returning({ let commit_block = commit_block.clone(); + let message_block = message_block.clone(); move |block_height| { let block = if commit_block.header().height() == block_height { commit_block.clone() - } else { - panic!("Shouldn't request any other block") - }; - Ok(block) - } - }); - - data.expect_block_by_id().times(1).returning({ - let message_block = message_block.clone(); - move |block_id| { - let block = if &message_block.id() == block_id { + } else if message_block.header().height() == block_height { message_block.clone() } else { panic!("Shouldn't request any other block") diff --git a/crates/fuel-core/src/query/subscriptions/test.rs b/crates/fuel-core/src/query/subscriptions/test.rs index 55f7e55fe7..191f9395a1 100644 --- a/crates/fuel-core/src/query/subscriptions/test.rs +++ b/crates/fuel-core/src/query/subscriptions/test.rs @@ -52,7 +52,7 @@ fn submitted() -> TransactionStatus { /// Returns a TransactionStatus with Success status, time set to 0, and result set to None fn success() -> TransactionStatus { TransactionStatus::Success { - block_id: Default::default(), + block_height: Default::default(), time: Tai64(0), result: None, receipts: vec![], @@ -62,7 +62,7 @@ fn success() -> TransactionStatus { /// Returns a TransactionStatus with Failed status, time set to 0, result set to None, and empty reason fn failed() -> TransactionStatus { TransactionStatus::Failed { - block_id: Default::default(), + block_height: Default::default(), time: Tai64(0), result: None, receipts: vec![], diff --git a/crates/fuel-core/src/schema/block.rs b/crates/fuel-core/src/schema/block.rs index 5c341a4c47..ea9e4da7c9 100644 --- a/crates/fuel-core/src/schema/block.rs +++ b/crates/fuel-core/src/schema/block.rs @@ -3,11 +3,10 @@ use super::scalars::{ Tai64Timestamp, }; use crate::{ - database::Database, fuel_core_graphql_api::{ api_service::ConsensusModule, database::ReadView, - ports::DatabaseBlocks, + ports::OffChainDatabase, Config as GraphQLConfig, IntoApiResult, }, @@ -92,12 +91,17 @@ impl Block { bytes.into() } + async fn height(&self) -> U32 { + let height: u32 = (*self.0.header().height()).into(); + height.into() + } + async fn header(&self) -> Header { self.0.header().clone().into() } async fn consensus(&self, ctx: &Context<'_>) -> async_graphql::Result { - let query: &Database = ctx.data_unchecked(); + let query: &ReadView = ctx.data_unchecked(); let height = self.0.header().height(); let core_consensus = query.consensus(height)?; diff --git a/crates/fuel-core/src/schema/message.rs b/crates/fuel-core/src/schema/message.rs index cc36502c98..43cdd8379f 100644 --- a/crates/fuel-core/src/schema/message.rs +++ b/crates/fuel-core/src/schema/message.rs @@ -12,7 +12,7 @@ use super::{ use crate::{ fuel_core_graphql_api::{ database::ReadView, - ports::DatabaseBlocks, + ports::OffChainDatabase, }, graphql_api::IntoApiResult, query::MessageQueryData, diff --git a/crates/fuel-core/src/schema/tx/types.rs b/crates/fuel-core/src/schema/tx/types.rs index 871e19ed6b..4279361177 100644 --- a/crates/fuel-core/src/schema/tx/types.rs +++ b/crates/fuel-core/src/schema/tx/types.rs @@ -7,7 +7,6 @@ use crate::{ fuel_core_graphql_api::{ api_service::TxPool, database::ReadView, - ports::DatabaseBlocks, Config, IntoApiResult, }, @@ -43,7 +42,6 @@ use async_graphql::{ }; use fuel_core_storage::Error as StorageError; use fuel_core_types::{ - blockchain::primitives, fuel_tx::{ self, field::{ @@ -150,7 +148,7 @@ impl SubmittedStatus { #[derive(Debug)] pub struct SuccessStatus { tx_id: TxId, - block_id: primitives::BlockId, + block_height: fuel_core_types::fuel_types::BlockHeight, time: Tai64, result: Option, receipts: Vec, @@ -164,8 +162,7 @@ impl SuccessStatus { async fn block(&self, ctx: &Context<'_>) -> async_graphql::Result { let query: &ReadView = ctx.data_unchecked(); - let height = query.block_height(&self.block_id)?; - let block = query.block(&height)?; + let block = query.block(&self.block_height)?; Ok(block.into()) } @@ -185,7 +182,7 @@ impl SuccessStatus { #[derive(Debug)] pub struct FailureStatus { tx_id: TxId, - block_id: primitives::BlockId, + block_height: fuel_core_types::fuel_types::BlockHeight, time: Tai64, state: Option, receipts: Vec, @@ -199,8 +196,7 @@ impl FailureStatus { async fn block(&self, ctx: &Context<'_>) -> async_graphql::Result { let query: &ReadView = ctx.data_unchecked(); - let height = query.block_height(&self.block_id)?; - let block = query.block(&height)?; + let block = query.block(&self.block_height)?; Ok(block.into()) } @@ -240,13 +236,13 @@ impl TransactionStatus { TransactionStatus::Submitted(SubmittedStatus(time)) } TxStatus::Success { - block_id, + block_height, result, time, receipts, } => TransactionStatus::Success(SuccessStatus { tx_id, - block_id, + block_height, result, time, receipts, @@ -255,13 +251,13 @@ impl TransactionStatus { TransactionStatus::SqueezedOut(SqueezedOutStatus { reason }) } TxStatus::Failed { - block_id, + block_height, time, result, receipts, } => TransactionStatus::Failed(FailureStatus { tx_id, - block_id, + block_height, time, state: result, receipts, @@ -277,13 +273,13 @@ impl From for TxStatus { TxStatus::Submitted { time } } TransactionStatus::Success(SuccessStatus { - block_id, + block_height, result, time, receipts, .. }) => TxStatus::Success { - block_id, + block_height, result, time, receipts, @@ -292,13 +288,13 @@ impl From for TxStatus { TxStatus::SqueezedOut { reason } } TransactionStatus::Failed(FailureStatus { - block_id, + block_height, time, state: result, receipts, .. }) => TxStatus::Failed { - block_id, + block_height, time, result, receipts, 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 a42afa4a72..418d2f755b 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 @@ -23,6 +23,7 @@ use fuel_core_storage::{ }; use fuel_core_txpool::types::TxId; use fuel_core_types::{ + blockchain::primitives::BlockId, fuel_tx::{ Address, Bytes32, @@ -37,6 +38,11 @@ use fuel_core_types::{ }; impl OffChainDatabase for Database { + fn block_height(&self, id: &BlockId) -> StorageResult { + self.get_block_height(id) + .and_then(|height| height.ok_or(not_found!("BlockHeight"))) + } + fn tx_status(&self, tx_id: &TxId) -> StorageResult { self.get_tx_status(tx_id) .transpose() 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 7430e1f2ee..19ab02bea6 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 @@ -24,10 +24,7 @@ use fuel_core_txpool::types::ContractId; use fuel_core_types::{ blockchain::{ block::CompressedBlock, - primitives::{ - BlockId, - DaBlockHeight, - }, + primitives::DaBlockHeight, }, entities::message::Message, fuel_tx::AssetId, @@ -39,11 +36,6 @@ use fuel_core_types::{ }; impl DatabaseBlocks for Database { - fn block_height(&self, id: &BlockId) -> StorageResult { - self.get_block_height(id) - .and_then(|height| height.ok_or(not_found!("BlockHeight"))) - } - fn blocks( &self, height: Option, diff --git a/crates/services/txpool/src/service/update_sender/tests/test_e2e.rs b/crates/services/txpool/src/service/update_sender/tests/test_e2e.rs index c7f7587b07..3388c116f6 100644 --- a/crates/services/txpool/src/service/update_sender/tests/test_e2e.rs +++ b/crates/services/txpool/src/service/update_sender/tests/test_e2e.rs @@ -6,7 +6,6 @@ use super::*; use crate::service::update_sender::tests::test_sending::validate_send; -use fuel_core_types::blockchain::primitives::BlockId; use std::time::Duration; const MAX_CHANNELS: usize = 2; @@ -44,7 +43,7 @@ fn test_update_sender_reg() { Send( 0, Status(Success { - block_id: BlockId::from([0; 32]), + block_height: Default::default(), time: Tai64(0), result: None, receipts: vec![], diff --git a/crates/services/txpool/src/service/update_sender/tests/test_sending.rs b/crates/services/txpool/src/service/update_sender/tests/test_sending.rs index c0efcb4ba6..d1038e5cd3 100644 --- a/crates/services/txpool/src/service/update_sender/tests/test_sending.rs +++ b/crates/services/txpool/src/service/update_sender/tests/test_sending.rs @@ -1,7 +1,5 @@ use std::sync::Arc; -use fuel_core_types::blockchain::primitives::BlockId; - use crate::service::update_sender::tests::utils::{ construct_senders, SenderData, @@ -60,7 +58,7 @@ fn test_send_reg() { let update = TxUpdate { tx_id: Bytes32::from([2; 32]), message: TxStatusMessage::Status(TransactionStatus::Success { - block_id: BlockId::from([0; 32]), + block_height: Default::default(), time: Tai64(0), result: None, receipts: vec![], diff --git a/crates/services/txpool/src/service/update_sender/tests/utils.rs b/crates/services/txpool/src/service/update_sender/tests/utils.rs index 58ce69ad7d..c02a3e0579 100644 --- a/crates/services/txpool/src/service/update_sender/tests/utils.rs +++ b/crates/services/txpool/src/service/update_sender/tests/utils.rs @@ -4,13 +4,13 @@ pub fn transaction_status_strategy() -> impl Strategy prop_oneof![ Just(TransactionStatus::Submitted { time: Tai64(0) }), Just(TransactionStatus::Success { - block_id: Default::default(), + block_height: Default::default(), time: Tai64(0), result: None, receipts: vec![], }), Just(TransactionStatus::Failed { - block_id: Default::default(), + block_height: Default::default(), time: Tai64(0), result: None, receipts: vec![], diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index b40f2488fd..1771c26a70 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -42,6 +42,7 @@ fuel-core-types = { workspace = true, default-features = false, features = [ "random", "test-helpers", ] } +test-case = { workspace = true } [features] test-helpers = ["dep:mockall", "dep:rand"] diff --git a/crates/storage/src/blueprint.rs b/crates/storage/src/blueprint.rs index 3db1ee73e8..1afc45cf66 100644 --- a/crates/storage/src/blueprint.rs +++ b/crates/storage/src/blueprint.rs @@ -16,7 +16,9 @@ use crate::{ Mappable, Result as StorageResult, }; +use fuel_vm_private::prelude::MerkleRoot; +pub mod merklized; pub mod plain; pub mod sparse; @@ -137,3 +139,14 @@ where Iter: 'a + Iterator, M::Key: 'a; } + +/// It is an extension of the blueprint that supporting creation of the Merkle tree over the storage. +pub trait SupportsMerkle: Blueprint +where + Key: ?Sized, + M: Mappable, + S: KeyValueStore, +{ + /// Returns the root of the Merkle tree. + fn root(storage: &S, key: &Key) -> StorageResult; +} diff --git a/crates/storage/src/blueprint/merklized.rs b/crates/storage/src/blueprint/merklized.rs new file mode 100644 index 0000000000..5583eced49 --- /dev/null +++ b/crates/storage/src/blueprint/merklized.rs @@ -0,0 +1,543 @@ +//! The module defines the `Merklized` blueprint for the storage. +//! The `Merklized` blueprint implements the binary merkle tree on top of the storage +//! for all entries. + +use crate::{ + blueprint::{ + Blueprint, + SupportsBatching, + SupportsMerkle, + }, + codec::{ + Decode, + Encode, + Encoder as EncoderTrait, + }, + kv_store::{ + BatchOperations, + KeyValueStore, + }, + not_found, + structured_storage::StructuredStorage, + tables::merkle::{ + DenseMerkleMetadata, + DenseMerkleMetadataV1, + DenseMetadataKey, + }, + Error as StorageError, + Mappable, + MerkleRoot, + Result as StorageResult, + StorageAsMut, + StorageInspect, + StorageMutate, +}; +use fuel_core_types::fuel_merkle::binary::Primitive; + +/// The `Merklized` blueprint builds the storage as a [`Plain`](super::plain::Plain) +/// blueprint and maintains the binary merkle tree by the `Metadata` table. +/// +/// It uses the `KeyCodec` and `ValueCodec` to encode/decode the key and value in the +/// same way as a plain blueprint. +/// +/// The `Metadata` table stores the metadata of the binary merkle tree(like a root of the tree and leaves count). +/// +/// The `ValueEncoder` is used to encode the value for merklelization. +pub struct Merklized { + _marker: + core::marker::PhantomData<(KeyCodec, ValueCodec, Metadata, Nodes, ValueEncoder)>, +} + +impl + Merklized +where + Nodes: Mappable, +{ + fn insert_into_tree(storage: &mut S, key: K, value: &V) -> StorageResult<()> + where + V: ?Sized, + Metadata: Mappable< + Key = DenseMetadataKey, + Value = DenseMerkleMetadata, + OwnedValue = DenseMerkleMetadata, + >, + for<'a> StructuredStorage<&'a mut S>: StorageMutate + + StorageMutate, + Encoder: Encode, + { + let mut storage = StructuredStorage::new(storage); + // Get latest metadata entry + let prev_metadata = storage + .storage::() + .get(&DenseMetadataKey::Latest)? + .unwrap_or_default(); + let previous_version = prev_metadata.version(); + + let mut tree: fuel_core_types::fuel_merkle::binary::MerkleTree = + fuel_core_types::fuel_merkle::binary::MerkleTree::load( + &mut storage, + previous_version, + ) + .map_err(|err| StorageError::Other(anyhow::anyhow!(err)))?; + let encoder = Encoder::encode(value); + tree.push(encoder.as_bytes().as_ref())?; + + // Generate new metadata for the updated tree + let version = tree.leaves_count(); + let root = tree.root(); + let metadata = DenseMerkleMetadata::V1(DenseMerkleMetadataV1 { version, root }); + storage + .storage::() + .insert(&DenseMetadataKey::Primary(key), &metadata)?; + // Duplicate the metadata entry for the latest key. + storage + .storage::() + .insert(&DenseMetadataKey::Latest, &metadata)?; + + Ok(()) + } + + fn remove(storage: &mut S, key: &[u8], column: S::Column) -> StorageResult<()> + where + S: KeyValueStore, + { + if storage.exists(key, column)? { + Err(anyhow::anyhow!( + "It is not allowed to remove or override entries in the merklelized table" + ) + .into()) + } else { + Ok(()) + } + } +} + +impl Blueprint + for Merklized +where + M: Mappable, + S: KeyValueStore, + KeyCodec: Encode + Decode, + ValueCodec: Encode + Decode, + Encoder: Encode, + Metadata: Mappable< + Key = DenseMetadataKey, + OwnedKey = DenseMetadataKey, + Value = DenseMerkleMetadata, + OwnedValue = DenseMerkleMetadata, + >, + Nodes: Mappable, + for<'a> StructuredStorage<&'a mut S>: StorageMutate + + StorageMutate, +{ + type KeyCodec = KeyCodec; + type ValueCodec = ValueCodec; + + fn put( + storage: &mut S, + key: &M::Key, + column: S::Column, + value: &M::Value, + ) -> StorageResult<()> { + let key_encoder = KeyCodec::encode(key); + let key_bytes = key_encoder.as_bytes(); + let encoded_value = ValueCodec::encode_as_value(value); + storage.put(key_bytes.as_ref(), column, encoded_value)?; + let key = key.to_owned().into(); + Self::insert_into_tree(storage, key, value) + } + + fn replace( + storage: &mut S, + key: &M::Key, + column: S::Column, + value: &M::Value, + ) -> StorageResult> { + let key_encoder = KeyCodec::encode(key); + let key_bytes = key_encoder.as_bytes(); + let encoded_value = ValueCodec::encode_as_value(value); + let prev = storage + .replace(key_bytes.as_ref(), column, encoded_value)? + .map(|value| { + ValueCodec::decode_from_value(value).map_err(StorageError::Codec) + }) + .transpose()?; + + if prev.is_some() { + Self::remove(storage, key_bytes.as_ref(), column)?; + } + + let key = key.to_owned().into(); + Self::insert_into_tree(storage, key, value)?; + Ok(prev) + } + + fn take( + storage: &mut S, + key: &M::Key, + column: S::Column, + ) -> StorageResult> { + let key_encoder = KeyCodec::encode(key); + let key_bytes = key_encoder.as_bytes(); + Self::remove(storage, key_bytes.as_ref(), column)?; + let prev = storage + .take(key_bytes.as_ref(), column)? + .map(|value| { + ValueCodec::decode_from_value(value).map_err(StorageError::Codec) + }) + .transpose()?; + Ok(prev) + } + + fn delete(storage: &mut S, key: &M::Key, column: S::Column) -> StorageResult<()> { + let key_encoder = KeyCodec::encode(key); + let key_bytes = key_encoder.as_bytes(); + Self::remove(storage, key_bytes.as_ref(), column) + } +} + +impl SupportsMerkle + for Merklized +where + M: Mappable, + S: KeyValueStore, + Metadata: Mappable< + Key = DenseMetadataKey, + OwnedKey = DenseMetadataKey, + Value = DenseMerkleMetadata, + OwnedValue = DenseMerkleMetadata, + >, + Self: Blueprint, + for<'a> StructuredStorage<&'a S>: StorageInspect, +{ + fn root(storage: &S, key: &M::Key) -> StorageResult { + use crate::StorageAsRef; + let storage = StructuredStorage::new(storage); + let key = key.to_owned().into(); + let metadata = storage + .storage_as_ref::() + .get(&DenseMetadataKey::Primary(key))? + .ok_or(not_found!(Metadata))?; + Ok(*metadata.root()) + } +} + +impl SupportsBatching + for Merklized +where + M: Mappable, + S: BatchOperations, + KeyCodec: Encode + Decode, + ValueCodec: Encode + Decode, + Encoder: Encode, + Metadata: Mappable< + Key = DenseMetadataKey, + OwnedKey = DenseMetadataKey, + Value = DenseMerkleMetadata, + OwnedValue = DenseMerkleMetadata, + >, + Nodes: Mappable, + for<'a> StructuredStorage<&'a mut S>: StorageMutate + + StorageMutate, +{ + fn init<'a, Iter>(storage: &mut S, column: S::Column, set: Iter) -> StorageResult<()> + where + Iter: 'a + Iterator, + M::Key: 'a, + M::Value: 'a, + { + >::insert(storage, column, set) + } + + fn insert<'a, Iter>( + storage: &mut S, + column: S::Column, + set: Iter, + ) -> StorageResult<()> + where + Iter: 'a + Iterator, + M::Key: 'a, + M::Value: 'a, + { + for (key, value) in set { + >::replace(storage, key, column, value)?; + } + + Ok(()) + } + + fn remove<'a, Iter>( + storage: &mut S, + column: S::Column, + set: Iter, + ) -> StorageResult<()> + where + Iter: 'a + Iterator, + M::Key: 'a, + { + for item in set { + let key_encoder = KeyCodec::encode(item); + let key_bytes = key_encoder.as_bytes(); + Self::remove(storage, key_bytes.as_ref(), column)?; + } + Ok(()) + } +} + +/// The macro that generates basic storage tests for the table with the merklelized structure. +/// It uses the [`InMemoryStorage`](crate::structured_storage::test::InMemoryStorage). +#[cfg(feature = "test-helpers")] +#[macro_export] +macro_rules! basic_merklelized_storage_tests { + ($table:ident, $key:expr, $value_insert:expr, $value_return:expr, $random_key:expr) => { + $crate::paste::item! { + #[cfg(test)] + #[allow(unused_imports)] + mod [< $table:snake _basic_tests >] { + use super::*; + use $crate::{ + structured_storage::{ + test::InMemoryStorage, + StructuredStorage, + }, + StorageAsMut, + }; + use $crate::StorageInspect; + use $crate::StorageMutate; + use $crate::rand; + use $crate::tables::merkle::DenseMetadataKey; + use rand::SeedableRng; + + #[allow(dead_code)] + fn random(rng: &mut R) -> T + where + rand::distributions::Standard: rand::distributions::Distribution, + R: rand::Rng, + { + use rand::Rng; + rng.gen() + } + + #[test] + fn get() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + let key = $key; + + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &$value_insert) + .unwrap(); + + assert_eq!( + structured_storage + .storage_as_mut::<$table>() + .get(&key) + .expect("Should get without errors") + .expect("Should not be empty") + .into_owned(), + $value_return + ); + } + + #[test] + fn insert() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + let key = $key; + + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &$value_insert) + .unwrap(); + + let returned = structured_storage + .storage_as_mut::<$table>() + .get(&key) + .unwrap() + .unwrap() + .into_owned(); + assert_eq!(returned, $value_return); + } + + #[test] + fn remove_returns_error() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + let key = $key; + + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &$value_insert) + .unwrap(); + + let result = structured_storage.storage_as_mut::<$table>().remove(&key); + + assert!(result.is_err()); + } + + #[test] + fn exists() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + let key = $key; + + // Given + assert!(!structured_storage + .storage_as_mut::<$table>() + .contains_key(&key) + .unwrap()); + + // When + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &$value_insert) + .unwrap(); + + // Then + assert!(structured_storage + .storage_as_mut::<$table>() + .contains_key(&key) + .unwrap()); + } + + #[test] + fn batch_mutate_works() { + use $crate::rand::{ + Rng, + rngs::StdRng, + RngCore, + SeedableRng, + }; + + let empty_storage = InMemoryStorage::default(); + + let mut init_storage = InMemoryStorage::default(); + let mut init_structured_storage = StructuredStorage::new(&mut init_storage); + + let mut rng = &mut StdRng::seed_from_u64(1234); + let gen = || Some($random_key(&mut rng)); + let data = core::iter::from_fn(gen).take(5_000).collect::>(); + let value = $value_insert; + + <_ as $crate::StorageBatchMutate<$table>>::init_storage( + &mut init_structured_storage, + &mut data.iter().map(|k| { + let value: &<$table as $crate::Mappable>::Value = &value; + (k, value) + }) + ).expect("Should initialize the storage successfully"); + + let mut insert_storage = InMemoryStorage::default(); + let mut insert_structured_storage = StructuredStorage::new(&mut insert_storage); + + <_ as $crate::StorageBatchMutate<$table>>::insert_batch( + &mut insert_structured_storage, + &mut data.iter().map(|k| { + let value: &<$table as $crate::Mappable>::Value = &value; + (k, value) + }) + ).expect("Should insert batch successfully"); + + assert_eq!(init_storage, insert_storage); + assert_ne!(init_storage, empty_storage); + assert_ne!(insert_storage, empty_storage); + } + + #[test] + fn batch_remove_fails() { + use $crate::rand::{ + Rng, + rngs::StdRng, + RngCore, + SeedableRng, + }; + + let mut init_storage = InMemoryStorage::default(); + let mut init_structured_storage = StructuredStorage::new(&mut init_storage); + + let mut rng = &mut StdRng::seed_from_u64(1234); + let gen = || Some($random_key(&mut rng)); + let data = core::iter::from_fn(gen).take(5_000).collect::>(); + let value = $value_insert; + + <_ as $crate::StorageBatchMutate<$table>>::init_storage( + &mut init_structured_storage, + &mut data.iter().map(|k| { + let value: &<$table as $crate::Mappable>::Value = &value; + (k, value) + }) + ).expect("Should initialize the storage successfully"); + + let result = <_ as $crate::StorageBatchMutate<$table>>::remove_batch( + &mut init_structured_storage, + &mut data.iter() + ); + + assert!(result.is_err()); + } + + #[test] + fn root_returns_error_empty_metadata() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let root = structured_storage + .storage_as_mut::<$table>() + .root(&$key); + assert!(root.is_err()) + } + + #[test] + fn update_produces_non_zero_root() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let mut rng = rand::rngs::StdRng::seed_from_u64(1234); + let key = $random_key(&mut rng); + let value = $value_insert; + structured_storage.storage_as_mut::<$table>().insert(&key, &value) + .unwrap(); + + let root = structured_storage.storage_as_mut::<$table>().root(&key) + .expect("Should get the root"); + let empty_root = fuel_core_types::fuel_merkle::binary::in_memory::MerkleTree::new().root(); + assert_ne!(root, empty_root); + } + + #[test] + fn has_different_root_after_each_update() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let mut rng = rand::rngs::StdRng::seed_from_u64(1234); + + let mut prev_root = fuel_core_types::fuel_merkle::binary::in_memory::MerkleTree::new().root(); + + for _ in 0..10 { + let key = $random_key(&mut rng); + let value = $value_insert; + structured_storage.storage_as_mut::<$table>().insert(&key, &value) + .unwrap(); + + let root = structured_storage.storage_as_mut::<$table>().root(&key) + .expect("Should get the root"); + assert_ne!(root, prev_root); + prev_root = root; + } + } + }} + }; + ($table:ident, $key:expr, $value_insert:expr, $value_return:expr) => { + $crate::basic_merklelized_storage_tests!( + $table, + $key, + $value_insert, + $value_return, + random + ); + }; + ($table:ident, $key:expr, $value:expr) => { + $crate::basic_merklelized_storage_tests!($table, $key, $value, $value); + }; +} diff --git a/crates/storage/src/blueprint/plain.rs b/crates/storage/src/blueprint/plain.rs index 22d02a771e..51a5dcc7d3 100644 --- a/crates/storage/src/blueprint/plain.rs +++ b/crates/storage/src/blueprint/plain.rs @@ -145,3 +145,214 @@ where })) } } + +/// The macro that generates basic storage tests for the table with the plain structure. +/// It uses the [`InMemoryStorage`](crate::structured_storage::test::InMemoryStorage). +#[cfg(feature = "test-helpers")] +#[macro_export] +macro_rules! basic_storage_tests { + ($table:ident, $key:expr, $value_insert:expr, $value_return:expr, $random_key:expr) => { + $crate::paste::item! { + #[cfg(test)] + #[allow(unused_imports)] + mod [< $table:snake _basic_tests >] { + use super::*; + use $crate::{ + structured_storage::{ + test::InMemoryStorage, + StructuredStorage, + }, + StorageAsMut, + }; + use $crate::StorageInspect; + use $crate::StorageMutate; + use $crate::rand; + + #[allow(dead_code)] + fn random(rng: &mut R) -> T + where + rand::distributions::Standard: rand::distributions::Distribution, + R: rand::Rng, + { + use rand::Rng; + rng.gen() + } + + #[test] + fn get() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + let key = $key; + + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &$value_insert) + .unwrap(); + + assert_eq!( + structured_storage + .storage_as_mut::<$table>() + .get(&key) + .expect("Should get without errors") + .expect("Should not be empty") + .into_owned(), + $value_return + ); + } + + #[test] + fn insert() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + let key = $key; + + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &$value_insert) + .unwrap(); + + let returned = structured_storage + .storage_as_mut::<$table>() + .get(&key) + .unwrap() + .unwrap() + .into_owned(); + assert_eq!(returned, $value_return); + } + + #[test] + fn remove() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + let key = $key; + + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &$value_insert) + .unwrap(); + + structured_storage.storage_as_mut::<$table>().remove(&key).unwrap(); + + assert!(!structured_storage + .storage_as_mut::<$table>() + .contains_key(&key) + .unwrap()); + } + + #[test] + fn exists() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + let key = $key; + + // Given + assert!(!structured_storage + .storage_as_mut::<$table>() + .contains_key(&key) + .unwrap()); + + // When + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &$value_insert) + .unwrap(); + + // Then + assert!(structured_storage + .storage_as_mut::<$table>() + .contains_key(&key) + .unwrap()); + } + + #[test] + fn exists_false_after_removing() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + let key = $key; + + // Given + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &$value_insert) + .unwrap(); + + // When + structured_storage + .storage_as_mut::<$table>() + .remove(&key) + .unwrap(); + + // Then + assert!(!structured_storage + .storage_as_mut::<$table>() + .contains_key(&key) + .unwrap()); + } + + #[test] + fn batch_mutate_works() { + use $crate::rand::{ + Rng, + rngs::StdRng, + RngCore, + SeedableRng, + }; + + let empty_storage = InMemoryStorage::default(); + + let mut init_storage = InMemoryStorage::default(); + let mut init_structured_storage = StructuredStorage::new(&mut init_storage); + + let mut rng = &mut StdRng::seed_from_u64(1234); + let gen = || Some($random_key(&mut rng)); + let data = core::iter::from_fn(gen).take(5_000).collect::>(); + let value = $value_insert; + + <_ as $crate::StorageBatchMutate<$table>>::init_storage( + &mut init_structured_storage, + &mut data.iter().map(|k| { + let value: &<$table as $crate::Mappable>::Value = &value; + (k, value) + }) + ).expect("Should initialize the storage successfully"); + + let mut insert_storage = InMemoryStorage::default(); + let mut insert_structured_storage = StructuredStorage::new(&mut insert_storage); + + <_ as $crate::StorageBatchMutate<$table>>::insert_batch( + &mut insert_structured_storage, + &mut data.iter().map(|k| { + let value: &<$table as $crate::Mappable>::Value = &value; + (k, value) + }) + ).expect("Should insert batch successfully"); + + assert_eq!(init_storage, insert_storage); + assert_ne!(init_storage, empty_storage); + assert_ne!(insert_storage, empty_storage); + + let mut remove_from_insert_structured_storage = StructuredStorage::new(&mut insert_storage); + <_ as $crate::StorageBatchMutate<$table>>::remove_batch( + &mut remove_from_insert_structured_storage, + &mut data.iter() + ).expect("Should remove all entries successfully from insert storage"); + assert_ne!(init_storage, insert_storage); + assert_eq!(insert_storage, empty_storage); + + let mut remove_from_init_structured_storage = StructuredStorage::new(&mut init_storage); + <_ as $crate::StorageBatchMutate<$table>>::remove_batch( + &mut remove_from_init_structured_storage, + &mut data.iter() + ).expect("Should remove all entries successfully from init storage"); + assert_eq!(init_storage, insert_storage); + assert_eq!(init_storage, empty_storage); + } + }} + }; + ($table:ident, $key:expr, $value_insert:expr, $value_return:expr) => { + $crate::basic_storage_tests!($table, $key, $value_insert, $value_return, random); + }; + ($table:ident, $key:expr, $value:expr) => { + $crate::basic_storage_tests!($table, $key, $value, $value); + }; +} diff --git a/crates/storage/src/blueprint/sparse.rs b/crates/storage/src/blueprint/sparse.rs index 9e0deb6310..9bcaaeaf12 100644 --- a/crates/storage/src/blueprint/sparse.rs +++ b/crates/storage/src/blueprint/sparse.rs @@ -7,6 +7,7 @@ use crate::{ blueprint::{ Blueprint, SupportsBatching, + SupportsMerkle, }, codec::{ Decode, @@ -27,7 +28,6 @@ use crate::{ Error as StorageError, Mappable, MerkleRoot, - MerkleRootStorage, Result as StorageResult, StorageAsMut, StorageInspect, @@ -240,24 +240,21 @@ where } } -impl - MerkleRootStorage for StructuredStorage +impl + SupportsMerkle + for Sparse where - S: KeyValueStore, - M: Mappable - + TableWithBlueprint< - Blueprint = Sparse, - Column = Column, - >, - Self: StorageMutate - + StorageInspect, + M: Mappable, + S: KeyValueStore, Metadata: Mappable, - Metadata::Key: Sized, + Self: Blueprint, + for<'a> StructuredStorage<&'a S>: StorageInspect, { - fn root(&self, key: &Metadata::Key) -> StorageResult { + fn root(storage: &S, key: &Metadata::Key) -> StorageResult { use crate::StorageAsRef; + let storage = StructuredStorage::new(storage); let metadata: Option> = - self.storage_as_ref::().get(key)?; + storage.storage_as_ref::().get(key)?; let root = metadata .map(|metadata| *metadata.root()) .unwrap_or_else(|| in_memory::MerkleTree::new().root()); @@ -472,3 +469,240 @@ where Ok(()) } } + +/// The macro that generates SMT storage tests for the table with [`crate::structured_storage::test::InMemoryStorage`]. +#[cfg(feature = "test-helpers")] +#[macro_export] +macro_rules! root_storage_tests { + ($table:ident, $metadata_table:ident, $current_key:expr, $foreign_key:expr, $generate_key:ident, $generate_value:ident) => { + paste::item! { + #[cfg(test)] + mod [< $table:snake _root_tests >] { + use super::*; + use $crate::{ + structured_storage::{ + test::InMemoryStorage, + StructuredStorage, + }, + StorageAsMut, + }; + use $crate::rand::{ + rngs::StdRng, + SeedableRng, + }; + + #[test] + fn root() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let rng = &mut StdRng::seed_from_u64(1234); + let key = $generate_key(&$current_key, rng); + let value = $generate_value(rng); + structured_storage.storage_as_mut::<$table>().insert(&key, &value) + .unwrap(); + + let root = structured_storage.storage_as_mut::<$table>().root(&$current_key); + assert!(root.is_ok()) + } + + #[test] + fn root_returns_empty_root_for_empty_metadata() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let empty_root = fuel_core_types::fuel_merkle::sparse::in_memory::MerkleTree::new().root(); + let root = structured_storage + .storage_as_mut::<$table>() + .root(&$current_key) + .unwrap(); + assert_eq!(root, empty_root) + } + + #[test] + fn put_updates_the_state_merkle_root_for_the_given_metadata() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let rng = &mut StdRng::seed_from_u64(1234); + let key = $generate_key(&$current_key, rng); + let state = $generate_value(rng); + + // Write the first contract state + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &state) + .unwrap(); + + // Read the first Merkle root + let root_1 = structured_storage + .storage_as_mut::<$table>() + .root(&$current_key) + .unwrap(); + + // Write the second contract state + let key = $generate_key(&$current_key, rng); + let state = $generate_value(rng); + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &state) + .unwrap(); + + // Read the second Merkle root + let root_2 = structured_storage + .storage_as_mut::<$table>() + .root(&$current_key) + .unwrap(); + + assert_ne!(root_1, root_2); + } + + #[test] + fn remove_updates_the_state_merkle_root_for_the_given_metadata() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let rng = &mut StdRng::seed_from_u64(1234); + + // Write the first contract state + let first_key = $generate_key(&$current_key, rng); + let first_state = $generate_value(rng); + structured_storage + .storage_as_mut::<$table>() + .insert(&first_key, &first_state) + .unwrap(); + let root_0 = structured_storage + .storage_as_mut::<$table>() + .root(&$current_key) + .unwrap(); + + // Write the second contract state + let second_key = $generate_key(&$current_key, rng); + let second_state = $generate_value(rng); + structured_storage + .storage_as_mut::<$table>() + .insert(&second_key, &second_state) + .unwrap(); + + // Read the first Merkle root + let root_1 = structured_storage + .storage_as_mut::<$table>() + .root(&$current_key) + .unwrap(); + + // Remove the second contract state + structured_storage.storage_as_mut::<$table>().remove(&second_key).unwrap(); + + // Read the second Merkle root + let root_2 = structured_storage + .storage_as_mut::<$table>() + .root(&$current_key) + .unwrap(); + + assert_ne!(root_1, root_2); + assert_eq!(root_0, root_2); + } + + #[test] + fn updating_foreign_metadata_does_not_affect_the_given_metadata_insertion() { + let given_primary_key = $current_key; + let foreign_primary_key = $foreign_key; + + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let rng = &mut StdRng::seed_from_u64(1234); + + let state_value = $generate_value(rng); + + // Given + let given_key = $generate_key(&given_primary_key, rng); + let foreign_key = $generate_key(&foreign_primary_key, rng); + structured_storage + .storage_as_mut::<$table>() + .insert(&given_key, &state_value) + .unwrap(); + + // When + structured_storage + .storage_as_mut::<$table>() + .insert(&foreign_key, &state_value) + .unwrap(); + structured_storage + .storage_as_mut::<$table>() + .remove(&foreign_key) + .unwrap(); + + // Then + let result = structured_storage + .storage_as_mut::<$table>() + .insert(&given_key, &state_value) + .unwrap(); + + assert!(result.is_some()); + } + + #[test] + fn put_creates_merkle_metadata_when_empty() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let rng = &mut StdRng::seed_from_u64(1234); + + // Given + let key = $generate_key(&$current_key, rng); + let state = $generate_value(rng); + + // Write a contract state + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &state) + .unwrap(); + + // Read the Merkle metadata + let metadata = structured_storage + .storage_as_mut::<$metadata_table>() + .get(&$current_key) + .unwrap(); + + assert!(metadata.is_some()); + } + + #[test] + fn remove_deletes_merkle_metadata_when_empty() { + let mut storage = InMemoryStorage::default(); + let mut structured_storage = StructuredStorage::new(&mut storage); + + let rng = &mut StdRng::seed_from_u64(1234); + + // Given + let key = $generate_key(&$current_key, rng); + let state = $generate_value(rng); + + // Write a contract state + structured_storage + .storage_as_mut::<$table>() + .insert(&key, &state) + .unwrap(); + + // Read the Merkle metadata + structured_storage + .storage_as_mut::<$metadata_table>() + .get(&$current_key) + .unwrap() + .expect("Expected Merkle metadata to be present"); + + // Remove the contract asset + structured_storage.storage_as_mut::<$table>().remove(&key).unwrap(); + + // Read the Merkle metadata + let metadata = structured_storage + .storage_as_mut::<$metadata_table>() + .get(&$current_key) + .unwrap(); + + assert!(metadata.is_none()); + } + }} + }; +} diff --git a/crates/storage/src/column.rs b/crates/storage/src/column.rs index 6aa5d1ddab..130c36d222 100644 --- a/crates/storage/src/column.rs +++ b/crates/storage/src/column.rs @@ -62,10 +62,8 @@ pub enum Column { // Below are the tables used for p2p, block production, starting the node. /// The column id of metadata about the blockchain Metadata = 17, - /// See `FuelBlockSecondaryKeyBlockHeights` - FuelBlockSecondaryKeyBlockHeights = 18, /// See [`SealedBlockConsensus`](crate::tables::SealedBlockConsensus) - FuelBlockConsensus = 19, + FuelBlockConsensus = 18, } impl Column { diff --git a/crates/storage/src/structured_storage.rs b/crates/storage/src/structured_storage.rs index 4ca74ac6b0..615bff7ae2 100644 --- a/crates/storage/src/structured_storage.rs +++ b/crates/storage/src/structured_storage.rs @@ -5,6 +5,7 @@ use crate::{ blueprint::{ Blueprint, SupportsBatching, + SupportsMerkle, }, kv_store::{ BatchOperations, @@ -13,6 +14,8 @@ use crate::{ }, Error as StorageError, Mappable, + MerkleRoot, + MerkleRootStorage, StorageBatchMutate, StorageInspect, StorageMutate, @@ -159,6 +162,17 @@ where } } +impl MerkleRootStorage for StructuredStorage +where + S: KeyValueStore, + M: Mappable + TableWithBlueprint, + M::Blueprint: SupportsMerkle, +{ + fn root(&self, key: &Key) -> Result { + ::Blueprint::root(&self.storage, key) + } +} + /// The module that provides helper macros for testing the structured storage. #[cfg(feature = "test-helpers")] pub mod test { @@ -231,450 +245,4 @@ pub mod test { } impl BatchOperations for InMemoryStorage where Column: StorageColumn {} - - /// The macro that generates basic storage tests for the table with [`InMemoryStorage`]. - #[macro_export] - macro_rules! basic_storage_tests { - ($table:ident, $key:expr, $value_insert:expr, $value_return:expr, $random_key:expr) => { - $crate::paste::item! { - #[cfg(test)] - #[allow(unused_imports)] - mod [< $table:snake _basic_tests >] { - use super::*; - use $crate::{ - structured_storage::{ - test::InMemoryStorage, - StructuredStorage, - TableWithBlueprint, - }, - StorageAsMut, - }; - use $crate::StorageInspect; - use $crate::StorageMutate; - use $crate::rand; - - #[allow(dead_code)] - fn random(rng: &mut R) -> T - where - rand::distributions::Standard: rand::distributions::Distribution, - R: rand::Rng, - { - use rand::Rng; - rng.gen() - } - - #[test] - fn get() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - let key = $key; - - structured_storage - .storage_as_mut::<$table>() - .insert(&key, &$value_insert) - .unwrap(); - - assert_eq!( - structured_storage - .storage_as_mut::<$table>() - .get(&key) - .expect("Should get without errors") - .expect("Should not be empty") - .into_owned(), - $value_return - ); - } - - #[test] - fn insert() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - let key = $key; - - structured_storage - .storage_as_mut::<$table>() - .insert(&key, &$value_insert) - .unwrap(); - - let returned = structured_storage - .storage_as_mut::<$table>() - .get(&key) - .unwrap() - .unwrap() - .into_owned(); - assert_eq!(returned, $value_return); - } - - #[test] - fn remove() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - let key = $key; - - structured_storage - .storage_as_mut::<$table>() - .insert(&key, &$value_insert) - .unwrap(); - - structured_storage.storage_as_mut::<$table>().remove(&key).unwrap(); - - assert!(!structured_storage - .storage_as_mut::<$table>() - .contains_key(&key) - .unwrap()); - } - - #[test] - fn exists() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - let key = $key; - - // Given - assert!(!structured_storage - .storage_as_mut::<$table>() - .contains_key(&key) - .unwrap()); - - // When - structured_storage - .storage_as_mut::<$table>() - .insert(&key, &$value_insert) - .unwrap(); - - // Then - assert!(structured_storage - .storage_as_mut::<$table>() - .contains_key(&key) - .unwrap()); - } - - #[test] - fn exists_false_after_removing() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - let key = $key; - - // Given - structured_storage - .storage_as_mut::<$table>() - .insert(&key, &$value_insert) - .unwrap(); - - // When - structured_storage - .storage_as_mut::<$table>() - .remove(&key) - .unwrap(); - - // Then - assert!(!structured_storage - .storage_as_mut::<$table>() - .contains_key(&key) - .unwrap()); - } - - #[test] - fn batch_mutate_works() { - use $crate::rand::{ - Rng, - rngs::StdRng, - RngCore, - SeedableRng, - }; - - let empty_storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - - let mut init_storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut init_structured_storage = StructuredStorage::new(&mut init_storage); - - let mut rng = &mut StdRng::seed_from_u64(1234); - let gen = || Some($random_key(&mut rng)); - let data = core::iter::from_fn(gen).take(5_000).collect::>(); - let value = $value_insert; - - <_ as $crate::StorageBatchMutate<$table>>::init_storage( - &mut init_structured_storage, - &mut data.iter().map(|k| { - let value: &<$table as $crate::Mappable>::Value = &value; - (k, value) - }) - ).expect("Should initialize the storage successfully"); - - let mut insert_storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut insert_structured_storage = StructuredStorage::new(&mut insert_storage); - - <_ as $crate::StorageBatchMutate<$table>>::insert_batch( - &mut insert_structured_storage, - &mut data.iter().map(|k| { - let value: &<$table as $crate::Mappable>::Value = &value; - (k, value) - }) - ).expect("Should insert batch successfully"); - - assert_eq!(init_storage, insert_storage); - assert_ne!(init_storage, empty_storage); - assert_ne!(insert_storage, empty_storage); - - let mut remove_from_insert_structured_storage = StructuredStorage::new(&mut insert_storage); - <_ as $crate::StorageBatchMutate<$table>>::remove_batch( - &mut remove_from_insert_structured_storage, - &mut data.iter() - ).expect("Should remove all entries successfully from insert storage"); - assert_ne!(init_storage, insert_storage); - assert_eq!(insert_storage, empty_storage); - - let mut remove_from_init_structured_storage = StructuredStorage::new(&mut init_storage); - <_ as $crate::StorageBatchMutate<$table>>::remove_batch( - &mut remove_from_init_structured_storage, - &mut data.iter() - ).expect("Should remove all entries successfully from init storage"); - assert_eq!(init_storage, insert_storage); - assert_eq!(init_storage, empty_storage); - } - }} - }; - ($table:ident, $key:expr, $value_insert:expr, $value_return:expr) => { - $crate::basic_storage_tests!($table, $key, $value_insert, $value_return, random); - }; - ($table:ident, $key:expr, $value:expr) => { - $crate::basic_storage_tests!($table, $key, $value, $value); - }; - } - - /// The macro that generates SMT storage tests for the table with [`InMemoryStorage`]. - #[macro_export] - macro_rules! root_storage_tests { - ($table:ident, $metadata_table:ident, $current_key:expr, $foreign_key:expr, $generate_key:ident, $generate_value:ident) => { - paste::item! { - #[cfg(test)] - mod [< $table:snake _root_tests >] { - use super::*; - use $crate::{ - structured_storage::{ - test::InMemoryStorage, - StructuredStorage, - }, - StorageAsMut, - }; - use $crate::rand::{ - rngs::StdRng, - SeedableRng, - }; - - #[test] - fn root() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - - let rng = &mut StdRng::seed_from_u64(1234); - let key = $generate_key(&$current_key, rng); - let value = $generate_value(rng); - structured_storage.storage_as_mut::<$table>().insert(&key, &value) - .unwrap(); - - let root = structured_storage.storage_as_mut::<$table>().root(&$current_key); - assert!(root.is_ok()) - } - - #[test] - fn root_returns_empty_root_for_empty_metadata() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - - let empty_root = fuel_core_types::fuel_merkle::sparse::in_memory::MerkleTree::new().root(); - let root = structured_storage - .storage_as_mut::<$table>() - .root(&$current_key) - .unwrap(); - assert_eq!(root, empty_root) - } - - #[test] - fn put_updates_the_state_merkle_root_for_the_given_metadata() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - - let rng = &mut StdRng::seed_from_u64(1234); - let key = $generate_key(&$current_key, rng); - let state = $generate_value(rng); - - // Write the first contract state - structured_storage - .storage_as_mut::<$table>() - .insert(&key, &state) - .unwrap(); - - // Read the first Merkle root - let root_1 = structured_storage - .storage_as_mut::<$table>() - .root(&$current_key) - .unwrap(); - - // Write the second contract state - let key = $generate_key(&$current_key, rng); - let state = $generate_value(rng); - structured_storage - .storage_as_mut::<$table>() - .insert(&key, &state) - .unwrap(); - - // Read the second Merkle root - let root_2 = structured_storage - .storage_as_mut::<$table>() - .root(&$current_key) - .unwrap(); - - assert_ne!(root_1, root_2); - } - - #[test] - fn remove_updates_the_state_merkle_root_for_the_given_metadata() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - - let rng = &mut StdRng::seed_from_u64(1234); - - // Write the first contract state - let first_key = $generate_key(&$current_key, rng); - let first_state = $generate_value(rng); - structured_storage - .storage_as_mut::<$table>() - .insert(&first_key, &first_state) - .unwrap(); - let root_0 = structured_storage - .storage_as_mut::<$table>() - .root(&$current_key) - .unwrap(); - - // Write the second contract state - let second_key = $generate_key(&$current_key, rng); - let second_state = $generate_value(rng); - structured_storage - .storage_as_mut::<$table>() - .insert(&second_key, &second_state) - .unwrap(); - - // Read the first Merkle root - let root_1 = structured_storage - .storage_as_mut::<$table>() - .root(&$current_key) - .unwrap(); - - // Remove the second contract state - structured_storage.storage_as_mut::<$table>().remove(&second_key).unwrap(); - - // Read the second Merkle root - let root_2 = structured_storage - .storage_as_mut::<$table>() - .root(&$current_key) - .unwrap(); - - assert_ne!(root_1, root_2); - assert_eq!(root_0, root_2); - } - - #[test] - fn updating_foreign_metadata_does_not_affect_the_given_metadata_insertion() { - let given_primary_key = $current_key; - let foreign_primary_key = $foreign_key; - - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - - let rng = &mut StdRng::seed_from_u64(1234); - - let state_value = $generate_value(rng); - - // Given - let given_key = $generate_key(&given_primary_key, rng); - let foreign_key = $generate_key(&foreign_primary_key, rng); - structured_storage - .storage_as_mut::<$table>() - .insert(&given_key, &state_value) - .unwrap(); - - // When - structured_storage - .storage_as_mut::<$table>() - .insert(&foreign_key, &state_value) - .unwrap(); - structured_storage - .storage_as_mut::<$table>() - .remove(&foreign_key) - .unwrap(); - - // Then - let result = structured_storage - .storage_as_mut::<$table>() - .insert(&given_key, &state_value) - .unwrap(); - - assert!(result.is_some()); - } - - #[test] - fn put_creates_merkle_metadata_when_empty() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - - let rng = &mut StdRng::seed_from_u64(1234); - - // Given - let key = $generate_key(&$current_key, rng); - let state = $generate_value(rng); - - // Write a contract state - structured_storage - .storage_as_mut::<$table>() - .insert(&key, &state) - .unwrap(); - - // Read the Merkle metadata - let metadata = structured_storage - .storage_as_mut::<$metadata_table>() - .get(&$current_key) - .unwrap(); - - assert!(metadata.is_some()); - } - - #[test] - fn remove_deletes_merkle_metadata_when_empty() { - let mut storage = InMemoryStorage::<<$table as TableWithBlueprint>::Column>::default(); - let mut structured_storage = StructuredStorage::new(&mut storage); - - let rng = &mut StdRng::seed_from_u64(1234); - - // Given - let key = $generate_key(&$current_key, rng); - let state = $generate_value(rng); - - // Write a contract state - structured_storage - .storage_as_mut::<$table>() - .insert(&key, &state) - .unwrap(); - - // Read the Merkle metadata - structured_storage - .storage_as_mut::<$metadata_table>() - .get(&$current_key) - .unwrap() - .expect("Expected Merkle metadata to be present"); - - // Remove the contract asset - structured_storage.storage_as_mut::<$table>().remove(&key).unwrap(); - - // Read the Merkle metadata - let metadata = structured_storage - .storage_as_mut::<$metadata_table>() - .get(&$current_key) - .unwrap(); - - assert!(metadata.is_none()); - } - }} - }; - } } diff --git a/crates/storage/src/structured_storage/blocks.rs b/crates/storage/src/structured_storage/blocks.rs index d09259255b..a7edac6cf9 100644 --- a/crates/storage/src/structured_storage/blocks.rs +++ b/crates/storage/src/structured_storage/blocks.rs @@ -1,18 +1,45 @@ //! The module contains implementations and tests for the `FuelBlocks` table. use crate::{ - blueprint::plain::Plain, + blueprint::merklized::Merklized, codec::{ postcard::Postcard, primitive::Primitive, + Encode, }, column::Column, structured_storage::TableWithBlueprint, - tables::FuelBlocks, + tables::{ + merkle::{ + FuelBlockMerkleData, + FuelBlockMerkleMetadata, + }, + FuelBlocks, + }, }; +use fuel_core_types::blockchain::block::CompressedBlock; +use fuel_vm_private::fuel_tx::Bytes32; + +/// The encoder of `CompressedBlock` for the `FuelBlocks` table. +pub struct BlockEncoder; + +impl Encode for BlockEncoder { + type Encoder<'a> = [u8; Bytes32::LEN]; + + fn encode(value: &CompressedBlock) -> Self::Encoder<'_> { + let bytes: Bytes32 = value.id().into(); + bytes.into() + } +} impl TableWithBlueprint for FuelBlocks { - type Blueprint = Plain, Postcard>; + type Blueprint = Merklized< + Primitive<4>, + Postcard, + FuelBlockMerkleMetadata, + FuelBlockMerkleData, + BlockEncoder, + >; type Column = Column; fn column() -> Column { @@ -21,8 +48,106 @@ impl TableWithBlueprint for FuelBlocks { } #[cfg(test)] -crate::basic_storage_tests!( - FuelBlocks, - ::Key::default(), - ::Value::default() -); +mod tests { + use crate::{ + structured_storage::{ + test::InMemoryStorage, + StructuredStorage, + TableWithBlueprint, + }, + tables::FuelBlocks, + StorageAsMut, + StorageMutate, + }; + use fuel_core_types::{ + blockchain::{ + block::PartialFuelBlock, + header::{ + ConsensusHeader, + PartialBlockHeader, + }, + primitives::Empty, + }, + fuel_types::ChainId, + }; + use fuel_vm_private::crypto::ephemeral_merkle_root; + + crate::basic_merklelized_storage_tests!( + FuelBlocks, + ::Key::default(), + ::Value::default() + ); + + #[test_case::test_case(&[0]; "initial block with height 0")] + #[test_case::test_case(&[1337]; "initial block with arbitrary height")] + #[test_case::test_case(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; "ten sequential blocks starting from height 0")] + #[test_case::test_case(&[100, 101, 102, 103, 104, 105]; "five sequential blocks starting from height 100")] + #[test_case::test_case(&[0, 2, 5, 7, 11]; "five non-sequential blocks starting from height 0")] + #[test_case::test_case(&[100, 102, 105, 107, 111]; "five non-sequential blocks starting from height 100")] + fn can_get_merkle_root_of_inserted_blocks(heights: &[u32]) { + let mut storage = + InMemoryStorage::<::Column>::default(); + let mut database = StructuredStorage::new(&mut storage); + let blocks = heights + .iter() + .copied() + .map(|height| { + let header = PartialBlockHeader { + application: Default::default(), + consensus: ConsensusHeader:: { + height: height.into(), + ..Default::default() + }, + }; + let block = PartialFuelBlock::new(header, vec![]); + block.generate(&[]) + }) + .collect::>(); + + // Insert the blocks. Each insertion creates a new version of Block + // metadata, including a new root. + for block in &blocks { + StorageMutate::::insert( + &mut database, + block.header().height(), + &block.compress(&ChainId::default()), + ) + .unwrap(); + } + + // Check each version + for version in 1..=blocks.len() { + // Generate the expected root for the version + let blocks = blocks.iter().take(version).collect::>(); + let block_ids = blocks.iter().map(|block| block.id()); + let expected_root = ephemeral_merkle_root(block_ids); + + // Check that root for the version is present + let last_block = blocks.last().unwrap(); + let actual_root = database + .storage::() + .root(last_block.header().height()) + .expect("root to exist") + .into(); + + assert_eq!(expected_root, actual_root); + } + } + + #[test] + fn get_merkle_root_with_no_blocks_returns_not_found_error() { + use crate::StorageAsRef; + + let storage = + InMemoryStorage::<::Column>::default(); + let database = StructuredStorage::new(&storage); + + // check that root is not present + let err = database + .storage::() + .root(&0u32.into()) + .expect_err("expected error getting invalid Block Merkle root"); + + assert!(matches!(err, crate::Error::NotFound(_, _))); + } +} diff --git a/crates/storage/src/structured_storage/merkle_data.rs b/crates/storage/src/structured_storage/merkle_data.rs index 23bb0865be..a80a6c56a9 100644 --- a/crates/storage/src/structured_storage/merkle_data.rs +++ b/crates/storage/src/structured_storage/merkle_data.rs @@ -43,10 +43,9 @@ macro_rules! merkle_table { } type U64Codec = Primitive<8>; -type BlockHeightCodec = Primitive<4>; merkle_table!(FuelBlockMerkleData, U64Codec); -merkle_table!(FuelBlockMerkleMetadata, BlockHeightCodec); +merkle_table!(FuelBlockMerkleMetadata, Postcard); merkle_table!(ContractsAssetsMerkleData); merkle_table!(ContractsAssetsMerkleMetadata); merkle_table!(ContractsStateMerkleData); diff --git a/crates/storage/src/tables.rs b/crates/storage/src/tables.rs index ce8d98233e..e9a73bedc1 100644 --- a/crates/storage/src/tables.rs +++ b/crates/storage/src/tables.rs @@ -132,6 +132,31 @@ pub mod merkle { fuel_types::BlockHeight, }; + /// The key for the corresponding `DenseMerkleMetadata` type. + /// The `Latest` variant is used to have the access to the latest dense Merkle tree. + #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] + pub enum DenseMetadataKey { + /// The primary key of the `DenseMerkleMetadata`. + Primary(PrimaryKey), + #[default] + /// The latest `DenseMerkleMetadata` of the table. + Latest, + } + + #[cfg(feature = "test-helpers")] + impl rand::distributions::Distribution> + for rand::distributions::Standard + where + rand::distributions::Standard: rand::distributions::Distribution, + { + fn sample( + &self, + rng: &mut R, + ) -> DenseMetadataKey { + DenseMetadataKey::Primary(rng.gen()) + } + } + /// Metadata for dense Merkle trees #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub enum DenseMerkleMetadata { @@ -260,7 +285,7 @@ pub mod merkle { pub struct FuelBlockMerkleMetadata; impl Mappable for FuelBlockMerkleMetadata { - type Key = BlockHeight; + type Key = DenseMetadataKey; type OwnedKey = Self::Key; type Value = DenseMerkleMetadata; type OwnedValue = Self::Value; diff --git a/crates/types/src/blockchain/header.rs b/crates/types/src/blockchain/header.rs index 84c9f148dd..faac4a0ec6 100644 --- a/crates/types/src/blockchain/header.rs +++ b/crates/types/src/blockchain/header.rs @@ -81,7 +81,10 @@ impl BlockHeader { BlockHeader::V1(v1) => &mut v1.consensus, } } +} +#[cfg(feature = "test-helpers")] +impl BlockHeader { /// Set the entire consensus header pub fn set_consensus_header( &mut self, @@ -112,26 +115,31 @@ impl BlockHeader { /// Set the block height for the header pub fn set_block_height(&mut self, height: BlockHeight) { self.consensus_mut().height = height; + self.recalculate_metadata(); } /// Set the previous root for the header pub fn set_previous_root(&mut self, root: Bytes32) { self.consensus_mut().prev_root = root; + self.recalculate_metadata(); } /// Set the time for the header pub fn set_time(&mut self, time: Tai64) { self.consensus_mut().time = time; + self.recalculate_metadata(); } /// Set the transaction root for the header pub fn set_transaction_root(&mut self, root: Bytes32) { self.application_mut().generated.transactions_root = root; + self.recalculate_metadata(); } /// Set the DA height for the header pub fn set_da_height(&mut self, da_height: DaBlockHeight) { self.application_mut().da_height = da_height; + self.recalculate_metadata(); } } diff --git a/crates/types/src/services/txpool.rs b/crates/types/src/services/txpool.rs index 39d6bc248b..cc57d3c9f7 100644 --- a/crates/types/src/services/txpool.rs +++ b/crates/types/src/services/txpool.rs @@ -1,10 +1,7 @@ //! Types for interoperability with the txpool service use crate::{ - blockchain::{ - block::Block, - primitives::BlockId, - }, + blockchain::block::Block, fuel_asm::Word, fuel_tx::{ field::{ @@ -33,9 +30,12 @@ use crate::{ }, services::executor::TransactionExecutionResult, }; -use fuel_vm_private::checked_transaction::{ - CheckError, - CheckedTransaction, +use fuel_vm_private::{ + checked_transaction::{ + CheckError, + CheckedTransaction, + }, + fuel_types::BlockHeight, }; use std::{ sync::Arc, @@ -183,7 +183,7 @@ pub enum TransactionStatus { /// Transaction was successfully included in a block Success { /// Included in this block - block_id: BlockId, + block_height: BlockHeight, /// Time when the block was generated time: Tai64, /// Result of executing the transaction for scripts @@ -199,7 +199,7 @@ pub enum TransactionStatus { /// Transaction was included in a block, but the exection was reverted Failed { /// Included in this block - block_id: BlockId, + block_height: BlockHeight, /// Time when the block was generated time: Tai64, /// Result of executing the transaction for scripts @@ -215,11 +215,11 @@ pub fn from_executor_to_status( result: TransactionExecutionResult, ) -> TransactionStatus { let time = block.header().time(); - let block_id = block.id(); + let block_height = *block.header().height(); match result { TransactionExecutionResult::Success { result, receipts } => { TransactionStatus::Success { - block_id, + block_height, time, result, receipts, @@ -227,7 +227,7 @@ pub fn from_executor_to_status( } TransactionExecutionResult::Failed { result, receipts } => { TransactionStatus::Failed { - block_id, + block_height, time, result, receipts, diff --git a/tests/tests/blocks.rs b/tests/tests/blocks.rs index 6300976ace..b92390cf46 100644 --- a/tests/tests/blocks.rs +++ b/tests/tests/blocks.rs @@ -45,8 +45,9 @@ use std::{ #[tokio::test] async fn block() { // setup test data in the node - let block = CompressedBlock::default(); - let height = block.header().height(); + let mut block = CompressedBlock::default(); + let height = 1.into(); + block.header_mut().set_block_height(height); let mut db = Database::default(); // setup server & client let srv = FuelService::from_database(db.clone(), Config::local_node()) @@ -54,13 +55,13 @@ async fn block() { .unwrap(); let client = FuelClient::from(srv.bound_address); - db.storage::().insert(height, &block).unwrap(); + db.storage::().insert(&height, &block).unwrap(); db.storage::() - .insert(height, &Consensus::PoA(Default::default())) + .insert(&height, &Consensus::PoA(Default::default())) .unwrap(); // run test - let block = client.block_by_height(**height).await.unwrap(); + let block = client.block_by_height(height).await.unwrap(); assert!(block.is_some()); } @@ -76,7 +77,7 @@ async fn get_genesis_block() { let tx = Transaction::default_test_tx(); client.submit_and_await_commit(&tx).await.unwrap(); - let block = client.block_by_height(13).await.unwrap().unwrap(); + let block = client.block_by_height(13.into()).await.unwrap().unwrap(); assert_eq!(block.header.height, 13); assert!(matches!( block.consensus, @@ -102,11 +103,10 @@ async fn produce_block() { .await .unwrap(); - if let TransactionStatus::Success { block_id, .. } = + if let TransactionStatus::Success { block_height, .. } = transaction_response.unwrap().status { - let block_id = block_id.parse().unwrap(); - let block = client.block(&block_id).await.unwrap().unwrap(); + let block = client.block_by_height(block_height).await.unwrap().unwrap(); let actual_pub_key = block.block_producer().unwrap(); let block_height: u32 = block.header.height; let expected_pub_key = config @@ -138,7 +138,7 @@ async fn produce_block_manually() { let new_height = client.produce_blocks(1, None).await.unwrap(); assert_eq!(1, *new_height); - let block = client.block_by_height(1).await.unwrap().unwrap(); + let block = client.block_by_height(1.into()).await.unwrap().unwrap(); assert_eq!(block.header.height, 1); let actual_pub_key = block.block_producer().unwrap(); let expected_pub_key = config diff --git a/tests/tests/poa.rs b/tests/tests/poa.rs index b48b2799ae..9649a3a04d 100644 --- a/tests/tests/poa.rs +++ b/tests/tests/poa.rs @@ -1,6 +1,5 @@ use fuel_core::{ - database::Database, - fuel_core_graphql_api::ports::DatabaseBlocks, + combined_database::CombinedDatabase, service::{ Config, FuelService, @@ -11,10 +10,7 @@ use fuel_core_client::client::{ FuelClient, }; use fuel_core_types::{ - blockchain::{ - consensus::Consensus, - primitives::BlockId, - }, + blockchain::consensus::Consensus, fuel_crypto::SecretKey, fuel_tx::Transaction, secrecy::Secret, @@ -23,7 +19,6 @@ use rand::{ rngs::StdRng, SeedableRng, }; -use std::str::FromStr; #[tokio::test] async fn can_get_sealed_block_from_poa_produced_block() { @@ -31,10 +26,10 @@ async fn can_get_sealed_block_from_poa_produced_block() { let poa_secret = SecretKey::random(&mut rng); let poa_public = poa_secret.public_key(); - let db = Database::default(); + let db = CombinedDatabase::default(); let mut config = Config::local_node(); config.consensus_key = Some(Secret::new(poa_secret.into())); - let srv = FuelService::from_database(db.clone(), config) + let srv = FuelService::from_combined_database(db.clone(), config) .await .unwrap(); let client = FuelClient::from(srv.bound_address); @@ -44,24 +39,23 @@ async fn can_get_sealed_block_from_poa_produced_block() { .await .unwrap(); - let block_id = match status { - TransactionStatus::Success { block_id, .. } => block_id, + let block_height = match status { + TransactionStatus::Success { block_height, .. } => block_height, _ => { panic!("unexpected result") } }; - let block_id = BlockId::from_str(&block_id).unwrap(); - - let block_height = db.block_height(&block_id).unwrap(); // check sealed block header is correct let sealed_block_header = db + .on_chain() .get_sealed_block_header(&block_height) .unwrap() .expect("expected sealed header to be available"); // verify signature let block_id = sealed_block_header.entity.id(); + let block_height = sealed_block_header.entity.height(); let signature = match sealed_block_header.consensus { Consensus::PoA(poa) => poa.signature, _ => panic!("Not expected consensus"), @@ -70,10 +64,10 @@ async fn can_get_sealed_block_from_poa_produced_block() { .verify(&poa_public, &block_id.into_message()) .expect("failed to verify signature"); - let block_height = db.block_height(&block_id).unwrap(); // check sealed block is correct let sealed_block = db - .get_sealed_block_by_height(&block_height) + .on_chain() + .get_sealed_block_by_height(block_height) .unwrap() .expect("expected sealed header to be available"); diff --git a/tests/tests/tx/txpool.rs b/tests/tests/tx/txpool.rs index ede0a23e27..030b1ef225 100644 --- a/tests/tests/tx/txpool.rs +++ b/tests/tests/tx/txpool.rs @@ -69,7 +69,7 @@ async fn txs_max_script_gas_limit() { tokio::time::sleep(Duration::from_secs(1)).await; - let block = client.block_by_height(1).await.unwrap().unwrap(); + let block = client.block_by_height(1.into()).await.unwrap().unwrap(); assert_eq!( block.transactions.len(), transactions.len() + 1 // coinbase diff --git a/tests/tests/tx/utxo_validation.rs b/tests/tests/tx/utxo_validation.rs index 4b8eb8163e..fedeaca149 100644 --- a/tests/tests/tx/utxo_validation.rs +++ b/tests/tests/tx/utxo_validation.rs @@ -89,9 +89,10 @@ async fn submit_utxo_verified_tx_with_min_gas_price() { .ok() .unwrap(); - if let TransactionStatus::Success { block_id, .. } = transaction_result.clone() { - let block_id = block_id.parse().unwrap(); - let block_exists = client.block(&block_id).await.unwrap(); + if let TransactionStatus::Success { block_height, .. } = + transaction_result.clone() + { + let block_exists = client.block_by_height(block_height).await.unwrap(); assert!(block_exists.is_some()); }