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());
}