diff --git a/CHANGELOG.md b/CHANGELOG.md index 48706e23a15..a875ec88c41 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 00f95fee3c9..f8a0b334403 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 f2b5ea5474c..a760d45c8e1 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 03bb0b7c060..b0df84c7c98 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 54bee132431..5c1ae9f3bda 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 815f7e8353a..3fd81a8cc2d 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 904701e375f..1832b7afa63 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 6b7d4597774..62b69418a2f 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 05ad55b65ce..5f5e7b0143b 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 803a185d809..dc4d3f04209 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 5cbab01afc7..f0db1e76aa9 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 33b81787b6b..bc1f63905d0 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 a7189e8c5a1..9927af08516 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 a801aa70bda..9be0a638932 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 7477eadb7fc..5ab0e09eadf 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 62dc5124546..720431c3966 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 89b7ec1427c..d06df8bfedf 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 00000000000..f2652d31294 --- /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 247d16f1afc..0fcd0dc6278 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 2d7edbd0b3f..0367c79c1e1 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 93b96e47380..8820a1521e0 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 b7a8800f98e..2c40700fdf4 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 55f7e55fe71..191f9395a15 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 5c341a4c47c..ea9e4da7c97 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 cc36502c980..43cdd8379f7 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 871e19ed6bb..4279361177e 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 a42afa4a724..418d2f755b9 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 7430e1f2ee3..19ab02bea69 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 c7f7587b074..3388c116f65 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 c0efcb4ba62..d1038e5cd33 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 58ce69ad7d5..c02a3e05796 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 b40f2488fdd..1771c26a70b 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 3db1ee73e80..1afc45cf66c 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 00000000000..5583eced49e --- /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 22d02a771ed..51a5dcc7d31 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 9e0deb63105..9bcaaeaf124 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 6aa5d1ddab7..130c36d2229 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 4ca74ac6b0a..615bff7ae24 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 d09259255b3..a7edac6cf92 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 23bb0865be8..a80a6c56a94 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 ce8d98233e0..e9a73bedc13 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 84c9f148ddf..faac4a0ec61 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 39d6bc248b2..cc57d3c9f79 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 6300976ace5..b92390cf468 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 b48b2799aed..9649a3a04d6 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 ede0a23e271..030b1ef2258 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 4b8eb8163e1..fedeaca149c 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()); }