From 6a1f6c381c8e8d53cdc3960f4280418a4ec86d35 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 30 Oct 2025 10:20:00 +1100 Subject: [PATCH] void-app-node Moves common app logic into void-app-node. Keeping void-app-node in this repo for now so we can iterate quickly. Transfers now has publisher / observer modes --- .github/workflows/ci.yml | 3 +- Cargo.lock | 92 ++- Cargo.toml | 9 +- apps/increment/Cargo.toml | 16 +- apps/increment/sql/insert/current_proof.sql | 4 - apps/increment/src/api.rs | 36 + apps/increment/src/app.rs | 137 ++++ apps/increment/src/data.rs | 257 -------- apps/increment/src/db.rs | 382 +++-------- apps/increment/src/lib.rs | 325 +-------- apps/increment/src/main.rs | 120 ++-- apps/increment/src/proof.rs | 138 ---- apps/increment/src/server.rs | 85 +-- apps/increment/src/signing.rs | 31 - apps/increment/src/state.rs | 111 ---- apps/increment/src/state_reads.rs | 48 ++ apps/increment/tests/tests.rs | 148 +++-- apps/transfers/Cargo.toml | 12 +- apps/transfers/src/api.rs | 106 +++ apps/transfers/src/api_state.rs | 136 ---- apps/transfers/src/{app_state.rs => app.rs} | 283 ++++---- apps/transfers/src/app/types.rs | 84 +++ apps/transfers/src/lib.rs | 99 +-- apps/transfers/src/main.rs | 85 ++- apps/transfers/src/server.rs | 104 ++- apps/transfers/src/state.rs | 43 -- apps/transfers/src/state_reads.rs | 62 ++ apps/transfers/tests/balance_only_test.rs | 2 +- apps/transfers/tests/balance_test.rs | 4 +- apps/transfers/tests/panic_safety_test.rs | 13 +- apps/transfers/tests/tests.rs | 74 ++- node/Cargo.toml | 33 + .../sql/create/current_proof.sql | 4 +- .../sql/create/latest_header.sql | 0 node/sql/insert/current_proof.sql | 4 + .../sql/insert/current_proof_limit.sql | 0 .../sql/insert/latest_header.sql | 0 .../sql/query/current_proof.sql | 4 +- .../sql/query/get_proof.sql | 4 +- .../sql/query/latest_header.sql | 0 node/src/db.rs | 229 +++++++ node/src/lib.rs | 622 ++++++++++++++++++ node/src/memory.rs | 183 ++++++ node/src/oracle.rs | 30 + node/src/proof.rs | 111 ++++ node/src/proof/replicate.rs | 137 ++++ node/src/server.rs | 83 +++ {apps/transfers => node}/src/signing.rs | 8 +- node/src/storage.rs | 172 +++++ node/src/transitions.rs | 45 ++ 50 files changed, 2853 insertions(+), 1865 deletions(-) delete mode 100644 apps/increment/sql/insert/current_proof.sql create mode 100644 apps/increment/src/api.rs create mode 100644 apps/increment/src/app.rs delete mode 100644 apps/increment/src/data.rs delete mode 100644 apps/increment/src/proof.rs delete mode 100644 apps/increment/src/signing.rs delete mode 100644 apps/increment/src/state.rs create mode 100644 apps/increment/src/state_reads.rs create mode 100644 apps/transfers/src/api.rs delete mode 100644 apps/transfers/src/api_state.rs rename apps/transfers/src/{app_state.rs => app.rs} (58%) create mode 100644 apps/transfers/src/app/types.rs delete mode 100644 apps/transfers/src/state.rs create mode 100644 apps/transfers/src/state_reads.rs create mode 100644 node/Cargo.toml rename {apps/increment => node}/sql/create/current_proof.sql (58%) rename {apps/increment => node}/sql/create/latest_header.sql (100%) create mode 100644 node/sql/insert/current_proof.sql rename {apps/increment => node}/sql/insert/current_proof_limit.sql (100%) rename {apps/increment => node}/sql/insert/latest_header.sql (100%) rename {apps/increment => node}/sql/query/current_proof.sql (85%) rename {apps/increment => node}/sql/query/get_proof.sql (59%) rename {apps/increment => node}/sql/query/latest_header.sql (100%) create mode 100644 node/src/db.rs create mode 100644 node/src/lib.rs create mode 100644 node/src/memory.rs create mode 100644 node/src/oracle.rs create mode 100644 node/src/proof.rs create mode 100644 node/src/proof/replicate.rs create mode 100644 node/src/server.rs rename {apps/transfers => node}/src/signing.rs (84%) create mode 100644 node/src/storage.rs create mode 100644 node/src/transitions.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99389f5..b02451e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,8 @@ jobs: matrix: include: - command: cargo test test_complete_bridge_flow --locked -- --ignored - - command: cargo test test_api --locked -- --ignored + - command: cargo test test_api_mem --locked -- --ignored + - command: cargo test test_api_db --locked -- --ignored steps: - uses: actions/create-github-app-token@v1 id: app-token diff --git a/Cargo.lock b/Cargo.lock index e8be512..3f10de0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2401,18 +2401,15 @@ dependencies = [ "axum", "clap", "futures", - "http", - "pin-project-lite", "reqwest", "rusqlite", "serde", - "serde_json", - "serde_yaml_ng", - "sha2", "tempfile", "tokio", "tokio-util", - "tower-http", + "tracing", + "tracing-subscriber", + "void-app-node", "void-toolkit", ] @@ -2665,6 +2662,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3755,6 +3761,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3994,6 +4009,15 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -4263,6 +4287,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -4275,16 +4325,17 @@ dependencies = [ "clap", "futures", "hex", - "http", "rand 0.8.5", "reqwest", + "rusqlite", "serde", "serde_json", "tempfile", "tokio", "tokio-util", - "tower-http", "tracing", + "tracing-subscriber", + "void-app-node", "void-toolkit", ] @@ -4428,6 +4479,31 @@ dependencies = [ "void-types", ] +[[package]] +name = "void-app-node" +version = "0.1.0" +dependencies = [ + "alloy", + "anyhow", + "axum", + "clap", + "futures", + "http", + "pin-project-lite", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "serde_yaml_ng", + "sha2", + "tempfile", + "tokio", + "tokio-util", + "tower-http", + "tracing-subscriber", + "void-toolkit", +] + [[package]] name = "void-hash" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 708f2fd..0577ca9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,16 @@ [workspace] -members = ["apps/*"] +members = ["apps/*", "node"] resolver = "2" [workspace.package] -edition = "2024" authors = ["Essential Contributions "] +edition = "2024" homepage = "https://essential.builders/" license = "Apache-2.0" repository = "https://github.com/essential-contributions/transfers" [workspace.dependencies] -alloy = { version = "1.0.23", features = ["rlp", "signers", "signer-mnemonic"] } +alloy = { version = "1.0.23", features = ["rlp", "signer-mnemonic", "signers"] } anyhow = "1.0.98" axum = "0.8.4" clap = { version = "4.5.38", features = ["derive"] } @@ -29,4 +29,7 @@ tokio = { version = "1.45.1", features = ["full"] } tokio-util = "0.7.15" tower-http = { version = "0.6.6", features = ["cors"] } tracing = { version = "0.1.41" } +tracing-subscriber = "0.3.20" void-toolkit = { git = "ssh://git@github.com/essential-contributions/void-toolkit.git" } + +void-app-node = { path = "node" } diff --git a/apps/increment/Cargo.toml b/apps/increment/Cargo.toml index 7319a50..00cbeff 100644 --- a/apps/increment/Cargo.toml +++ b/apps/increment/Cargo.toml @@ -13,20 +13,16 @@ anyhow.workspace = true axum.workspace = true clap.workspace = true futures.workspace = true -http.workspace = true -pin-project-lite.workspace = true -reqwest.workspace = true rusqlite.workspace = true serde.workspace = true -serde_json.workspace = true -serde_yaml_ng.workspace = true -sha2.workspace = true -tempfile.workspace = true tokio.workspace = true -tokio-util.workspace = true -tower-http.workspace = true -void-toolkit = { workspace = true, features = ["merkle", "app", "network-channel", "oracle-sqlite", "oracle" ] } +tracing.workspace = true +tracing-subscriber.workspace = true +void-app-node.workspace = true +void-toolkit = { workspace = true, features = ["merkle", "app", "network-channel", "oracle-sqlite", "oracle", "hash", "oracle-decoder"] } [dev-dependencies] alloy = { workspace = true, features = [ "node-bindings", "rlp", "signer-mnemonic" ] } +reqwest.workspace = true +tempfile.workspace = true tokio-util.workspace = true \ No newline at end of file diff --git a/apps/increment/sql/insert/current_proof.sql b/apps/increment/sql/insert/current_proof.sql deleted file mode 100644 index eba6b16..0000000 --- a/apps/increment/sql/insert/current_proof.sql +++ /dev/null @@ -1,4 +0,0 @@ -INSERT - OR REPLACE INTO current_proof (count, root, height, signature) -VALUES - (?, ?, ?, ?); \ No newline at end of file diff --git a/apps/increment/src/api.rs b/apps/increment/src/api.rs new file mode 100644 index 0000000..df2993d --- /dev/null +++ b/apps/increment/src/api.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use alloy::primitives::Address; +use void_toolkit::types::Block; + +use crate::FullDataStore; + +#[derive(Default)] +pub struct Api { + pub counts_per_owner_index: HashMap>, +} + +pub trait ApiState { + fn update_owners_index(&mut self) -> anyhow::Result<()>; +} + +impl ApiState for FullDataStore<'_, '_> { + fn update_owners_index(&mut self) -> anyhow::Result<()> { + match self { + void_app_node::storage::DataStorage::Db(tx) => tx.update_owners_index(), + void_app_node::storage::DataStorage::Memory(mem) => { + mem.api + .counts_per_owner_index + .entry(mem.app.owners.get(mem.app.count)?.into()) + .or_default() + .push(mem.app.count); + Ok(()) + } + } + } +} + +pub fn api_update(_block: &Block, mut state: impl ApiState) -> anyhow::Result<()> { + state.update_owners_index()?; + Ok(()) +} diff --git a/apps/increment/src/app.rs b/apps/increment/src/app.rs new file mode 100644 index 0000000..cf7568f --- /dev/null +++ b/apps/increment/src/app.rs @@ -0,0 +1,137 @@ +use alloy::{primitives::keccak256, sol_types::SolValue}; +use void_app_node::{proof::ProofConversions, storage::DataStorage}; +use void_toolkit::{ + merkle::fixed_sparse_merkle::{Sha256, SparseMerkleTree, get_hashes_height}, + oracle_decoder::decode_events, + types::Block, +}; + +use crate::{Increment::Incremented, MerkleData}; + +use crate::DataStore; + +decode_events!(Incremented,); + +pub type InMemoryMerkle = SparseMerkleTree<{ get_hashes_height(64) }, Sha256>; + +pub struct App { + /// The current count of increments. + pub count: u64, + /// A mapping of increment counts to sender addresses that caused them. + pub owners: InMemoryMerkle, +} + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Proof { + pub count: u64, + pub root: [u8; 32], +} + +pub trait AppState { + fn increment_count(&mut self) -> anyhow::Result<()>; + fn add_owner(&mut self, owner: [u8; 20]) -> anyhow::Result<()>; +} + +impl AppState for App { + fn increment_count(&mut self) -> anyhow::Result<()> { + self.count = self.count.saturating_add(1); + Ok(()) + } + + fn add_owner(&mut self, owner: [u8; 20]) -> anyhow::Result<()> { + Ok(self.owners.insert(self.count, owner)?) + } +} + +impl AppState for DataStore<'_, '_> { + fn increment_count(&mut self) -> anyhow::Result<()> { + match self { + void_app_node::storage::DataStorage::Db(tx) => tx.increment_count(), + void_app_node::storage::DataStorage::Memory(app) => app.increment_count(), + } + } + + fn add_owner(&mut self, owner: [u8; 20]) -> anyhow::Result<()> { + match self { + void_app_node::storage::DataStorage::Db(tx) => tx.add_owner(owner), + void_app_node::storage::DataStorage::Memory(app) => app.add_owner(owner), + } + } +} + +pub fn state_transition_function(block: &Block, mut state: impl AppState) -> anyhow::Result<()> { + for bytes in &block.events { + let Ok(input) = decode_input(bytes) else { + continue; + }; + match input { + AppEvent::Incremented(incremented) => increment(incremented, &mut state)?, + } + } + Ok(()) +} + +fn increment(event: Incremented, state: &mut impl AppState) -> anyhow::Result<()> { + // Increment the count + state.increment_count()?; + + // Insert the sender address into the owners map + state.add_owner(event.sender.into())?; + Ok(()) +} + +impl Default for App { + fn default() -> Self { + Self { + count: Default::default(), + owners: InMemoryMerkle::new_in_memory(), + } + } +} + +impl ProofConversions for Proof { + type App = App; + type DbData = MerkleData; + fn digest(&self) -> [u8; 32] { + let encoded = (self.count, self.root).abi_encode_packed(); + + // Hash the encoded data + *keccak256(encoded) + } + + fn into_bytes(self) -> Vec { + let mut vec = Vec::with_capacity(std::mem::size_of::() + 32); + vec.extend(self.count.to_be_bytes()); + vec.extend(self.root); + vec + } + + fn try_from_bytes(bytes: &[u8]) -> anyhow::Result + where + Self: Sized, + { + if bytes.len() != std::mem::size_of::() + 32 { + return Err(anyhow::anyhow!( + "Invalid proof length: expected {}, got {}", + std::mem::size_of::() + 32, + bytes.len() + )); + } + let count = u64::from_be_bytes(bytes[0..std::mem::size_of::()].try_into()?); + let root = bytes[std::mem::size_of::()..].try_into()?; + Ok(Proof { count, root }) + } + + fn try_from_storage(store: DataStorage<'_, '_, Self::App, Self::DbData>) -> anyhow::Result + where + Self: Sized, + { + match store { + void_app_node::storage::DataStorage::Db(tx) => tx.try_into(), + void_app_node::storage::DataStorage::Memory(mem) => Ok(Proof { + count: mem.count, + root: mem.owners.root()?, + }), + } + } +} diff --git a/apps/increment/src/data.rs b/apps/increment/src/data.rs deleted file mode 100644 index c6fa9a8..0000000 --- a/apps/increment/src/data.rs +++ /dev/null @@ -1,257 +0,0 @@ -use std::collections::HashMap; -use std::convert::Infallible; - -use alloy::primitives::Address; -use void_toolkit::app::UpdateLatestBlock; -use void_toolkit::types::{Height, Lock, Signed}; - -use crate::state::State; -use crate::{ - db::Db, - state::{MemoryState, Witness}, -}; - -#[derive(Clone)] -pub enum Data { - Db(Db), - Memory(Memory), -} - -#[derive(Clone)] -pub struct Memory { - pub state: Lock, - pub current_proofs: Lock>>>, - pub counts_per_owner_index: Lock>>, - pub latest_header: Lock, -} - -#[derive(Clone)] -pub enum DataType { - Db(String), - Memory, -} - -#[derive(Default)] -pub enum LatestHeaderMem { - #[default] - Empty, - Header { - height: u64, - hash: [u8; 32], - }, -} - -pub struct KeyValueBuffer { - map: std::collections::BTreeMap, -} - -pub struct MemoryStatePair<'a> { - pub memory_state: &'a mut MemoryState, - pub header: &'a mut LatestHeaderMem, -} - -impl Data { - pub fn new(data_type: DataType) -> Self { - match data_type { - DataType::Db(path) => Data::Db(Db::new(&path)), - DataType::Memory => Data::Memory(Memory { - state: Lock::new(MemoryState::default()), - current_proofs: Lock::new(KeyValueBuffer::new()), - counts_per_owner_index: Lock::new(HashMap::new()), - latest_header: Lock::new(LatestHeaderMem::default()), - }), - } - } - - pub async fn update_owners_index(&self) -> anyhow::Result<()> { - match self { - Data::Db(db) => db.update_owners_index().await, - Data::Memory(mem) => mem.state.access(|s| { - mem.counts_per_owner_index.access(|index| { - index - .entry(s.owners.get(s.count)?.into()) - .or_default() - .push(s.count); - Ok(()) - }) - }), - } - } - - pub async fn update_current_proof( - &self, - signed_witness: Signed>, - ) -> anyhow::Result<()> { - match self { - Data::Db(db) => db.update_current_proof(signed_witness).await, - Data::Memory(mem) => { - mem.current_proofs.access(|p| { - p.insert(signed_witness.data.block_height, signed_witness); - }); - Ok(()) - } - } - } - - pub async fn get_proof(&self, height: u64) -> anyhow::Result>>> { - match self { - Data::Db(db) => db.get_proof(height).await, - Data::Memory(mem) => Ok(mem.current_proofs.access(|p| p.get(&height).cloned())), - } - } - - pub async fn get_current_proof(&self) -> anyhow::Result>>> { - match self { - Data::Db(db) => db.get_current_proof().await, - Data::Memory(mem) => mem.current_proofs.access(|p| Ok(p.last().cloned())), - } - } - - pub async fn get_current_count(&self) -> anyhow::Result>> { - match self { - Data::Db(db) => db.get_current_count().await, - Data::Memory(mem) => mem.state.access(|s| { - mem.latest_header.access(|h| { - let Some(block_height) = h.height() else { - return Ok(None); - }; - Ok(Some(Height { - block_height, - data: s.count, - })) - }) - }), - } - } - - pub async fn generate_merkle_proof( - &self, - index: u64, - ) -> anyhow::Result>>> { - match self { - Data::Db(db) => db.generate_proof(index).await, - Data::Memory(mem) => mem.state.access(|s| { - mem.latest_header.access(|h| { - let Some(block_height) = h.height() else { - return Ok(None); - }; - let proof = s.owners.generate_proof(index)?; - Ok(Some(Height { - block_height, - data: proof, - })) - }) - }), - } - } - - pub async fn get_owner_counts(&self, address: Address) -> anyhow::Result> { - match self { - Data::Db(db) => db.get_owner_counts(address).await, - Data::Memory(mem) => mem - .counts_per_owner_index - .access(|index| Ok(index.get(&address).cloned().unwrap_or_default())), - } - } - - pub async fn get_latest_header(&self) -> anyhow::Result> { - match self { - Data::Db(db) => db.get_latest_header().await, - Data::Memory(mem) => mem.latest_header.access(|h| Ok(h.get())), - } - } -} - -impl UpdateLatestBlock for LatestHeaderMem { - type Error = std::convert::Infallible; - - fn update_latest_block(&mut self, height: u64, hash: [u8; 32]) -> Result<(), Self::Error> { - *self = LatestHeaderMem::Header { height, hash }; - Ok(()) - } -} - -impl LatestHeaderMem { - pub fn height(&self) -> Option { - match self { - LatestHeaderMem::Empty => None, - LatestHeaderMem::Header { height, .. } => Some(*height), - } - } - - pub fn hash(&self) -> Option<[u8; 32]> { - match self { - LatestHeaderMem::Empty => None, - LatestHeaderMem::Header { hash, .. } => Some(*hash), - } - } - - pub fn get(&self) -> Option<(u64, [u8; 32])> { - match self { - LatestHeaderMem::Empty => None, - LatestHeaderMem::Header { height, hash } => Some((*height, *hash)), - } - } -} - -impl KeyValueBuffer { - pub fn new() -> Self { - Self { - map: std::collections::BTreeMap::new(), - } - } - - pub fn insert(&mut self, key: K, value: V) - where - K: Ord, - { - self.map.insert(key, value); - if self.map.len() > LENGTH { - self.map.first_entry().unwrap().remove_entry(); - } - } - - pub fn get(&self, key: &K) -> Option<&V> - where - K: Ord, - { - self.map.get(key) - } - - pub fn last(&self) -> Option<&V> - where - K: Ord, - { - self.map.last_key_value().map(|(_, v)| v) - } -} - -impl Default for KeyValueBuffer { - fn default() -> Self { - Self::new() - } -} - -impl State for MemoryStatePair<'_> { - type Error = Infallible; - fn increment_count(&mut self) -> Result<(), Self::Error> { - self.memory_state.increment_count() - } - - fn add_owner(&mut self, owner: [u8; 20]) -> Result<(), Self::Error> { - self.memory_state.add_owner(owner) - } -} - -impl UpdateLatestBlock for MemoryStatePair<'_> { - type Error = Infallible; - fn update_latest_block(&mut self, height: u64, hash: [u8; 32]) -> Result<(), Self::Error> { - self.header.update_latest_block(height, hash) - } -} - -impl From<&mut MemoryStatePair<'_>> for Witness { - fn from(state: &mut MemoryStatePair) -> Self { - Witness::from(&mut *state.memory_state) - } -} diff --git a/apps/increment/src/db.rs b/apps/increment/src/db.rs index b93a4c4..50af995 100644 --- a/apps/increment/src/db.rs +++ b/apps/increment/src/db.rs @@ -1,366 +1,140 @@ -use std::sync::Arc; -use std::sync::Mutex; - -use crate::state::State; -use crate::state::Witness; - -use alloy::primitives::Address; -use rusqlite::OptionalExtension; -use rusqlite::params; -use void_toolkit::app::UpdateLatestBlock; +use rusqlite::{OptionalExtension, params}; +use void_app_node::db::Tx; use void_toolkit::types::Height; -use void_toolkit::types::Signed; -mod merkle; +use crate::{MerkleData, Proof, api::ApiState, app::AppState}; -/// Short-hand for including an SQL string from the `sql/` subdir at compile time. -macro_rules! include_sql_str { - ($subpath:expr) => { - include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/sql/", $subpath)) - }; -} - -/// Short-hand for declaring a `const` SQL str and presenting the SQL via the doc comment. -macro_rules! decl_const_sql_str { - ($name:ident, $subpath:expr) => { - /// ```sql - #[doc = include_sql_str!($subpath)] - /// ``` - pub const $name: &str = include_sql_str!($subpath); - }; -} +pub mod merkle; -#[derive(Clone)] -pub struct Db { - pub permit: Arc, - pub conn: Arc>, - pub tree: Arc, +pub fn init_db(tx: &mut rusqlite::Transaction) -> anyhow::Result<()> { + create_tables(tx) } -pub struct Tx<'conn> { - pub tx: rusqlite::Transaction<'conn>, - pub tree: &'conn merkle::MerkleTreeHashes, +pub fn create_tables(tx: &rusqlite::Transaction) -> anyhow::Result<()> { + tx.execute(create::COUNT, [])?; + tx.execute(create::INCREMENT, [])?; + tx.execute(create::OWNER, [])?; + tx.execute(create::TREE_LEAF, [])?; + tx.execute(create::TREE_NODE, [])?; + Ok(()) } -impl Db { - pub fn new(path: &str) -> Self { - let mut conn = rusqlite::Connection::open(path).unwrap(); - let tx = conn.transaction().unwrap(); - create_tables(&tx); - tx.commit().unwrap(); - - Self { - permit: Arc::new(tokio::sync::Semaphore::new(1)), - conn: Arc::new(Mutex::new(conn)), - tree: Arc::new(merkle::MerkleTreeHashes::new()), - } - } - - pub async fn apply(&self, f: F) -> anyhow::Result - where - R: Send + 'static, - E: Send + 'static, - F: FnOnce(&mut Tx<'_>) -> Result + Send + 'static, - anyhow::Error: From, - { - let _permit = self.permit.acquire().await.unwrap(); - let db = self.clone(); - - tokio::task::spawn_blocking(move || { - let mut conn = db.conn.lock().unwrap(); - let tree = &db.tree; - let mut tx = Tx { - tx: conn.transaction()?, - tree, - }; - let r = f(&mut tx)?; - tx.tx.commit()?; - Ok(r) - }) - .await - .unwrap() - } - - pub async fn update_owners_index(&self) -> anyhow::Result<()> { - self.apply(|tx| { - tx.update_owner()?; - tx.update_owner_count() - }) - .await - } - - pub async fn update_current_proof( - &self, - signed_witness: Signed>, - ) -> anyhow::Result<()> { - self.apply(|tx| tx.update_current_proof(signed_witness)) - .await - } - - pub async fn get_current_proof(&self) -> anyhow::Result>>> { - self.apply(|tx| tx.get_current_proof()).await - } +pub mod create { + use void_app_node::decl_const_sql_str; + use void_app_node::include_sql_str; - pub async fn get_proof(&self, height: u64) -> anyhow::Result>>> { - self.apply(move |tx| tx.get_proof(height)).await - } + decl_const_sql_str!(COUNT, "create/count.sql"); + decl_const_sql_str!(INCREMENT, "create/increment.sql"); + decl_const_sql_str!(OWNER, "create/owner.sql"); + decl_const_sql_str!(TREE_LEAF, "create/tree_leaf.sql"); + decl_const_sql_str!(TREE_NODE, "create/tree_node.sql"); +} - pub async fn get_current_count(&self) -> anyhow::Result>> { - self.apply(|tx| tx.get_current_count()).await - } +pub mod insert { + use void_app_node::decl_const_sql_str; + use void_app_node::include_sql_str; - pub async fn generate_proof( - &self, - index: u64, - ) -> anyhow::Result>>> { - self.apply(move |tx| tx.generate_proof(index)).await - } + decl_const_sql_str!(COUNT, "insert/count.sql"); + decl_const_sql_str!(INCREMENT, "insert/increment.sql"); + decl_const_sql_str!(OWNER, "insert/owner.sql"); + decl_const_sql_str!(TREE_LEAF, "insert/tree_leaf.sql"); + decl_const_sql_str!(TREE_NODE, "insert/tree_node.sql"); +} - pub async fn get_owner_counts(&self, address: Address) -> anyhow::Result> { - self.apply(move |tx| tx.get_owner_counts(address.into())) - .await - } +pub mod query { + use void_app_node::decl_const_sql_str; + use void_app_node::include_sql_str; - pub async fn get_latest_header(&self) -> anyhow::Result> { - self.apply(|tx| tx.get_latest_header()).await - } + decl_const_sql_str!(INCREMENT, "query/increment.sql"); + decl_const_sql_str!(OWNERS_COUNT, "query/owners_count.sql"); + decl_const_sql_str!(TREE_LEAF, "query/tree_leaf.sql"); + decl_const_sql_str!(TREE_NODE, "query/tree_node.sql"); } -impl State for Tx<'_> { - type Error = rusqlite::Error; - - fn increment_count(&mut self) -> rusqlite::Result<()> { +impl AppState for Tx<'_, MerkleData> { + fn increment_count(&mut self) -> anyhow::Result<()> { let current_count: u64 = self .tx .query_row(query::INCREMENT, params![], |row| row.get(0)) .optional()? .unwrap_or(0); - let new_count = current_count + 1; + let new_count = current_count.saturating_add(1); self.tx.execute(insert::INCREMENT, params![new_count])?; Ok(()) } - fn add_owner(&mut self, owner: [u8; 20]) -> rusqlite::Result<()> { + fn add_owner(&mut self, owner: [u8; 20]) -> anyhow::Result<()> { let index = self .tx .query_row(query::INCREMENT, params![], |row| row.get(0)) .optional()? .unwrap_or(0); - let mut tx = merkle::DbMerkleTreeMut::new(&mut self.tx); - let mut tree = self.tree.mut_tree(&mut tx); + let mut tree = self.data.0.mut_tree(&mut tx); tree.insert(index, owner)?; - Ok(()) } } -impl Tx<'_> { - fn update_owner(&mut self) -> rusqlite::Result<()> { +impl ApiState for Tx<'_, MerkleData> { + fn update_owners_index(&mut self) -> anyhow::Result<()> { self.tx.execute(insert::OWNER, params![])?; - Ok(()) - } - - fn update_owner_count(&mut self) -> rusqlite::Result<()> { self.tx.execute(insert::COUNT, params![])?; Ok(()) } - - fn update_current_proof( - &mut self, - signed_witness: Signed>, - ) -> rusqlite::Result<()> { - self.tx.execute( - insert::CURRENT_PROOF, - params![ - signed_witness.data.data.count, - signed_witness.data.data.root.as_ref(), - signed_witness.data.block_height, - signed_witness.signature - ], - )?; - Ok(()) - } - - fn get_current_proof(&mut self) -> rusqlite::Result>>> { - let row = self - .tx - .query_row(query::CURRENT_PROOF, params![], |row| { - let count: u64 = row.get(0)?; - let root: [u8; 32] = row.get(1)?; - let block_height: u64 = row.get(2)?; - let signature: Vec = row.get(3)?; - Ok((count, root, block_height, signature)) - }) - .optional()?; - - let Some((count, root, block_height, signature)) = row else { - return Ok(None); - }; - - Ok(Some(Signed { - signature, - data: Height { - block_height, - data: Witness { count, root }, - }, - })) - } - - fn get_proof(&mut self, height: u64) -> rusqlite::Result>>> { - let row = self - .tx - .query_row(query::GET_PROOF, params![height], |row| { - let count: u64 = row.get(0)?; - let root: [u8; 32] = row.get(1)?; - let signature: Vec = row.get(2)?; - Ok((count, root, signature)) - }) - .optional()?; - - let Some((count, root, signature)) = row else { - return Ok(None); - }; - - Ok(Some(Signed { - signature, - data: Height { - block_height: height, - data: Witness { count, root }, - }, - })) - } - - fn get_current_count(&self) -> rusqlite::Result>> { - let Some(block_height) = self - .tx - .query_row(query::LATEST_HEADER, params![], |row| row.get(0)) - .optional()? - else { - return Ok(None); - }; - - let count = self - .tx - .query_row(query::INCREMENT, params![], |row| row.get(0)) - .optional()? - .unwrap_or(0); - Ok(Some(Height { - block_height, - data: count, - })) - } - - fn generate_proof(&self, index: u64) -> rusqlite::Result>>> { - let Some(block_height) = self - .tx - .query_row(query::LATEST_HEADER, params![], |row| row.get(0)) - .optional()? - else { - return Ok(None); - }; - - let tx = merkle::DbMerkleTree::new(&self.tx); - let tree = self.tree.as_tree(&tx); - let proof = tree.generate_proof(index)?; - Ok(Some(Height { - block_height, - data: proof, - })) - } - - fn get_owner_counts(&self, address: [u8; 20]) -> rusqlite::Result> { - let mut stmt = self.tx.prepare(query::OWNERS_COUNT)?; - stmt.query_map(params![address], |row| row.get(0))? - .try_fold(Vec::new(), |mut acc, res| { - acc.push(res?); - Ok(acc) - }) - } - - fn get_latest_header(&self) -> rusqlite::Result> { - self.tx - .query_row(query::LATEST_HEADER, params![], |row| { - let block_height: u64 = row.get(0)?; - let block_hash: [u8; 32] = row.get(1)?; - Ok((block_height, block_hash)) - }) - .optional() - } - - pub fn update_latest_header(&mut self, height: u64, hash: [u8; 32]) -> rusqlite::Result<()> { - self.tx - .execute(insert::LATEST_HEADER, params![height, hash.as_ref()])?; - Ok(()) - } } -impl TryFrom<&mut Tx<'_>> for Witness { - type Error = rusqlite::Error; +impl TryFrom<&mut Tx<'_, MerkleData>> for Proof { + type Error = anyhow::Error; - fn try_from(state: &mut Tx<'_>) -> Result { + fn try_from(state: &mut Tx<'_, MerkleData>) -> Result { let current_count: u64 = state .tx .query_row(query::INCREMENT, params![], |row| row.get(0)) .optional()? .unwrap_or(0); let tx = merkle::DbMerkleTree::new(&state.tx); - let tree = state.tree.as_tree(&tx); + let tree = state.data.0.as_tree(&tx); let root = tree.root()?; - Ok(Witness { + Ok(Proof { count: current_count, root, }) } } -impl UpdateLatestBlock for Tx<'_> { - type Error = rusqlite::Error; - - fn update_latest_block(&mut self, height: u64, hash: [u8; 32]) -> Result<(), Self::Error> { - self.update_latest_header(height, hash)?; - Ok(()) - } -} +pub fn get_current_count(tx: &Tx<'_, MerkleData>) -> anyhow::Result>> { + let Some(block_height) = tx.get_current_block_height_and_hash()?.map(|(h, _)| h) else { + return Ok(None); + }; -pub fn create_tables(tx: &rusqlite::Transaction) { - tx.execute(create::TREE_LEAF, []).unwrap(); - tx.execute(create::TREE_NODE, []).unwrap(); - tx.execute(create::INCREMENT, []).unwrap(); - tx.execute(create::COUNT, []).unwrap(); - tx.execute(create::OWNER, []).unwrap(); - tx.execute(create::CURRENT_PROOF, []).unwrap(); - tx.execute(create::LATEST_HEADER, []).unwrap(); - tx.execute(insert::CURRENT_PROOF_LIMIT, []).unwrap(); + let count = tx + .tx + .query_row(query::INCREMENT, params![], |row| row.get(0)) + .optional()? + .unwrap_or(0); + Ok(Some(Height::new(block_height, count))) } -pub mod create { - decl_const_sql_str!(INCREMENT, "create/increment.sql"); - decl_const_sql_str!(TREE_LEAF, "create/tree_leaf.sql"); - decl_const_sql_str!(TREE_NODE, "create/tree_node.sql"); - decl_const_sql_str!(CURRENT_PROOF, "create/current_proof.sql"); - decl_const_sql_str!(OWNER, "create/owner.sql"); - decl_const_sql_str!(COUNT, "create/count.sql"); - decl_const_sql_str!(LATEST_HEADER, "create/latest_header.sql"); -} +pub fn generate_merkle_proof( + tx: &Tx<'_, MerkleData>, + index: u64, +) -> anyhow::Result>>> { + let Some(block_height) = tx.get_current_block_height_and_hash()?.map(|(h, _)| h) else { + return Ok(None); + }; -pub mod insert { - decl_const_sql_str!(INCREMENT, "insert/increment.sql"); - decl_const_sql_str!(TREE_LEAF, "insert/tree_leaf.sql"); - decl_const_sql_str!(TREE_NODE, "insert/tree_node.sql"); - decl_const_sql_str!(CURRENT_PROOF, "insert/current_proof.sql"); - decl_const_sql_str!(CURRENT_PROOF_LIMIT, "insert/current_proof_limit.sql"); - decl_const_sql_str!(OWNER, "insert/owner.sql"); - decl_const_sql_str!(COUNT, "insert/count.sql"); - decl_const_sql_str!(LATEST_HEADER, "insert/latest_header.sql"); + let tx_merkle = merkle::DbMerkleTree::new(&tx.tx); + let tree = tx.data.0.as_tree(&tx_merkle); + let proof = tree.generate_proof(index)?; + Ok(Some(Height::new(block_height, proof))) } -pub mod query { - decl_const_sql_str!(INCREMENT, "query/increment.sql"); - decl_const_sql_str!(TREE_LEAF, "query/tree_leaf.sql"); - decl_const_sql_str!(TREE_NODE, "query/tree_node.sql"); - decl_const_sql_str!(CURRENT_PROOF, "query/current_proof.sql"); - decl_const_sql_str!(GET_PROOF, "query/get_proof.sql"); - decl_const_sql_str!(OWNERS_COUNT, "query/owners_count.sql"); - decl_const_sql_str!(LATEST_HEADER, "query/latest_header.sql"); +pub fn get_owner_counts(tx: &Tx<'_, MerkleData>, owner: [u8; 20]) -> anyhow::Result> { + let mut stmt = tx.tx.prepare(query::OWNERS_COUNT)?; + stmt.query_map(params![owner], |row| row.get(0))? + .try_fold(Vec::new(), |mut acc, res| { + acc.push(res?); + Ok(acc) + }) } diff --git a/apps/increment/src/lib.rs b/apps/increment/src/lib.rs index 626d5db..8a7cbab 100644 --- a/apps/increment/src/lib.rs +++ b/apps/increment/src/lib.rs @@ -1,24 +1,17 @@ -use std::net::SocketAddr; -use std::path::PathBuf; +use std::sync::Arc; -use alloy::{signers::local::PrivateKeySigner, sol}; -use futures::TryStreamExt; -use serde::de::DeserializeOwned; -use void_toolkit::app::apply_transition; -use void_toolkit::app::{Notification, VoidStream}; -use void_toolkit::oracle::observer_oracle; -use void_toolkit::oracle::publisher_oracle; -use void_toolkit::oracle_types::config::Config as OracleConfig; -use void_toolkit::oracle_types::config::OracleBlocksConfig; -use void_toolkit::types::{Block, Height}; +use alloy::sol; +use void_app_node::run_signing; +use void_toolkit::types::Block; -use crate::data::Data; -use crate::data::MemoryStatePair; -use crate::proof::Proof; -use crate::signing::sign; -use crate::state::Witness; +use crate::{ + api::{Api, api_update}, + app::{App, state_transition_function}, + db::{init_db, merkle::MerkleTreeHashes}, + server::add_handlers, +}; -pub use data::DataType; +pub use app::Proof; sol!( #[allow(missing_docs)] @@ -27,282 +20,22 @@ sol!( "contracts/build/increment_abi.json" ); -pub mod proof; -pub mod server; -pub mod signing; -pub mod state; - -mod data; +mod api; +mod app; mod db; - -#[derive(Clone)] -pub struct App { - pub data: Data, - pub state_notification: Notification, - pub current_proof: Proof, -} - -pub struct Mode { - pub data_type: DataType, - pub mode_type: ModeType, - pub server_bind_address: SocketAddr, - pub oracle_db_path: Option, -} - -pub enum ModeType { - Publisher(Box), - Observer(Observer), -} - -pub struct Publisher { - pub signer: PrivateKeySigner, - pub oracle_config: OracleConfig, - pub oracle_bind_address: SocketAddr, - pub app_network_bind_address: SocketAddr, -} - -pub struct Observer { - pub oracle_config: OracleBlocksConfig, - pub app_network_endpoint: String, -} - -impl App { - pub fn new(data_type: DataType) -> Self { - let data = Data::new(data_type); - Self { - data: data.clone(), - state_notification: Notification::new(), - current_proof: Proof::new(data), - } - } -} - -impl Default for App { - fn default() -> Self { - Self::new(DataType::Memory) - } -} - -pub async fn run_app(mode: Mode) -> anyhow::Result<()> { - let app = App::new(mode.data_type); - tokio::spawn({ - let app = app.clone(); - let server_bind_address = mode.server_bind_address; - async move { - server::run(app, server_bind_address).await; - } - }); - match mode.mode_type { - ModeType::Publisher(full) => { - run_publisher( - app, - full.signer, - full.oracle_config, - full.oracle_bind_address, - full.app_network_bind_address, - mode.oracle_db_path.as_deref(), - ) - .await? - } - ModeType::Observer(read_only) => { - run_observer( - app, - read_only.oracle_config, - read_only.app_network_endpoint, - mode.oracle_db_path.as_deref(), - ) - .await? - } - } - Ok(()) -} - -/// Update any information that is specific to the app but not part of the state or proof. -async fn update_app(app: &App) -> anyhow::Result<()> { - app.data.update_owners_index().await -} - -async fn state_transition_with_data( - block: Block, - data: &Data, -) -> anyhow::Result<(Block, Height)> { - match data { - Data::Db(db) => { - db.apply(move |tx| -> anyhow::Result<_> { - apply_transition(block, tx, state::state_transition) - }) - .await - } - Data::Memory(mem) => mem.state.access(|s| { - mem.latest_header.access(|h| { - let mut s = MemoryStatePair { - memory_state: s, - header: h, - }; - apply_transition(block, &mut s, state::state_transition) - }) - }), - } -} - -pub async fn run_publisher( - app: App, - signer: PrivateKeySigner, - oracle_config: OracleConfig, - oracle_bind_address: SocketAddr, - app_network_bind_address: SocketAddr, - oracle_db_path: Option<&str>, -) -> anyhow::Result<()> { - let channel_jh = tokio::spawn(void_toolkit::network_channel::replicate::sender( - app.current_proof.clone(), - app_network_bind_address, - 10, - )); - let stream_jh = tokio::spawn(run_publisher_stream( - app, - signer, - oracle_config, - oracle_bind_address, - oracle_db_path.map(|s| s.to_string()), - )); - let (channel_res, stream_res) = futures::future::try_join(channel_jh, stream_jh).await?; - channel_res?; - stream_res?; - Ok(()) -} - -async fn run_publisher_stream( - app: App, - signer: PrivateKeySigner, - oracle_config: OracleConfig, - oracle_bind_address: SocketAddr, - oracle_db_path: Option, -) -> anyhow::Result<()> { - let oracle_db = oracle_db_path - .and_then(|path| void_toolkit::oracle::Db::sqlite(path.into()).ok()) - .unwrap_or_else(void_toolkit::oracle::Db::memory); - - let (last_parent_height, last_parent_hash) = app - .data - .get_latest_header() - .await? - .map_or((None, [0; 32]), |(h, ph)| (Some(h), ph)); - - publisher_oracle( - oracle_config, - oracle_db, - last_parent_height.map_or(0, |h| h + 1), - oracle_bind_address, - 10, - Some(signer.clone()), - ) - .map_err(|e| anyhow::anyhow!("Oracle error: {}", e)) - .block_height_parent(last_parent_height, last_parent_hash) - .and_then(|block| state_transition_with_data(block, &app.data)) - .push_notification(app.state_notification.clone()) - .sign(signer, sign) - .try_for_each(|signed_witness| async { - update_app(&app).await?; - app.current_proof.update(signed_witness).await?; - Ok(()) - }) - .await?; - Ok(()) -} - -pub async fn run_observer( - app: App, - oracle_config: OracleBlocksConfig, - app_network_endpoint: String, - oracle_db_path: Option<&str>, -) -> anyhow::Result<()> { - let (proof_locations_stream_tx, proof_locations_stream_rx) = tokio::sync::mpsc::channel(100); - let receiver_jh = tokio::spawn({ - let current_proof = app.current_proof.clone(); - async move { - void_toolkit::network_channel::replicate::receiver(0, app_network_endpoint) - .try_for_each(|delta| async { - proof::recv_delta(¤t_proof, delta, proof_locations_stream_tx.clone()) - .await; - Ok(()) - }) - .await - } - }); - - let observer_jh = tokio::spawn(run_observer_stream( - app, - oracle_config, - oracle_db_path.map(|s| s.to_string()), - proof_locations_stream_rx, - )); - let (receiver_res, observer_res) = futures::future::try_join(receiver_jh, observer_jh).await?; - receiver_res?; - observer_res?; - Ok(()) -} - -async fn run_observer_stream( - app: App, - mut oracle_config: OracleBlocksConfig, - oracle_db_path: Option, - proof_locations_stream_rx: tokio::sync::mpsc::Receiver, -) -> anyhow::Result<()> { - let proof_locations_stream = - futures::stream::unfold(proof_locations_stream_rx, |mut rx| async { - rx.recv().await.map(|loc| (loc, rx)) - }); - - let oracle_db = oracle_db_path - .and_then(|path| void_toolkit::oracle::Db::sqlite(path.into()).ok()) - .unwrap_or_else(void_toolkit::oracle::Db::memory); - - let (last_parent_height, last_parent_hash) = app - .data - .get_latest_header() - .await? - .map_or((None, [0; 32]), |(h, ph)| (Some(h), ph)); - - oracle_config.height = last_parent_height.map_or(0, |h| h + 1); - - observer_oracle(oracle_config, oracle_db) - .map_err(|e| anyhow::anyhow!("Oracle error: {}", e)) - .block_height_parent(last_parent_height, last_parent_hash) - .blocks_await_proofs(proof_locations_stream) - .and_then(|block| state_transition_with_data(block, &app.data)) - .push_notification(app.state_notification.clone()) - .try_for_each(|_: (_, Height)| async { - update_app(&app).await?; - Ok(()) - }) - .await?; - Ok(()) -} - -pub fn load_config(config_path: &PathBuf) -> anyhow::Result { - load_config_inner(config_path) -} - -pub fn load_blocks_config(config_path: &PathBuf) -> anyhow::Result { - load_config_inner(config_path) -} - -fn load_config_inner(config_path: &PathBuf) -> anyhow::Result { - let config_str = std::fs::read_to_string(config_path) - .map_err(|e| anyhow::anyhow!("Failed to read config file {:?}: {}", config_path, e))?; - - let config = match config_path.extension() { - Some(ext) if ext == "yaml" || ext == "yml" => serde_yaml_ng::from_str(&config_str) - .map_err(|e| anyhow::anyhow!("Failed to parse YAML config: {}", e))?, - Some(ext) if ext == "json" => serde_json::from_str(&config_str) - .map_err(|e| anyhow::anyhow!("Failed to parse JSON config: {}", e))?, - _ => { - return Err(anyhow::anyhow!( - "Unsupported configuration file format: {:?}. Supported formats are .yaml, .yml, and .json", - config_path - )); - } - }; - - Ok(config) -} +mod server; +mod state_reads; + +#[derive(Clone, Default)] +pub struct MerkleData(Arc); + +run_signing!( + add_handlers, + init_db, + state_transition_function, + api_update, + App, + Api, + MerkleData, + Proof +); diff --git a/apps/increment/src/main.rs b/apps/increment/src/main.rs index 502f08e..d2407ae 100644 --- a/apps/increment/src/main.rs +++ b/apps/increment/src/main.rs @@ -1,11 +1,29 @@ use std::{net::SocketAddr, path::PathBuf}; use clap::{Args, Parser, Subcommand}; -use increment::{Mode, signing::get_signer}; +use void_app_node::{ + Mode, Options, + oracle::{ObserverOracle, Oracle, OracleStorageType}, + signing::get_signer, +}; +use void_toolkit::load_config::load_config; #[derive(Parser)] #[command(version, about, long_about = None)] struct Cli { + #[arg(long, short)] + /// The server bind address + server: SocketAddr, + #[arg(long, short)] + tracing: bool, + #[arg(long, short)] + /// Path to the database file + /// If not provided, an in-memory database will be used + db_path: Option, + #[arg(long, short)] + /// Path to the oracle database file + /// If not provided, an in-memory database will be used + oracle_db_path: Option, #[command(subcommand)] command: Commands, } @@ -23,86 +41,58 @@ struct Publisher { #[arg(long, short)] /// The environment variable for the signing private key key: Option, - #[arg(long, short)] - /// Path to the database file - /// If not provided, an in-memory database will be used - db_path: Option, - #[arg(long, short)] - /// Path to the oracle database file - /// If not provided, an in-memory database will be used - oracle_db_path: Option, /// Path to the oracle config file oracle_config: PathBuf, - /// The server bind address - server: SocketAddr, /// The app network bind address - app_network_bind_address: SocketAddr, + node_network_bind_address: SocketAddr, /// The oracle bind address oracle_bind_address: SocketAddr, } #[derive(Args, Clone)] struct Observer { - #[arg(long, short)] - /// Path to the database file - /// If not provided, an in-memory database will be used - db_path: Option, - #[arg(long, short)] - /// Path to the oracle database file - /// If not provided, an in-memory database will be used - oracle_db_path: Option, /// Path to the oracle config file oracle_config: PathBuf, - /// The server bind address - server: SocketAddr, /// The app network bind address - app_network_endpoint: String, + node_network_endpoint: String, } #[tokio::main] async fn main() { let cli = Cli::parse(); - match cli.command { - Commands::Publisher(publisher) => { - let signer = get_signer(publisher.key).unwrap(); - let oracle_config = increment::load_config(&publisher.oracle_config).unwrap(); - let data_type = match publisher.db_path { - Some(db_path) => increment::DataType::Db(db_path), - None => increment::DataType::Memory, - }; - let mode = Mode { - data_type, - server_bind_address: publisher.server, - mode_type: increment::ModeType::Publisher( - increment::Publisher { - oracle_config, - app_network_bind_address: publisher.app_network_bind_address, - signer, - oracle_bind_address: publisher.oracle_bind_address, - } - .into(), - ), - oracle_db_path: publisher.oracle_db_path, - }; - increment::run_app(mode).await.unwrap(); - } - Commands::Observer(observer) => { - let oracle_config = increment::load_blocks_config(&observer.oracle_config).unwrap(); - let data_type = match observer.db_path { - Some(db_path) => increment::DataType::Db(db_path), - None => increment::DataType::Memory, - }; - let mode = Mode { - data_type, - server_bind_address: observer.server, - mode_type: increment::ModeType::Observer(increment::Observer { - oracle_config, - app_network_endpoint: observer.app_network_endpoint, - }), - oracle_db_path: observer.oracle_db_path, - }; - increment::run_app(mode).await.unwrap(); - } - } + let mode = match cli.command { + Commands::Publisher(publisher) => Mode::Publisher( + void_app_node::Publisher { + signer: get_signer(publisher.key).unwrap(), + oracle: Oracle { + oracle_config: load_config(&publisher.oracle_config).unwrap(), + oracle_bind_address: publisher.oracle_bind_address, + oracle_storage: cli + .oracle_db_path + .map_or(OracleStorageType::Memory, OracleStorageType::Db), + }, + node_network_bind_address: publisher.node_network_bind_address, + } + .into(), + ), + Commands::Observer(observer) => Mode::Observer(void_app_node::Observer { + oracle: ObserverOracle { + oracle_config: load_config(&observer.oracle_config).unwrap(), + oracle_storage: cli + .oracle_db_path + .map_or(OracleStorageType::Memory, OracleStorageType::Db), + }, + node_network_endpoint: observer.node_network_endpoint, + }), + }; + + let options = Options { + tracing: cli.tracing, + server_bind_address: cli.server, + db_path: cli.db_path, + mode, + }; + + increment::run_signing_node(options).await.unwrap(); } diff --git a/apps/increment/src/proof.rs b/apps/increment/src/proof.rs deleted file mode 100644 index 459affa..0000000 --- a/apps/increment/src/proof.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::io::Read; - -use futures::StreamExt; -use tokio::sync::{mpsc, watch}; -use void_toolkit::network_channel::replicate::Sender; -use void_toolkit::types::{Height, Signed}; - -use crate::data::Data; -use crate::state::Witness; - -#[derive(Clone)] -pub struct Proof { - data: Data, - new_proof: watch::Sender<()>, -} - -impl Proof { - pub fn new(data: Data) -> Self { - let (tx, _rx) = watch::channel(()); - Self { - data, - new_proof: tx, - } - } - - pub async fn update(&self, signed_witness: Signed>) -> anyhow::Result<()> { - self.data.update_current_proof(signed_witness).await?; - let _ = self.new_proof.send(()); - Ok(()) - } -} - -pub struct SignedWitnessBytes { - pub bytes: Option>, -} - -pub struct IncomingProof(Option>>); - -impl Sender for Proof { - type Marker = String; - - type Delta = Option>>; - - type Bytes = SignedWitnessBytes; - - fn replicate_from( - &self, - _marker: Self::Marker, - ) -> impl futures::Stream + use<> + Send { - let data = self.data.clone(); - futures::stream::unfold( - (true, self.new_proof.subscribe(), data), - move |(first, mut rx, data)| async move { - if !first { - let _ = rx.changed().await; - } - // TODO: Is it worth retrying some amount of times if this fails? - let proof = data.get_current_proof().await.ok(); - Some((proof, (false, rx, data))) - }, - ) - // Can't do much about errors here, just skip them - .filter_map(|x| async move { x }) - } -} - -pub async fn recv_delta( - proof: &Proof, - delta: IncomingProof, - proof_locations_stream_tx: mpsc::Sender, -) { - let IncomingProof(signed) = delta; - if let Some(signed) = signed { - let height = signed.data.block_height; - if proof.update(signed).await.is_ok() { - let _ = proof_locations_stream_tx.send(height).await; - let _ = proof.new_proof.send(()); - } - } -} - -impl From>>> for SignedWitnessBytes { - fn from(value: Option>>) -> Self { - let bytes = value.map(|s| { - let mut bytes = vec![]; - bytes.extend_from_slice(&(s.signature.len() as u64).to_be_bytes()); - bytes.extend_from_slice(&s.signature); - bytes.extend_from_slice(&s.data.block_height.to_be_bytes()); - bytes.extend_from_slice(&s.data.data.count.to_be_bytes()); - bytes.extend_from_slice(&s.data.data.root); - bytes - }); - Self { bytes } - } -} - -impl AsRef<[u8]> for SignedWitnessBytes { - fn as_ref(&self) -> &[u8] { - self.bytes.as_deref().unwrap_or(&[]) - } -} - -impl TryFrom> for IncomingProof { - type Error = anyhow::Error; - - fn try_from(value: Vec) -> Result { - if value.is_empty() { - return Ok(Self(None)); - } - let mut cursor = std::io::Cursor::new(&value); - let mut len_bytes = [0u8; 8]; - cursor.read_exact(&mut len_bytes)?; - let sig_len = u64::from_be_bytes(len_bytes) as usize; - let mut sig_bytes = vec![0u8; sig_len]; - cursor.read_exact(&mut sig_bytes)?; - let mut height_bytes = [0u8; 8]; - cursor.read_exact(&mut height_bytes)?; - let block_height = u64::from_be_bytes(height_bytes); - let mut count_bytes = [0u8; 8]; - cursor.read_exact(&mut count_bytes)?; - let count = u64::from_be_bytes(count_bytes); - let mut root_bytes = [0u8; 32]; - cursor.read_exact(&mut root_bytes)?; - let witness = Witness { - count, - root: root_bytes, - }; - let witness = Height { - block_height, - data: witness, - }; - let signed = Signed { - signature: sig_bytes, - data: witness, - }; - Ok(Self(Some(signed))) - } -} diff --git a/apps/increment/src/server.rs b/apps/increment/src/server.rs index e8fce79..1ee81a5 100644 --- a/apps/increment/src/server.rs +++ b/apps/increment/src/server.rs @@ -1,5 +1,3 @@ -use std::net::SocketAddr; - use alloy::primitives::Address; use axum::{ Json, Router, @@ -8,54 +6,36 @@ use axum::{ routing::get, }; use futures::{Stream, StreamExt}; -use tower_http::cors::CorsLayer; -use void_toolkit::types::{Height, Signed}; +use void_toolkit::types::Height; -use crate::{App, state::Witness}; +use crate::{ + FullDataStoreRef, Node, + state_reads::{generate_merkle_proof, get_current_count, get_owner_counts}, +}; -pub async fn run(app: App, server: SocketAddr) { - let app = Router::new() - .route("/", get(|| async { "OK" })) +pub fn add_handlers(router: Router) -> Router { + router .route("/current-count", get(current_count)) - .route("/get-app-proof/{height}/", get(get_app_proof)) - .route("/get-app-proof/{height}", get(get_app_proof)) .route("/get-merkle-proof/{count}/", get(get_merkle_proof)) .route("/get-merkle-proof/{count}", get(get_merkle_proof)) .route("/get-counts/{address}/", get(get_counts)) .route("/get-counts/{address}", get(get_counts)) - .route("/get-current-count", get(get_current_count)) - .layer(cors_layer()) - .with_state(app); - let listener = tokio::net::TcpListener::bind(server).await.unwrap(); - println!( - "Server running on http://{}", - listener.local_addr().unwrap() - ); - axum::serve(listener, app).await.unwrap(); -} - -/// The default CORS layer. -pub fn cors_layer() -> CorsLayer { - CorsLayer::new() - .allow_origin(tower_http::cors::Any) - .allow_methods([http::Method::GET, http::Method::OPTIONS]) - .allow_headers([http::header::CONTENT_TYPE]) + .route("/get-current-count", get(get_count)) } async fn current_count( - State(app): State, + State(node): State, ) -> Sse>> { - let sse_events = futures::stream::unfold(app, |mut app| async move { - if let Err(e) = app.state_notification.wait().await { - return Some((Err(e.to_string()), app)); + let sse_events = futures::stream::unfold(node, |mut node| async move { + if let Err(e) = node.state_notification().wait().await { + return Some((Err(e.to_string()), node)); } - let count = app - .data - .get_current_count() + let count = node + .read_storage(get_current_count) .await .map(|height| height.map_or(0, |h| h.data)) .map_err(|e| e.to_string()); - Some((count, app)) + Some((count, node)) }) .map(|res| { let event = sse::Event::default() @@ -69,51 +49,34 @@ async fn current_count( Sse::new(sse_events).keep_alive(sse::KeepAlive::default()) } -pub async fn get_app_proof( - State(app): State, - Path(height): Path, -) -> Result>>>, String> { - let proof = app - .data - .get_proof(height) - .await - .map_err(|e| e.to_string())?; - Ok(Json(proof)) -} - pub async fn get_merkle_proof( - State(app): State, + State(node): State, Path(count): Path, ) -> Result>>>, String> { - let proof = app - .data - .generate_merkle_proof(count) + let proof = node + .read_storage(move |s: FullDataStoreRef| generate_merkle_proof(s, count)) .await .map_err(|e| e.to_string())?; Ok(Json(proof)) } pub async fn get_counts( - State(app): State, + State(node): State, Path(address): Path, ) -> Result>, String> { let address: Address = address .parse() .map_err(|e| format!("Invalid address: {e}"))?; - let counts = app - .data - .get_owner_counts(address) + let counts = node + .read_storage(move |s: FullDataStoreRef| get_owner_counts(s, address)) .await .map_err(|e| e.to_string())?; Ok(Json(counts)) } -pub async fn get_current_count( - State(app): State, -) -> Result>>, String> { - let count = app - .data - .get_current_count() +pub async fn get_count(State(node): State) -> Result>>, String> { + let count = node + .read_storage(get_current_count) .await .map_err(|e| e.to_string())?; Ok(Json(count)) diff --git a/apps/increment/src/signing.rs b/apps/increment/src/signing.rs deleted file mode 100644 index f67d2a7..0000000 --- a/apps/increment/src/signing.rs +++ /dev/null @@ -1,31 +0,0 @@ -use alloy::{ - hex::FromHex, - primitives::{FixedBytes, keccak256}, - signers::{SignerSync, local::PrivateKeySigner}, - sol_types::SolValue, -}; -use void_toolkit::types::Height; - -/// Get a signer from an environment variable or generate a random one. -pub fn get_signer(key: Option) -> anyhow::Result { - match key { - Some(key) => { - let private_key = std::env::var(key)?; - Ok(PrivateKeySigner::from_bytes(&FixedBytes::from_hex( - private_key, - )?)?) - } - None => Ok(PrivateKeySigner::random()), - } -} - -/// Sign a proof using the provided signer. -pub fn sign(signer: &PrivateKeySigner, summary: Height<[u8; 32]>) -> anyhow::Result> { - let encoded = (summary.block_height, summary.data).abi_encode_packed(); - - // Hash the encoded data - let hash = keccak256(encoded); - - // Sign the hash - Ok(signer.sign_hash_sync(&hash)?.into()) -} diff --git a/apps/increment/src/state.rs b/apps/increment/src/state.rs deleted file mode 100644 index 26941e3..0000000 --- a/apps/increment/src/state.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::convert::Infallible; - -use alloy::{ - primitives::{FixedBytes, Log, keccak256}, - sol_types::{SolEvent, SolValue}, -}; -use serde::{Deserialize, Serialize}; -use void_toolkit::merkle::fixed_sparse_merkle::{Sha256, SparseMerkleTree, get_hashes_height}; -use void_toolkit::oracle_types::Event; - -use crate::Increment::Incremented; - -pub type InMemoryMerkle = SparseMerkleTree<{ get_hashes_height(64) }, Sha256>; - -pub trait State { - type Error; - fn increment_count(&mut self) -> Result<(), Self::Error>; - fn add_owner(&mut self, owner: [u8; 20]) -> Result<(), Self::Error>; -} -/// The state of the increment application. -#[derive(Debug)] -pub struct MemoryState { - /// The current count of increments. - pub count: u64, - /// A mapping of increment counts to sender addresses that caused them. - pub owners: InMemoryMerkle, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Witness { - pub count: u64, - pub root: [u8; 32], -} - -impl Default for MemoryState { - fn default() -> Self { - Self { - count: Default::default(), - owners: InMemoryMerkle::new_in_memory(), - } - } -} - -pub fn state_transition(block: &void_toolkit::types::Block, state: &mut S) -> anyhow::Result<()> -where - anyhow::Error: From<::Error>, - S: State, -{ - for bytes in &block.events { - let Some(input) = decode_input(bytes) else { - continue; - }; - update(state, input)?; - } - Ok(()) -} - -fn decode_input(bytes: &[u8]) -> Option { - let event = Event::from_bytes(bytes).ok()?; - let (log, _, _): (Log, Option>, Option) = - serde_json::from_slice(&event.data).ok()?; - let event = Incremented::decode_log(&log).ok()?.data; - Some(event) -} - -/// The actual state transition function -pub fn update(state: &mut S, event: Incremented) -> anyhow::Result<()> -where - anyhow::Error: From<::Error>, - S: State, -{ - // Increment the count - state.increment_count()?; - - // Insert the sender address into the owners map - state.add_owner(event.sender.into())?; - - Ok(()) -} - -impl State for MemoryState { - type Error = Infallible; - - fn increment_count(&mut self) -> Result<(), Self::Error> { - self.count = self.count.saturating_add(1); - Ok(()) - } - - fn add_owner(&mut self, owner: [u8; 20]) -> Result<(), Self::Error> { - self.owners.insert(self.count, owner) - } -} - -impl From<&mut MemoryState> for Witness { - fn from(state: &mut MemoryState) -> Self { - let root = state.owners.root().expect("infallible"); - Witness { - count: state.count, - root, - } - } -} - -impl From<&Witness> for [u8; 32] { - fn from(witness: &Witness) -> Self { - let encoded = (witness.count, witness.root).abi_encode_packed(); - - // Hash the encoded data - *keccak256(encoded) - } -} diff --git a/apps/increment/src/state_reads.rs b/apps/increment/src/state_reads.rs new file mode 100644 index 0000000..0c2f9f9 --- /dev/null +++ b/apps/increment/src/state_reads.rs @@ -0,0 +1,48 @@ +use alloy::primitives::Address; +use void_toolkit::types::Height; + +use crate::FullDataStoreRef; + +pub fn get_current_count(storage: FullDataStoreRef) -> anyhow::Result>> { + match storage { + void_app_node::storage::DataStorageRef::Db(tx) => crate::db::get_current_count(tx), + void_app_node::storage::DataStorageRef::Memory(mem) => { + Ok(mem.header.height().map(|height| Height { + block_height: height, + data: mem.app.count, + })) + } + } +} + +pub fn generate_merkle_proof( + storage: FullDataStoreRef, + count: u64, +) -> anyhow::Result>>> { + match storage { + void_app_node::storage::DataStorageRef::Db(tx) => { + crate::db::generate_merkle_proof(tx, count) + } + void_app_node::storage::DataStorageRef::Memory(mem) => match mem.header.height() { + Some(block_height) => Ok(Some(Height { + block_height, + data: mem.app.owners.generate_proof(count)?, + })), + None => Ok(None), + }, + } +} + +pub fn get_owner_counts(storage: FullDataStoreRef, address: Address) -> anyhow::Result> { + match storage { + void_app_node::storage::DataStorageRef::Db(tx) => { + crate::db::get_owner_counts(tx, address.into()) + } + void_app_node::storage::DataStorageRef::Memory(mem) => Ok(mem + .api + .counts_per_owner_index + .get(&address) + .cloned() + .unwrap_or_default()), + } +} diff --git a/apps/increment/tests/tests.rs b/apps/increment/tests/tests.rs index 5f2ef21..9c685ef 100644 --- a/apps/increment/tests/tests.rs +++ b/apps/increment/tests/tests.rs @@ -1,4 +1,4 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, path::PathBuf}; use alloy::{ node_bindings::Reth, @@ -9,14 +9,19 @@ use alloy::{ sol_types::SolEvent, }; use futures::{StreamExt, TryStreamExt}; -use increment::{Mode, ModeType, Observer, Publisher, state::Witness}; +use increment::{self as increment, Proof}; use reqwest::ClientBuilder; +use tempfile::TempDir; use tokio::sync::mpsc; use tokio_util::{ codec::{FramedRead, LinesCodec}, io::StreamReader, }; -use void_toolkit::types::{Height, Signed}; +use void_app_node::{ + oracle::{ObserverOracle, Oracle, OracleStorageType}, + proof::SignedProof, +}; +use void_toolkit::types::Height; sol!( #[allow(missing_docs)] @@ -25,12 +30,51 @@ sol!( "contracts/build/increment_abi.json" ); +struct DbPaths { + _db_path: TempDir, + db_file: PathBuf, + oracle_db_file: PathBuf, +} + +struct Nodes { + publisher: DbPaths, + observer: DbPaths, +} + #[tokio::test] #[ignore = "Must be run in a shell that has a Reth node on path"] -async fn test_api() { - let path = tempfile::tempdir().unwrap(); +async fn test_api_mem() { + test_api(None).await; +} + +#[tokio::test] +#[ignore = "Must be run in a shell that has a Reth node on path"] +async fn test_api_db() { let db_path = tempfile::tempdir().unwrap(); - let observer_db_path = tempfile::tempdir().unwrap(); + let db_file = db_path.path().join("increment.db").to_owned(); + let oracle_db_file = db_path.path().join("oracle.db").to_owned(); + let publisher = DbPaths { + _db_path: db_path, + db_file, + oracle_db_file, + }; + let db_path = tempfile::tempdir().unwrap(); + let db_file = db_path.path().join("increment.db").to_owned(); + let oracle_db_file = db_path.path().join("oracle.db").to_owned(); + let observer = DbPaths { + _db_path: db_path, + db_file, + oracle_db_file, + }; + let db_paths = Nodes { + publisher, + observer, + }; + test_api(Some(db_paths)).await; +} + +async fn test_api(nodes: Option) { + let path = tempfile::tempdir().unwrap(); let reth = Reth::new() .dev() .block_time("2s") @@ -81,33 +125,35 @@ async fn test_api() { }; let config = void_toolkit::oracle_types::config::Config { query, block }; + let oracle = Oracle { + oracle_config: config, + oracle_bind_address: SocketAddr::from(([127, 0, 0, 1], 4000)), + oracle_storage: nodes.as_ref().map_or(OracleStorageType::Memory, |nodes| { + OracleStorageType::Db(nodes.publisher.oracle_db_file.clone()) + }), + }; let increment_endpoint = "127.0.0.1:3500"; - let db_file = db_path.path().join("increment.db"); - let oracle_db_file = db_path.path().join("oracle.db"); - let data_type = increment::DataType::Db(db_file.to_str().unwrap().to_string()); - let oracle_db_path = oracle_db_file.to_str().unwrap().to_string(); - + let db_path = nodes.as_ref().map(|nodes| nodes.publisher.db_file.clone()); tokio::spawn({ let increment_endpoint = increment_endpoint.to_string(); let signer = signer.clone(); async move { - let mode = Mode { - data_type, + let options = void_app_node::Options { + tracing: true, server_bind_address: increment_endpoint.parse().unwrap(), - mode_type: ModeType::Publisher( - Publisher { - oracle_config: config, + db_path, + mode: void_app_node::Mode::Publisher( + void_app_node::Publisher { + oracle, signer, - oracle_bind_address: SocketAddr::from(([127, 0, 0, 1], 4000)), - app_network_bind_address: SocketAddr::from(([127, 0, 0, 1], 5000)), + node_network_bind_address: SocketAddr::from(([127, 0, 0, 1], 5000)), } .into(), ), - oracle_db_path: Some(oracle_db_path), }; - increment::run_app(mode).await.unwrap(); + increment::run_signing_node(options).await.unwrap(); panic!("increment app closed unexpectedly"); } }); @@ -127,24 +173,31 @@ async fn test_api() { let read_only_increment_endpoint = "127.0.0.1:3600"; - let db_file = observer_db_path.path().join("increment.db"); - let oracle_db_file = observer_db_path.path().join("oracle.db"); - let data_type = increment::DataType::Db(db_file.to_str().unwrap().to_string()); - let oracle_db_path = oracle_db_file.to_str().unwrap().to_string(); + // let db_file = observer_db_path.path().join("increment.db"); + // let oracle_db_file = observer_db_path.path().join("oracle.db"); + // let data_type = increment::DataType::Db(db_file.to_str().unwrap().to_string()); + // let oracle_db_path = oracle_db_file.to_str().unwrap().to_string(); + let oracle = ObserverOracle { + oracle_config: config, + oracle_storage: nodes.as_ref().map_or(OracleStorageType::Memory, |nodes| { + OracleStorageType::Db(nodes.observer.oracle_db_file.clone()) + }), + }; + let db_path = nodes.as_ref().map(|nodes| nodes.observer.db_file.clone()); tokio::spawn({ let increment_endpoint = read_only_increment_endpoint.to_string(); async move { - let mode = Mode { - data_type, + let options = void_app_node::Options { + tracing: true, server_bind_address: increment_endpoint.parse().unwrap(), - mode_type: ModeType::Observer(Observer { - oracle_config: config, - app_network_endpoint: "http://localhost:5000".to_string(), + db_path, + mode: void_app_node::Mode::Observer(void_app_node::Observer { + oracle, + node_network_endpoint: "http://localhost:5000".to_string(), }), - oracle_db_path: Some(oracle_db_path), }; - increment::run_app(mode).await.unwrap(); + increment::run_signing_node(options).await.unwrap(); panic!("increment app closed unexpectedly"); } }); @@ -190,7 +243,8 @@ async fn test_api() { assert_eq!(current_state.data, 2); let witness = get_app_proof(increment_endpoint, current_state.block_height) .await - .unwrap(); + .unwrap() + .proof; assert_eq!(witness.data.data.count, 2); tokio::time::sleep(std::time::Duration::from_secs(1)).await; @@ -201,7 +255,8 @@ async fn test_api() { assert_eq!(current_state.data, 2); let witness = get_app_proof(read_only_increment_endpoint, current_state.block_height) .await - .unwrap(); + .unwrap() + .proof; assert_eq!(witness.data.data.count, 2); let merkle_proof = get_merkle_proof(increment_endpoint, 2).await.unwrap(); @@ -214,7 +269,7 @@ async fn test_api() { increment_contract .claim_count( witness.data.data.count, - witness.data.block_height, + current_state.block_height, witness.signature.into(), witness.data.data.root.into(), merkle_proof.data.into_iter().map(Into::into).collect(), @@ -289,27 +344,32 @@ async fn test_run_for_front_end() { let config = void_toolkit::oracle_types::config::Config { query, block }; + let oracle = Oracle { + oracle_config: config, + oracle_bind_address: SocketAddr::from(([127, 0, 0, 1], 4000)), + oracle_storage: OracleStorageType::Memory, + }; + let increment_endpoint = "127.0.0.1:3500"; tokio::spawn({ let increment_endpoint = increment_endpoint.to_string(); let signer = signer.clone(); async move { - let mode = Mode { - data_type: increment::DataType::Memory, + let options = void_app_node::Options { + tracing: true, server_bind_address: increment_endpoint.parse().unwrap(), - mode_type: ModeType::Publisher( - Publisher { - oracle_config: config, + db_path: None, + mode: void_app_node::Mode::Publisher( + void_app_node::Publisher { + oracle, signer, - oracle_bind_address: SocketAddr::from(([127, 0, 0, 1], 4000)), - app_network_bind_address: SocketAddr::from(([127, 0, 0, 1], 5000)), + node_network_bind_address: SocketAddr::from(([127, 0, 0, 1], 5000)), } .into(), ), - oracle_db_path: None, }; - increment::run_app(mode).await.unwrap(); + increment::run_signing_node(options).await.unwrap(); panic!("increment app closed unexpectedly"); } }); @@ -381,7 +441,7 @@ pub async fn subscribe(endpoint: String) -> mpsc::Receiver { rx } -async fn get_app_proof(endpoint: &str, block_height: u64) -> Option>> { +async fn get_app_proof(endpoint: &str, block_height: u64) -> Option> { get(endpoint, format!("get-app-proof/{block_height}")).await } diff --git a/apps/transfers/Cargo.toml b/apps/transfers/Cargo.toml index 8b820bf..787427c 100644 --- a/apps/transfers/Cargo.toml +++ b/apps/transfers/Cargo.toml @@ -14,15 +14,12 @@ axum.workspace = true clap.workspace = true futures.workspace = true hex.workspace = true -http.workspace = true -reqwest.workspace = true +rusqlite.workspace = true serde.workspace = true -serde_json.workspace = true -tempfile.workspace = true tokio.workspace = true -tokio-util.workspace = true -tower-http.workspace = true tracing.workspace = true +tracing-subscriber.workspace = true +void-app-node.workspace = true void-toolkit = { workspace = true, features = [ "app", "hash", @@ -40,4 +37,7 @@ alloy = { workspace = true, features = [ "signer-mnemonic", ] } rand = "0.8" +reqwest.workspace = true +serde_json.workspace = true +tempfile.workspace = true tokio-util.workspace = true diff --git a/apps/transfers/src/api.rs b/apps/transfers/src/api.rs new file mode 100644 index 0000000..776286b --- /dev/null +++ b/apps/transfers/src/api.rs @@ -0,0 +1,106 @@ +use std::collections::HashMap; + +use alloy::primitives::B256; +use void_toolkit::{merkle::GenericMerkleProof as MerkleProof, types::Block}; + +use crate::{ + FullDataStore, + app::{AppProofs, decode_input, types::Burn}, +}; + +#[derive(Default)] +pub struct Api { + /// Stored withdrawal burns (burn_index -> burn) + pub stored_burns: HashMap, +} + +pub trait ApiState { + fn balance_root(&self) -> B256; + fn burn_merkle_proof(&self, withdrawal_id: B256) -> MerkleProof; + fn insert_stored_burn(&mut self, withdrawal_id: B256, burn: Burn); + fn remove_stored_burn(&mut self, withdrawal_id: B256) -> bool; +} + +impl ApiState for FullDataStore<'_, '_> { + fn balance_root(&self) -> B256 { + match self { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => mem.app.balance_root(), + } + } + + fn burn_merkle_proof(&self, withdrawal_id: B256) -> MerkleProof { + match self { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => { + mem.app.burn_merkle_proof(withdrawal_id) + } + } + } + + fn insert_stored_burn(&mut self, withdrawal_id: B256, burn: Burn) { + match self { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => { + mem.api.stored_burns.insert(withdrawal_id, burn); + } + } + } + + fn remove_stored_burn(&mut self, withdrawal_id: B256) -> bool { + match self { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => { + mem.api.stored_burns.remove(&withdrawal_id).is_some() + } + } + } +} + +pub fn api_update(block: &Block, mut state: impl ApiState) -> anyhow::Result<()> { + for bytes in &block.events { + let Ok(input) = decode_input(bytes) else { + continue; + }; + match input { + crate::app::AppEvent::WithdrawalRequest(withdrawal_request) => { + // Prepare burn event + let burn = Burn { + withdrawal_id: withdrawal_request.withdrawalId, + user: withdrawal_request.user, + token: withdrawal_request.token, + amount: withdrawal_request.amount, + salt: withdrawal_request.salt, + chain_id: withdrawal_request.chain_id, + }; + handle_withdrawal_request(&mut state, burn) + } + crate::app::AppEvent::WithdrawalCompleted(withdrawal_completed) => { + handle_withdrawal_completed(&mut state, withdrawal_completed.withdrawalId) + } + _ => (), + } + } + Ok(()) +} + +/// Process a withdrawal request - burns tokens and generates withdrawal proof +pub fn handle_withdrawal_request(state: &mut impl ApiState, burn: Burn) { + // Store burn for user to retrieve + state.insert_stored_burn(burn.withdrawal_id, burn); +} + +/// Handle withdrawal completed event - cleanup stored burn +pub fn handle_withdrawal_completed(state: &mut impl ApiState, withdrawal_id: B256) { + if state.remove_stored_burn(withdrawal_id) { + println!( + "Cleaned up withdrawal burn for withdrawal ID: {}", + withdrawal_id + ); + } else { + println!( + "Warning: No stored burn found for withdrawal ID: {}", + withdrawal_id + ); + } +} diff --git a/apps/transfers/src/api_state.rs b/apps/transfers/src/api_state.rs deleted file mode 100644 index 60d5b57..0000000 --- a/apps/transfers/src/api_state.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::collections::HashMap; - -use alloy::primitives::{Address, B256, U256}; -use void_toolkit::{ - app::UpdateLatestBlock, - types::{Block, Height, Signed}, -}; - -use crate::{ - app_state::{ - AccountProofs, AccountStateRef, AppState, Burn, Commit, PendingWithdrawal, decode_input, - }, - state::State, -}; - -#[derive(Default)] -pub struct ApiState { - pub latest_block_height: Option, - pub latest_block_hash: [u8; 32], - - /// Stored withdrawal proofs (burn_index -> proof) - pub stored_proofs: HashMap, - - /// Signed Commitment of the latest state - // TODO: Consider removing the Option and always having a commitment (a signed empty commitment - // at genesis) - pub latest_commitment: Option>>, -} - -impl UpdateLatestBlock for ApiState { - type Error = std::convert::Infallible; - - fn update_latest_block(&mut self, height: u64, hash: [u8; 32]) -> Result<(), Self::Error> { - self.latest_block_height = Some(height); - self.latest_block_hash = hash; - Ok(()) - } -} - -pub struct AllState<'a, App: AccountStateRef> { - pub app: &'a App, - pub api: &'a mut ApiState, -} - -pub fn update(block: &Block, mut state: AllState<'_, impl AccountStateRef + AccountProofs>) { - for bytes in &block.events { - let Ok(input) = decode_input(bytes) else { - continue; - }; - match input { - crate::app_state::AppEvent::WithdrawalRequest(withdrawal_request) => { - handle_withdrawal_request( - &mut state, - withdrawal_request.user, - withdrawal_request.token, - withdrawal_request.amount, - withdrawal_request.withdrawalId, - withdrawal_request.salt, - withdrawal_request.chain_id, - ) - } - crate::app_state::AppEvent::WithdrawalCompleted(withdrawal_completed) => { - handle_withdrawal_completed(&mut state, withdrawal_completed.withdrawalId) - } - _ => (), - } - } -} - -/// Process a withdrawal request - burns tokens and generates withdrawal proof -pub fn handle_withdrawal_request( - state: &mut AllState<'_, impl AccountStateRef + AccountProofs>, - user: Address, - token: Address, - amount: U256, - withdrawal_id: B256, - salt: B256, - chain_id: U256, -) { - // Prepare burn event - let burn = Burn { - user, - token, - amount, - salt, - withdrawal_id, - chain_id, - }; - - // Step 2: Get balance root at burn time (after balance update) - let balance_root_at_burn = state.app.balance_root(); - - // Step 4: Generate burn proof - let burn_proof = state.app.burn_merkle_proof(withdrawal_id); - - // Step 5: Create pending withdrawal - let pending_withdrawal = PendingWithdrawal { - burn: burn.clone(), - burn_proof, - burn_index: withdrawal_id, - balance_root_at_burn, - }; - - // Step 6: Store proof for user to retrieve - state - .api - .stored_proofs - .insert(withdrawal_id, pending_withdrawal.clone()); -} - -/// Handle withdrawal completed event - cleanup stored proof -pub fn handle_withdrawal_completed( - state: &mut AllState<'_, impl AccountStateRef>, - withdrawal_id: B256, -) { - if state.api.stored_proofs.remove(&withdrawal_id).is_some() { - println!( - "Cleaned up withdrawal proof for withdrawal ID: {}", - withdrawal_id - ); - } else { - println!( - "Warning: No stored proof found for withdrawal ID: {}", - withdrawal_id - ); - } -} - -impl<'a> From<&'a mut State> for AllState<'a, AppState> { - fn from(state: &'a mut State) -> Self { - AllState { - app: &state.app_state, - api: &mut state.api_state, - } - } -} diff --git a/apps/transfers/src/app_state.rs b/apps/transfers/src/app.rs similarity index 58% rename from apps/transfers/src/app_state.rs rename to apps/transfers/src/app.rs index 14f53e4..ef9a9e3 100644 --- a/apps/transfers/src/app_state.rs +++ b/apps/transfers/src/app.rs @@ -1,123 +1,29 @@ -use alloy::{ - primitives::{Address, B256, U256, keccak256}, - sol_types::SolValue, -}; -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use void_toolkit::oracle_decoder::decode_events; -use void_toolkit::types::Signed; +use alloy::primitives::{Address, B256, U256}; +use alloy::{primitives::keccak256, sol_types::SolValue}; +use void_app_node::proof::ProofConversions; +use void_toolkit::merkle::GenericMerkleProof as MerkleProof; use void_toolkit::{ - merkle::generic_sparse_merkle::{ - GenericSparseMerkleTree, KeccakHasher, MerkleProof, MerkleValue, - }, - types::Height, + merkle::{GenericSparseMerkleTree, generic_sparse_merkle::KeccakHasher}, + oracle_decoder::decode_events, + types::Block, }; use crate::Transfers::{Deposit, TransferRequest, WithdrawalCompleted, WithdrawalRequest}; +use crate::DataStore; +use crate::app::types::{Balance, Burn}; + decode_events!( Deposit, TransferRequest, + WithdrawalCompleted, WithdrawalRequest, - WithdrawalCompleted ); -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct Balance(pub U256); - -impl Balance { - /// Safe addition with overflow checking - pub fn checked_add(self, amount: U256) -> Result { - self.0 - .checked_add(amount) - .map(Balance) - .ok_or_else(|| anyhow::anyhow!("Balance overflow: {} + {}", self.0, amount)) - } - - /// Safe subtraction with underflow checking - pub fn checked_sub(self, amount: U256) -> Result { - self.0 - .checked_sub(amount) - .map(Balance) - .ok_or_else(|| anyhow::anyhow!("Insufficient balance: {} - {}", self.0, amount)) - } -} - -impl MerkleValue for Balance { - fn to_bytes(&self) -> impl AsRef<[u8]> { - self.0.to_be_bytes::<32>() - } - - fn empty() -> Self { - Balance(U256::ZERO) - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Burn { - pub user: Address, - pub token: Address, - pub amount: U256, - pub salt: B256, - pub withdrawal_id: B256, - pub chain_id: U256, -} - -impl MerkleValue for Burn { - fn to_bytes(&self) -> impl AsRef<[u8]> { - let mut bytes = Vec::new(); - bytes.extend_from_slice(self.user.as_slice()); // 20 bytes - bytes.extend_from_slice(self.token.as_slice()); // 20 bytes - bytes.extend_from_slice(&self.amount.to_be_bytes::<32>()); // 32 bytes - bytes.extend_from_slice(self.salt.as_ref()); // 32 bytes - bytes.extend_from_slice(self.withdrawal_id.as_ref()); // 32 bytes - bytes.extend_from_slice(&self.chain_id.to_be_bytes::<32>()); // 32 bytes for U256 - bytes - } - - fn empty() -> Self { - Self { - user: Address::ZERO, - token: Address::ZERO, - amount: U256::ZERO, - salt: B256::ZERO, - withdrawal_id: B256::ZERO, - chain_id: U256::ZERO, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Withdrawal { - pub burn: Burn, - pub burn_proof: MerkleProof, - pub burn_index: B256, - pub balance_root_at_burn: B256, - pub state_commitment: Signed>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PendingWithdrawal { - pub burn: Burn, - pub burn_proof: MerkleProof, - pub burn_index: B256, - pub balance_root_at_burn: B256, -} - -/// The witness struct for the bridge state -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Commit { - pub balance_root: [u8; 32], - pub burn_root: [u8; 32], -} +pub mod types; -// NOTE: Currently, State contains data that is included in the proofs generated by the app, as well -// as data that is not included in the proofs (like stored_proofs and latest_commitment)! These -// non-proven fields will most likely be migrated to the App struct when the toolkit supports doing -// this in a clean way. -/// The state of the bridge application #[derive(Debug)] -pub struct AppState { +pub struct App { /// Account balances tree pub balance_tree: GenericSparseMerkleTree, @@ -125,27 +31,31 @@ pub struct AppState { pub burn_tree: GenericSparseMerkleTree, } -pub trait AccountStateRef { - fn get_balance(&self, user: Address, token: Address) -> Balance; +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Proof { + pub balance_root: [u8; 32], + pub burn_root: [u8; 32], } -pub trait AccountState: AccountStateRef { +pub trait AppStateRef { + fn get_balance(&self, user: Address, token: Address) -> Balance; +} +pub trait AppState: AppStateRef { fn insert_balance(&mut self, user: Address, token: Address, new_balance: Balance); fn insert_burn(&mut self, withdrawal_id: B256, burn: Burn); } -pub trait AccountProofs { +pub trait AppProofs { fn balance_root(&self) -> B256; fn burn_merkle_proof(&self, withdrawal_id: B256) -> MerkleProof; } -impl AccountStateRef for AppState { +impl AppStateRef for App { fn get_balance(&self, user: Address, token: Address) -> Balance { self.balance_tree.get(user, token) } } - -impl AccountState for AppState { +impl AppState for App { fn insert_balance(&mut self, user: Address, token: Address, new_balance: Balance) { self.balance_tree.insert(user, token, new_balance); } @@ -155,16 +65,7 @@ impl AccountState for AppState { } } -impl Default for AppState { - fn default() -> Self { - Self { - balance_tree: GenericSparseMerkleTree::new(), - burn_tree: GenericSparseMerkleTree::new(), - } - } -} - -impl AccountProofs for AppState { +impl AppProofs for App { fn balance_root(&self) -> B256 { self.balance_tree.root() } @@ -174,25 +75,50 @@ impl AccountProofs for AppState { } } -impl AppState { - /// Create a new bridge state - pub fn new() -> Self { - Self::default() +impl AppStateRef for DataStore<'_, '_> { + fn get_balance(&self, user: Address, token: Address) -> Balance { + match self { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => mem.get_balance(user, token), + } + } +} + +impl AppState for DataStore<'_, '_> { + fn insert_balance(&mut self, user: Address, token: Address, new_balance: Balance) { + match self { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => { + mem.insert_balance(user, token, new_balance) + } + } } - /// Get current balance for a user and token - pub fn get_balance(&self, user: Address, token: Address) -> Balance { - get_balance(self, user, token) + fn insert_burn(&mut self, withdrawal_id: B256, burn: Burn) { + match self { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => { + mem.insert_burn(withdrawal_id, burn) + } + } } +} - /// Get the current balance tree root - pub fn current_balance_root(&self) -> B256 { - self.balance_tree.root() +impl AppProofs for DataStore<'_, '_> { + fn balance_root(&self) -> B256 { + match self { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => mem.balance_root(), + } } - /// Get the current burn tree root - pub fn current_burn_root(&self) -> B256 { - self.burn_tree.root() + fn burn_merkle_proof(&self, withdrawal_id: B256) -> MerkleProof { + match self { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => { + mem.burn_merkle_proof(withdrawal_id) + } + } } } @@ -200,17 +126,14 @@ impl AppState { // FUNCTIONS FOR STATE TRANSITIONS // ================================ -pub fn state_transition( - block: &void_toolkit::types::Block, - state: &mut impl AccountState, -) -> Result<()> { +pub fn state_transition_function(block: &Block, mut state: impl AppState) -> anyhow::Result<()> { for bytes in &block.events { let Ok(input) = decode_input(bytes) else { continue; }; // Log errors but continue processing other events to maintain system availability - if let Err(e) = update(state, input) { + if let Err(e) = update(&mut state, input) { // NOTE: if any errors are detected in the state transition logic they will currently // be returned and logged here. The system will continue processing other events, but // state transitions will be skipped for events that cause errors! @@ -219,6 +142,7 @@ pub fn state_transition( // - transfer to self // - overflow/underflow in balance calculations // TODO: add tracing. + // FIX: Return db errors eprintln!("Warning: Failed to process event: {}", e); // Continue processing other events rather than failing the entire block } @@ -227,7 +151,7 @@ pub fn state_transition( } /// Handle all the different variants of AppEvent and update state accordingly -pub fn update(state: &mut impl AccountState, event: AppEvent) -> Result<()> { +pub fn update(state: &mut impl AppState, event: AppEvent) -> anyhow::Result<()> { match event { AppEvent::Deposit(d) => { handle_deposit(state, d.user, d.token, d.amount) @@ -259,17 +183,17 @@ pub fn update(state: &mut impl AccountState, event: AppEvent) -> Result<()> { } /// Get current balance for a user and token -pub fn get_balance(state: &impl AccountState, user: Address, token: Address) -> Balance { +pub fn get_balance(state: &impl AppState, user: Address, token: Address) -> Balance { state.get_balance(user, token) } /// Process a deposit - increases user's balance pub fn handle_deposit( - state: &mut impl AccountState, + state: &mut impl AppState, user: Address, token: Address, amount: U256, -) -> Result<()> { +) -> anyhow::Result<()> { if amount == U256::ZERO { return Err(anyhow::anyhow!("Deposit amount must be greater than zero")); } @@ -283,12 +207,12 @@ pub fn handle_deposit( /// Process a transfer between users - decreases sender balance, increases recipient balance pub fn handle_transfer( - state: &mut impl AccountState, + state: &mut impl AppState, from: Address, to: Address, token: Address, amount: U256, -) -> Result<()> { +) -> anyhow::Result<()> { if amount == U256::ZERO { return Err(anyhow::anyhow!("Transfer amount must be greater than zero")); } @@ -313,14 +237,14 @@ pub fn handle_transfer( /// Process a withdrawal request - burns tokens and generates withdrawal proof pub fn handle_withdrawal_request( - state: &mut impl AccountState, + state: &mut impl AppState, user: Address, token: Address, amount: U256, withdrawal_id: B256, salt: B256, chain_id: U256, -) -> Result<()> { +) -> anyhow::Result<()> { if amount == U256::ZERO { return Err(anyhow::anyhow!( "Withdrawal amount must be greater than zero" @@ -355,25 +279,62 @@ pub fn handle_withdrawal_request( // TRAIT IMPLEMENTATIONS // ================================ -impl From<&mut AppState> for Commit { - fn from(state: &mut AppState) -> Commit { - let balance_root = state.balance_tree.root(); - let burn_root = state.burn_tree.root(); - - Commit { - balance_root: *balance_root, - burn_root: *burn_root, +impl Default for App { + fn default() -> Self { + Self { + balance_tree: GenericSparseMerkleTree::new(), + burn_tree: GenericSparseMerkleTree::new(), } } } -impl From<&Commit> for [u8; 32] { - fn from(commit: &Commit) -> [u8; 32] { +impl ProofConversions for Proof { + type App = App; + type DbData = (); + + fn digest(&self) -> [u8; 32] { // Pack witness data for hashing - let encoded = (commit.balance_root, commit.burn_root).abi_encode_packed(); + let encoded = (self.balance_root, self.burn_root).abi_encode_packed(); // Hash the encoded data *keccak256(encoded) } + + fn into_bytes(self) -> Vec { + let mut vec = Vec::with_capacity(32 + 32); + vec.extend(self.balance_root); + vec.extend(self.burn_root); + vec + } + + fn try_from_bytes(bytes: &[u8]) -> anyhow::Result { + if bytes.len() != 64 { + return Err(anyhow::anyhow!( + "Invalid proof length: expected 64, got {}", + bytes.len() + )); + } + let balance_root = bytes[0..32].try_into()?; + let burn_root = bytes[32..64].try_into()?; + Ok(Proof { + balance_root, + burn_root, + }) + } + + fn try_from_storage(store: DataStore<'_, '_>) -> anyhow::Result { + match store { + void_app_node::storage::DataStorage::Db(_) => todo!(), + void_app_node::storage::DataStorage::Memory(mem) => { + let balance_root = mem.balance_tree.root(); + let burn_root = mem.burn_tree.root(); + + Ok(Proof { + balance_root: *balance_root, + burn_root: *burn_root, + }) + } + } + } } // ================================ diff --git a/apps/transfers/src/app/types.rs b/apps/transfers/src/app/types.rs new file mode 100644 index 0000000..8c5c2f0 --- /dev/null +++ b/apps/transfers/src/app/types.rs @@ -0,0 +1,84 @@ +use alloy::primitives::{Address, B256, U256}; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use void_toolkit::merkle::generic_sparse_merkle::{MerkleProof, MerkleValue}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Balance(pub U256); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Burn { + pub user: Address, + pub token: Address, + pub amount: U256, + pub salt: B256, + pub withdrawal_id: B256, + pub chain_id: U256, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Withdrawal { + pub burn: Burn, + pub burn_proof: MerkleProof, + pub burn_index: B256, + pub balance_root_at_burn: B256, +} + +#[derive(Debug, Clone)] +pub struct MerkleData { + pub burn_proof: MerkleProof, + pub burn_index: B256, + pub balance_root_at_burn: B256, +} + +impl Balance { + /// Safe addition with overflow checking + pub fn checked_add(self, amount: U256) -> Result { + self.0 + .checked_add(amount) + .map(Balance) + .ok_or_else(|| anyhow::anyhow!("Balance overflow: {} + {}", self.0, amount)) + } + + /// Safe subtraction with underflow checking + pub fn checked_sub(self, amount: U256) -> Result { + self.0 + .checked_sub(amount) + .map(Balance) + .ok_or_else(|| anyhow::anyhow!("Insufficient balance: {} - {}", self.0, amount)) + } +} + +impl MerkleValue for Balance { + fn to_bytes(&self) -> impl AsRef<[u8]> { + self.0.to_be_bytes::<32>() + } + + fn empty() -> Self { + Balance(U256::ZERO) + } +} + +impl MerkleValue for Burn { + fn to_bytes(&self) -> impl AsRef<[u8]> { + let mut bytes = Vec::new(); + bytes.extend_from_slice(self.user.as_slice()); // 20 bytes + bytes.extend_from_slice(self.token.as_slice()); // 20 bytes + bytes.extend_from_slice(&self.amount.to_be_bytes::<32>()); // 32 bytes + bytes.extend_from_slice(self.salt.as_ref()); // 32 bytes + bytes.extend_from_slice(self.withdrawal_id.as_ref()); // 32 bytes + bytes.extend_from_slice(&self.chain_id.to_be_bytes::<32>()); // 32 bytes for U256 + bytes + } + + fn empty() -> Self { + Self { + user: Address::ZERO, + token: Address::ZERO, + amount: U256::ZERO, + salt: B256::ZERO, + withdrawal_id: B256::ZERO, + chain_id: U256::ZERO, + } + } +} diff --git a/apps/transfers/src/lib.rs b/apps/transfers/src/lib.rs index 1669e98..53c9155 100644 --- a/apps/transfers/src/lib.rs +++ b/apps/transfers/src/lib.rs @@ -1,22 +1,15 @@ -use alloy::{signers::local::PrivateKeySigner, sol}; -use futures::TryStreamExt; -use void_toolkit::app::VoidStream; -use void_toolkit::app::apply_transition; -use void_toolkit::oracle::oracle; -use void_toolkit::oracle_types::config::Config as OracleConfig; +use alloy::sol; +use void_app_node::run_signing; use void_toolkit::types::Block; -use void_toolkit::types::Height; -use void_toolkit::types::Lock; -use crate::app_state::Commit; -use crate::signing::sign; -use crate::state::State; +use crate::{ + api::{Api, api_update}, + app::{App, state_transition_function}, + server::add_handlers, +}; -pub mod api_state; -pub mod app_state; -pub mod server; -pub mod signing; -pub mod state; +pub use app::Proof; +pub use app::compute_withdrawal_id; sol!( #[allow(missing_docs)] @@ -26,64 +19,22 @@ sol!( "contracts/build/transfers_abi.json" ); -#[derive(Clone)] -pub struct App { - pub state: Lock, -} - -impl App { - pub fn new() -> Self { - Self { - state: Lock::new(State::default()), - } - } -} - -impl Default for App { - fn default() -> Self { - Self::new() - } -} - -pub async fn run_app( - signer: PrivateKeySigner, - server_endpoint: String, - oracle_config: OracleConfig, -) -> anyhow::Result<()> { - let app = App::new(); - tokio::spawn({ - let app = app.clone(); - async move { - server::run(app, server_endpoint).await; - } - }); - run(app, signer, oracle_config).await.unwrap(); - Ok(()) -} +pub mod api; +pub mod app; +pub mod server; +pub mod state_reads; + +run_signing!( + add_handlers, + init_db, + state_transition_function, + api_update, + App, + Api, + (), + Proof +); -pub async fn run( - app: App, - signer: PrivateKeySigner, - oracle_config: OracleConfig, -) -> anyhow::Result<()> { - oracle(oracle_config, void_toolkit::oracle::Db::memory(), 0) - .map_err(|e| anyhow::anyhow!("Oracle Error {}", e)) - .and_then(|block| { - futures::future::ready(app.state.access(|state| transition(block, state))) - }) - .sign(signer, sign) - .try_for_each(|signed_commit| { - app.state - .access(|s| s.api_state.latest_commitment = Some(signed_commit)); - futures::future::ready(Ok(())) - }) - .await?; +fn init_db(_tx: &mut rusqlite::Transaction) -> anyhow::Result<()> { Ok(()) } - -fn transition(block: Block, state: &mut State) -> anyhow::Result<(Block, Height)> { - let res: anyhow::Result<_> = apply_transition(block, state, app_state::state_transition); - let (block, commit) = res?; - api_state::update(&block, state.into()); - Ok((block, commit)) -} diff --git a/apps/transfers/src/main.rs b/apps/transfers/src/main.rs index 584762f..a6b7937 100644 --- a/apps/transfers/src/main.rs +++ b/apps/transfers/src/main.rs @@ -1,11 +1,29 @@ -use std::path::PathBuf; +use std::{net::SocketAddr, path::PathBuf}; use clap::{Args, Parser, Subcommand}; -use transfers::signing::get_signer; +use void_app_node::{ + Mode, Options, + oracle::{ObserverOracle, Oracle, OracleStorageType}, + signing::get_signer, +}; +use void_toolkit::load_config::load_config; #[derive(Parser)] #[command(version, about, long_about = None)] struct Cli { + #[arg(long, short)] + /// The server bind address + server: SocketAddr, + #[arg(long, short)] + tracing: bool, + #[arg(long, short)] + /// Path to the database file + /// If not provided, an in-memory database will be used + db_path: Option, + #[arg(long, short)] + /// Path to the oracle database file + /// If not provided, an in-memory database will be used + oracle_db_path: Option, #[command(subcommand)] command: Commands, } @@ -13,31 +31,68 @@ struct Cli { #[derive(Subcommand, Clone)] enum Commands { /// Run the state stream - Run(Run), + Publisher(Publisher), + /// Run the state stream in observer mode + Observer(Observer), } #[derive(Args, Clone)] -struct Run { +struct Publisher { #[arg(long, short)] /// The environment variable for the signing private key key: Option, /// Path to the oracle config file oracle_config: PathBuf, - /// The server endpoint - server: String, + /// The app network bind address + node_network_bind_address: SocketAddr, + /// The oracle bind address + oracle_bind_address: SocketAddr, +} + +#[derive(Args, Clone)] +struct Observer { + /// Path to the oracle config file + oracle_config: PathBuf, + /// The app network bind address + node_network_endpoint: String, } #[tokio::main] async fn main() { let cli = Cli::parse(); - match cli.command { - Commands::Run(run) => { - let signer = get_signer(run.key).unwrap(); - let oracle_config = void_toolkit::load_config::load_config(&run.oracle_config).unwrap(); - transfers::run_app(signer, run.server, oracle_config) - .await - .unwrap(); - } - } + let mode = match cli.command { + Commands::Publisher(publisher) => Mode::Publisher( + void_app_node::Publisher { + signer: get_signer(publisher.key).unwrap(), + oracle: Oracle { + oracle_config: load_config(&publisher.oracle_config).unwrap(), + oracle_bind_address: publisher.oracle_bind_address, + oracle_storage: cli + .oracle_db_path + .map_or(OracleStorageType::Memory, OracleStorageType::Db), + }, + node_network_bind_address: publisher.node_network_bind_address, + } + .into(), + ), + Commands::Observer(observer) => Mode::Observer(void_app_node::Observer { + oracle: ObserverOracle { + oracle_config: load_config(&observer.oracle_config).unwrap(), + oracle_storage: cli + .oracle_db_path + .map_or(OracleStorageType::Memory, OracleStorageType::Db), + }, + node_network_endpoint: observer.node_network_endpoint, + }), + }; + + let options = Options { + tracing: cli.tracing, + server_bind_address: cli.server, + db_path: cli.db_path, + mode, + }; + + transfers::run_signing_node(options).await.unwrap(); } diff --git a/apps/transfers/src/server.rs b/apps/transfers/src/server.rs index d227622..17cfe0d 100644 --- a/apps/transfers/src/server.rs +++ b/apps/transfers/src/server.rs @@ -6,81 +6,67 @@ use axum::{ extract::{Path, State}, routing::get, }; -use tower_http::cors::CorsLayer; -use void_toolkit::types::{Height, Signed}; +use void_toolkit::types::Height; use crate::{ - App, - app_state::{self, Commit}, + FullDataStoreRef, Node, + app::types::Withdrawal, + state_reads::{get_balance, get_merkle_data, get_stored_burn}, }; -pub async fn run(app: App, server: String) { - let app = Router::new() - .route("/", get(|| async { "OK" })) - .route("/get-balance/{user}/{token}", get(get_balance)) - .route("/get-state-commitment/", get(get_state_commitment)) - .route("/get-state-commitment", get(get_state_commitment)) +pub fn add_handlers(router: Router) -> Router { + router + .route("/get-balance/{user}/{token}", get(get_user_balance)) .route("/get-merkle-proof/{withdrawal_id}/", get(get_merkle_proof)) .route("/get-merkle-proof/{withdrawal_id}", get(get_merkle_proof)) - .layer(cors_layer()) - .with_state(app); - let listener = tokio::net::TcpListener::bind(server).await.unwrap(); - println!( - "Server running on http://{}", - listener.local_addr().unwrap() - ); - axum::serve(listener, app).await.unwrap(); } -/// The default CORS layer. -pub fn cors_layer() -> CorsLayer { - CorsLayer::new() - .allow_origin(tower_http::cors::Any) - .allow_methods([http::Method::GET, http::Method::OPTIONS]) - .allow_headers([http::header::CONTENT_TYPE]) -} - -pub async fn get_balance( - State(app): State, +pub async fn get_user_balance( + State(node): State, Path((user, token)): Path<(Address, Address)>, -) -> Json { - let balance = app.state.access(|s| s.app_state.get_balance(user, token).0); - Json(balance) -} - -pub async fn get_state_commitment(State(app): State) -> Json>>> { - let commitment = app.state.access(|s| s.api_state.latest_commitment.clone()); - Json(commitment) +) -> Result, String> { + let balance = node + .read_storage(move |s: FullDataStoreRef| get_balance(s, user, token)) + .await + .map_err(|e| e.to_string())?; + Ok(Json(balance)) } pub async fn get_merkle_proof( - State(app): State, + State(node): State, Path(withdrawal_id_str): Path, -) -> Json> { +) -> Result>>, String> { // Parse withdrawal ID from hex string let withdrawal_id = match B256::from_str(&withdrawal_id_str) { Ok(id) => id, - Err(_) => return Json(None), - }; - - // Get the pending withdrawal and the latest commitment from State - let result = app.state.access(|s| { - let pending_withdrawal = s.api_state.stored_proofs.get(&withdrawal_id).cloned(); - let latest_commitment = s.api_state.latest_commitment.clone(); - (pending_withdrawal, latest_commitment) - }); - - // Convert PendingWithdrawal to Withdrawal with the latest commitment - let withdrawal = match result { - (Some(pending), Some(commitment)) => Some(app_state::Withdrawal { - burn: pending.burn, - burn_proof: pending.burn_proof, - burn_index: pending.burn_index, - balance_root_at_burn: pending.balance_root_at_burn, - state_commitment: commitment, - }), - _ => None, // No pending withdrawal or no commitment yet + Err(e) => return Err(format!("Invalid withdrawal ID: {}", e)), }; - Json(withdrawal) + // Generate a merkle proof for the given withdrawal ID + let burn = node + .read_storage(move |s: FullDataStoreRef| get_stored_burn(s, &withdrawal_id)) + .await + .map_err(|e| e.to_string())?; + match burn { + Some(burn) => { + let Some(Height { + block_height, + data: merkle_data, + }) = node + .read_storage(move |s: FullDataStoreRef| get_merkle_data(s, withdrawal_id)) + .await + .map_err(|e| e.to_string())? + else { + return Ok(Json(None)); // No blocks processed yet + }; + let withdrawal = Withdrawal { + burn, + burn_proof: merkle_data.burn_proof, + burn_index: withdrawal_id, + balance_root_at_burn: merkle_data.balance_root_at_burn, + }; + Ok(Json(Some(Height::new(block_height, withdrawal)))) + } + None => Ok(Json(None)), // No pending withdrawal found + } } diff --git a/apps/transfers/src/state.rs b/apps/transfers/src/state.rs deleted file mode 100644 index a25cc0f..0000000 --- a/apps/transfers/src/state.rs +++ /dev/null @@ -1,43 +0,0 @@ -use alloy::primitives::{Address, B256}; -use void_toolkit::app::UpdateLatestBlock; - -use crate::{ - api_state::ApiState, - app_state::{AccountState, AccountStateRef, AppState, Balance, Burn, Commit}, -}; - -#[derive(Default)] -pub struct State { - pub app_state: AppState, - pub api_state: ApiState, -} - -impl From<&mut State> for Commit { - fn from(state: &mut State) -> Commit { - (&mut state.app_state).into() - } -} - -impl UpdateLatestBlock for State { - type Error = ::Error; - - fn update_latest_block(&mut self, height: u64, hash: [u8; 32]) -> Result<(), Self::Error> { - self.api_state.update_latest_block(height, hash) - } -} - -impl AccountStateRef for State { - fn get_balance(&self, user: Address, token: Address) -> Balance { - self.app_state.get_balance(user, token) - } -} - -impl AccountState for State { - fn insert_balance(&mut self, user: Address, token: Address, new_balance: Balance) { - self.app_state.insert_balance(user, token, new_balance); - } - - fn insert_burn(&mut self, withdrawal_id: B256, burn: Burn) { - self.app_state.insert_burn(withdrawal_id, burn); - } -} diff --git a/apps/transfers/src/state_reads.rs b/apps/transfers/src/state_reads.rs new file mode 100644 index 0000000..eb15ede --- /dev/null +++ b/apps/transfers/src/state_reads.rs @@ -0,0 +1,62 @@ +use alloy::primitives::{Address, B256, U256}; +use void_toolkit::types::Height; + +use crate::{ + FullDataStoreRef, + app::{ + AppProofs, AppStateRef, + types::{Burn, MerkleData}, + }, +}; + +pub fn get_balance( + storage: FullDataStoreRef, + user: Address, + token: Address, +) -> anyhow::Result { + match storage { + void_app_node::storage::DataStorageRef::Db(_) => todo!(), + void_app_node::storage::DataStorageRef::Memory(mem) => { + Ok(mem.app.get_balance(user, token).0) + } + } +} + +pub fn get_stored_burn( + storage: FullDataStoreRef, + withdrawal_id: &B256, +) -> anyhow::Result> { + match storage { + void_app_node::storage::DataStorageRef::Db(_) => todo!(), + void_app_node::storage::DataStorageRef::Memory(mem) => { + Ok(mem.api.stored_burns.get(withdrawal_id).cloned()) + } + } +} + +pub fn get_merkle_data( + storage: FullDataStoreRef, + withdrawal_id: B256, +) -> anyhow::Result>> { + match storage { + void_app_node::storage::DataStorageRef::Db(_) => todo!(), + void_app_node::storage::DataStorageRef::Memory(mem) => { + let Some(block_height) = mem.header.height() else { + return Ok(None); + }; + // Step 1: Get balance root at burn time (after balance update) + let balance_root_at_burn = mem.app.balance_root(); + + // Step 2: Generate burn proof + let burn_proof = mem.app.burn_merkle_proof(withdrawal_id); + + // Step 3: Create merkle data + let merkle_data = MerkleData { + burn_proof, + burn_index: withdrawal_id, + balance_root_at_burn, + }; + Ok(Some(Height::new(block_height, merkle_data))) + } + } +} diff --git a/apps/transfers/tests/balance_only_test.rs b/apps/transfers/tests/balance_only_test.rs index 692138d..b9cd3f6 100644 --- a/apps/transfers/tests/balance_only_test.rs +++ b/apps/transfers/tests/balance_only_test.rs @@ -1,6 +1,6 @@ // Test just the Balance type without requiring the full merkle tree implementation use alloy::primitives::U256; -use transfers::app_state::Balance; +use transfers::app::types::Balance; #[test] fn test_balance_checked_operations() { diff --git a/apps/transfers/tests/balance_test.rs b/apps/transfers/tests/balance_test.rs index adeb5b1..808cb20 100644 --- a/apps/transfers/tests/balance_test.rs +++ b/apps/transfers/tests/balance_test.rs @@ -1,10 +1,10 @@ use alloy::primitives::{Address, U256}; -use transfers::app_state::{AppState, get_balance, handle_deposit, handle_transfer}; +use transfers::app::{App, get_balance, handle_deposit, handle_transfer}; use void_toolkit::merkle::generic_sparse_merkle::GenericSparseMerkleTree; #[test] fn test_balance_newtype() { - let mut state = AppState { + let mut state = App { balance_tree: GenericSparseMerkleTree::new(), burn_tree: GenericSparseMerkleTree::new(), }; diff --git a/apps/transfers/tests/panic_safety_test.rs b/apps/transfers/tests/panic_safety_test.rs index 3803105..679bb7e 100644 --- a/apps/transfers/tests/panic_safety_test.rs +++ b/apps/transfers/tests/panic_safety_test.rs @@ -1,5 +1,7 @@ use alloy::primitives::{Address, B256, U256}; -use transfers::app_state::{Balance, handle_deposit, handle_transfer, handle_withdrawal_request}; +use transfers::app::{ + App, handle_deposit, handle_transfer, handle_withdrawal_request, types::Balance, +}; #[test] fn test_balance_overflow_protection() { @@ -53,10 +55,9 @@ fn test_checked_arithmetic_edge_cases() { #[test] fn test_zero_amount_protection() { - use transfers::app_state::AppState; use void_toolkit::merkle::generic_sparse_merkle::GenericSparseMerkleTree; - let mut state = AppState { + let mut state = App { balance_tree: GenericSparseMerkleTree::new(), burn_tree: GenericSparseMerkleTree::new(), }; @@ -106,10 +107,9 @@ fn test_zero_amount_protection() { #[test] fn test_self_transfer_protection() { - use transfers::app_state::AppState; use void_toolkit::merkle::generic_sparse_merkle::GenericSparseMerkleTree; - let mut state = AppState { + let mut state = App { balance_tree: GenericSparseMerkleTree::new(), burn_tree: GenericSparseMerkleTree::new(), }; @@ -130,10 +130,9 @@ fn test_self_transfer_protection() { #[test] fn test_graceful_error_propagation() { - use transfers::app_state::AppState; use void_toolkit::merkle::generic_sparse_merkle::GenericSparseMerkleTree; - let mut state = AppState { + let mut state = App { balance_tree: GenericSparseMerkleTree::new(), burn_tree: GenericSparseMerkleTree::new(), }; diff --git a/apps/transfers/tests/tests.rs b/apps/transfers/tests/tests.rs index c6c6660..6a5c1aa 100644 --- a/apps/transfers/tests/tests.rs +++ b/apps/transfers/tests/tests.rs @@ -7,6 +7,10 @@ use alloy::{ sol_types::SolEvent, }; use std::str::FromStr; +use void_app_node::{ + Mode, Options, Publisher, + oracle::{Oracle, OracleStorageType}, +}; use void_toolkit::oracle_types::config::{ BlockConfig, ChainContractLogsConfig, Config, ConnectionType, QueryConfig, StreamConfig, }; @@ -108,16 +112,35 @@ async fn test_complete_bridge_flow() { let oracle_config = Config { query, block }; let transfers_endpoint = "127.0.0.1:3500"; + let transfers_oracle_endpoint = "127.0.0.1:3600"; + let transfers_node_network_endpoint = "127.0.0.1:3700"; // Start the bridge application with integrated oracle tokio::spawn({ let transfers_endpoint = transfers_endpoint.to_string(); let signer = signer.clone(); let oracle_config = oracle_config.clone(); + let oracle = Oracle { + oracle_config, + oracle_bind_address: transfers_oracle_endpoint.parse().unwrap(), + oracle_storage: OracleStorageType::Memory, + }; + let mode = Mode::Publisher( + Publisher { + signer, + oracle, + node_network_bind_address: transfers_node_network_endpoint.parse().unwrap(), + } + .into(), + ); + let options = Options { + tracing: true, + server_bind_address: transfers_endpoint.parse().unwrap(), + db_path: None, + mode, + }; async move { - transfers::run_app(signer, transfers_endpoint, oracle_config) - .await - .unwrap(); + transfers::run_signing_node(options).await.unwrap(); } }); @@ -276,7 +299,7 @@ async fn test_complete_bridge_flow() { // ============================================================================= // Get merkle proof from the server using withdrawal ID - let withdrawal_proof = { + let merkle_proof = { let client = reqwest::Client::new(); let response = client .get(format!( @@ -292,13 +315,37 @@ async fn test_complete_bridge_flow() { proof.expect("Merkle proof should be available") }; + // Note in a real scenario, the user would need to wait a little bit between calls + // for the state proof to be available. Here we assume it's ready immediately. + + let height = merkle_proof["block_height"] + .as_u64() + .expect("Height should be u64"); + + let state_proof = { + let client = reqwest::Client::new(); + let response = client + .get(format!( + "http://{transfers_endpoint}/get-app-proof/{height}", + )) + .send() + .await + .expect("Failed to request merkle proof"); + + let proof: Option = + response.json().await.expect("Failed to parse response"); + proof.expect("State proof should be available") + }; + + let merkle_proof = &merkle_proof["data"]; + // Convert server proof to contract format let contract_proof = { - let burn = &withdrawal_proof["burn"]; - let merkle_proof = &withdrawal_proof["burn_proof"]; + let burn = &merkle_proof["burn"]; + let burn_proof = &merkle_proof["burn_proof"]; // Extract siblings array (they are byte arrays, not hex strings) - let siblings: Vec = merkle_proof["siblings"] + let siblings: Vec = burn_proof["siblings"] .as_array() .expect("Siblings should be array") .iter() @@ -313,26 +360,21 @@ async fn test_complete_bridge_flow() { .collect(); // Parse burn root (also a byte array) - unused since we get it from commitment - let _burn_root_bytes: Vec = merkle_proof["root"] + let _burn_root_bytes: Vec = burn_proof["root"] .as_array() .expect("Root should be byte array") .iter() .map(|b| b.as_u64().expect("Byte should be number") as u8) .collect(); - // Extract commitment and signature from the state_commitment - let state_commitment = &withdrawal_proof["state_commitment"] - .as_object() - .expect("State commitment should be an object"); - // Parse the commitment data - let commitment_data = state_commitment["data"] + let commitment_data = state_proof["proof"]["data"] .as_object() .expect("Commitment data should be an object")["data"] .as_object() .expect("Commitment inner data should be an object"); - let height = state_commitment["data"]["block_height"] + let height = state_proof["proof"]["data"]["block_height"] .as_u64() .expect("Height should be u64"); @@ -353,7 +395,7 @@ async fn test_complete_bridge_flow() { .collect(); // Parse signature (it's a byte array) - let signature_bytes: Vec = state_commitment["signature"] + let signature_bytes: Vec = state_proof["proof"]["signature"] .as_array() .expect("Signature should be byte array") .iter() diff --git a/node/Cargo.toml b/node/Cargo.toml new file mode 100644 index 0000000..619068d --- /dev/null +++ b/node/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "void-app-node" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +alloy.workspace = true +anyhow.workspace = true +axum.workspace = true +clap.workspace = true +futures.workspace = true +http.workspace = true +pin-project-lite.workspace = true +reqwest.workspace = true +rusqlite.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_yaml_ng.workspace = true +sha2.workspace = true +tempfile.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tower-http.workspace = true +tracing-subscriber.workspace = true +void-toolkit = { workspace = true, features = ["merkle", "app", "network-channel", "oracle-sqlite", "oracle" ] } + +[dev-dependencies] +alloy = { workspace = true, features = [ "node-bindings", "rlp", "signer-mnemonic" ] } +tokio-util.workspace = true \ No newline at end of file diff --git a/apps/increment/sql/create/current_proof.sql b/node/sql/create/current_proof.sql similarity index 58% rename from apps/increment/sql/create/current_proof.sql rename to node/sql/create/current_proof.sql index 0d2f6be..778043e 100644 --- a/apps/increment/sql/create/current_proof.sql +++ b/node/sql/create/current_proof.sql @@ -1,7 +1,5 @@ CREATE TABLE IF NOT EXISTS current_proof ( id INTEGER PRIMARY KEY, - count INTEGER NOT NULL, - root BLOB NOT NULL, height INTEGER NOT NULL UNIQUE, - signature BLOB NOT NULL + data BLOB NOT NULL ) \ No newline at end of file diff --git a/apps/increment/sql/create/latest_header.sql b/node/sql/create/latest_header.sql similarity index 100% rename from apps/increment/sql/create/latest_header.sql rename to node/sql/create/latest_header.sql diff --git a/node/sql/insert/current_proof.sql b/node/sql/insert/current_proof.sql new file mode 100644 index 0000000..2dfcb1d --- /dev/null +++ b/node/sql/insert/current_proof.sql @@ -0,0 +1,4 @@ +INSERT + OR REPLACE INTO current_proof (height, data) +VALUES + (?, ?); \ No newline at end of file diff --git a/apps/increment/sql/insert/current_proof_limit.sql b/node/sql/insert/current_proof_limit.sql similarity index 100% rename from apps/increment/sql/insert/current_proof_limit.sql rename to node/sql/insert/current_proof_limit.sql diff --git a/apps/increment/sql/insert/latest_header.sql b/node/sql/insert/latest_header.sql similarity index 100% rename from apps/increment/sql/insert/latest_header.sql rename to node/sql/insert/latest_header.sql diff --git a/apps/increment/sql/query/current_proof.sql b/node/sql/query/current_proof.sql similarity index 85% rename from apps/increment/sql/query/current_proof.sql rename to node/sql/query/current_proof.sql index 69868a4..8c13639 100644 --- a/apps/increment/sql/query/current_proof.sql +++ b/node/sql/query/current_proof.sql @@ -1,8 +1,6 @@ SELECT - count, - root, height, - signature + data FROM current_proof WHERE diff --git a/apps/increment/sql/query/get_proof.sql b/node/sql/query/get_proof.sql similarity index 59% rename from apps/increment/sql/query/get_proof.sql rename to node/sql/query/get_proof.sql index 5c8a097..e165622 100644 --- a/apps/increment/sql/query/get_proof.sql +++ b/node/sql/query/get_proof.sql @@ -1,7 +1,5 @@ SELECT - count, - root, - signature + data FROM current_proof WHERE diff --git a/apps/increment/sql/query/latest_header.sql b/node/sql/query/latest_header.sql similarity index 100% rename from apps/increment/sql/query/latest_header.sql rename to node/sql/query/latest_header.sql diff --git a/node/src/db.rs b/node/src/db.rs new file mode 100644 index 0000000..ceb838d --- /dev/null +++ b/node/src/db.rs @@ -0,0 +1,229 @@ +use std::{ + path::Path, + sync::{Arc, Mutex}, +}; + +use rusqlite::OptionalExtension; +use void_toolkit::{app::UpdateLatestBlock, types::Height}; + +#[macro_export] +/// Short-hand for including an SQL string from the `sql/` subdir at compile time. +macro_rules! include_sql_str { + ($subpath:expr) => { + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/sql/", $subpath)) + }; +} + +#[macro_export] +/// Short-hand for declaring a `const` SQL str and presenting the SQL via the doc comment. +macro_rules! decl_const_sql_str { + ($name:ident, $subpath:expr) => { + /// ```sql + #[doc = include_sql_str!($subpath)] + /// ``` + pub const $name: &str = $crate::include_sql_str!($subpath); + }; +} + +pub trait DbDataConstraints: Clone + Send + Sync + 'static {} + +impl DbDataConstraints for DbData where DbData: Clone + Send + Sync + 'static {} + +pub trait InitDb: Send + 'static { + fn init_db(self, tx: &mut rusqlite::Transaction) -> anyhow::Result<()>; +} + +impl InitDb for F +where + F: FnOnce(&mut rusqlite::Transaction) -> anyhow::Result<()> + Send + 'static, +{ + fn init_db(self, tx: &mut rusqlite::Transaction) -> anyhow::Result<()> { + (self)(tx) + } +} + +#[derive(Clone)] +pub struct Db { + permit: Arc, + conn: Arc>, + data: DbData, +} + +pub struct Tx<'a, DbData> { + pub tx: rusqlite::Transaction<'a>, + pub data: &'a DbData, +} + +impl Db { + pub fn new(path: &Path, data: DbData) -> anyhow::Result { + let mut conn = rusqlite::Connection::open(path)?; + let tx = conn.transaction()?; + create_tables(&tx)?; + tx.commit()?; + Ok(Db { + permit: Arc::new(tokio::sync::Semaphore::new(1)), + conn: Arc::new(Mutex::new(conn)), + data, + }) + } + + pub async fn apply(&self, f: F) -> anyhow::Result + where + DbData: Clone + Send + 'static, + R: Send + 'static, + F: FnOnce(&mut Tx<'_, DbData>) -> anyhow::Result + Send + 'static, + { + let _permit = self.permit.acquire().await.unwrap(); + let db = self.clone(); + + tokio::task::spawn_blocking(move || { + let mut conn = db.conn.lock().unwrap(); + let data = &db.data; + let mut tx = Tx { + tx: conn.transaction()?, + data, + }; + let r = f(&mut tx)?; + tx.tx.commit()?; + Ok(r) + }) + .await? + } +} + +impl Db +where + DbData: Clone + Send + 'static, +{ + pub async fn append_proof(&self, proof: Height) -> anyhow::Result<()> + where + AppProof: Send + 'static, + Vec: From, + { + self.apply(move |tx| { + tx.tx.execute( + insert::CURRENT_PROOF, + rusqlite::params![proof.block_height, Vec::::from(proof.data)], + )?; + anyhow::Result::Ok(()) + }) + .await + } + + pub async fn get_proof(&self, height: u64) -> anyhow::Result> + where + AppProof: Send + 'static, + AppProof: for<'a> TryFrom<&'a [u8]>, + for<'a> >::Error: Into, + { + self.apply(move |tx| tx.get_proof(height)).await + } + + pub async fn get_latest_proof(&self) -> anyhow::Result>> + where + AppProof: Send + 'static, + AppProof: for<'a> TryFrom<&'a [u8]>, + for<'a> >::Error: Into, + { + self.apply(|tx| tx.get_latest_proof()).await + } + + pub async fn get_current_block_height_and_hash( + &self, + ) -> anyhow::Result> { + self.apply(|tx| tx.get_current_block_height_and_hash()) + .await + } +} + +impl Tx<'_, DbData> { + pub fn get_proof(&self, height: u64) -> anyhow::Result> + where + AppProof: for<'a> TryFrom<&'a [u8]>, + for<'a> >::Error: Into, + { + let mut stmt = self.tx.prepare(query::GET_PROOF)?; + let proof = stmt + .query_row(rusqlite::params![height], |row| { + let data: Vec = row.get(0)?; + Ok(data) + }) + .optional()?; + match proof { + Some(proof) => Ok(Some( + AppProof::try_from(proof.as_slice()).map_err(|e| e.into())?, + )), + None => Ok(None), + } + } + + pub fn get_latest_proof(&self) -> anyhow::Result>> + where + AppProof: for<'a> TryFrom<&'a [u8]>, + for<'a> >::Error: Into, + { + let mut stmt = self.tx.prepare(query::CURRENT_PROOF)?; + let proof = stmt + .query_row(rusqlite::params![], |row| { + let block_height: u64 = row.get(0)?; + let data: Vec = row.get(1)?; + Ok(Height::new(block_height, data)) + }) + .optional()?; + match proof { + Some(proof) => Ok(Some(Height::new( + proof.block_height, + AppProof::try_from(proof.data.as_slice()).map_err(|e| e.into())?, + ))), + None => Ok(None), + } + } + + pub fn get_current_block_height_and_hash(&self) -> anyhow::Result> { + let mut stmt = self.tx.prepare(query::LATEST_HEADER)?; + let proof = stmt + .query_row(rusqlite::params![], |row| { + let block_height: u64 = row.get(0)?; + let block_hash: [u8; 32] = row.get(1)?; + Ok((block_height, block_hash)) + }) + .optional()?; + Ok(proof) + } +} + +impl UpdateLatestBlock for Tx<'_, DbData> { + type Error = rusqlite::Error; + + fn update_latest_block(&mut self, height: u64, hash: [u8; 32]) -> Result<(), Self::Error> { + self.tx.execute( + insert::LATEST_HEADER, + rusqlite::params![height, Vec::::from(hash)], + )?; + Ok(()) + } +} + +pub fn create_tables(tx: &rusqlite::Transaction) -> anyhow::Result<()> { + tx.execute(create::CURRENT_PROOF, [])?; + tx.execute(create::LATEST_HEADER, [])?; + tx.execute(insert::CURRENT_PROOF_LIMIT, [])?; + Ok(()) +} + +pub mod create { + decl_const_sql_str!(CURRENT_PROOF, "create/current_proof.sql"); + decl_const_sql_str!(LATEST_HEADER, "create/latest_header.sql"); +} + +pub mod insert { + decl_const_sql_str!(CURRENT_PROOF, "insert/current_proof.sql"); + decl_const_sql_str!(CURRENT_PROOF_LIMIT, "insert/current_proof_limit.sql"); + decl_const_sql_str!(LATEST_HEADER, "insert/latest_header.sql"); +} + +pub mod query { + decl_const_sql_str!(CURRENT_PROOF, "query/current_proof.sql"); + decl_const_sql_str!(GET_PROOF, "query/get_proof.sql"); + decl_const_sql_str!(LATEST_HEADER, "query/latest_header.sql"); +} diff --git a/node/src/lib.rs b/node/src/lib.rs new file mode 100644 index 0000000..8797209 --- /dev/null +++ b/node/src/lib.rs @@ -0,0 +1,622 @@ +use std::{net::SocketAddr, path::PathBuf, sync::Arc}; + +use alloy::signers::local::PrivateKeySigner; +use futures::TryStreamExt; +use tokio::sync::mpsc; +use void_toolkit::{ + app::{Notification, UpdateLatestBlock, VoidStream}, + oracle::{observer_oracle, publisher_oracle}, + oracle_decoder::tracing, + types::{Block, Height}, +}; + +use crate::{ + db::{DbDataConstraints, InitDb}, + oracle::{ObserverOracle, Oracle}, + proof::{DigestFromProof, Proof, SignedProof}, + server::AddHandlers, + storage::{ + DataStorage, DataStorageRef, FullMemState, FullMemStateRef, ProofStorageConstraints, + ReadStorage, Storage, StorageType, Store, + }, + transitions::{ApiTransition, AppTransition}, +}; + +#[macro_export] +macro_rules! run_signing { + ($add_handlers:expr, $init_db:expr, $stf:expr, $api_update:expr, $app:ty, $api:ty, $db_data:ty, $proof:ty) => { + pub async fn run_signing_node(options: $crate::Options) -> anyhow::Result<()> { + void_app_node::run_signing_node_with_options( + options, + $add_handlers, + $init_db, + stf, + api, + ) + .await?; + Ok(()) + } + + pub type Node = + void_app_node::Node<$app, $api, $crate::proof::SignedProof<$proof>, $db_data>; + + pub type DataStore<'a, 'conn> = $crate::storage::DataStorage<'a, 'conn, $app, $db_data>; + + pub type FullDataStore<'a, 'conn> = + $crate::storage::FullDataStorage<'a, 'conn, $app, $api, $db_data>; + + pub type FullDataStoreRef<'a, 'conn> = + $crate::storage::FullDataStorageRef<'a, 'conn, $app, $api, $db_data>; + + fn stf(block: &Block, state: DataStore) -> anyhow::Result<()> { + $stf(block, state) + } + + fn api(block: &Block, state: FullDataStore) -> anyhow::Result<()> { + $api_update(block, state) + } + + impl From<&$proof> for [u8; 32] { + fn from(proof: &$proof) -> Self { + $crate::proof::ProofConversions::digest(proof) + } + } + + impl From<$proof> for Vec { + fn from(proof: $proof) -> Self { + $crate::proof::ProofConversions::into_bytes(proof) + } + } + + impl TryFrom<&[u8]> for $proof { + type Error = anyhow::Error; + fn try_from(bytes: &[u8]) -> Result { + $crate::proof::ProofConversions::try_from_bytes(bytes) + } + } + + impl + TryFrom< + $crate::storage::DataStorage< + '_, + '_, + <$proof as $crate::proof::ProofConversions>::App, + <$proof as $crate::proof::ProofConversions>::DbData, + >, + > for $proof + { + type Error = anyhow::Error; + fn try_from( + store: $crate::storage::DataStorage< + '_, + '_, + <$proof as $crate::proof::ProofConversions>::App, + <$proof as $crate::proof::ProofConversions>::DbData, + >, + ) -> Result { + $crate::proof::ProofConversions::try_from_storage(store) + } + } + }; +} + +pub mod db; +pub mod memory; +pub mod oracle; +pub mod proof; +pub mod server; +pub mod signing; +pub mod storage; +pub mod transitions; + +pub trait Runner { + type App: Send + 'static; + type Api: Send + 'static; + type DbData: DbDataConstraints; + + fn runner(self) -> Run; +} + +impl Runner for Run +where + DbData: DbDataConstraints, + App: Send + 'static, + Api: Send + 'static, +{ + type App = App; + type Api = Api; + type DbData = DbData; + + fn runner(self) -> Run { + self + } +} + +pub struct Run { + pub storage_type: StorageType, + pub server_bind_address: SocketAddr, + pub mode: Mode, +} + +pub struct Options { + pub tracing: bool, + pub server_bind_address: SocketAddr, + pub db_path: Option, + pub mode: Mode, +} + +pub enum Mode { + Publisher(Box), + Observer(Observer), +} + +pub struct Publisher { + pub signer: PrivateKeySigner, + pub oracle: Oracle, + pub node_network_bind_address: SocketAddr, +} + +pub struct Observer { + pub oracle: ObserverOracle, + pub node_network_endpoint: String, +} + +pub struct Node { + storage: Storage, + state_notification: Notification, + replicator: proof::replicate::ProofReplicator, +} + +impl Node { + pub fn new(storage: StorageType) -> anyhow::Result { + let storage = Storage::new(storage)?; + let state_notification = Notification::new(); + let replicator = proof::replicate::ProofReplicator::new(storage.clone()); + Ok(Self { + storage, + state_notification, + replicator, + }) + } + + pub fn state_notification(&mut self) -> &mut Notification { + &mut self.state_notification + } + + pub async fn read_storage(&self, read: F) -> anyhow::Result + where + F: ReadStorage, + R: Send + 'static, + DbData: DbDataConstraints, + { + match &self.storage { + Storage::Db(db) => db.apply(move |tx| read.read(DataStorageRef::Db(tx))).await, + Storage::Memory(mem) => mem.apply(|apply| { + read.read(DataStorageRef::Memory(&FullMemStateRef::new( + apply.app, + apply.api, + apply.latest_header, + ))) + }), + } + } +} + +impl Node +where + AppProof: ProofStorageConstraints, + Vec: From, + for<'a> >::Error: Into, + DbData: DbDataConstraints, +{ + pub async fn get_proof(&self, height: u64) -> anyhow::Result> { + self.storage.get_proof(height).await + } + + pub async fn get_latest_proof(&self) -> anyhow::Result>> { + self.storage.get_latest_proof().await + } +} + +impl Node +where + DbData: DbDataConstraints, +{ + pub async fn get_current_block_height_and_hash( + &self, + ) -> anyhow::Result> { + self.storage.get_current_block_height_and_hash().await + } +} + +pub enum DataType { + Memory(App, Api), + Db(DbData), +} + +pub async fn run_signing_node_with_options( + options: Options, + add_handlers: F, + init: Init, + stf: Stf, + api_update: ApiU, +) -> anyhow::Result<()> +where + Stf: AppTransition, + ApiU: ApiTransition, + P: Proof, + for<'a>

>::Error: Into, + SignedProof

: ProofStorageConstraints + serde::Serialize, + Vec: From>, + for<'a> as TryFrom<&'a [u8]>>::Error: Into, + [u8; 32]: DigestFromProof

, + Vec: From

, + F: AddHandlers, DbData>, + Init: InitDb, + App: Default + Send + 'static, + Api: Default + Send + 'static, + DbData: Default + DbDataConstraints, +{ + if options.tracing { + tracing_subscriber::fmt::try_init().ok(); + } + let run = Run { + storage_type: options + .db_path + .map(|path| StorageType::Db(path, DbData::default())) + .unwrap_or_else(|| StorageType::Memory(App::default(), Api::default())), + server_bind_address: options.server_bind_address, + mode: options.mode, + }; + + run_signing_node(run, add_handlers, init, stf, api_update).await +} + +pub async fn run_signing_node( + run: R, + add_handlers: F, + init: Init, + stf: Stf, + api_update: ApiU, +) -> anyhow::Result<()> +where + R: Runner, + Stf: AppTransition, + ApiU: ApiTransition, + P: Proof, + for<'a>

>::Error: Into, + SignedProof

: ProofStorageConstraints + serde::Serialize, + Vec: From>, + for<'a> as TryFrom<&'a [u8]>>::Error: Into, + [u8; 32]: DigestFromProof

, + Vec: From

, + F: AddHandlers, R::DbData>, + Init: InitDb, +{ + let Run { + storage_type, + server_bind_address, + mode, + } = run.runner(); + let node = Node::new(storage_type)?; + if let Storage::Db(db) = &node.storage { + init_db(db, init).await?; + } + let server_jh = tokio::spawn({ + let node = node.clone(); + async move { server::run(node, server_bind_address, add_handlers).await } + }); + let node_jh = tokio::spawn(async move { + match mode { + Mode::Publisher(publisher) => { + run_signing_publisher( + node, + publisher.node_network_bind_address, + publisher.signer, + publisher.oracle, + stf, + api_update, + ) + .await + } + Mode::Observer(observer) => { + run_signing_observer( + node, + observer.node_network_endpoint, + observer.oracle, + stf, + api_update, + ) + .await + } + } + }); + let (server_res, node_res) = futures::future::try_join(server_jh, node_jh).await?; + server_res?; + node_res?; + Ok(()) +} + +async fn init_db(db: &db::Db, init: F) -> anyhow::Result<()> +where + F: InitDb, + DbData: DbDataConstraints, +{ + db.apply(move |tx| init.init_db(&mut tx.tx)).await +} + +async fn state_transition_with_storage( + block: Block, + storage: &S, + stf: Arc, + api_update: Arc, +) -> anyhow::Result<(Block, Height

)> +where + S: Store, + Stf: AppTransition, + ApiU: ApiTransition, + P: Proof, +{ + match storage.storage() { + Storage::Db(db) => { + db.apply(move |tx| { + stf.apply(&block, DataStorage::Db(tx))?; + tx.update_latest_block(block.height, void_toolkit::hash::Hash::hash(&block))?; + api_update.apply(&block, DataStorage::Db(tx))?; + let proof = P::try_from(DataStorage::Db(tx))?; + let height = block.height; + Ok((block, Height::new(height, proof))) + }) + .await + } + Storage::Memory(mem) => mem.apply(|apply| { + stf.apply(&block, DataStorage::Memory(apply.app))?; + apply + .latest_header + .update_latest_block(block.height, void_toolkit::hash::Hash::hash(&block))?; + let proof = P::try_from(DataStorage::Memory(apply.app))?; + api_update.apply( + &block, + DataStorage::Memory(&mut FullMemState::new(apply.app, apply.api)), + )?; + let height = block.height; + Ok((block, Height::new(height, proof))) + }), + } +} + +async fn observer_state_transition_with_storage( + block: Block, + storage: &S, + stf: Arc, + api_update: Arc, +) -> anyhow::Result<()> +where + S: Store, + Stf: AppTransition, + ApiU: ApiTransition, +{ + match storage.storage() { + Storage::Db(db) => { + db.apply(move |tx| { + stf.apply(&block, DataStorage::Db(tx))?; + tx.update_latest_block(block.height, void_toolkit::hash::Hash::hash(&block))?; + api_update.apply(&block, DataStorage::Db(tx))?; + Ok(()) + }) + .await + } + Storage::Memory(mem) => mem.apply(|apply| { + stf.apply(&block, DataStorage::Memory(apply.app))?; + apply + .latest_header + .update_latest_block(block.height, void_toolkit::hash::Hash::hash(&block))?; + api_update.apply( + &block, + DataStorage::Memory(&mut FullMemState::new(apply.app, apply.api)), + )?; + Ok(()) + }), + } +} + +pub async fn run_signing_publisher( + node: Node, DbData>, + node_bind_address: SocketAddr, + signer: PrivateKeySigner, + oracle: Oracle, + stf: Stf, + api_update: ApiU, +) -> anyhow::Result<()> +where + Stf: AppTransition, + ApiU: ApiTransition, + P: Proof, + [u8; 32]: DigestFromProof

, + Vec: From

, + SignedProof

: ProofStorageConstraints + serde::Serialize, + Vec: From>, + for<'a> as TryFrom<&'a [u8]>>::Error: Into, + App: Send + 'static, + Api: Send + 'static, + DbData: DbDataConstraints, +{ + let replicator_jh = tokio::spawn(void_toolkit::network_channel::replicate::sender( + node.replicator.clone(), + node_bind_address, + 10, + )); + + let publisher_jh = tokio::spawn(run_signing_publisher_stream( + node, signer, oracle, stf, api_update, + )); + let (replicator_res, publisher_res) = + futures::future::try_join(replicator_jh, publisher_jh).await?; + replicator_res?; + publisher_res?; + Ok(()) +} + +async fn run_signing_publisher_stream( + node: Node, DbData>, + signer: PrivateKeySigner, + oracle: Oracle, + stf: Stf, + api_update: ApiU, +) -> anyhow::Result<()> +where + Stf: AppTransition, + ApiU: ApiTransition, + P: Proof, + [u8; 32]: DigestFromProof

, + Vec: From

, + DbData: DbDataConstraints, + SignedProof

: ProofStorageConstraints + serde::Serialize, + Vec: From>, + for<'a> as TryFrom<&'a [u8]>>::Error: Into, +{ + let Oracle { + oracle_config, + oracle_bind_address, + oracle_storage, + } = oracle; + let oracle_storage = oracle::oracle_storage(oracle_storage)?; + + let (last_parent_height, last_parent_hash) = node + .storage + .get_current_block_height_and_hash() + .await? + .map_or((None, [0; 32]), |(h, ph)| (Some(h), ph)); + + let stf = Arc::new(stf); + let api_update = Arc::new(api_update); + + publisher_oracle( + oracle_config, + oracle_storage, + last_parent_height.map_or(0, |h| h + 1), + oracle_bind_address, + 10, + Some(signer.clone()), + ) + .map_err(|e| anyhow::anyhow!("Oracle error: {}", e)) + .block_height_parent(last_parent_height, last_parent_hash) + .and_then(|block| { + state_transition_with_storage(block, &node.storage, stf.clone(), api_update.clone()) + }) + .push_notification(node.state_notification.clone()) + .sign(signer, signing::sign) + .try_for_each(|signed_proof| async { + let signed_proof = Height::new(signed_proof.data.block_height, signed_proof.into()); + node.replicator.update(signed_proof).await?; + Ok(()) + }) + .await?; + Ok(()) +} + +async fn run_signing_observer( + node: Node, DbData>, + node_network_endpoint: String, + oracle: ObserverOracle, + stf: Stf, + api_update: ApiU, +) -> anyhow::Result<()> +where + Stf: AppTransition, + ApiU: ApiTransition, + Vec: From>, + for<'a>

>::Error: Into, + P: Proof, + App: Send + 'static, + Api: Send + 'static, + DbData: DbDataConstraints, +{ + let (proof_heights_tx, proof_heights_rx) = tokio::sync::mpsc::channel(100); + let receiver_jh = tokio::spawn({ + let replicator = node.replicator.clone(); + async move { + void_toolkit::network_channel::replicate::receiver(0, node_network_endpoint) + .try_for_each(|delta| async { + proof::replicate::recv_delta(&replicator, delta, proof_heights_tx.clone()) + .await; + Ok(()) + }) + .await + } + }); + + let observer_jh = tokio::spawn(run_signing_observer_stream( + node, + oracle, + proof_heights_rx, + stf, + api_update, + )); + let (receiver_res, observer_res) = futures::future::try_join(receiver_jh, observer_jh).await?; + receiver_res?; + observer_res?; + Ok(()) +} + +async fn run_signing_observer_stream( + node: Node, DbData>, + oracle: ObserverOracle, + proof_heights: mpsc::Receiver, + stf: Stf, + api_update: ApiU, +) -> anyhow::Result<()> +where + Stf: AppTransition, + ApiU: ApiTransition, + DbData: DbDataConstraints, +{ + let proof_heights_stream = futures::stream::unfold(proof_heights, |mut rx| async { + let r = rx.recv().await.map(|height| (height, rx)); + if r.is_none() { + tracing::warn!("Proof heights channel closed unexpectedly"); + } + r + }); + + let ObserverOracle { + oracle_config, + oracle_storage, + } = oracle; + let oracle_storage = oracle::oracle_storage(oracle_storage)?; + + let (last_parent_height, last_parent_hash) = node + .storage + .get_current_block_height_and_hash() + .await? + .map_or((None, [0; 32]), |(h, ph)| (Some(h), ph)); + let stf = Arc::new(stf); + let api_update = Arc::new(api_update); + + observer_oracle(oracle_config, oracle_storage) + .map_err(|e| anyhow::anyhow!("Oracle error: {}", e)) + .block_height_parent(last_parent_height, last_parent_hash) + .blocks_await_proofs(proof_heights_stream) + .and_then(|block| { + observer_state_transition_with_storage( + block, + &node.storage, + stf.clone(), + api_update.clone(), + ) + }) + .push_notification(node.state_notification.clone()) + .try_for_each(|_| std::future::ready(Ok(()))) + .await?; + Ok(()) +} + +impl Clone for Node { + fn clone(&self) -> Self { + Self { + storage: self.storage.clone(), + state_notification: self.state_notification.clone(), + replicator: self.replicator.clone(), + } + } +} diff --git a/node/src/memory.rs b/node/src/memory.rs new file mode 100644 index 0000000..50089ed --- /dev/null +++ b/node/src/memory.rs @@ -0,0 +1,183 @@ +use void_toolkit::{ + app::UpdateLatestBlock, + types::{Height, Lock}, +}; + +pub struct Memory { + inner: Lock>, +} + +pub struct Apply<'a, App, Api> { + pub app: &'a mut App, + pub api: &'a mut Api, + pub latest_header: &'a mut LatestHeaderMem, +} + +struct MemoryInner { + app_state: App, + api_state: Api, + proofs: KeyValueBuffer<10, u64, AppProof>, + latest_header: LatestHeaderMem, +} + +#[derive(Default)] +pub enum LatestHeaderMem { + #[default] + Empty, + Header { + height: u64, + hash: [u8; 32], + }, +} + +pub struct KeyValueBuffer { + map: std::collections::BTreeMap, +} + +impl UpdateLatestBlock for Apply<'_, App, Api> { + type Error = std::convert::Infallible; + + fn update_latest_block(&mut self, height: u64, hash: [u8; 32]) -> Result<(), Self::Error> { + self.latest_header.update_latest_block(height, hash) + } +} + +impl Memory { + pub fn new(app_state: App, api_state: Api) -> Self { + Self { + inner: Lock::new(MemoryInner { + app_state, + api_state, + proofs: KeyValueBuffer::new(), + latest_header: LatestHeaderMem::default(), + }), + } + } + + pub fn append_proof(&self, proof: Height) { + self.inner + .access(|inner| inner.proofs.insert(proof.block_height, proof.data)); + } + + pub fn get_proof(&self, height: u64) -> Option + where + AppProof: Clone, + { + self.inner + .access(|inner| inner.proofs.get(&height).cloned()) + } + + pub fn get_latest_proof(&self) -> Option> + where + AppProof: Clone, + { + self.inner.access(|inner| { + inner + .proofs + .last_key_value() + .map(|(k, v)| Height::new(*k, v.clone())) + }) + } + + pub fn get_current_block_height_and_hash(&self) -> Option<(u64, [u8; 32])> { + self.inner.access(|inner| inner.latest_header.get()) + } + + pub fn apply(&self, f: F) -> R + where + F: FnOnce(Apply<'_, App, Api>) -> R, + { + self.inner.access(|inner| { + f(Apply { + app: &mut inner.app_state, + api: &mut inner.api_state, + latest_header: &mut inner.latest_header, + }) + }) + } +} + +impl UpdateLatestBlock for LatestHeaderMem { + type Error = std::convert::Infallible; + + fn update_latest_block(&mut self, height: u64, hash: [u8; 32]) -> Result<(), Self::Error> { + *self = LatestHeaderMem::Header { height, hash }; + Ok(()) + } +} + +impl LatestHeaderMem { + pub fn height(&self) -> Option { + match self { + LatestHeaderMem::Empty => None, + LatestHeaderMem::Header { height, .. } => Some(*height), + } + } + + pub fn hash(&self) -> Option<[u8; 32]> { + match self { + LatestHeaderMem::Empty => None, + LatestHeaderMem::Header { hash, .. } => Some(*hash), + } + } + + pub fn get(&self) -> Option<(u64, [u8; 32])> { + match self { + LatestHeaderMem::Empty => None, + LatestHeaderMem::Header { height, hash } => Some((*height, *hash)), + } + } +} + +impl KeyValueBuffer { + pub fn new() -> Self { + Self { + map: std::collections::BTreeMap::new(), + } + } + + pub fn insert(&mut self, key: K, value: V) + where + K: Ord, + { + self.map.insert(key, value); + if self.map.len() > LENGTH { + self.map.first_entry().unwrap().remove_entry(); + } + } + + pub fn get(&self, key: &K) -> Option<&V> + where + K: Ord, + { + self.map.get(key) + } + + pub fn last(&self) -> Option<&V> + where + K: Ord, + { + self.map.last_key_value().map(|(_, v)| v) + } + + pub fn last_key_value(&self) -> Option<(&K, &V)> + where + K: Ord, + { + self.map.last_key_value() + } +} + +impl Default for KeyValueBuffer { + fn default() -> Self { + Self::new() + } +} + +impl Clone for Memory { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} diff --git a/node/src/oracle.rs b/node/src/oracle.rs new file mode 100644 index 0000000..0b9be2e --- /dev/null +++ b/node/src/oracle.rs @@ -0,0 +1,30 @@ +use std::{net::SocketAddr, path::PathBuf}; + +pub use void_toolkit::oracle_types::config::Config as OracleConfig; +use void_toolkit::oracle_types::config::OracleBlocksConfig; + +#[derive(Clone, Debug)] +pub struct Oracle { + pub oracle_config: OracleConfig, + pub oracle_bind_address: SocketAddr, + pub oracle_storage: OracleStorageType, +} + +#[derive(Clone, Debug)] +pub struct ObserverOracle { + pub oracle_config: OracleBlocksConfig, + pub oracle_storage: OracleStorageType, +} + +#[derive(Clone, Debug)] +pub enum OracleStorageType { + Db(PathBuf), + Memory, +} + +pub fn oracle_storage(storage_type: OracleStorageType) -> anyhow::Result { + match storage_type { + OracleStorageType::Db(path) => Ok(void_toolkit::oracle::Db::sqlite(path)?), + OracleStorageType::Memory => Ok(void_toolkit::oracle::Db::memory()), + } +} diff --git a/node/src/proof.rs b/node/src/proof.rs new file mode 100644 index 0000000..f83affc --- /dev/null +++ b/node/src/proof.rs @@ -0,0 +1,111 @@ +use std::io::Read; + +use void_toolkit::types::{Height, Signed}; + +use crate::storage::DataStorage; + +pub mod replicate; + +pub trait Proof: serde::Serialize + Clone + Send + 'static +where + Self: for<'a> TryFrom<&'a [u8]>, + Self: for<'a, 'b> TryFrom, Error = anyhow::Error>, +{ +} + +pub trait DigestFromProof

+where + Self: for<'a> From<&'a P>, +{ +} + +impl Proof for P +where + P: serde::Serialize + Clone + Send + 'static, + P: for<'a> TryFrom<&'a [u8]>, + P: for<'a, 'b> TryFrom, Error = anyhow::Error>, +{ +} + +impl

DigestFromProof

for [u8; 32] where [u8; 32]: for<'a> From<&'a P> {} + +pub trait ProofConversions { + type App; + type DbData; + fn digest(&self) -> [u8; 32]; + fn into_bytes(self) -> Vec; + fn try_from_bytes(bytes: &[u8]) -> anyhow::Result + where + Self: Sized; + fn try_from_storage( + store: DataStorage<'_, '_, Self::App, Self::DbData>, + ) -> anyhow::Result + where + Self: Sized; +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SignedProof

{ + pub proof: Signed>, +} + +impl

From>> for SignedProof

{ + fn from(signed_height: Signed>) -> Self { + Self { + proof: signed_height, + } + } +} + +impl

From> for Vec +where + Vec: From

, +{ + fn from(signed_proof: SignedProof

) -> Self { + let Signed { signature, data } = signed_proof.proof; + let Height { block_height, data } = data; + let mut buf = vec![]; + buf.extend(block_height.to_be_bytes()); + buf.extend((signature.len() as u64).to_be_bytes()); + buf.extend(signature); + buf.extend(Vec::::from(data)); + buf + } +} + +impl

TryFrom<&[u8]> for SignedProof

+where + P: for<'a> TryFrom<&'a [u8]>, + for<'a>

>::Error: Into, +{ + type Error = anyhow::Error; + + fn try_from(value: &[u8]) -> Result { + let mut cursor = std::io::Cursor::new(&value); + + // Read block height + let mut height_bytes = [0u8; 8]; + cursor.read_exact(&mut height_bytes)?; + let block_height = u64::from_be_bytes(height_bytes); + + // Read signature length + let mut sig_len_bytes = [0u8; 8]; + cursor.read_exact(&mut sig_len_bytes)?; + let sig_len = u64::from_be_bytes(sig_len_bytes) as usize; + + // Read signature + let mut sig_bytes = vec![0u8; sig_len]; + cursor.read_exact(&mut sig_bytes)?; + + let proof = P::try_from(&value[(8 + 8 + sig_len)..]).map_err(|e| e.into())?; + let height = Height { + block_height, + data: proof, + }; + let signed = Signed { + signature: sig_bytes, + data: height, + }; + Ok(SignedProof { proof: signed }) + } +} diff --git a/node/src/proof/replicate.rs b/node/src/proof/replicate.rs new file mode 100644 index 0000000..59562f0 --- /dev/null +++ b/node/src/proof/replicate.rs @@ -0,0 +1,137 @@ +use tokio::sync::{mpsc, watch}; +use void_toolkit::{network_channel::replicate::Sender, types::Height}; + +use crate::{ + db::DbDataConstraints, + storage::{ProofStorageConstraints, Storage}, +}; + +pub struct ProofReplicator { + storage: Storage, + new_proof: watch::Sender<()>, +} + +impl ProofReplicator { + pub fn new(storage: Storage) -> Self { + let (tx, _rx) = watch::channel(()); + Self { + storage, + new_proof: tx, + } + } + + pub async fn update(&self, proof: Height) -> anyhow::Result<()> + where + AppProof: ProofStorageConstraints, + Vec: From, + for<'a> >::Error: Into, + DbData: DbDataConstraints, + { + self.storage.append_proof(proof).await?; + let _ = self.new_proof.send(()); + Ok(()) + } +} + +pub struct Delta(Height); + +impl Sender for ProofReplicator +where + Vec: From, + AppProof: ProofStorageConstraints, + for<'a> >::Error: Into, + App: Send, + Api: Send, + DbData: DbDataConstraints, +{ + type Marker = String; + + type Delta = Delta; + + type Bytes = Vec; + + fn replicate_from( + &self, + _marker: Self::Marker, + ) -> impl futures::Stream + use + Send { + futures::stream::unfold( + (true, self.new_proof.subscribe(), self.storage.clone()), + move |(mut first, mut rx, storage)| async move { + loop { + if !first { + let _ = rx.changed().await; + } + let proof = storage.get_latest_proof().await.ok()?; + if let Some(proof) = proof { + return Some((Delta(proof), (false, rx, storage))); + } + first = false; + } + }, + ) + } +} + +pub async fn recv_delta( + replicator: &ProofReplicator, + delta: Delta, + received_proof_height: mpsc::Sender, +) where + AppProof: ProofStorageConstraints, + Vec: From, + for<'a> >::Error: Into, + DbData: DbDataConstraints, +{ + let Delta(proof) = delta; + let Height { + block_height, + data: proof, + } = proof; + if replicator + .update(Height::new(block_height, proof)) + .await + .is_ok() + { + let _ = received_proof_height.send(block_height).await; + } +} + +impl From> for Vec +where + Vec: From, +{ + fn from(proof: Delta) -> Self { + let mut buf = vec![]; + buf.extend_from_slice(&proof.0.block_height.to_be_bytes()); + buf.extend(Vec::::from(proof.0.data)); + buf + } +} + +impl TryFrom> for Delta +where + AppProof: for<'a> TryFrom<&'a [u8]>, + anyhow::Error: for<'a> From<>::Error>, +{ + type Error = anyhow::Error; + + fn try_from(bytes: Vec) -> Result { + if bytes.len() < 8 { + return Err(anyhow::anyhow!("Not enough bytes to decode proof")); + } + let height_bytes = &bytes[..8]; + let data_bytes = &bytes[8..]; + let block_height = u64::from_be_bytes(height_bytes.try_into().expect("Size checked above")); + let data = AppProof::try_from(data_bytes)?; + Ok(Delta(Height::new(block_height, data))) + } +} + +impl Clone for ProofReplicator { + fn clone(&self) -> Self { + Self { + storage: self.storage.clone(), + new_proof: self.new_proof.clone(), + } + } +} diff --git a/node/src/server.rs b/node/src/server.rs new file mode 100644 index 0000000..a8b8b4a --- /dev/null +++ b/node/src/server.rs @@ -0,0 +1,83 @@ +use std::net::SocketAddr; + +use axum::{ + Json, Router, + extract::{Path, State}, + routing::get, +}; +use tower_http::cors::CorsLayer; + +use crate::{Node, db::DbDataConstraints, storage::ProofStorageConstraints}; + +pub trait AddHandlers: Send + 'static { + fn add( + self, + router: Router>, + ) -> Router>; +} + +impl AddHandlers for F +where + F: FnOnce(Router>) -> Router> + + Send + + 'static, +{ + fn add( + self, + router: Router>, + ) -> Router> { + (self)(router) + } +} + +pub async fn run( + node: Node, + server: SocketAddr, + add_handlers: impl AddHandlers, +) -> anyhow::Result<()> +where + DbData: DbDataConstraints, + App: Send + 'static, + Api: Send + 'static, + AppProof: ProofStorageConstraints + serde::Serialize, + Vec: From, + for<'a> >::Error: Into, +{ + let app = add_handlers + .add(Router::new()) + .route("/", get(|| async { "OK" })) + .route("/get-app-proof/{height}/", get(get_app_proof)) + .route("/get-app-proof/{height}", get(get_app_proof)) + .layer(cors_layer()) + .with_state(node); + let listener = tokio::net::TcpListener::bind(server).await?; + println!("Server running on http://{}", listener.local_addr()?); + axum::serve(listener, app).await?; + Ok(()) +} + +/// The default CORS layer. +pub fn cors_layer() -> CorsLayer { + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([http::Method::GET, http::Method::OPTIONS]) + .allow_headers([http::header::CONTENT_TYPE]) +} + +pub async fn get_app_proof( + State(node): State>, + Path(height): Path, +) -> Result>, String> +where + AppProof: ProofStorageConstraints, + Vec: From, + for<'a> >::Error: Into, + DbData: DbDataConstraints, +{ + let proof = node + .storage + .get_proof(height) + .await + .map_err(|e| e.to_string())?; + Ok(Json(proof)) +} diff --git a/apps/transfers/src/signing.rs b/node/src/signing.rs similarity index 84% rename from apps/transfers/src/signing.rs rename to node/src/signing.rs index 1b50b2c..4ce22e8 100644 --- a/apps/transfers/src/signing.rs +++ b/node/src/signing.rs @@ -19,11 +19,13 @@ pub fn get_signer(key: Option) -> anyhow::Result { } } -/// Sign a proof using the provided signer with Ethereum message prefix. +/// Sign a proof using the provided signer. pub fn sign(signer: &PrivateKeySigner, digest: Height<[u8; 32]>) -> anyhow::Result> { let encoded = (digest.block_height, digest.data).abi_encode_packed(); + // Hash the encoded data - let data = keccak256(encoded); + let hash = keccak256(encoded); + // Sign the hash - Ok(signer.sign_hash_sync(&data)?.into()) + Ok(signer.sign_hash_sync(&hash)?.into()) } diff --git a/node/src/storage.rs b/node/src/storage.rs new file mode 100644 index 0000000..3cbdc2d --- /dev/null +++ b/node/src/storage.rs @@ -0,0 +1,172 @@ +use std::path::PathBuf; + +use void_toolkit::types::Height; + +use crate::{ + db::{Db, DbDataConstraints, Tx}, + memory::{LatestHeaderMem, Memory}, +}; + +pub trait Store { + type App; + type Api; + type AppProof; + type DbData: Clone + Send + 'static; + + fn storage(&self) -> &Storage; +} + +impl Store + for Storage +{ + type App = App; + type Api = Api; + type AppProof = AppProof; + type DbData = DbData; + + fn storage(&self) -> &Storage { + self + } +} + +pub trait ProofStorageConstraints: Clone + Send + 'static +where + Self: for<'a> TryFrom<&'a [u8]>, +{ +} + +impl ProofStorageConstraints for AppProof where + AppProof: for<'a> TryFrom<&'a [u8]> + Clone + Send + 'static +{ +} + +pub enum Storage { + Db(Db), + Memory(Memory), +} + +pub enum StorageType { + Db(PathBuf, DbData), + Memory(App, Api), +} + +pub struct FullMemState<'a, App, Api> { + pub app: &'a App, + pub api: &'a mut Api, +} + +pub struct FullMemStateRef<'a, App, Api> { + pub app: &'a App, + pub api: &'a Api, + pub header: &'a LatestHeaderMem, +} + +pub type FullDataStorage<'a, 'conn, App, Api, DbData = ()> = + DataStorage<'a, 'conn, FullMemState<'a, App, Api>, DbData>; + +pub type FullDataStorageRef<'a, 'conn, App, Api, DbData = ()> = + DataStorageRef<'a, 'conn, FullMemStateRef<'a, App, Api>, DbData>; + +pub enum DataStorage<'a, 'conn, Mem, DbData = ()> { + Db(&'a mut Tx<'conn, DbData>), + Memory(&'a mut Mem), +} + +pub enum DataStorageRef<'a, 'conn, Mem, DbData = ()> { + Db(&'a Tx<'conn, DbData>), + Memory(&'a Mem), +} + +pub trait ReadStorage: Send + Sync + 'static { + fn read(self, storage: FullDataStorageRef<'_, '_, App, Api, DbData>) -> anyhow::Result; +} + +impl ReadStorage for F +where + F: FnOnce(FullDataStorageRef<'_, '_, App, Api, DbData>) -> anyhow::Result + + Send + + Sync + + 'static, +{ + fn read(self, storage: FullDataStorageRef<'_, '_, App, Api, DbData>) -> anyhow::Result { + (self)(storage) + } +} + +impl<'a, App, Api> FullMemState<'a, App, Api> { + pub fn new(app: &'a mut App, api: &'a mut Api) -> Self { + Self { app, api } + } +} + +impl<'a, App, Api> FullMemStateRef<'a, App, Api> { + pub fn new(app: &'a App, api: &'a Api, header: &'a LatestHeaderMem) -> Self { + Self { app, api, header } + } +} + +impl Storage { + pub fn new(data_type: StorageType) -> anyhow::Result { + match data_type { + StorageType::Db(path, data) => Ok(Storage::Db(Db::new(&path, data)?)), + StorageType::Memory(app_state, api_state) => { + Ok(Storage::Memory(Memory::new(app_state, api_state))) + } + } + } +} + +impl Storage +where + AppProof: ProofStorageConstraints, + Vec: From, + for<'a> >::Error: Into, + DbData: DbDataConstraints, +{ + pub async fn append_proof(&self, proof: Height) -> anyhow::Result<()> { + match self { + Storage::Db(db) => db.append_proof(proof).await, + Storage::Memory(mem) => { + mem.append_proof(proof); + Ok(()) + } + } + } + + pub async fn get_proof(&self, height: u64) -> anyhow::Result> { + match self { + Storage::Db(db) => db.get_proof(height).await, + Storage::Memory(mem) => Ok(mem.get_proof(height)), + } + } + + pub async fn get_latest_proof(&self) -> anyhow::Result>> { + match self { + Storage::Db(db) => db.get_latest_proof().await, + Storage::Memory(mem) => Ok(mem.get_latest_proof()), + } + } +} + +impl Storage +where + DbData: DbDataConstraints, +{ + pub async fn get_current_block_height_and_hash( + &self, + ) -> anyhow::Result> { + match self { + Storage::Db(db) => db.get_current_block_height_and_hash().await, + Storage::Memory(mem) => Ok(mem.get_current_block_height_and_hash()), + } + } +} + +impl Clone for Storage { + fn clone(&self) -> Self { + match self { + Self::Db(arg0) => Self::Db(arg0.clone()), + Self::Memory(arg0) => Self::Memory(arg0.clone()), + } + } +} diff --git a/node/src/transitions.rs b/node/src/transitions.rs new file mode 100644 index 0000000..a4a7939 --- /dev/null +++ b/node/src/transitions.rs @@ -0,0 +1,45 @@ +use void_toolkit::types::Block; + +use crate::storage::{DataStorage, FullDataStorage}; + +pub trait AppTransition: Send + Sync + 'static { + fn apply(&self, block: &Block, storage: DataStorage<'_, '_, App, DbData>) + -> anyhow::Result<()>; +} + +impl AppTransition for F +where + F: Fn(&Block, DataStorage<'_, '_, App, DbData>) -> anyhow::Result<()> + Send + Sync + 'static, +{ + fn apply( + &self, + block: &Block, + storage: DataStorage<'_, '_, App, DbData>, + ) -> anyhow::Result<()> { + (self)(block, storage) + } +} + +pub trait ApiTransition: Send + Sync + 'static { + fn apply( + &self, + block: &Block, + storage: FullDataStorage<'_, '_, App, Api, DbData>, + ) -> anyhow::Result<()>; +} + +impl ApiTransition for F +where + F: Fn(&Block, FullDataStorage<'_, '_, App, Api, DbData>) -> anyhow::Result<()> + + Send + + Sync + + 'static, +{ + fn apply( + &self, + block: &Block, + storage: FullDataStorage<'_, '_, App, Api, DbData>, + ) -> anyhow::Result<()> { + (self)(block, storage) + } +}