diff --git a/Cargo.lock b/Cargo.lock index a186096..6d7bbb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,6 +100,9 @@ name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +dependencies = [ + "backtrace", +] [[package]] name = "arrayref" @@ -859,7 +862,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1596,6 +1599,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "miden-block-prover" +version = "0.13.0" +source = "git+https://github.com/0xMiden/miden-base.git?rev=e5ae2329735d2a89b6589375ca7fd91e4f104422#e5ae2329735d2a89b6589375ca7fd91e4f104422" +dependencies = [ + "miden-objects", + "thiserror", +] + [[package]] name = "miden-core" version = "0.19.0" @@ -1681,6 +1693,23 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "miden-lib" +version = "0.13.0" +source = "git+https://github.com/0xMiden/miden-base.git?rev=e5ae2329735d2a89b6589375ca7fd91e4f104422#e5ae2329735d2a89b6589375ca7fd91e4f104422" +dependencies = [ + "fs-err", + "miden-assembly", + "miden-core", + "miden-objects", + "miden-processor", + "miden-stdlib", + "rand", + "regex", + "thiserror", + "walkdir", +] + [[package]] name = "miden-mast-package" version = "0.19.0" @@ -1747,8 +1776,11 @@ dependencies = [ "deadpool-sync", "diesel", "diesel_migrations", + "miden-lib", + "miden-note-transport-node", "miden-note-transport-proto", "miden-objects", + "miden-testing", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", @@ -1781,6 +1813,7 @@ dependencies = [ name = "miden-note-transport-proto" version = "0.1.0" dependencies = [ + "anyhow", "fs-err", "miette", "prost 0.14.1", @@ -1844,6 +1877,20 @@ dependencies = [ "winter-prover", ] +[[package]] +name = "miden-prover" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c30a5d10baeec17b9336de8544cb7f9b96b32de757c4cfb8d95ee0521bb5cd" +dependencies = [ + "miden-air", + "miden-debug-types", + "miden-processor", + "tracing", + "winter-maybe-async", + "winter-prover", +] + [[package]] name = "miden-stdlib" version = "0.19.1" @@ -1860,6 +1907,46 @@ dependencies = [ "thiserror", ] +[[package]] +name = "miden-testing" +version = "0.13.0" +source = "git+https://github.com/0xMiden/miden-base.git?rev=e5ae2329735d2a89b6589375ca7fd91e4f104422#e5ae2329735d2a89b6589375ca7fd91e4f104422" +dependencies = [ + "anyhow", + "itertools", + "miden-block-prover", + "miden-lib", + "miden-objects", + "miden-processor", + "miden-tx", + "miden-tx-batch-prover", + "rand", + "rand_chacha", + "winterfell", +] + +[[package]] +name = "miden-tx" +version = "0.13.0" +source = "git+https://github.com/0xMiden/miden-base.git?rev=e5ae2329735d2a89b6589375ca7fd91e4f104422#e5ae2329735d2a89b6589375ca7fd91e4f104422" +dependencies = [ + "miden-lib", + "miden-objects", + "miden-processor", + "miden-prover", + "miden-verifier", + "thiserror", +] + +[[package]] +name = "miden-tx-batch-prover" +version = "0.13.0" +source = "git+https://github.com/0xMiden/miden-base.git?rev=e5ae2329735d2a89b6589375ca7fd91e4f104422#e5ae2329735d2a89b6589375ca7fd91e4f104422" +dependencies = [ + "miden-objects", + "miden-tx", +] + [[package]] name = "miden-utils-diagnostics" version = "0.19.0" @@ -2744,7 +2831,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3191,7 +3278,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3894,7 +3981,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4372,6 +4459,17 @@ dependencies = [ "winter-utils", ] +[[package]] +name = "winterfell" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f824ddd5aec8ca6a54307f20c115485a8a919ea94dd26d496d856ca6185f4f" +dependencies = [ + "winter-air", + "winter-prover", + "winter-verifier", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 0fa5a7f..019fc46 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -67,4 +67,7 @@ chrono = { workspace = true } rand = { workspace = true } [dev-dependencies] -serial_test = { workspace = true } +miden-lib = { default-features = false, git = "https://github.com/0xMiden/miden-base.git", rev = "e5ae2329735d2a89b6589375ca7fd91e4f104422" } +miden-note-transport-node = { features = ["testing"], workspace = true } +miden-testing = { default-features = false, git = "https://github.com/0xMiden/miden-base.git", rev = "e5ae2329735d2a89b6589375ca7fd91e4f104422" } +serial_test = { workspace = true } diff --git a/crates/node/tests/common/client.rs b/crates/node/tests/common/client.rs new file mode 100644 index 0000000..370fbbb --- /dev/null +++ b/crates/node/tests/common/client.rs @@ -0,0 +1,117 @@ +//! Mock client + +use std::time::Duration; + +use anyhow::Result; +use miden_lib::account::wallets::BasicWallet; +use miden_note_transport_proto::miden_note_transport::miden_note_transport_client::MidenNoteTransportClient; +use miden_note_transport_proto::miden_note_transport::{ + FetchNotesRequest, + SendNoteRequest, + TransportNote, +}; +use miden_objects::account::{Account, AccountBuilder, AccountStorageMode}; +use miden_objects::address::{Address, AddressInterface, RoutingParameters}; +use miden_objects::crypto::dsa::eddsa_25519::SecretKey; +use miden_objects::crypto::ies::{SealedMessage, SealingKey, UnsealingKey}; +use miden_objects::note::{Note, NoteDetails, NoteHeader, NoteTag}; +use miden_objects::utils::{Deserializable, Serializable}; +use miden_testing::Auth; +use rand::Rng; +use tonic::Request; +use tonic::transport::Channel; + +pub struct Client { + pub grpc: MidenNoteTransportClient, + pub unsealing_key: UnsealingKey, + pub address: Address, +} + +impl Client { + pub async fn init(endpoint: String, timeout_ms: u64) -> Result { + let channel = tonic::transport::Endpoint::try_from(endpoint)? + .timeout(Duration::from_millis(timeout_ms)) + .connect() + .await?; + let grpc = MidenNoteTransportClient::new(channel); + + let account = mock_account(); + + let secret_key = SecretKey::with_rng(&mut rand::rng()); + + let public_key = secret_key.public_key(); + let encryption_key = SealingKey::X25519XChaCha20Poly1305(public_key); + let address = Address::new(account.id()) + .with_routing_parameters( + RoutingParameters::new(AddressInterface::BasicWallet) + .with_encryption_key(encryption_key), + ) + .unwrap(); + + let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key); + + Ok(Self { grpc, unsealing_key, address }) + } + + pub async fn send_note(&mut self, note: Note, address: &Address) -> Result<()> { + let header = *note.header(); + let details: NoteDetails = note.into(); + + let details_enc = address + .encryption_key() + .unwrap() + .seal_bytes(&mut rand::rng(), &details.to_bytes()) + .unwrap(); + + let request = SendNoteRequest { + note: Some(TransportNote { + header: header.to_bytes(), + details: details_enc.to_bytes(), + }), + }; + + self.grpc.send_note(Request::new(request)).await?; + + Ok(()) + } + + pub async fn fetch_notes(&mut self, tags: &[NoteTag], cursor: u64) -> Result<(Vec, u64)> { + let tags_int = tags.iter().map(NoteTag::as_u32).collect(); + let request = FetchNotesRequest { tags: tags_int, cursor }; + + let response = self.grpc.fetch_notes(Request::new(request)).await?; + + let response = response.into_inner(); + + let mut notes = Vec::new(); + + for pnote in response.notes { + let sealed_msg = SealedMessage::read_from_bytes(&pnote.details)?; + // try decrypt, if fail just ignore + let Ok(details_bytes) = self.unsealing_key.unseal_bytes(sealed_msg) else { + continue; + }; + let details = NoteDetails::read_from_bytes(&details_bytes)?; + let header = NoteHeader::read_from_bytes(&pnote.header)?; + + let note = Note::new( + details.assets().clone(), + *header.metadata(), + details.recipient().clone(), + ); + notes.push(note); + } + + Ok((notes, response.cursor)) + } +} + +pub fn mock_account() -> Account { + let mut rng = rand::rng(); + AccountBuilder::new(rng.random()) + .storage_mode(AccountStorageMode::Private) + .with_component(BasicWallet) + .with_auth_component(Auth::BasicAuth) + .build() + .unwrap() +} diff --git a/crates/node/tests/common/mod.rs b/crates/node/tests/common/mod.rs new file mode 100644 index 0000000..e0d18ab --- /dev/null +++ b/crates/node/tests/common/mod.rs @@ -0,0 +1,52 @@ +mod client; + +use miden_lib::note::create_p2id_note; +use miden_note_transport_node::node::grpc::GrpcServerConfig; +use miden_note_transport_node::{Node, NodeConfig}; +use miden_objects::Felt; +use miden_objects::account::AccountId; +use miden_objects::address::{Address, AddressId}; +use miden_objects::crypto::rand::RpoRandomCoin; +use miden_objects::note::{Note, NoteType}; +use rand::RngCore; +use tokio::task::JoinHandle; +use tokio::time::{Duration, sleep}; + +use self::client::Client; + +pub async fn spawn_test_server(port: u16) -> JoinHandle<()> { + let config = NodeConfig { + grpc: GrpcServerConfig { port, ..Default::default() }, + ..Default::default() + }; + + let server = Node::init(config).await.unwrap(); + let handle = tokio::spawn(server.entrypoint()); + // Wait for startup + sleep(Duration::from_millis(100)).await; + handle +} + +pub async fn mock_client(port: u16) -> Client { + let timeout_ms = 1000; + let url = format!("http://127.0.0.1:{port}"); + Client::init(url, timeout_ms).await.unwrap() +} + +pub fn mock_note_p2id_with_addresses(sender: &Address, target: &Address) -> Note { + let mut randrng = rand::rng(); + let seed: [Felt; 4] = core::array::from_fn(|_| Felt::new(randrng.next_u64())); + let mut rng = RpoRandomCoin::new(seed.into()); + let sender_id = addrid_to_accid(&sender.id()); + let target_id = addrid_to_accid(&target.id()); + create_p2id_note(sender_id, target_id, vec![], NoteType::Private, Felt::default(), &mut rng) + .unwrap() +} + +pub fn addrid_to_accid(addrid: &AddressId) -> AccountId { + if let AddressId::AccountId(accid) = addrid { + *accid + } else { + panic!() + } +} diff --git a/crates/node/tests/test_basic.rs b/crates/node/tests/test_basic.rs new file mode 100644 index 0000000..f86d983 --- /dev/null +++ b/crates/node/tests/test_basic.rs @@ -0,0 +1,64 @@ +mod common; + +use anyhow::Result; + +use self::common::*; + +#[tokio::test] +async fn test_basic_exchange() -> Result<()> { + let port = 9627; + let handle = spawn_test_server(port).await; + + let mut client0 = mock_client(port).await; + let mut client1 = mock_client(port).await; + + let tag = client1.address.to_note_tag(); + + let note = mock_note_p2id_with_addresses(&client0.address, &client1.address); + + client0.send_note(note, &client1.address).await?; + let (notes, _cursor) = client1.fetch_notes(&[tag], 0).await?; + + assert_eq!(notes.len(), 1); + let header = notes[0].header(); + let rx_tag = header.metadata().tag(); + assert_eq!(rx_tag, tag); + + handle.abort(); + Ok(()) +} + +#[tokio::test] +async fn test_basic_pagination() -> Result<()> { + let port = 9628; + let handle = spawn_test_server(port).await; + + let mut client0 = mock_client(port).await; + let mut client1 = mock_client(port).await; + + let tag = client1.address.to_note_tag(); + + let note_a = mock_note_p2id_with_addresses(&client0.address, &client1.address); + let note_b = mock_note_p2id_with_addresses(&client0.address, &client1.address); + + client0.send_note(note_a, &client1.address).await?; + let (notes, cursor_a) = client1.fetch_notes(&[tag], 0).await?; + assert_eq!(notes.len(), 1); + + client0.send_note(note_b, &client1.address).await?; + // no pagination (fetch all) + let (notes, _) = client1.fetch_notes(&[tag], 0).await?; + assert_eq!(notes.len(), 2); + + // pagination, cursor after first note + let (notes, cursor_b) = client1.fetch_notes(&[tag], cursor_a).await?; + assert_eq!(notes.len(), 1); + + // pagination, all notes fetched, cursor should not change + let (notes, cursor_c) = client1.fetch_notes(&[tag], cursor_b).await?; + assert_eq!(notes.len(), 0); + assert_eq!(cursor_c, cursor_b, "expected replied cursor same as request"); + + handle.abort(); + Ok(()) +} diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index 78d0621..a7c6875 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -20,6 +20,7 @@ tonic = { features = ["codegen"], workspace = true } tonic-prost = { workspace = true } [build-dependencies] +anyhow = { workspace = true } fs-err = { workspace = true } miette = { workspace = true } protox = { workspace = true }