diff --git a/Cargo.lock b/Cargo.lock index 72fba08cd..46022266a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -991,6 +991,7 @@ dependencies = [ "calimero-storage", "eyre", "fragile", + "hex", "ouroboros", "owo-colors", "rand 0.8.5", @@ -4440,6 +4441,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" +[[package]] +name = "merkle-crdt" +version = "0.1.0" +dependencies = [ + "calimero-sdk", + "calimero-storage", + "calimero-storage-macros", +] + [[package]] name = "meroctl" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 154131746..5574c7db9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ "./apps/kv-store", "./apps/only-peers", "./apps/gen-ext", + "./apps/merkle-crdt", "./contracts/context-config", "./contracts/registry", diff --git a/apps/merkle-crdt/Cargo.toml b/apps/merkle-crdt/Cargo.toml new file mode 100644 index 000000000..1350f9469 --- /dev/null +++ b/apps/merkle-crdt/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "merkle-crdt" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +calimero-sdk = { path = "../../crates/sdk" } +calimero-storage = { path = "../../crates/storage" } +calimero-storage-macros = { path = "../../crates/storage-macros" } diff --git a/apps/merkle-crdt/build.sh b/apps/merkle-crdt/build.sh new file mode 100755 index 000000000..9c9d2b284 --- /dev/null +++ b/apps/merkle-crdt/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-../../target}" + +rustup target add wasm32-unknown-unknown + +cargo build --target wasm32-unknown-unknown --profile app-release + +mkdir -p res + +cp $TARGET/wasm32-unknown-unknown/app-release/merkle_crdt.wasm ./res/ + +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/merkle_crdt.wasm -o ./res/merkle_crdt.wasm +fi diff --git a/apps/merkle-crdt/src/lib.rs b/apps/merkle-crdt/src/lib.rs new file mode 100644 index 000000000..c35c0bd16 --- /dev/null +++ b/apps/merkle-crdt/src/lib.rs @@ -0,0 +1,161 @@ +use calimero_sdk::app; +use calimero_sdk::borsh::to_vec; +use calimero_storage::address::{Id, Path}; +use calimero_storage::entities::{Data, Element}; +use calimero_storage::integration::Comparison; +use calimero_storage::interface::StorageError::ActionNotAllowed; +use calimero_storage::interface::{Action, Interface, StorageError}; +use calimero_storage_macros::{AtomicUnit, Collection}; + +#[app::state(emits = for<'a> Event<'a>)] +#[derive(AtomicUnit, Clone, Debug, PartialEq, PartialOrd)] +#[root] +#[type_id(11)] +pub struct Library { + #[collection] + books: Books, + #[storage] + storage: Element, +} + +#[derive(Collection, Clone, Debug, Eq, PartialEq, PartialOrd)] +#[children(Book)] +pub struct Books; + +#[derive(AtomicUnit, Clone, Debug, PartialEq, PartialOrd)] +#[type_id(12)] +pub struct Book { + authors: Vec, + isbn: String, + publisher: String, + year: u16, + rating: f32, + #[collection] + reviews: Reviews, + #[collection] + pages: Pages, + #[storage] + storage: Element, +} + +#[derive(Collection, Clone, Debug, Eq, PartialEq, PartialOrd)] +#[children(Page)] +pub struct Pages; + +#[derive(AtomicUnit, Clone, Debug, Eq, PartialEq, PartialOrd)] +#[type_id(13)] +pub struct Page { + content: String, + number: u16, + title: String, + #[collection] + paragraphs: Paragraphs, + #[storage] + storage: Element, +} + +#[derive(Collection, Clone, Debug, Eq, PartialEq, PartialOrd)] +#[children(Paragraph)] +pub struct Paragraphs; + +#[derive(AtomicUnit, Clone, Debug, Eq, PartialEq, PartialOrd)] +#[type_id(14)] +pub struct Paragraph { + content: String, + #[storage] + storage: Element, +} + +#[derive(Collection, Clone, Debug, Eq, PartialEq, PartialOrd)] +#[children(Review)] +pub struct Reviews; + +#[derive(AtomicUnit, Clone, Debug, Eq, PartialEq, PartialOrd)] +#[type_id(15)] +pub struct Review { + author: String, + content: String, + rating: u8, + #[storage] + storage: Element, +} + +#[app::event] +pub enum Event<'a> { + Inserted { key: &'a str, value: &'a str }, + Updated { key: &'a str, value: &'a str }, + Removed { key: &'a str }, + Cleared, +} + +#[app::logic] +impl Library { + #[app::init] + pub fn init() -> Library { + Library { + books: Books {}, + storage: Element::new(&Path::new("::library").unwrap()), + } + } + + pub fn apply_action(&self, action: Action) -> Result, StorageError> { + match action { + Action::Add { type_id, .. } | Action::Update { type_id, .. } => { + // TODO: This is long-hand - it will be put inside an enum and generated + // TODO: with a macro + match type_id { + 11 => Interface::apply_action::(action), + 12 => Interface::apply_action::(action), + 13 => Interface::apply_action::(action), + 14 => Interface::apply_action::(action), + 15 => Interface::apply_action::(action), + _ => Err(StorageError::UnknownType(type_id)), + } + } + Action::Delete { .. } => Interface::apply_action::(action), + Action::Compare { .. } => Err(ActionNotAllowed("Compare".to_owned())), + } + } + + pub fn compare_trees( + &self, + comparison: Comparison, + ) -> Result<(Vec, Vec), StorageError> { + fn instantiate(data: &[u8]) -> Result { + D::try_from_slice(data).map_err(StorageError::DeserializationError) + } + let Comparison { + type_id, + data, + comparison_data, + } = comparison; + match type_id { + 11 => Interface::compare_trees(&instantiate::(&data)?, &comparison_data), + 12 => Interface::compare_trees(&instantiate::(&data)?, &comparison_data), + 13 => Interface::compare_trees(&instantiate::(&data)?, &comparison_data), + 14 => Interface::compare_trees(&instantiate::(&data)?, &comparison_data), + 15 => Interface::compare_trees(&instantiate::(&data)?, &comparison_data), + _ => Err(StorageError::UnknownType(type_id)), + } + } + + pub fn generate_comparison_data(&self, id: Id) -> Result { + fn generate_for(id: Id) -> Result { + let data = Interface::find_by_id::(id)?.ok_or(StorageError::NotFound(id))?; + Ok(Comparison { + type_id: D::type_id(), + data: to_vec(&data).map_err(StorageError::SerializationError)?, + comparison_data: Interface::generate_comparison_data(&data)?, + }) + } + let type_id = Interface::type_of(id)?; + match type_id { + 11 => generate_for::(id), + 12 => generate_for::(id), + 13 => generate_for::(id), + 14 => generate_for::(id), + 15 => generate_for::(id), + _ => Err(StorageError::UnknownType(type_id)), + } + } +} diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index d31c5fe51..dc196a51b 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true [dependencies] borsh = { workspace = true, features = ["derive"] } fragile.workspace = true +hex.workspace = true ouroboros.workspace = true owo-colors = { workspace = true, optional = true } rand.workspace = true diff --git a/crates/runtime/examples/crdt.rs b/crates/runtime/examples/crdt.rs new file mode 100644 index 000000000..47d14fd6c --- /dev/null +++ b/crates/runtime/examples/crdt.rs @@ -0,0 +1,238 @@ +#![allow(unused_crate_dependencies, reason = "Not actually unused")] + +use std::env; +use std::fs::File; +use std::io::Read; +use std::path::Path; + +use borsh::BorshSerialize; +use calimero_runtime::errors::FunctionCallError; +use calimero_runtime::logic::{VMContext, VMLimits}; +use calimero_runtime::store::InMemoryStorage; +use calimero_runtime::{run, Constraint}; +use calimero_storage::address::{Id, Path as EntityPath}; +use calimero_storage::entities::{ChildInfo, Element}; +use calimero_storage::interface::Action; +use eyre::Result as EyreResult; +use owo_colors::OwoColorize; +use serde_json::json; + +fn main() -> EyreResult<()> { + let args: Vec = env::args().collect(); + if args.len() != 2 { + println!("Usage: {args:?} "); + return Ok(()); + } + + let path = &args[1]; + let path = Path::new(path); + if !path.exists() { + eyre::bail!("KV wasm file not found"); + } + + let file = File::open(path)?.bytes().collect::, _>>()?; + + let mut storage = InMemoryStorage::default(); + + let limits = VMLimits::new( + /*max_stack_size:*/ 200 << 10, // 200 KiB + /*max_memory_pages:*/ 1 << 10, // 1 KiB + /*max_registers:*/ 100, + /*max_register_size:*/ (100 << 20).validate()?, // 100 MiB + /*max_registers_capacity:*/ 1 << 30, // 1 GiB + /*max_logs:*/ 100, + /*max_log_size:*/ 16 << 10, // 16 KiB + /*max_events:*/ 100, + /*max_event_kind_size:*/ 100, + /*max_event_data_size:*/ 16 << 10, // 16 KiB + /*max_storage_key_size:*/ (1 << 20).try_into()?, // 1 MiB + /*max_storage_value_size:*/ (10 << 20).try_into()?, // 10 MiB + ); + + let cx = VMContext::new(Vec::new(), [0; 32]); + + let _outcome = run(&file, "init", cx, &mut storage, &limits)?; + // dbg!(&outcome); + + // Set up Library + println!("{}", "--".repeat(20).dimmed()); + println!("{:>35}", "Setting up Library".bold()); + println!("{}", "--".repeat(20).dimmed()); + + #[derive(BorshSerialize)] + pub struct Library { + books: Books, + storage: Element, + } + #[derive(BorshSerialize)] + pub struct Books; + + // let library_id: [u8; 16] = + // hex::decode("deadbeef-1122-3344-5566-778899aabb01".replace("-", ""))? + // .as_slice() + // .try_into()?; + // let library_data = json!({ + // "books": [], + // "storage": { + // "id": hex::encode(library_id), + // "is_dirty": false, + // "merkle_hash": hex::encode([0; 32]), + // "metadata": { + // "created_at": 0, + // "updated_at": 0 + // }, + // "path": "::library" + // } + // }); + let library_data = Library { + books: Books {}, + storage: Element::new(&EntityPath::new("::library")?), + }; + let library_id = library_data.storage.id(); + + let action = Action::Add { + id: Id::from(library_id), + type_id: 11, + // data: serde_json::to_vec(&library_data)?, + data: borsh::to_vec(&library_data)?, + ancestors: Vec::new(), + }; + let serialized_action = serde_json::to_string(&action)?; + let input = std::fmt::format(format_args!("{{\"action\": {}}}", serialized_action)); + println!("Input: {}", input); + + println!("Action: {serialized_action}"); + match run( + &file, + "apply_action", + VMContext::new(input.as_bytes().to_owned(), [0; 32]), + &mut storage, + &limits, + ) { + Ok(outcome) => { + // dbg!(&outcome); + match outcome.returns { + Ok(returns) => { + println!("Outcome: {}", String::from_utf8_lossy(&returns.unwrap())); + } + Err(err) => match err { + FunctionCallError::ExecutionError(data) => { + println!("ExecutionError: {}", String::from_utf8_lossy(&data)) + } + _ => { + println!("Other error: {err}"); + } + }, + } + } + Err(err) => { + println!("Error: {err}"); + } + } + + // Add a Book + println!("{}", "--".repeat(20).dimmed()); + println!("{:>35}", "Adding Book".bold()); + println!("{}", "--".repeat(20).dimmed()); + + // let book_id: [u8; 16] = hex::decode("deadbeef-1122-3344-5566-778899aabb02".replace("-", ""))? + // .as_slice() + // .try_into()?; + // let book_data = json!({ + // "authors": ["John Doe"], + // "isbn": "1234567890", + // "publisher": "Example Publishing", + // "year": 2023, + // "rating": 4.5, + // "reviews": [], + // "pages": [], + // "storage": { + // "id": hex::encode(book_id), + // "is_dirty": false, + // "merkle_hash": hex::encode([0; 32]), + // "metadata": { + // "created_at": 0, + // "updated_at": 0 + // }, + // "path": "::library::books::1" + // } + // }); + #[derive(BorshSerialize)] + pub struct Book { + authors: Vec, + isbn: String, + publisher: String, + year: u16, + rating: f32, + reviews: Reviews, + pages: Pages, + storage: Element, + } + #[derive(BorshSerialize)] + pub struct Pages; + #[derive(BorshSerialize)] + pub struct Reviews; + + let book_data = Book { + authors: vec!["John Doe".to_owned()], + isbn: "1234567890".to_owned(), + publisher: "Example Publishing".to_owned(), + year: 2023, + rating: 4.5, + reviews: Reviews {}, + pages: Pages {}, + storage: Element::new(&EntityPath::new("::library::books::1")?), + }; + let book_id = book_data.storage.id(); + let add_book_action = Action::Add { + id: Id::from(book_id), + type_id: 12, + // data: serde_json::to_vec(&book_data)?, + data: borsh::to_vec(&book_data)?, + ancestors: vec![ChildInfo::new(Id::from(library_id), [0; 32])], + }; + + let serialized_add_book_action = serde_json::to_string(&add_book_action)?; + let input = std::fmt::format(format_args!( + "{{\"action\": {}}}", + serialized_add_book_action + )); + println!("Input: {}", input); + + println!("Action: {serialized_add_book_action}"); + match run( + &file, + "apply_action", + VMContext::new(input.as_bytes().to_owned(), [0; 32]), + &mut storage, + &limits, + ) { + Ok(outcome) => { + // dbg!(&outcome); + match outcome.returns { + Ok(returns) => { + println!("Outcome: {}", String::from_utf8_lossy(&returns.unwrap())); + } + Err(err) => match err { + FunctionCallError::ExecutionError(data) => { + println!("ExecutionError: {}", String::from_utf8_lossy(&data)) + } + _ => { + println!("Other error: {err}"); + } + }, + } + } + Err(err) => { + println!("Error: {err}"); + } + } + + println!("{}", "--".repeat(20).dimmed()); + println!("{:>35}", "Now, let's inspect the storage".bold()); + println!("{}", "--".repeat(20).dimmed()); + + dbg!(storage); + + Ok(()) +} diff --git a/crates/storage/src/tests/interface.rs b/crates/storage/src/tests/interface.rs index 1f16e2a86..7c04eb0a3 100644 --- a/crates/storage/src/tests/interface.rs +++ b/crates/storage/src/tests/interface.rs @@ -10,8 +10,16 @@ use crate::tests::common::{Page, Paragraph}; #[cfg(test)] mod interface__public_methods { + // use crate::integration::Action; + use super::*; + // #[test] + // fn serde() { + // let action = Action::Add(Id::new(), String::new(), Vec::new()); + // let serialized = serde_json::to_string(&action).unwrap(); + // } + #[test] fn children_of() { let element = Element::new(&Path::new("::root::node").unwrap());