Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature/jmt 1/4] Jmt: Integration of Storage traits with crate jmt #891

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

acerone85
Copy link
Contributor

@acerone85 acerone85 commented Jan 16, 2025

In order to integrate Jellyfish Merkle Trees from the jmt crate, we must implement three traits from that crate:

  • TreeReader - used to read and cache updates to the tree in memory
  • TreeWriter - used to flush updates from the tree cache into the underlying storage backend
  • HasPreimage - used for proof generation and verification.

This PR connects the storage traits on the fuel-vm side with jmt crate as follows:

  • It defines a data structure parametric in several types that require an implementation of Mappable each,
  • It implements the three storage traits above for mappable

Tests are provided in the last PR of the feature branch.

[Short description of the changes.]

Checklist

  • Breaking changes are clearly marked as such in the PR description and changelog
  • New behavior is reflected in tests
  • If performance characteristic of an instruction change, update gas costs as well or make a follow-up PR for that
  • The specification matches the implemented behavior (link update PR if changes are needed)

Before requesting review

  • I have reviewed the code myself
  • I have created follow-up issues caused by this PR and linked them here

After merging, notify other teams

[Add or remove entries as needed]

let storage = self.storage_read();
let Some(value) =
<StorageType as StorageInspect<ValueTableType>>::get(&*storage, &key_hash)
.map_err(|_e| anyhow::anyhow!("Version Storage Error"))?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps

Suggested change
.map_err(|_e| anyhow::anyhow!("Version Storage Error"))?
.map_err(|e| anyhow::anyhow!("could not get value: {}", e.err()))?

let Some(value) =
<StorageType as StorageInspect<ValueTableType>>::get(&*storage, &key_hash)
.map_err(|_e| anyhow::anyhow!("Version Storage Error"))?
.filter(|v| v.0 <= max_version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any way we can optimize this?

Copy link
Contributor

@netrome netrome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial questions

@@ -11,13 +11,16 @@ repository = { workspace = true }
description = "Fuel Merkle tree libraries."

[dependencies]
anyhow = { version = "1.0", default-features = false }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While benchmarking I think this is fine, but I'd advocate for using dedicated error types before merging to master (or as a follow-up)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the jmt crate that exposes traits whose signature return a anyhow::Result value.

See https://github.com/FuelLabs/fuel-vm/blob/68bbb4c52d4a749832cb07c11a9f467b54ec6b1f/fuel-merkle/src/jellyfish/jmt_integration.rs#L47 for example

Otherwise I'd be very happy to remove it :)

StorageType,
StorageError,
> {
inner: Arc<RwLock<StorageType>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels weird to dictate the lock type over the Storage. Shouldn't we just use traits to dictate the storage behavior, and let any potential locking mechanism be an implementation detail of the particular storage implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. In practice I don't think we'll need a Lock, and we can use RefCell instead.
On the other hand, it could be wise to have a trait for the "locking/borrowing" functions and parameterise over it. WIll amend

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a cell even? Can't this just hold the StorageType directly the same way we do in the SMT struct:

#[derive(Debug)]
pub struct MerkleTree<TableType, StorageType> {
    root_node: Node,
    storage: StorageType,
    phantom_table: PhantomData<TableType>,
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should, but again it's because of a constraint on the Jmt trait signatures.
Specifically TreeWriter::write_node_batch(&self, _: &NodeBatch) takes a non-mutable reference, but its implementation requires to call StorageInspect::inspect, which instead requires a mutable reference.

If we decide to fork jmt at some point, we can amend the trait signatures to avoid having to use cells or locks

{
fn write_node_batch(&self, node_batch: &JmtNodeBatch) -> anyhow::Result<()> {
for (key, node) in node_batch.nodes() {
// TODO: Do we really need locks here?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah commented above. I don't think we should have to care about locks in this context.

};

#[derive(Debug, Clone)]
pub struct JellyfishMerkleTreeStorage<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it will be more obvious when I look at the following PRs, but why do we need to declare a specific storage type for the JMTs? I don't find anything directly equivalent in our SMT implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe JellyFishMerkleTreeStorage is not the best name, but the Sparse merkle tree implementation follows the same pattern:

pub struct MerkleTree<TableType, StorageType> {
    root_node: Node,
    storage: StorageType,
    phantom_table: PhantomData<TableType>,
}

and at a later point StorageType is required to implement StorageInspect<TableType>, with TableType: Mappable<...> see

impl<TableType, StorageType, StorageError> MerkleTree<TableType, StorageType>
where
TableType: Mappable<Key = Bytes32, Value = Primitive, OwnedValue = Primitive>,
StorageType: StorageInspect<TableType, Error = StorageError>,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right yeah, perhaps we should just omit Storage from the name then?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants