diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74b78166e..933a6136a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,7 @@ jobs: cargo make check --tests - name: Test run: | - cargo make test -E 'not (package(miden-integration-tests) or package(miden-integration-node-tests) or package(cargo-miden))' + cargo make test -E 'not (package(miden-integration-tests) or package(midenc-integration-network-tests) or package(cargo-miden))' check_release: name: release checks @@ -323,7 +323,7 @@ jobs: fi - name: Test run: | - cargo make test -E 'package(miden-integration-node-tests)' + cargo make test -E 'package(midenc-integration-network-tests)' cargo_publish_dry_run: name: cargo publish dry run diff --git a/Cargo.lock b/Cargo.lock index eb52bacd8..6e85395fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,9 @@ name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +dependencies = [ + "backtrace", +] [[package]] name = "anymap2" @@ -896,36 +899,6 @@ dependencies = [ "syn", ] -[[package]] -name = "deadpool" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" -dependencies = [ - "deadpool-runtime", - "lazy_static", - "num_cpus", - "tokio", -] - -[[package]] -name = "deadpool-runtime" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -dependencies = [ - "tokio", -] - -[[package]] -name = "deadpool-sync" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" -dependencies = [ - "deadpool-runtime", -] - [[package]] name = "der" version = "0.7.10" @@ -1130,12 +1103,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fastrand" version = "2.3.0" @@ -1231,16 +1198,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "funty" version = "2.0.0" @@ -1490,27 +1447,12 @@ dependencies = [ "serde_core", ] -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hex" version = "0.4.3" @@ -1950,17 +1892,6 @@ dependencies = [ "redox_syscall 0.6.0", ] -[[package]] -name = "libsqlite3-sys" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91632f3b4fb6bd1d72aa3d78f41ffecfcf2b1a6648d8c241dbe7dbfaf4875e15" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2263,6 +2194,17 @@ dependencies = [ "miden-stdlib-sys", ] +[[package]] +name = "miden-block-prover" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec766587e838664ded55fa926d0611244cac2fe23b7cec202d8db0a85d9e536e" +dependencies = [ + "miden-lib", + "miden-objects", + "thiserror 2.0.17", +] + [[package]] name = "miden-client" version = "0.12.5" @@ -2280,6 +2222,7 @@ dependencies = [ "miden-note-transport-proto-build", "miden-objects", "miden-remote-prover-client", + "miden-testing", "miden-tx", "miette", "prost 0.14.1", @@ -2295,28 +2238,10 @@ dependencies = [ "tonic-prost-build", "tonic-web-wasm-client", "tracing", + "uuid", "web-sys", ] -[[package]] -name = "miden-client-sqlite-store" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "594b08393cd63ff092acabae229ae8cc92953471831efdf63650496f4c22dbd6" -dependencies = [ - "anyhow", - "async-trait", - "chrono", - "deadpool", - "deadpool-sync", - "miden-client", - "miden-objects", - "rusqlite", - "rusqlite_migration", - "thiserror 2.0.17", - "tokio", -] - [[package]] name = "miden-core" version = "0.19.1" @@ -2477,26 +2402,6 @@ dependencies = [ "unicode-width 0.1.14", ] -[[package]] -name = "miden-integration-node-tests" -version = "0.6.0" -dependencies = [ - "anyhow", - "fs2", - "miden-client", - "miden-client-sqlite-store", - "miden-core", - "miden-felt-repr-offchain", - "miden-integration-tests", - "miden-mast-package", - "miden-objects", - "midenc-frontend-wasm", - "rand", - "temp-dir", - "tokio", - "uuid", -] - [[package]] name = "miden-integration-tests" version = "0.6.0" @@ -2546,6 +2451,7 @@ dependencies = [ "miden-objects", "miden-processor", "miden-stdlib", + "rand", "regex", "thiserror 2.0.17", "walkdir", @@ -2647,10 +2553,12 @@ dependencies = [ "miden-utils-sync", "miden-verifier", "rand", + "rand_xoshiro", "semver 1.0.27", "serde", "thiserror 2.0.17", "toml 0.9.10+spec-1.1.0", + "winter-rand-utils", ] [[package]] @@ -2731,6 +2639,26 @@ dependencies = [ name = "miden-stdlib-sys" version = "0.8.0" +[[package]] +name = "miden-testing" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda0d572d7415682ed168f616becf006825aa04b89692f9907cbb3e3586bf46a" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "miden-block-prover", + "miden-lib", + "miden-objects", + "miden-processor", + "miden-tx", + "miden-tx-batch-prover", + "rand", + "rand_chacha", + "thiserror 2.0.17", + "winterfell", +] + [[package]] name = "miden-thiserror" version = "1.0.59" @@ -2767,6 +2695,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "miden-tx-batch-prover" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5029810b106654a1ec5d7d7123945db91b96bc4f4187715d0c2cfe0b0a53af4" +dependencies = [ + "miden-objects", + "miden-tx", +] + [[package]] name = "miden-utils-diagnostics" version = "0.19.1" @@ -3087,6 +3025,22 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "midenc-integration-network-tests" +version = "0.6.0" +dependencies = [ + "miden-client", + "miden-core", + "miden-felt-repr-offchain", + "miden-integration-tests", + "miden-lib", + "miden-mast-package", + "miden-objects", + "midenc-frontend-wasm", + "rand", + "tokio", +] + [[package]] name = "midenc-session" version = "0.6.0" @@ -3304,16 +3258,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "objc2" version = "0.6.3" @@ -4142,6 +4086,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.3", +] + [[package]] name = "ratatui" version = "0.29.0" @@ -4254,30 +4207,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rusqlite" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de23c3319433716cf134eed225fe9986bc24f63bed9be9f20c329029e672dc7" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - -[[package]] -name = "rusqlite_migration" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a324f81362b5cd8f2eeef82d032172fdf2ca70aeec64962ff55a56874fe5ec41" -dependencies = [ - "log", - "rusqlite", -] - [[package]] name = "rustc-demangle" version = "0.1.26" @@ -5527,6 +5456,7 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -5536,12 +5466,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -6171,6 +6095,16 @@ dependencies = [ "winter-utils", ] +[[package]] +name = "winter-rand-utils" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ff3b651754a7bd216f959764d0a5ab6f4b551c9a3a08fb9ccecbed594b614a" +dependencies = [ + "rand", + "winter-utils", +] + [[package]] name = "winter-utils" version = "0.13.1" @@ -6193,6 +6127,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/Cargo.toml b/Cargo.toml index 58b2d82e5..1f81b4c47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ "sdk/stdlib-sys", "tools/*", "tests/integration", - "tests/integration-node", + "tests/integration-network", ] exclude = [ "sdk/.cargo", @@ -194,7 +194,7 @@ opt-level = 3 # Speed up the test profile (proving times) # ============================================================ # The test package itself needs optimization -[profile.test.package.miden-integration-node-tests] +[profile.test.package.midenc-integration-network-tests] opt-level = 3 # Core Miden packages diff --git a/Makefile.toml b/Makefile.toml index 5a1411acd..6160f9669 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -277,7 +277,7 @@ args = ["audit"] [tasks.test] category = "Test" -description = "Runs all tests including integration node tests" +description = "Runs all tests including network integration tests" dependencies = ["test-rust"] [tasks.install-cargo-miden] diff --git a/tests/integration-node/Cargo.toml b/tests/integration-network/Cargo.toml similarity index 72% rename from tests/integration-node/Cargo.toml rename to tests/integration-network/Cargo.toml index 687270ff8..69102d80c 100644 --- a/tests/integration-node/Cargo.toml +++ b/tests/integration-network/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "miden-integration-node-tests" +name = "midenc-integration-network-tests" version.workspace = true rust-version.workspace = true authors.workspace = true @@ -12,19 +12,15 @@ edition.workspace = true publish = false [dependencies] -anyhow.workspace = true -fs2 = "0.4" -miden-client = { version = "0.12", features = ["std", "tonic"] } -miden-client-sqlite-store = { version = "0.12" } +miden-client = { version = "0.12", features = ["std", "tonic", "testing"] } miden-core.workspace = true +miden-lib = { version = "0.12" } miden-felt-repr-offchain = { version = "0.7.1", path = "../../sdk/felt-repr/offchain" } miden-mast-package.workspace = true miden-objects = { workspace = true, features = ["std"] } midenc-frontend-wasm.workspace = true rand = "0.9" -temp-dir = "0.1" tokio.workspace = true -uuid = { version = "1.10", features = ["v4"] } # For accessing the test builder from the main integration tests miden-integration-tests = { path = "../integration" } diff --git a/tests/integration-network/README.md b/tests/integration-network/README.md new file mode 100644 index 000000000..7d78eca8e --- /dev/null +++ b/tests/integration-network/README.md @@ -0,0 +1,22 @@ +# Miden Integration Network Tests + +This crate contains integration tests that exercise contract deployment and execution on a mock +chain (`miden_client::testing::MockChain`). + +## Overview + +The tests in this crate are separated from the main integration tests because they: +- Exercise multi-step end-to-end scenarios (account setup, block production, tx execution) +- Can be slower due to proving and compiling example packages + +## Running Tests + +```bash +cargo test -p midenc-integration-network-tests +``` + +To see test output: + +```bash +cargo test -p midenc-integration-network-tests -- --nocapture +``` diff --git a/tests/integration-network/src/lib.rs b/tests/integration-network/src/lib.rs new file mode 100644 index 000000000..eb311288b --- /dev/null +++ b/tests/integration-network/src/lib.rs @@ -0,0 +1,4 @@ +//! Integration tests which exercise contract deployment and execution on a mock chain. + +#[cfg(test)] +mod mockchain; diff --git a/tests/integration-network/src/mockchain/basic_wallet.rs b/tests/integration-network/src/mockchain/basic_wallet.rs new file mode 100644 index 000000000..198bbd4bf --- /dev/null +++ b/tests/integration-network/src/mockchain/basic_wallet.rs @@ -0,0 +1,368 @@ +//! Basic wallet test module + +use miden_client::{ + asset::FungibleAsset, + crypto::RpoRandomCoin, + note::NoteAssets, + testing::{AccountState, Auth, MockChain}, + transaction::OutputNote, +}; +use miden_core::Felt; +use miden_felt_repr_offchain::{AccountIdFeltRepr, ToFeltRepr}; + +use super::helpers::{ + NoteCreationConfig, assert_account_has_fungible_asset, build_asset_transfer_tx, + build_existing_basic_wallet_account_builder, build_send_notes_script, compile_rust_package, + create_note_from_package, execute_tx, +}; + +/// Tests the basic-wallet contract deployment and p2id note consumption workflow on a mock chain. +#[test] +pub fn test_basic_wallet_p2id() { + // Compile the contracts first (before creating any runtime) + let wallet_package = compile_rust_package("../../examples/basic-wallet", true); + let note_package = compile_rust_package("../../examples/p2id-note", true); + let tx_script_package = compile_rust_package("../../examples/basic-wallet-tx-script", true); + + let mut builder = MockChain::builder(); + let max_supply = 1_000_000_000u64; + let faucet_account = builder + .add_existing_basic_faucet(Auth::BasicAuth, "TEST", max_supply, None) + .unwrap(); + let faucet_id = faucet_account.id(); + + let alice_account = builder + .add_account_from_builder( + Auth::BasicAuth, + build_existing_basic_wallet_account_builder(wallet_package.clone(), false, [1_u8; 32]), + AccountState::Exists, + ) + .unwrap(); + let alice_id = alice_account.id(); + + let bob_account = builder + .add_account_from_builder( + Auth::BasicAuth, + build_existing_basic_wallet_account_builder(wallet_package, false, [2_u8; 32]), + AccountState::Exists, + ) + .unwrap(); + let bob_id = bob_account.id(); + + let mut chain = builder.build().unwrap(); + chain.prove_next_block().unwrap(); + chain.prove_next_block().unwrap(); + + eprintln!("\n=== Step 1: Minting tokens from faucet to Alice ==="); + let mint_amount = 100_000u64; // 100,000 tokens + let mint_asset = FungibleAsset::new(faucet_id, mint_amount).unwrap(); + + let mut note_rng = RpoRandomCoin::new(note_package.unwrap_program().hash()); + let p2id_note_mint = create_note_from_package( + note_package.clone(), + faucet_id, + NoteCreationConfig { + assets: NoteAssets::new(vec![mint_asset.into()]).unwrap(), + inputs: AccountIdFeltRepr(&alice_id).to_felt_repr(), + ..Default::default() + }, + &mut note_rng, + ); + + let faucet_account = chain.committed_account(faucet_id).unwrap().clone(); + let mint_tx_script = + build_send_notes_script(&faucet_account, std::slice::from_ref(&p2id_note_mint)); + let mint_tx_context_builder = chain + .build_tx_context(faucet_id, &[], &[]) + .unwrap() + .tx_script(mint_tx_script) + .extend_expected_output_notes(vec![OutputNote::Full(p2id_note_mint.clone())]); + execute_tx(&mut chain, mint_tx_context_builder); + + eprintln!("\n=== Step 2: Alice consumes mint note ==="); + let consume_tx_context_builder = + chain.build_tx_context(alice_id, &[p2id_note_mint.id()], &[]).unwrap(); + execute_tx(&mut chain, consume_tx_context_builder); + + eprintln!("\n=== Checking Alice's account has the minted asset ==="); + let alice_account = chain.committed_account(alice_id).unwrap(); + assert_account_has_fungible_asset(alice_account, faucet_id, mint_amount); + + eprintln!("\n=== Step 3: Alice creates p2id note for Bob (custom tx script) ==="); + let transfer_amount = 10_000u64; // 10,000 tokens + let transfer_asset = FungibleAsset::new(faucet_id, transfer_amount).unwrap(); + + let (alice_tx_context_builder, bob_note) = build_asset_transfer_tx( + &chain, + alice_id, + bob_id, + transfer_asset, + note_package, + tx_script_package, + &mut note_rng, + ); + execute_tx(&mut chain, alice_tx_context_builder); + + eprintln!("\n=== Step 4: Bob consumes p2id note ==="); + let consume_tx_context_builder = chain.build_tx_context(bob_id, &[bob_note.id()], &[]).unwrap(); + execute_tx(&mut chain, consume_tx_context_builder); + + eprintln!("\n=== Checking Bob's account has the transferred asset ==="); + let bob_account = chain.committed_account(bob_id).unwrap(); + assert_account_has_fungible_asset(bob_account, faucet_id, transfer_amount); + + eprintln!("\n=== Checking Alice's account reflects the new token amount ==="); + let alice_account = chain.committed_account(alice_id).unwrap(); + assert_account_has_fungible_asset(alice_account, faucet_id, mint_amount - transfer_amount); +} + +/// Tests the basic-wallet contract deployment and p2ide note consumption workflow on a mock chain. +/// +/// Flow: +/// - Create fungible faucet and mint tokens to Alice +/// - Alice creates a p2ide note for Bob (with timelock=0, reclaim=0) +/// - Bob consumes the p2ide note and receives the assets +#[test] +pub fn test_basic_wallet_p2ide() { + // Compile the contracts first (before creating any runtime) + let wallet_package = compile_rust_package("../../examples/basic-wallet", true); + let p2id_note_package = compile_rust_package("../../examples/p2id-note", true); + let p2ide_note_package = compile_rust_package("../../examples/p2ide-note", true); + + let mut builder = MockChain::builder(); + let max_supply = 1_000_000_000u64; + let faucet_account = builder + .add_existing_basic_faucet(Auth::BasicAuth, "TEST", max_supply, None) + .unwrap(); + let faucet_id = faucet_account.id(); + + let alice_account = builder + .add_account_from_builder( + Auth::BasicAuth, + build_existing_basic_wallet_account_builder(wallet_package.clone(), true, [3_u8; 32]), + AccountState::Exists, + ) + .unwrap(); + let alice_id = alice_account.id(); + + let bob_account = builder + .add_account_from_builder( + Auth::BasicAuth, + build_existing_basic_wallet_account_builder(wallet_package, false, [4_u8; 32]), + AccountState::Exists, + ) + .unwrap(); + let bob_id = bob_account.id(); + + let mut chain = builder.build().unwrap(); + chain.prove_next_block().unwrap(); + chain.prove_next_block().unwrap(); + + // Step 1: Mint assets from faucet to Alice using p2id note + let mint_amount = 100_000u64; + let mint_asset = FungibleAsset::new(faucet_id, mint_amount).unwrap(); + + let mut p2id_rng = RpoRandomCoin::new(p2id_note_package.unwrap_program().hash()); + let p2id_note_mint = create_note_from_package( + p2id_note_package.clone(), + faucet_id, + NoteCreationConfig { + assets: NoteAssets::new(vec![mint_asset.into()]).unwrap(), + inputs: AccountIdFeltRepr(&alice_id).to_felt_repr(), + ..Default::default() + }, + &mut p2id_rng, + ); + + let faucet_account = chain.committed_account(faucet_id).unwrap().clone(); + let mint_tx_script = + build_send_notes_script(&faucet_account, std::slice::from_ref(&p2id_note_mint)); + let mint_tx_context_builder = chain + .build_tx_context(faucet_id, &[], &[]) + .unwrap() + .tx_script(mint_tx_script) + .extend_expected_output_notes(vec![OutputNote::Full(p2id_note_mint.clone())]); + execute_tx(&mut chain, mint_tx_context_builder); + + // Step 2: Alice consumes the p2id note + let consume_tx_context_builder = + chain.build_tx_context(alice_id, &[p2id_note_mint.id()], &[]).unwrap(); + execute_tx(&mut chain, consume_tx_context_builder); + + let alice_account = chain.committed_account(alice_id).unwrap(); + assert_account_has_fungible_asset(alice_account, faucet_id, mint_amount); + + // Step 3: Alice creates p2ide note for Bob + let transfer_amount = 10_000u64; + let transfer_asset = FungibleAsset::new(faucet_id, transfer_amount).unwrap(); + let timelock_height = Felt::new(0); + let reclaim_height = Felt::new(0); + + let mut p2ide_rng = RpoRandomCoin::new(p2ide_note_package.unwrap_program().hash()); + let p2ide_note = create_note_from_package( + p2ide_note_package, + alice_id, + NoteCreationConfig { + assets: NoteAssets::new(vec![transfer_asset.into()]).unwrap(), + inputs: { + let mut inputs = AccountIdFeltRepr(&bob_id).to_felt_repr(); + inputs.extend([timelock_height, reclaim_height]); + inputs + }, + ..Default::default() + }, + &mut p2ide_rng, + ); + + let alice_account = chain.committed_account(alice_id).unwrap().clone(); + let transfer_tx_script = + build_send_notes_script(&alice_account, std::slice::from_ref(&p2ide_note)); + let transfer_tx_context_builder = chain + .build_tx_context(alice_id, &[], &[]) + .unwrap() + .tx_script(transfer_tx_script) + .extend_expected_output_notes(vec![OutputNote::Full(p2ide_note.clone())]); + execute_tx(&mut chain, transfer_tx_context_builder); + + // Step 4: Bob consumes the p2ide note + let consume_tx_context_builder = + chain.build_tx_context(bob_id, &[p2ide_note.id()], &[]).unwrap(); + execute_tx(&mut chain, consume_tx_context_builder); + + // Step 5: verify balances + let bob_account = chain.committed_account(bob_id).unwrap(); + assert_account_has_fungible_asset(bob_account, faucet_id, transfer_amount); + + let alice_account = chain.committed_account(alice_id).unwrap(); + assert_account_has_fungible_asset(alice_account, faucet_id, mint_amount - transfer_amount); +} + +/// Tests the p2ide note reclaim functionality. +/// +/// Flow: +/// - Create fungible faucet and mint tokens to Alice +/// - Alice creates a p2ide note intended for Bob (with reclaim enabled) +/// - Alice reclaims the note herself (exercises the reclaim branch) +/// - Verify Alice has her original balance back +#[test] +pub fn test_basic_wallet_p2ide_reclaim() { + // Compile the contracts first (before creating any runtime) + let wallet_package = compile_rust_package("../../examples/basic-wallet", true); + let p2id_note_package = compile_rust_package("../../examples/p2id-note", true); + let p2ide_note_package = compile_rust_package("../../examples/p2ide-note", true); + + let mut builder = MockChain::builder(); + let max_supply = 1_000_000_000u64; + let faucet_account = builder + .add_existing_basic_faucet(Auth::BasicAuth, "TEST", max_supply, None) + .unwrap(); + let faucet_id = faucet_account.id(); + + let alice_account = builder + .add_account_from_builder( + Auth::BasicAuth, + build_existing_basic_wallet_account_builder(wallet_package.clone(), true, [5_u8; 32]), + AccountState::Exists, + ) + .unwrap(); + let alice_id = alice_account.id(); + + let bob_account = builder + .add_account_from_builder( + Auth::BasicAuth, + build_existing_basic_wallet_account_builder(wallet_package, false, [6_u8; 32]), + AccountState::Exists, + ) + .unwrap(); + let bob_id = bob_account.id(); + + let mut chain = builder.build().unwrap(); + chain.prove_next_block().unwrap(); + chain.prove_next_block().unwrap(); + + // Step 1: Mint assets from faucet to Alice using p2id note + let mint_amount = 100_000u64; + let mint_asset = FungibleAsset::new(faucet_id, mint_amount).unwrap(); + + let mut p2id_rng = RpoRandomCoin::new(p2id_note_package.unwrap_program().hash()); + let p2id_note_mint = create_note_from_package( + p2id_note_package.clone(), + faucet_id, + NoteCreationConfig { + assets: NoteAssets::new(vec![mint_asset.into()]).unwrap(), + inputs: AccountIdFeltRepr(&alice_id).to_felt_repr(), + ..Default::default() + }, + &mut p2id_rng, + ); + + let faucet_account = chain.committed_account(faucet_id).unwrap().clone(); + let mint_tx_script = + build_send_notes_script(&faucet_account, std::slice::from_ref(&p2id_note_mint)); + let mint_tx_context_builder = chain + .build_tx_context(faucet_id, &[], &[]) + .unwrap() + .tx_script(mint_tx_script) + .extend_expected_output_notes(vec![OutputNote::Full(p2id_note_mint.clone())]); + execute_tx(&mut chain, mint_tx_context_builder); + + // Step 2: Alice consumes the p2id note + let consume_tx_context_builder = + chain.build_tx_context(alice_id, &[p2id_note_mint.id()], &[]).unwrap(); + execute_tx(&mut chain, consume_tx_context_builder); + + let alice_account = chain.committed_account(alice_id).unwrap(); + assert_account_has_fungible_asset(alice_account, faucet_id, mint_amount); + + // Step 3: Alice creates p2ide note for Bob with reclaim enabled + let transfer_amount = 10_000u64; + let transfer_asset = FungibleAsset::new(faucet_id, transfer_amount).unwrap(); + let timelock_height = Felt::new(0); + let reclaim_height = Felt::new(1000); + + let mut p2ide_rng = RpoRandomCoin::new(p2ide_note_package.unwrap_program().hash()); + let p2ide_note = create_note_from_package( + p2ide_note_package, + alice_id, + NoteCreationConfig { + assets: NoteAssets::new(vec![transfer_asset.into()]).unwrap(), + inputs: { + let mut inputs = AccountIdFeltRepr(&bob_id).to_felt_repr(); + inputs.extend([timelock_height, reclaim_height]); + inputs + }, + ..Default::default() + }, + &mut p2ide_rng, + ); + + let alice_account = chain.committed_account(alice_id).unwrap().clone(); + let transfer_tx_script = + build_send_notes_script(&alice_account, std::slice::from_ref(&p2ide_note)); + let transfer_tx_context_builder = chain + .build_tx_context(alice_id, &[], &[]) + .unwrap() + .tx_script(transfer_tx_script) + .extend_expected_output_notes(vec![OutputNote::Full(p2ide_note.clone())]); + execute_tx(&mut chain, transfer_tx_context_builder); + + // Step 4: Alice reclaims the note (exercises the reclaim branch) + let reclaim_tx_context_builder = + chain.build_tx_context(alice_id, &[p2ide_note.id()], &[]).unwrap(); + execute_tx(&mut chain, reclaim_tx_context_builder); + + // Step 5: verify Alice has her original amount back + let alice_account = chain.committed_account(alice_id).unwrap(); + assert_account_has_fungible_asset(alice_account, faucet_id, mint_amount); + + // Ensure Bob did not receive the asset. + let bob_account = chain.committed_account(bob_id).unwrap(); + let bob_found = bob_account.vault().assets().find(|asset| { + matches!( + asset, + miden_objects::asset::Asset::Fungible(fungible_asset) + if fungible_asset.faucet_id() == faucet_id + ) + }); + assert!(bob_found.is_none(), "Bob unexpectedly received reclaimed assets"); +} diff --git a/tests/integration-network/src/mockchain/counter_contract.rs b/tests/integration-network/src/mockchain/counter_contract.rs new file mode 100644 index 000000000..d70e8052b --- /dev/null +++ b/tests/integration-network/src/mockchain/counter_contract.rs @@ -0,0 +1,73 @@ +//! Counter contract test module + +use miden_client::{ + Word, + account::component::BasicWallet, + crypto::RpoRandomCoin, + note::NoteTag, + testing::{AccountState, Auth, MockChain}, + transaction::OutputNote, +}; +use miden_core::{Felt, FieldElement}; +use miden_objects::account::{ + AccountBuilder, AccountStorageMode, AccountType, StorageMap, StorageSlot, +}; + +use super::helpers::{ + NoteCreationConfig, account_component_from_package, assert_counter_storage, + compile_rust_package, create_note_from_package, execute_tx, +}; + +/// Tests the counter contract deployment and note consumption workflow on a mock chain. +#[test] +pub fn test_counter_contract() { + // Compile the contracts first (before creating any runtime) + let contract_package = compile_rust_package("../../examples/counter-contract", true); + let note_package = compile_rust_package("../../examples/counter-note", true); + + let key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + let value = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + let storage_slots = vec![StorageSlot::Map(StorageMap::with_entries([(key, value)]).unwrap())]; + + let counter_component = account_component_from_package(contract_package, storage_slots); + let counter_account_builder = AccountBuilder::new([0_u8; 32]) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_component(BasicWallet) + .with_component(counter_component); + + let mut builder = MockChain::builder(); + let counter_account = builder + .add_account_from_builder(Auth::BasicAuth, counter_account_builder, AccountState::Exists) + .expect("failed to add counter account to mock chain builder"); + + let mut rng = RpoRandomCoin::new(note_package.clone().unwrap_program().hash()); + let counter_note = create_note_from_package( + note_package, + counter_account.id(), + NoteCreationConfig { + tag: NoteTag::from_account_id(counter_account.id()), + ..Default::default() + }, + &mut rng, + ); + builder.add_output_note(OutputNote::Full(counter_note.clone())); + + let mut chain = builder.build().expect("failed to build mock chain"); + chain.prove_next_block().unwrap(); + chain.prove_next_block().unwrap(); + + eprintln!("Counter account ID: {:?}", counter_account.id().to_hex()); + + // The counter contract storage value should be 1 after account creation (initialized to 1). + assert_counter_storage(chain.committed_account(counter_account.id()).unwrap().storage(), 1, 1); + + // Consume the note to increment the counter + let tx_context_builder = chain + .build_tx_context(counter_account.clone(), &[counter_note.id()], &[]) + .unwrap(); + execute_tx(&mut chain, tx_context_builder); + + // The counter contract storage value should be 2 after the note is consumed (incremented by 1). + assert_counter_storage(chain.committed_account(counter_account.id()).unwrap().storage(), 1, 2); +} diff --git a/tests/integration-network/src/mockchain/counter_contract_no_auth.rs b/tests/integration-network/src/mockchain/counter_contract_no_auth.rs new file mode 100644 index 000000000..9ad3cb267 --- /dev/null +++ b/tests/integration-network/src/mockchain/counter_contract_no_auth.rs @@ -0,0 +1,97 @@ +//! Counter contract test with no-auth authentication component + +use miden_client::{ + Word, + account::component::BasicWallet, + crypto::RpoRandomCoin, + note::NoteTag, + testing::{AccountState, Auth, MockChain}, + transaction::OutputNote, +}; +use miden_core::{Felt, FieldElement}; +use miden_objects::account::{ + AccountBuilder, AccountStorageMode, AccountType, StorageMap, StorageSlot, +}; + +use super::helpers::{ + NoteCreationConfig, assert_counter_storage, + build_existing_counter_account_builder_with_auth_package, compile_rust_package, + create_note_from_package, execute_tx, +}; + +/// Tests the counter contract with a "no-auth" authentication component. +/// +/// Flow: +/// - Build counter account using `examples/auth-component-no-auth` as the auth component +/// - Build a separate sender account (basic wallet) +/// - Sender issues a counter note to the network +/// - Counter account consumes the note without requiring authentication/signature +#[test] +pub fn test_counter_contract_no_auth() { + // Compile the contracts first (before creating any runtime) + let contract_package = compile_rust_package("../../examples/counter-contract", true); + let note_package = compile_rust_package("../../examples/counter-note", true); + let no_auth_auth_component = + compile_rust_package("../../examples/auth-component-no-auth", true); + + let key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + let value = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + let counter_storage_slots = + vec![StorageSlot::Map(StorageMap::with_entries([(key, value)]).unwrap())]; + + let mut builder = MockChain::builder(); + + let counter_account = build_existing_counter_account_builder_with_auth_package( + contract_package, + no_auth_auth_component, + vec![], + counter_storage_slots, + [0_u8; 32], + ) + .build_existing() + .expect("failed to build counter account"); + builder + .add_account(counter_account.clone()) + .expect("failed to add counter account to mock chain builder"); + eprintln!("Counter account (no-auth) ID: {:?}", counter_account.id().to_hex()); + + // Create a separate sender account using only the BasicWallet component + let seed = [1_u8; 32]; + let sender_builder = AccountBuilder::new(seed) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_component(BasicWallet); + let sender_account = builder + .add_account_from_builder(Auth::BasicAuth, sender_builder, AccountState::Exists) + .expect("failed to add sender account to mock chain builder"); + eprintln!("Sender account ID: {:?}", sender_account.id().to_hex()); + + // Sender creates the counter note (note script increments counter's storage on consumption) + let mut rng = RpoRandomCoin::new(note_package.unwrap_program().hash()); + let counter_note = create_note_from_package( + note_package.clone(), + sender_account.id(), + NoteCreationConfig { + tag: NoteTag::from_account_id(counter_account.id()), + ..Default::default() + }, + &mut rng, + ); + eprintln!("Counter note hash: {:?}", counter_note.id().to_hex()); + builder.add_output_note(OutputNote::Full(counter_note.clone())); + + let mut chain = builder.build().expect("failed to build mock chain"); + chain.prove_next_block().unwrap(); + chain.prove_next_block().unwrap(); + + assert_counter_storage(chain.committed_account(counter_account.id()).unwrap().storage(), 0, 1); + + // Consume the note with the counter account (no signature/auth required). + let tx_context_builder = chain + .build_tx_context(counter_account.clone(), &[counter_note.id()], &[]) + .unwrap(); + execute_tx(&mut chain, tx_context_builder); + + // The counter contract storage value should be 2 after the note is consumed + assert_counter_storage(chain.committed_account(counter_account.id()).unwrap().storage(), 0, 2); +} diff --git a/tests/integration-network/src/mockchain/counter_contract_rust_auth.rs b/tests/integration-network/src/mockchain/counter_contract_rust_auth.rs new file mode 100644 index 000000000..027d034fc --- /dev/null +++ b/tests/integration-network/src/mockchain/counter_contract_rust_auth.rs @@ -0,0 +1,105 @@ +//! Counter contract test using an auth component compiled from Rust (RPO-Falcon512) +//! +//! This test ensures that an account which does not possess the correct +//! RPO-Falcon512 secret key cannot create notes on behalf of the counter +//! contract account that uses the Rust-compiled auth component. + +use miden_client::{ + auth::{AuthSecretKey, BasicAuthenticator}, + crypto::RpoRandomCoin, + note::NoteTag, + testing::MockChain, + transaction::OutputNote, +}; + +use super::helpers::{ + NoteCreationConfig, assert_counter_storage, block_on, build_counter_account_with_rust_rpo_auth, + build_send_notes_script, compile_rust_package, create_note_from_package, +}; + +/// Verify that another client (without the RPO-Falcon512 key) cannot create notes for +/// the counter account which uses the Rust-compiled RPO-Falcon512 authentication component. +#[test] +pub fn test_counter_contract_rust_auth_blocks_unauthorized_note_creation() { + let contract_package = compile_rust_package("../../examples/counter-contract", true); + let note_package = compile_rust_package("../../examples/counter-note", true); + let rpo_auth_package = + compile_rust_package("../../examples/auth-component-rpo-falcon512", true); + + let (counter_account, secret_key) = + build_counter_account_with_rust_rpo_auth(contract_package, rpo_auth_package, [0_u8; 32]); + let counter_account_id = counter_account.id(); + + let mut builder = MockChain::builder(); + builder + .add_account(counter_account) + .expect("failed to add counter account to mock chain builder"); + + let mut chain = builder.build().expect("failed to build mock chain"); + chain.prove_next_block().unwrap(); + chain.prove_next_block().unwrap(); + + let counter_account = chain.committed_account(counter_account_id).unwrap().clone(); + eprintln!( + "Counter account (Rust RPO-Falcon512 auth) ID: {:?}", + counter_account.id().to_hex() + ); + + assert_counter_storage(chain.committed_account(counter_account.id()).unwrap().storage(), 1, 1); + + // Positive check: original client (with the key) can create a note + let mut rng = RpoRandomCoin::new(note_package.unwrap_program().hash()); + let own_note = create_note_from_package( + note_package.clone(), + counter_account.id(), + NoteCreationConfig { + tag: NoteTag::from_account_id(counter_account.id()), + ..Default::default() + }, + &mut rng, + ); + let tx_script = build_send_notes_script(&counter_account, std::slice::from_ref(&own_note)); + let authenticator = BasicAuthenticator::new(&[AuthSecretKey::RpoFalcon512(secret_key)]); + + let tx_context_builder = chain + .build_tx_context(counter_account.clone(), &[], &[]) + .unwrap() + .tx_script(tx_script) + .extend_expected_output_notes(vec![OutputNote::Full(own_note.clone())]) + .authenticator(Some(authenticator)); + let tx_context = tx_context_builder.build().unwrap(); + let executed_tx = + block_on(tx_context.execute()).expect("authorized client should be able to create a note"); + assert_eq!(executed_tx.output_notes().num_notes(), 1); + assert_eq!(executed_tx.output_notes().get_note(0).id(), own_note.id()); + + chain.add_pending_executed_transaction(&executed_tx).unwrap(); + chain.prove_next_block().unwrap(); + + // Negative check: without the RPO-Falcon512 key, creating output notes should fail. + let counter_account = chain.committed_account(counter_account_id).unwrap().clone(); + let forged_note = create_note_from_package( + note_package, + counter_account.id(), + NoteCreationConfig { + tag: NoteTag::from_account_id(counter_account.id()), + ..Default::default() + }, + &mut rng, + ); + let tx_script = build_send_notes_script(&counter_account, std::slice::from_ref(&forged_note)); + + let tx_context_builder = chain + .build_tx_context(counter_account, &[], &[]) + .unwrap() + .tx_script(tx_script) + .extend_expected_output_notes(vec![OutputNote::Full(forged_note)]) + .authenticator(None); + let tx_context = tx_context_builder.build().unwrap(); + + let result = block_on(tx_context.execute()); + assert!( + result.is_err(), + "Unauthorized executor unexpectedly created a transaction for the counter account" + ); +} diff --git a/tests/integration-network/src/mockchain/helpers.rs b/tests/integration-network/src/mockchain/helpers.rs new file mode 100644 index 000000000..133ad202b --- /dev/null +++ b/tests/integration-network/src/mockchain/helpers.rs @@ -0,0 +1,388 @@ +//! Common helper functions for mock-chain integration tests. + +use std::{borrow::Borrow, collections::BTreeSet, future::Future, sync::Arc}; + +use miden_client::{ + Word, + account::component::BasicWallet, + asset::FungibleAsset, + crypto::FeltRng, + note::{ + Note, NoteAssets, NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, NoteScript, + NoteTag, NoteType, + }, + testing::{MockChain, TransactionContextBuilder}, + transaction::OutputNote, + utils::Deserializable, +}; +use miden_core::{Felt, FieldElement, crypto::hash::Rpo256}; +use miden_felt_repr_offchain::{AccountIdFeltRepr, ToFeltRepr}; +use miden_integration_tests::CompilerTestBuilder; +use miden_lib::account::interface::AccountInterface; +use miden_mast_package::{Package, SectionId}; +use miden_objects::{ + account::{ + Account, AccountBuilder, AccountComponent, AccountComponentMetadata, + AccountComponentTemplate, AccountId, AccountStorageMode, AccountType, StorageMap, + StorageSlot, + }, + asset::Asset, +}; +use midenc_frontend_wasm::WasmTranslationConfig; +use rand::{SeedableRng, rngs::StdRng}; + +// ASYNC HELPERS +// ================================================================================================ + +thread_local! { + static TOKIO_RUNTIME: tokio::runtime::Runtime = tokio::runtime::Runtime::new() + .expect("failed to build tokio runtime for integration-network tests"); +} + +/// Runs the provided future to completion on a shared Tokio runtime. +pub(super) fn block_on(future: F) -> F::Output { + TOKIO_RUNTIME.with(|rt| rt.block_on(future)) +} + +// COMPILATION +// ================================================================================================ + +/// Helper to compile a Rust package to a Miden `Package`. +pub(super) fn compile_rust_package(package_path: &str, release: bool) -> Arc { + let config = WasmTranslationConfig::default(); + let mut builder = CompilerTestBuilder::rust_source_cargo_miden(package_path, config, []); + + if release { + builder.with_release(true); + } + + let mut test = builder.build(); + test.compiled_package() +} + +// NOTE HELPERS +// ================================================================================================ + +/// Configuration for creating a note. +#[derive(Debug, Clone)] +pub(super) struct NoteCreationConfig { + /// The note type (public/private). + pub note_type: NoteType, + /// The note tag. + pub tag: NoteTag, + /// Assets carried by the note. + pub assets: NoteAssets, + /// Note inputs (e.g. target account id, timelock/reclaim height, etc.). + pub inputs: Vec, + /// Execution hint for the note script. + pub execution_hint: NoteExecutionHint, + /// Auxiliary note metadata field. + pub aux: Felt, +} + +impl Default for NoteCreationConfig { + fn default() -> Self { + Self { + note_type: NoteType::Public, + tag: NoteTag::for_local_use_case(0, 0).unwrap(), + assets: Default::default(), + inputs: Default::default(), + execution_hint: NoteExecutionHint::always(), + aux: Felt::ZERO, + } + } +} + +/// Creates a note from a compiled note package without requiring a `miden_client::Client`. +pub(super) fn create_note_from_package( + package: Arc, + sender_id: AccountId, + config: NoteCreationConfig, + rng: &mut impl FeltRng, +) -> Note { + let note_program = package.unwrap_program(); + let note_script = + NoteScript::from_parts(note_program.mast_forest().clone(), note_program.entrypoint()); + + let serial_num = rng.draw_word(); + let note_inputs = NoteInputs::new(config.inputs).unwrap(); + let recipient = NoteRecipient::new(serial_num, note_script, note_inputs); + + let metadata = NoteMetadata::new( + sender_id, + config.note_type, + config.tag, + config.execution_hint, + config.aux, + ) + .unwrap(); + + Note::new(config.assets, metadata, recipient) +} + +// ACCOUNT COMPONENT HELPERS +// ================================================================================================ + +/// Creates an account component from a compiled package's component metadata. +pub(super) fn account_component_from_package( + package: Arc, + storage_slots: Vec, +) -> AccountComponent { + let account_component_metadata = package.sections.iter().find_map(|section| { + if section.id == SectionId::ACCOUNT_COMPONENT_METADATA { + Some(section.data.borrow()) + } else { + None + } + }); + + match account_component_metadata { + None => panic!("no account component metadata present"), + Some(bytes) => { + let metadata = AccountComponentMetadata::read_from_bytes(bytes).unwrap(); + let template = + AccountComponentTemplate::new(metadata, package.unwrap_library().as_ref().clone()); + + let supported_types = BTreeSet::from_iter([AccountType::RegularAccountUpdatableCode]); + AccountComponent::new(template.library().clone(), storage_slots) + .unwrap() + .with_supported_types(supported_types) + } + } +} + +// BASIC WALLET HELPERS +// ================================================================================================ + +/// Builds an account builder for an existing basic-wallet account based on the provided component +/// package. +pub(super) fn build_existing_basic_wallet_account_builder( + wallet_package: Arc, + with_std_basic_wallet: bool, + seed: [u8; 32], +) -> AccountBuilder { + let wallet_component = account_component_from_package(wallet_package, vec![]); + + let mut builder = AccountBuilder::new(seed) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_component(wallet_component); + + if with_std_basic_wallet { + builder = builder.with_component(BasicWallet); + } + + builder +} + +/// Asserts that the account vault contains a fungible asset from the expected faucet with the +/// expected total amount. +pub(super) fn assert_account_has_fungible_asset( + account: &Account, + expected_faucet_id: AccountId, + expected_amount: u64, +) { + let found_asset = account.vault().assets().find_map(|asset| match asset { + Asset::Fungible(fungible_asset) if fungible_asset.faucet_id() == expected_faucet_id => { + Some(fungible_asset) + } + _ => None, + }); + + match found_asset { + Some(fungible_asset) => assert_eq!( + fungible_asset.amount(), + expected_amount, + "Found asset from faucet {expected_faucet_id} but amount {} doesn't match expected \ + {expected_amount}", + fungible_asset.amount() + ), + None => { + panic!("Account does not contain a fungible asset from faucet {expected_faucet_id}") + } + } +} + +/// Builds a `send_notes` transaction script for accounts that support a standard note creation +/// interface (e.g. basic wallets and basic fungible faucets). +pub(super) fn build_send_notes_script( + account: &Account, + notes: &[Note], +) -> miden_objects::transaction::TransactionScript { + let partial_notes = + notes.iter().map(miden_objects::note::PartialNote::from).collect::>(); + + AccountInterface::from(account) + .build_send_notes_script(&partial_notes, None, false) + .expect("failed to build send_notes transaction script") +} + +/// Executes a transaction context against the chain and commits it in the next block. +pub(super) fn execute_tx(chain: &mut MockChain, tx_context_builder: TransactionContextBuilder) { + let tx_context = tx_context_builder.build().unwrap(); + let executed_tx = block_on(tx_context.execute()).unwrap(); + chain.add_pending_executed_transaction(&executed_tx).unwrap(); + chain.prove_next_block().unwrap(); +} + +/// Builds a transaction context which transfers an asset from `sender_id` to `recipient_id` using +/// the custom transaction script package. +/// +/// Builds the transaction context by constructing the same advice-map + script-arg commitment +/// expected by the tx script, without requiring a `miden_client::Client`. +/// +/// The caller provides an RNG used to generate a unique note serial number, to avoid accidental +/// note ID collisions across multiple transfers. +pub(super) fn build_asset_transfer_tx( + chain: &MockChain, + sender_id: AccountId, + recipient_id: AccountId, + asset: FungibleAsset, + p2id_note_package: Arc, + tx_script_package: Arc, + rng: &mut impl FeltRng, +) -> (TransactionContextBuilder, Note) { + let note_program = p2id_note_package.unwrap_program(); + let note_script = + NoteScript::from_parts(note_program.mast_forest().clone(), note_program.entrypoint()); + + let tx_script_program = tx_script_package.unwrap_program(); + let tx_script = miden_objects::transaction::TransactionScript::from_parts( + tx_script_program.mast_forest().clone(), + tx_script_program.entrypoint(), + ); + + let serial_num = rng.draw_word(); + let inputs = NoteInputs::new(AccountIdFeltRepr(&recipient_id).to_felt_repr()).unwrap(); + let note_recipient = NoteRecipient::new(serial_num, note_script, inputs); + + let config = NoteCreationConfig { + assets: NoteAssets::new(vec![asset.into()]).unwrap(), + ..Default::default() + }; + let metadata = NoteMetadata::new( + sender_id, + config.note_type, + config.tag, + config.execution_hint, + config.aux, + ) + .unwrap(); + let output_note = Note::new(config.assets, metadata, note_recipient.clone()); + + // Prepare commitment data + let mut commitment_input: Vec = vec![ + config.tag.into(), + config.aux, + Felt::from(config.note_type), + Felt::from(config.execution_hint), + ]; + let recipient_digest: [Felt; 4] = note_recipient.digest().into(); + commitment_input.extend(recipient_digest); + + let asset_arr: Word = asset.into(); + commitment_input.extend(asset_arr); + + let commitment_key: Word = Rpo256::hash_elements(&commitment_input); + assert_eq!(commitment_input.len() % 4, 0, "commitment input needs to be word-aligned"); + + // NOTE: passed on the stack reversed + let mut commitment_arg = commitment_key; + commitment_arg.reverse(); + + let tx_context_builder = chain + .build_tx_context(sender_id, &[], &[]) + .unwrap() + .tx_script(tx_script) + .tx_script_args(commitment_arg) + .extend_advice_map([(commitment_key, commitment_input)]) + .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]); + + (tx_context_builder, output_note) +} + +// COUNTER CONTRACT HELPERS +// ================================================================================================ + +/// Asserts the counter value stored in the counter contract's storage map at `storage_slot`. +pub(super) fn assert_counter_storage( + counter_account_storage: &miden_client::account::AccountStorage, + storage_slot: u8, + expected: u64, +) { + // according to `examples/counter-contract` for inner (slot, key) values + let counter_contract_storage_key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + + let word = counter_account_storage + .get_map_item(storage_slot, counter_contract_storage_key) + .expect("Failed to get counter value from storage slot"); + + let val = word.last().unwrap(); + assert_eq!( + val.as_int(), + expected, + "Counter value mismatch. Expected: {}, Got: {}", + expected, + val.as_int() + ); +} + +/// Builds an account builder for an existing public counter account containing the counter +/// contract component and a custom authentication component compiled as a package library. +pub(super) fn build_existing_counter_account_builder_with_auth_package( + contract_package: Arc, + auth_component_package: Arc, + auth_storage_slots: Vec, + counter_storage_slots: Vec, + seed: [u8; 32], +) -> AccountBuilder { + let supported_types = BTreeSet::from_iter([AccountType::RegularAccountUpdatableCode]); + let auth_component = AccountComponent::new( + auth_component_package.unwrap_library().as_ref().clone(), + auth_storage_slots, + ) + .unwrap() + .with_supported_types(supported_types); + let counter_component = account_component_from_package(contract_package, counter_storage_slots); + + AccountBuilder::new(seed) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(auth_component) + .with_component(BasicWallet) + .with_component(counter_component) +} + +/// Builds an existing counter account using a Rust-compiled RPO-Falcon512 authentication component. +/// +/// Returns the account along with the generated secret key which can authenticate transactions for +/// this account. +pub(super) fn build_counter_account_with_rust_rpo_auth( + component_package: Arc, + auth_component_package: Arc, + seed: [u8; 32], +) -> (Account, miden_client::crypto::rpo_falcon512::SecretKey) { + let key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + let value = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + let counter_storage_slots = + vec![StorageSlot::Map(StorageMap::with_entries([(key, value)]).unwrap())]; + + let mut rng = StdRng::seed_from_u64(1); + let secret_key = miden_client::crypto::rpo_falcon512::SecretKey::with_rng(&mut rng); + let pk_commitment: Word = + miden_client::auth::PublicKeyCommitment::from(secret_key.public_key()).into(); + + let auth_storage_slots = vec![StorageSlot::Value(pk_commitment)]; + + let account = build_existing_counter_account_builder_with_auth_package( + component_package, + auth_component_package, + auth_storage_slots, + counter_storage_slots, + seed, + ) + .build_existing() + .expect("failed to build counter account"); + + (account, secret_key) +} diff --git a/tests/integration-network/src/mockchain/mod.rs b/tests/integration-network/src/mockchain/mod.rs new file mode 100644 index 000000000..b6d2e6432 --- /dev/null +++ b/tests/integration-network/src/mockchain/mod.rs @@ -0,0 +1,7 @@ +//! Integration tests which exercise contract deployment and execution on a mock chain. + +mod basic_wallet; +mod counter_contract; +mod counter_contract_no_auth; +mod counter_contract_rust_auth; +mod helpers; diff --git a/tests/integration-node/README.md b/tests/integration-node/README.md deleted file mode 100644 index 0bdd85ddb..000000000 --- a/tests/integration-node/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Miden Integration Node Tests - -This crate contains integration tests that require a local Miden node instance or testnet connection. - -## Overview - -The tests in this crate are separated from the main integration tests because they: -- Require a local Miden node to be running or testnet connectivity -- Are slower due to network operations and multi-step nature of the test scenarios - -## Running Tests - -To see debug output from the node: - -```bash -MIDEN_NODE_OUTPUT=1 cargo test -p miden-integration-node-tests -``` - -## Process Cleanup - -The local node management system ensures that: -- Only one node instance runs at a time, shared across all tests -- The node is automatically stopped when the last test using the node is finished -- No orphaned miden-node processes remain after test execution diff --git a/tests/integration-node/src/lib.rs b/tests/integration-node/src/lib.rs deleted file mode 100644 index a11a749db..000000000 --- a/tests/integration-node/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Integration tests that are deploying code and runnning test scenarior on a local Miden node instance or testnet - -pub mod local_node; - -#[cfg(test)] -mod node_tests; diff --git a/tests/integration-node/src/local_node/handle.rs b/tests/integration-node/src/local_node/handle.rs deleted file mode 100644 index e97f85b3e..000000000 --- a/tests/integration-node/src/local_node/handle.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Handle for managing shared node instances - -use std::fs; - -use anyhow::{Context, Result}; -use uuid::Uuid; - -use super::{ - RPC_PORT, - process::{is_port_in_use, is_process_running, start_shared_node}, - ref_count_dir, rpc_url, - setup::LocalMidenNode, - sync::{ - acquire_lock, add_reference, get_ref_count, read_pid, stop_node_if_no_references, write_pid, - }, -}; - -/// Handle to the shared node instance. When dropped, decrements the reference count. -pub struct SharedNodeHandle { - /// The RPC URL of the shared node - rpc_url: String, - /// Unique ID for this handle - handle_id: String, -} - -impl SharedNodeHandle { - /// Get the RPC URL for connecting to the node - pub fn rpc_url(&self) -> &str { - &self.rpc_url - } -} - -impl Drop for SharedNodeHandle { - fn drop(&mut self) { - eprintln!("[SharedNode] Dropping handle {}", self.handle_id); - - // Remove our reference file - let ref_file = ref_count_dir().join(&self.handle_id); - if let Err(e) = fs::remove_file(&ref_file) { - eprintln!("[SharedNode] Warning: Failed to remove ref file: {e}"); - } - - stop_node_if_no_references(); - } -} - -/// Ensure the shared node is running and return a handle to it -pub async fn ensure_shared_node() -> Result { - LocalMidenNode::ensure_installed().context("Failed to ensure miden-node is installed")?; - - let handle_id = format!("handle-{}-{}", std::process::id(), Uuid::new_v4()); - let _lock = acquire_lock().context("Failed to acquire lock for node coordination")?; - - let existing_pid = read_pid().context("Failed to read PID file")?; - - let pid = match existing_pid { - Some(pid) if is_process_running(pid) => { - // Check if the node is actually responding - if is_port_in_use(RPC_PORT) { - eprintln!("[SharedNode] Using existing node process {pid}"); - pid - } else { - eprintln!("[SharedNode] Found dead node process {pid}, restarting..."); - // Node process exists but isn't responding, start a new one - let new_pid = start_shared_node() - .await - .context("Failed to start new node after finding dead process")?; - write_pid(new_pid).context("Failed to write PID file")?; - new_pid - } - } - _ => { - // No running node, start a new one - eprintln!("[SharedNode] No existing node found, starting new instance"); - let new_pid = start_shared_node().await.context("Failed to start new node instance")?; - write_pid(new_pid).context("Failed to write PID file")?; - new_pid - } - }; - - // Add our reference - add_reference(&handle_id).context("Failed to add reference for handle")?; - - // Log current state - let ref_count = get_ref_count().context("Failed to get reference count")?; - eprintln!("[SharedNode] Node PID: {pid}, Reference count: {ref_count}"); - - Ok(SharedNodeHandle { - rpc_url: rpc_url(), - handle_id, - }) -} diff --git a/tests/integration-node/src/local_node/mod.rs b/tests/integration-node/src/local_node/mod.rs deleted file mode 100644 index 729fe1c91..000000000 --- a/tests/integration-node/src/local_node/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Infrastructure for running a local Miden node for integration tests - -use std::path::PathBuf; - -mod handle; -mod process; -mod setup; -mod sync; - -pub use handle::{SharedNodeHandle, ensure_shared_node}; - -// Base directory for all miden test node files -const BASE_DIR: &str = "/tmp/miden-test-node"; - -// Re-export constants that are used in multiple modules -pub(crate) const COORD_DIR: &str = BASE_DIR; - -// Construct paths at runtime since concat! doesn't work with const values -pub(crate) fn pid_file() -> PathBuf { - PathBuf::from(BASE_DIR).join("node.pid") -} - -pub(crate) fn ref_count_dir() -> PathBuf { - PathBuf::from(BASE_DIR).join("refs") -} - -pub(crate) fn lock_file() -> PathBuf { - PathBuf::from(BASE_DIR).join("node.lock") -} - -pub(crate) fn data_dir() -> PathBuf { - PathBuf::from(BASE_DIR).join("data") -} - -pub(crate) const RPC_PORT: u16 = 57291; - -// Construct RPC URL using the port constant -pub(crate) fn rpc_url() -> String { - format!("http://127.0.0.1:{RPC_PORT}") -} diff --git a/tests/integration-node/src/local_node/process.rs b/tests/integration-node/src/local_node/process.rs deleted file mode 100644 index 8b9ef49fd..000000000 --- a/tests/integration-node/src/local_node/process.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Process management functionality for the shared node - -use std::{ - fs, - net::TcpStream, - process::{Command, Stdio}, - thread, - time::{Duration, Instant}, -}; - -use anyhow::{Context, Result, anyhow}; - -use super::{RPC_PORT, data_dir, rpc_url, setup::LocalMidenNode}; - -/// Check if a port is in use -pub fn is_port_in_use(port: u16) -> bool { - TcpStream::connect(("127.0.0.1", port)).is_ok() -} - -/// Check if a process is running -pub fn is_process_running(pid: u32) -> bool { - // Try to read from /proc/{pid}/stat on Linux/macOS - #[cfg(target_os = "linux")] - { - std::path::Path::new(&format!("/proc/{pid}")).exists() - } - - #[cfg(not(target_os = "linux"))] - { - // On macOS, use ps command - Command::new("ps") - .args(["-p", &pid.to_string()]) - .output() - .map(|output| output.status.success()) - .unwrap_or(false) - } -} - -/// Kill a process by PID -pub fn kill_process(pid: u32) -> Result<()> { - eprintln!("[SharedNode] Killing process {pid}"); - - // Use kill command for cross-platform compatibility - // First try SIGTERM - let term_result = Command::new("kill") - .args(["-TERM", &pid.to_string()]) - .output() - .context("Failed to execute kill command")?; - - if !term_result.status.success() { - let stderr = String::from_utf8_lossy(&term_result.stderr); - // If process doesn't exist, that's fine - if stderr.contains("No such process") { - return Ok(()); - } - return Err(anyhow!("Failed to send SIGTERM to process {pid}: {stderr}")); - } - - // Wait a bit for graceful shutdown - thread::sleep(Duration::from_millis(500)); - - // If still running, use SIGKILL - if is_process_running(pid) { - let kill_result = Command::new("kill") - .args(["-KILL", &pid.to_string()]) - .output() - .context("Failed to execute kill command")?; - - if !kill_result.status.success() { - let stderr = String::from_utf8_lossy(&kill_result.stderr); - if !stderr.contains("No such process") { - return Err(anyhow!("Failed to send SIGKILL to process {pid}: {stderr}")); - } - } - } - - Ok(()) -} - -/// Start the shared node process -pub async fn start_shared_node() -> Result { - eprintln!("[SharedNode] Starting shared node process..."); - - // Bootstrap if needed (data directory empty or doesn't exist) - let data_dir_path = data_dir(); - let needs_bootstrap = !data_dir_path.exists() - || fs::read_dir(&data_dir_path) - .map(|mut entries| entries.next().is_none()) - .unwrap_or(true); - - if needs_bootstrap { - // Ensure we have a clean, empty data directory for bootstrap - if data_dir_path.exists() { - fs::remove_dir_all(&data_dir_path).context("Failed to remove data directory")?; - } - fs::create_dir_all(&data_dir_path).context("Failed to create data directory")?; - LocalMidenNode::bootstrap(&data_dir_path).context("Failed to bootstrap miden-node")?; - } - - // Start the node process - // Use Stdio::null() for stdout/stderr to avoid buffer blocking issues. - // When pipes are used, the child process can block if the pipe buffer fills up - // and the reading end doesn't consume data fast enough. Using inherit() also - // causes issues with nextest's parallel test execution. - // - // For debugging, users can run the node manually with RUST_LOG=debug. - let child = Command::new("miden-node") - .args([ - "bundled", - "start", - "--data-directory", - data_dir_path.to_str().unwrap(), - "--rpc.url", - &rpc_url(), - "--block.interval", - "1sec", - ]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .context("Failed to start miden-node process")?; - - let pid = child.id(); - - // Detach the child process so it continues running after we exit - drop(child); - - // Wait for node to be ready - eprintln!("[SharedNode] Waiting for node to be ready..."); - let start = Instant::now(); - let timeout = Duration::from_secs(10); - - while start.elapsed() < timeout { - if is_port_in_use(RPC_PORT) { - eprintln!("[SharedNode] Node is ready"); - return Ok(pid); - } - tokio::time::sleep(Duration::from_millis(100)).await; - } - - // If we get here, node failed to start - kill_process(pid).context("Failed to kill unresponsive node process")?; - Err(anyhow!("Timeout waiting for node to be ready")) -} diff --git a/tests/integration-node/src/local_node/setup.rs b/tests/integration-node/src/local_node/setup.rs deleted file mode 100644 index 1e49339e1..000000000 --- a/tests/integration-node/src/local_node/setup.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Node installation and bootstrap functionality - -use std::{fs, path::Path, process::Command}; - -use anyhow::{Context, Result, anyhow}; - -use super::{COORD_DIR, process::kill_process, sync::read_pid}; - -// Version configuration for miden-node -// NOTE: When updating miden-client version in Cargo.toml, update this constant to match -// the compatible miden-node version. Both should typically use the same major.minor version. - -/// The exact miden-node version that is compatible with the miden-client version used in tests -const MIDEN_NODE_VERSION: &str = "0.12.2"; - -/// Manages the lifecycle of a local Miden node instance -pub struct LocalMidenNode; - -impl LocalMidenNode { - /// Install miden-node binary if not already installed - pub fn ensure_installed() -> Result<()> { - // Check if miden-node is already installed and get version - let check = Command::new("miden-node").arg("--version").output(); - - let need_install = match check { - Ok(output) if output.status.success() => { - let version = String::from_utf8_lossy(&output.stdout); - let version_line = version.lines().next().unwrap_or(""); - - // Check if it's the exact version we need - if version_line.contains(MIDEN_NODE_VERSION) { - eprintln!("miden-node already installed: {version_line}"); - false - } else { - eprintln!( - "Found incompatible miden-node version: {version_line} (need \ - {MIDEN_NODE_VERSION})" - ); - eprintln!("Uninstalling current version..."); - - // Uninstall the current version - let uninstall_output = Command::new("cargo") - .args(["uninstall", "miden-node"]) - .output() - .context("Failed to run cargo uninstall")?; - - if !uninstall_output.status.success() { - let stderr = String::from_utf8_lossy(&uninstall_output.stderr); - eprintln!("Warning: Failed to uninstall miden-node: {stderr}"); - } else { - eprintln!("Successfully uninstalled old version"); - } - - // Clean all node-related data when version changes - eprintln!("Cleaning node data due to version change..."); - - // Kill any running node process - if let Ok(Some(pid)) = read_pid() { - eprintln!("Stopping existing node process {pid}"); - let _ = kill_process(pid); - } - - // Clean the entire coordination directory - if let Err(e) = fs::remove_dir_all(COORD_DIR) - && e.kind() != std::io::ErrorKind::NotFound - { - eprintln!("Warning: Failed to clean coordination directory: {e}"); - } - - true - } - } - _ => { - eprintln!("miden-node not found"); - true - } - }; - - if need_install { - // Install specific version compatible with miden-client - eprintln!("Installing miden-node version {MIDEN_NODE_VERSION} from crates.io..."); - let output = Command::new("cargo") - .args(["install", "miden-node", "--version", MIDEN_NODE_VERSION, "--locked"]) - .output() - .context("Failed to run cargo install")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(anyhow!("Failed to install miden-node: {stderr}")); - } - - eprintln!("miden-node {MIDEN_NODE_VERSION} installed successfully"); - } - - Ok(()) - } - - /// Bootstrap the node with genesis data - pub fn bootstrap(data_dir: &Path) -> Result<()> { - eprintln!("Bootstrapping miden-node..."); - - let output = Command::new("miden-node") - .args([ - "bundled", - "bootstrap", - "--data-directory", - data_dir.to_str().unwrap(), - "--accounts-directory", - data_dir.to_str().unwrap(), - ]) - .output() - .context("Failed to run miden-node bootstrap command")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(anyhow!("Failed to bootstrap node: {stderr}")); - } - - eprintln!("Node bootstrapped successfully"); - Ok(()) - } -} diff --git a/tests/integration-node/src/local_node/sync.rs b/tests/integration-node/src/local_node/sync.rs deleted file mode 100644 index 8a41549fa..000000000 --- a/tests/integration-node/src/local_node/sync.rs +++ /dev/null @@ -1,164 +0,0 @@ -//! Synchronization and reference counting logic for the shared node - -use std::{ - fs::{self, File, OpenOptions}, - thread, - time::Duration, -}; - -use anyhow::{Context, Result, anyhow}; -use fs2::FileExt; - -use super::{ - COORD_DIR, lock_file, pid_file, - process::{is_process_running, kill_process}, - ref_count_dir, -}; - -/// Lock guard using fs2 file locking -pub struct LockGuard(File); - -impl Drop for LockGuard { - fn drop(&mut self) { - let _ = self.0.unlock(); - } -} - -/// Acquire a file lock for atomic operations -pub fn acquire_lock() -> Result { - // Ensure coordination directory exists - fs::create_dir_all(COORD_DIR).context("Failed to create coordination directory")?; - - // Open or create lock file - let file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(lock_file()) - .context("Failed to open lock file")?; - - // Try to acquire exclusive lock with retries - let mut attempts = 0; - const MAX_ATTEMPTS: u32 = 100; // 10 seconds max wait - - loop { - match file.try_lock_exclusive() { - Ok(_) => return Ok(LockGuard(file)), - Err(e) => { - if attempts >= MAX_ATTEMPTS { - return Err(anyhow!("Timeout acquiring lock: {e}")); - } - attempts += 1; - thread::sleep(Duration::from_millis(100)); - } - } - } -} - -/// Read PID from file -pub fn read_pid() -> Result> { - match fs::read_to_string(pid_file()) { - Ok(contents) => { - let pid = contents.trim().parse::().context("Failed to parse PID")?; - Ok(Some(pid)) - } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(e) => Err(anyhow!("Failed to read PID file: {e}")), - } -} - -/// Write PID to file -pub fn write_pid(pid: u32) -> Result<()> { - fs::write(pid_file(), pid.to_string()).context("Failed to write PID file") -} - -/// Remove PID file -pub fn remove_pid() -> Result<()> { - match fs::remove_file(pid_file()) { - Ok(_) => Ok(()), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(e) => Err(anyhow!("Failed to remove PID file: {e}")), - } -} - -/// Get count of active references, cleaning up stale ones -pub fn get_ref_count() -> Result { - fs::create_dir_all(ref_count_dir()).context("Failed to create reference count directory")?; - - let entries = - fs::read_dir(ref_count_dir()).context("Failed to read reference count directory")?; - - let mut active_count = 0; - for entry in entries.flatten() { - let file_name = entry.file_name(); - let file_name_str = file_name.to_string_lossy(); - - // Extract PID from handle name (format: handle-{pid}-{uuid}) - if let Some(pid_str) = file_name_str.split('-').nth(1) - && let Ok(pid) = pid_str.parse::() - { - if is_process_running(pid) { - active_count += 1; - } else { - // Clean up stale reference from dead process - eprintln!("[SharedNode] Cleaning up stale reference from dead process {pid}"); - let _ = fs::remove_file(entry.path()); - } - } - } - - Ok(active_count) -} - -/// Add a reference -pub fn add_reference(handle_id: &str) -> Result<()> { - fs::create_dir_all(ref_count_dir()).context("Failed to create reference count directory")?; - - let ref_file = ref_count_dir().join(handle_id); - File::create(&ref_file).context("Failed to create reference file")?; - - Ok(()) -} - -/// Check and stop the node if no more references exist -pub fn stop_node_if_no_references() { - // Acquire lock for atomic operation - let _lock = match acquire_lock() { - Ok(lock) => lock, - Err(e) => { - eprintln!("[SharedNode] Failed to acquire lock: {e}"); - return; - } - }; - - // Check reference count - let ref_count = match get_ref_count() { - Ok(count) => count, - Err(e) => { - eprintln!("[SharedNode] Failed to get reference count: {e}"); - return; - } - }; - - eprintln!("[SharedNode] Reference count: {ref_count}"); - - if ref_count == 0 { - // No more references, stop the node - if let Ok(Some(pid)) = read_pid() { - eprintln!("[SharedNode] No more references, stopping node process {pid}"); - - if let Err(e) = kill_process(pid) { - eprintln!("[SharedNode] Failed to kill node process: {e}"); - } - - if let Err(e) = remove_pid() { - eprintln!("[SharedNode] Failed to remove PID file: {e}"); - } - - // Clean up coordination directory - if let Err(e) = fs::remove_dir_all(COORD_DIR) { - eprintln!("[SharedNode] Failed to clean up coordination directory: {e}"); - } - } - } -} diff --git a/tests/integration-node/src/node_tests/basic_wallet.rs b/tests/integration-node/src/node_tests/basic_wallet.rs deleted file mode 100644 index afc543084..000000000 --- a/tests/integration-node/src/node_tests/basic_wallet.rs +++ /dev/null @@ -1,595 +0,0 @@ -//! Basic wallet test module - -use miden_client::{ - asset::{FungibleAsset, TokenSymbol}, - note::NoteAssets, - transaction::{OutputNote, TransactionRequestBuilder}, -}; -use miden_core::{Felt, utils::Serializable}; -use miden_felt_repr_offchain::{AccountIdFeltRepr, ToFeltRepr}; - -use super::helpers::*; -use crate::local_node::ensure_shared_node; - -/// Tests the basic-wallet contract deployment and p2id note consumption workflow on a local node. -#[test] -pub fn test_basic_wallet_p2id_local() { - // Compile the contracts first (before creating any runtime) - let wallet_package = compile_rust_package("../../examples/basic-wallet", true); - let note_package = compile_rust_package("../../examples/p2id-note", true); - let tx_script_package = compile_rust_package("../../examples/basic-wallet-tx-script", true); - - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - // Create temp directory and get node handle - let temp_dir = temp_dir::TempDir::with_prefix("test_basic_wallet_p2id_local_") - .expect("Failed to create temp directory"); - let node_handle = ensure_shared_node().await.expect("Failed to get shared node"); - - // Initialize test infrastructure - let TestSetup { - mut client, - keystore, - } = setup_test_infrastructure(&temp_dir, &node_handle) - .await - .expect("Failed to setup test infrastructure"); - - // Write wallet package to disk for potential future use - let wallet_package_path = temp_dir.path().join("basic_wallet.masp"); - std::fs::write(&wallet_package_path, wallet_package.to_bytes()) - .expect("Failed to write wallet"); - - // Create a fungible faucet account - let token_symbol = TokenSymbol::new("TEST").unwrap(); - let decimals = 8u8; - let max_supply = Felt::new(1_000_000_000); // 1 billion tokens - - let faucet_account = create_fungible_faucet_account( - &mut client, - keystore.clone(), - token_symbol, - decimals, - max_supply, - ) - .await - .unwrap(); - - eprintln!("Faucet account ID: {:?}", faucet_account.id().to_hex()); - - // Create Alice's account with basic-wallet component - let alice_config = AccountCreationConfig { - with_basic_wallet: false, - ..Default::default() - }; - let alice_account = create_account_with_component( - &mut client, - keystore.clone(), - wallet_package.clone(), - alice_config, - ) - .await - .unwrap(); - eprintln!("Alice account ID: {:?}", alice_account.id().to_hex()); - - eprintln!("\n=== Step 1: Minting tokens from faucet to Alice ==="); - - let mint_amount = 100_000u64; // 100,000 tokens - let fungible_asset = FungibleAsset::new(faucet_account.id(), mint_amount).unwrap(); - - // Create the p2id note from faucet to Alice - let p2id_note_mint = create_note_from_package( - &mut client, - note_package.clone(), - faucet_account.id(), - NoteCreationConfig { - assets: NoteAssets::new(vec![fungible_asset.into()]).unwrap(), - inputs: AccountIdFeltRepr(&alice_account.id()).to_felt_repr(), - ..Default::default() - }, - ); - eprintln!("P2ID mint note hash: {:?}", p2id_note_mint.id().to_hex()); - - let mint_request = TransactionRequestBuilder::new() - .own_output_notes(vec![OutputNote::Full(p2id_note_mint.clone())]) - .build() - .unwrap(); - - let mint_tx_id = - client.submit_new_transaction(faucet_account.id(), mint_request).await.unwrap(); - eprintln!("Submitted mint transaction. Tx ID: {mint_tx_id:?}"); - - eprintln!("\n=== Step 2: Alice attempts to consume mint note ==="); - - let consume_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes([(p2id_note_mint, None)]) - .build() - .unwrap(); - - let _consume_tx_id = client - .submit_new_transaction(alice_account.id(), consume_request) - .await - .map_err(|e| format!("{e:?}")) - .unwrap(); - - eprintln!("\n=== Checking Alice's account has the minted asset ==="); - - assert_account_has_fungible_asset( - &mut client, - alice_account.id(), - faucet_account.id(), - mint_amount, - ) - .await; - - eprintln!("\n=== Step 3: Creating Bob's account ==="); - - let bob_config = AccountCreationConfig { - with_basic_wallet: false, - ..Default::default() - }; - let bob_account = create_account_with_component( - &mut client, - keystore.clone(), - wallet_package, - bob_config, - ) - .await - .unwrap(); - eprintln!("Bob account ID: {:?}", bob_account.id().to_hex()); - - eprintln!("\n=== Step 4: Alice creates p2id note for Bob ==="); - - let transfer_amount = 10_000u64; // 10,000 tokens - let transfer_asset = FungibleAsset::new(faucet_account.id(), transfer_amount).unwrap(); - - let (alice_tx_id, bob_note) = send_asset_to_account( - &mut client, - alice_account.id(), - bob_account.id(), - transfer_asset, - note_package.clone(), - tx_script_package, - None, // Use default configuration - ) - .await - .unwrap(); - - eprintln!("Alice created p2id transaction. Tx ID: {alice_tx_id:?}"); - - // Step 5: Bob attempts to consume the p2id note - eprintln!("\n=== Step 5: Bob attempts to consume p2id note ==="); - - let consume_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes([(bob_note, None)]) - .build() - .unwrap(); - - let consume_tx_id = - client.submit_new_transaction(bob_account.id(), consume_request).await.unwrap(); - eprintln!("Bob created consume transaction. Tx ID: {consume_tx_id:?}"); - - eprintln!("\n=== Step 6: Checking Bob's account has the transferred asset ==="); - - assert_account_has_fungible_asset( - &mut client, - bob_account.id(), - faucet_account.id(), - transfer_amount, - ) - .await; - - eprintln!( - "\n=== Step 7: Checking Alice's account reflects the new token amount after sending \ - to Bob ===" - ); - - assert_account_has_fungible_asset( - &mut client, - alice_account.id(), - faucet_account.id(), - mint_amount - transfer_amount, - ) - .await; - }); -} - -/// Tests the basic-wallet contract deployment and p2ide note consumption workflow on a local node. -/// -/// Flow: -/// - Create fungible faucet and mint tokens to Alice -/// - Alice creates a p2ide note for Bob (with timelock=0, reclaim=0) -/// - Bob consumes the p2ide note and receives the assets -#[test] -pub fn test_basic_wallet_p2ide_local() { - // Compile the contracts first (before creating any runtime) - let wallet_package = compile_rust_package("../../examples/basic-wallet", true); - let p2id_note_package = compile_rust_package("../../examples/p2id-note", true); - let p2ide_note_package = compile_rust_package("../../examples/p2ide-note", true); - - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - // Create temp directory and get node handle - let temp_dir = temp_dir::TempDir::with_prefix("test_basic_wallet_p2ide_local_") - .expect("Failed to create temp directory"); - let node_handle = ensure_shared_node().await.expect("Failed to get shared node"); - - // Initialize test infrastructure - let TestSetup { - mut client, - keystore, - } = setup_test_infrastructure(&temp_dir, &node_handle) - .await - .expect("Failed to setup test infrastructure"); - - // Step 1: Create a fungible faucet account - eprintln!("\n=== Step 1: Creating fungible faucet ==="); - let token_symbol = TokenSymbol::new("TEST").unwrap(); - let decimals = 8u8; - let max_supply = Felt::new(1_000_000_000); // 1 billion tokens - - let faucet_account = create_fungible_faucet_account( - &mut client, - keystore.clone(), - token_symbol, - decimals, - max_supply, - ) - .await - .unwrap(); - - eprintln!("Faucet account ID: {:?}", faucet_account.id().to_hex()); - - // Create Alice's account with basic-wallet component - let alice_config = AccountCreationConfig { - with_basic_wallet: true, - ..Default::default() - }; - let alice_account = create_account_with_component( - &mut client, - keystore.clone(), - wallet_package.clone(), - alice_config, - ) - .await - .unwrap(); - eprintln!("Alice account ID: {:?}", alice_account.id().to_hex()); - - // Step 2: Mint assets from faucet to Alice using p2id note - eprintln!("\n=== Step 2: Minting tokens from faucet to Alice (p2id note) ==="); - - let mint_amount = 100_000u64; // 100,000 tokens - let fungible_asset = FungibleAsset::new(faucet_account.id(), mint_amount).unwrap(); - - // Create the p2id note from faucet to Alice - let p2id_note_mint = create_note_from_package( - &mut client, - p2id_note_package.clone(), - faucet_account.id(), - NoteCreationConfig { - assets: NoteAssets::new(vec![fungible_asset.into()]).unwrap(), - inputs: AccountIdFeltRepr(&alice_account.id()).to_felt_repr(), - ..Default::default() - }, - ); - eprintln!("P2ID mint note hash: {:?}", p2id_note_mint.id().to_hex()); - - let mint_request = TransactionRequestBuilder::new() - .own_output_notes(vec![OutputNote::Full(p2id_note_mint.clone())]) - .build() - .unwrap(); - - let mint_tx_id = - client.submit_new_transaction(faucet_account.id(), mint_request).await.unwrap(); - eprintln!("Submitted mint transaction. Tx ID: {mint_tx_id:?}"); - - // Step 3: Alice consumes the p2id note - eprintln!("\n=== Step 3: Alice consumes p2id mint note ==="); - - let consume_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes([(p2id_note_mint, None)]) - .build() - .unwrap(); - - let _consume_tx_id = client - .submit_new_transaction(alice_account.id(), consume_request) - .await - .map_err(|e| format!("{e:?}")) - .unwrap(); - - eprintln!("\n=== Checking Alice's account has the minted asset ==="); - - assert_account_has_fungible_asset( - &mut client, - alice_account.id(), - faucet_account.id(), - mint_amount, - ) - .await; - - // Create Bob's account - eprintln!("\n=== Creating Bob's account ==="); - - let bob_config = AccountCreationConfig { - with_basic_wallet: false, - ..Default::default() - }; - let bob_account = create_account_with_component( - &mut client, - keystore.clone(), - wallet_package, - bob_config, - ) - .await - .unwrap(); - eprintln!("Bob account ID: {:?}", bob_account.id().to_hex()); - - // Step 4: Alice creates p2ide note for Bob - eprintln!("\n=== Step 4: Alice creates p2ide note for Bob ==="); - - let transfer_amount = 10_000u64; // 10,000 tokens - let transfer_asset = FungibleAsset::new(faucet_account.id(), transfer_amount).unwrap(); - - let timelock_height = Felt::new(0); - let reclaim_height = Felt::new(0); - - // Create the p2ide note - let p2ide_note = create_note_from_package( - &mut client, - p2ide_note_package.clone(), - alice_account.id(), - NoteCreationConfig { - assets: NoteAssets::new(vec![transfer_asset.into()]).unwrap(), - inputs: { - let mut inputs = AccountIdFeltRepr(&bob_account.id()).to_felt_repr(); - inputs.extend([timelock_height, reclaim_height]); - inputs - }, - ..Default::default() - }, - ); - eprintln!("P2IDE note hash: {:?}", p2ide_note.id().to_hex()); - - let transfer_request = TransactionRequestBuilder::new() - .own_output_notes(vec![OutputNote::Full(p2ide_note.clone())]) - .build() - .unwrap(); - - let alice_tx_id = client - .submit_new_transaction(alice_account.id(), transfer_request) - .await - .unwrap(); - eprintln!("Submitted p2ide transaction. Tx ID: {alice_tx_id:?}"); - - // Step 5: Bob consumes the p2ide note - eprintln!("\n=== Step 5: Bob consumes p2ide note ==="); - - let consume_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes([(p2ide_note, None)]) - .build() - .unwrap(); - - let consume_tx_id = - client.submit_new_transaction(bob_account.id(), consume_request).await.unwrap(); - eprintln!("Bob created consume transaction. Tx ID: {consume_tx_id:?}"); - - eprintln!("\n=== Checking Bob's account has the transferred asset ==="); - - assert_account_has_fungible_asset( - &mut client, - bob_account.id(), - faucet_account.id(), - transfer_amount, - ) - .await; - - eprintln!("\n=== Checking Alice's account reflects the new token amount ==="); - - assert_account_has_fungible_asset( - &mut client, - alice_account.id(), - faucet_account.id(), - mint_amount - transfer_amount, - ) - .await; - }); -} - -/// Tests the p2ide note reclaim functionality. -/// -/// Flow: -/// - Create fungible faucet and mint tokens to Alice -/// - Alice creates a p2ide note intended for Bob (with reclaim enabled) -/// - Alice reclaims the note herself (exercises the reclaim branch) -/// - Verify Alice has her original balance back -#[test] -pub fn test_basic_wallet_p2ide_reclaim_local() { - // Compile the contracts first (before creating any runtime) - let wallet_package = compile_rust_package("../../examples/basic-wallet", true); - let p2id_note_package = compile_rust_package("../../examples/p2id-note", true); - let p2ide_note_package = compile_rust_package("../../examples/p2ide-note", true); - - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - // Create temp directory and get node handle - let temp_dir = temp_dir::TempDir::with_prefix("test_basic_wallet_p2ide_reclaim_local_") - .expect("Failed to create temp directory"); - let node_handle = ensure_shared_node().await.expect("Failed to get shared node"); - - // Initialize test infrastructure - let TestSetup { - mut client, - keystore, - } = setup_test_infrastructure(&temp_dir, &node_handle) - .await - .expect("Failed to setup test infrastructure"); - - // Step 1: Create a fungible faucet account - eprintln!("\n=== Step 1: Creating fungible faucet ==="); - let token_symbol = TokenSymbol::new("TEST").unwrap(); - let decimals = 8u8; - let max_supply = Felt::new(1_000_000_000); // 1 billion tokens - - let faucet_account = create_fungible_faucet_account( - &mut client, - keystore.clone(), - token_symbol, - decimals, - max_supply, - ) - .await - .unwrap(); - - eprintln!("Faucet account ID: {:?}", faucet_account.id().to_hex()); - - // Create Alice's account with basic-wallet component - let alice_config = AccountCreationConfig { - with_basic_wallet: true, - ..Default::default() - }; - let alice_account = create_account_with_component( - &mut client, - keystore.clone(), - wallet_package.clone(), - alice_config, - ) - .await - .unwrap(); - eprintln!("Alice account ID: {:?}", alice_account.id().to_hex()); - - // Step 2: Mint assets from faucet to Alice using p2id note - eprintln!("\n=== Step 2: Minting tokens from faucet to Alice (p2id note) ==="); - - let mint_amount = 100_000u64; // 100,000 tokens - let fungible_asset = FungibleAsset::new(faucet_account.id(), mint_amount).unwrap(); - - // Create the p2id note from faucet to Alice - let p2id_note_mint = create_note_from_package( - &mut client, - p2id_note_package.clone(), - faucet_account.id(), - NoteCreationConfig { - assets: NoteAssets::new(vec![fungible_asset.into()]).unwrap(), - inputs: AccountIdFeltRepr(&alice_account.id()).to_felt_repr(), - ..Default::default() - }, - ); - eprintln!("P2ID mint note hash: {:?}", p2id_note_mint.id().to_hex()); - - let mint_request = TransactionRequestBuilder::new() - .own_output_notes(vec![OutputNote::Full(p2id_note_mint.clone())]) - .build() - .unwrap(); - - let mint_tx_id = - client.submit_new_transaction(faucet_account.id(), mint_request).await.unwrap(); - eprintln!("Submitted mint transaction. Tx ID: {mint_tx_id:?}"); - - // Step 3: Alice consumes the p2id note - eprintln!("\n=== Step 3: Alice consumes p2id mint note ==="); - - let consume_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes([(p2id_note_mint, None)]) - .build() - .unwrap(); - - let _consume_tx_id = client - .submit_new_transaction(alice_account.id(), consume_request) - .await - .map_err(|e| format!("{e:?}")) - .unwrap(); - - eprintln!("\n=== Checking Alice's account has the minted asset ==="); - - assert_account_has_fungible_asset( - &mut client, - alice_account.id(), - faucet_account.id(), - mint_amount, - ) - .await; - - // Create Bob's account - eprintln!("\n=== Creating Bob's account ==="); - - let bob_config = AccountCreationConfig { - with_basic_wallet: false, - ..Default::default() - }; - let bob_account = create_account_with_component( - &mut client, - keystore.clone(), - wallet_package, - bob_config, - ) - .await - .unwrap(); - eprintln!("Bob account ID: {:?}", bob_account.id().to_hex()); - - // Step 4: Alice creates p2ide note for Bob with reclaim enabled - eprintln!("\n=== Step 4: Alice creates p2ide note for Bob with reclaim ==="); - - let transfer_amount = 10_000u64; // 10,000 tokens - let transfer_asset = FungibleAsset::new(faucet_account.id(), transfer_amount).unwrap(); - - // Set timelock to 0 (no timelock) and reclaim height to a future block - // This allows Alice to reclaim if she consumes the note herself - let timelock_height = Felt::new(0); - let reclaim_height = Felt::new(1000); // Future block height - - // Create the p2ide note - let p2ide_note = create_note_from_package( - &mut client, - p2ide_note_package.clone(), - alice_account.id(), - NoteCreationConfig { - assets: NoteAssets::new(vec![transfer_asset.into()]).unwrap(), - inputs: { - let mut inputs = AccountIdFeltRepr(&bob_account.id()).to_felt_repr(); - inputs.extend([timelock_height, reclaim_height]); - inputs - }, - ..Default::default() - }, - ); - eprintln!("P2IDE note hash: {:?}", p2ide_note.id().to_hex()); - - let transfer_request = TransactionRequestBuilder::new() - .own_output_notes(vec![OutputNote::Full(p2ide_note.clone())]) - .build() - .unwrap(); - - let alice_tx_id = client - .submit_new_transaction(alice_account.id(), transfer_request) - .await - .unwrap(); - eprintln!("Submitted p2ide transaction. Tx ID: {alice_tx_id:?}"); - - // Step 5: Alice reclaims the note (exercises the reclaim branch) - eprintln!("\n=== Step 5: Alice reclaims the p2ide note ==="); - - let reclaim_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes([(p2ide_note, None)]) - .build() - .unwrap(); - - let reclaim_tx_id = client - .submit_new_transaction(alice_account.id(), reclaim_request) - .await - .unwrap(); - eprintln!("Alice created reclaim transaction. Tx ID: {reclaim_tx_id:?}"); - - eprintln!("\n=== Checking Alice's account has reclaimed the asset ==="); - - // Alice should have her original amount back (mint_amount) - // because she reclaimed the note instead of Bob consuming it - assert_account_has_fungible_asset( - &mut client, - alice_account.id(), - faucet_account.id(), - mint_amount, - ) - .await; - - eprintln!("\n=== Test completed: Alice successfully reclaimed the p2ide note ==="); - }); -} diff --git a/tests/integration-node/src/node_tests/counter_contract.rs b/tests/integration-node/src/node_tests/counter_contract.rs deleted file mode 100644 index 36c75d1bd..000000000 --- a/tests/integration-node/src/node_tests/counter_contract.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! Counter contract test module - -use miden_client::{ - Word, - account::StorageMap, - transaction::{OutputNote, TransactionRequestBuilder}, -}; -use miden_core::{Felt, FieldElement}; - -use super::helpers::*; -use crate::local_node::ensure_shared_node; - -fn assert_counter_storage( - counter_account_storage: &miden_client::account::AccountStorage, - expected: u64, -) { - // according to `examples/counter-contract` for inner (slot, key) values - let counter_contract_storage_key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); - - // The counter contract is in slot 1 when deployed, auth_component takes slot 0 - let word = counter_account_storage - .get_map_item(1, counter_contract_storage_key) - .expect("Failed to get counter value from storage slot 1"); - - let val = word.last().unwrap(); - assert_eq!( - val.as_int(), - expected, - "Counter value mismatch. Expected: {}, Got: {}", - expected, - val.as_int() - ); -} - -/// Tests the counter contract deployment and note consumption workflow on a local node. -#[test] -pub fn test_counter_contract_local() { - // Compile the contracts first (before creating any runtime) - let contract_package = compile_rust_package("../../examples/counter-contract", true); - let note_package = compile_rust_package("../../examples/counter-note", true); - - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - // Create temp directory and get node handle - let temp_dir = temp_dir::TempDir::with_prefix("test_counter_contract_local_") - .expect("Failed to create temp directory"); - let node_handle = ensure_shared_node().await.expect("Failed to get shared node"); - - // Initialize test infrastructure - let TestSetup { - mut client, - keystore, - } = setup_test_infrastructure(&temp_dir, &node_handle) - .await - .expect("Failed to setup test infrastructure"); - - let sync_summary = client.sync_state().await.unwrap(); - eprintln!("Latest block: {}", sync_summary.block_num); - - // Create the counter account with initial storage - let key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); - let value = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); - let config = AccountCreationConfig { - storage_slots: vec![miden_client::account::StorageSlot::Map( - StorageMap::with_entries([(key, value)]).unwrap(), - )], - ..Default::default() - }; - - let counter_account = - create_account_with_component(&mut client, keystore.clone(), contract_package, config) - .await - .unwrap(); - eprintln!("Counter account ID: {:?}", counter_account.id().to_hex()); - - // The counter contract storage value should be zero after the account creation - assert_counter_storage( - client - .get_account(counter_account.id()) - .await - .unwrap() - .unwrap() - .account() - .storage(), - 1, - ); - - // Create the counter note from sender to counter - let counter_note = create_note_from_package( - &mut client, - note_package, - counter_account.id(), - NoteCreationConfig::default(), - ); - eprintln!("Counter note hash: {:?}", counter_note.id().to_hex()); - - // Submit transaction to create the note - let note_request = TransactionRequestBuilder::new() - .own_output_notes(vec![OutputNote::Full(counter_note.clone())]) - .build() - .unwrap(); - - let create_note_tx_id = - client.submit_new_transaction(counter_account.id(), note_request).await.unwrap(); - eprintln!("Created counter note tx: {create_note_tx_id:?}"); - - // Consume the note to increment the counter - let consume_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes([(counter_note, None)]) - .build() - .unwrap(); - - let _consume_tx_id = client - .submit_new_transaction(counter_account.id(), consume_request) - .await - .unwrap(); - - let sync_result = client.sync_state().await.unwrap(); - eprintln!("Synced to block: {}", sync_result.block_num); - - // The counter contract storage value should be 1 (incremented) after the note is consumed - assert_counter_storage( - client - .get_account(counter_account.id()) - .await - .unwrap() - .unwrap() - .account() - .storage(), - 2, - ); - }); -} diff --git a/tests/integration-node/src/node_tests/counter_contract_no_auth.rs b/tests/integration-node/src/node_tests/counter_contract_no_auth.rs deleted file mode 100644 index cd5f144f3..000000000 --- a/tests/integration-node/src/node_tests/counter_contract_no_auth.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Counter contract test with no-auth authentication component - -use miden_client::{ - Word, - account::StorageMap, - transaction::{OutputNote, TransactionRequestBuilder}, -}; -use miden_core::{Felt, FieldElement}; - -use super::helpers::*; -use crate::local_node::ensure_shared_node; - -fn assert_counter_storage( - counter_account_storage: &miden_client::account::AccountStorage, - expected: u64, -) { - // according to `examples/counter-contract` for inner (slot, key) values - let counter_contract_storage_key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); - - // With no-auth auth component (no storage), the counter component occupies slot 0 - let word = counter_account_storage - .get_map_item(0, counter_contract_storage_key) - .expect("Failed to get counter value from storage slot 1"); - - let val = word.last().unwrap(); - assert_eq!( - val.as_int(), - expected, - "Counter value mismatch. Expected: {}, Got: {}", - expected, - val.as_int() - ); -} - -/// Tests the counter contract with a "no-auth" authentication component. -/// -/// Flow: -/// - Build counter account using `examples/auth-component-no-auth` as the auth component -/// - Build a separate sender account (basic wallet) -/// - Sender issues a counter note to the network -/// - Counter account consumes the note without requiring authentication/signature -#[test] -pub fn test_counter_contract_no_auth_local() { - // Compile the contracts first (before creating any runtime) - let contract_package = compile_rust_package("../../examples/counter-contract", true); - let note_package = compile_rust_package("../../examples/counter-note", true); - let no_auth_auth_component = - compile_rust_package("../../examples/auth-component-no-auth", true); - - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - let temp_dir = temp_dir::TempDir::with_prefix("test_counter_contract_no_auth_") - .expect("Failed to create temp directory"); - let node_handle = ensure_shared_node().await.expect("Failed to get shared node"); - - let TestSetup { - mut client, - keystore, - } = setup_test_infrastructure(&temp_dir, &node_handle) - .await - .expect("Failed to setup test infrastructure"); - - let sync_summary = client.sync_state().await.unwrap(); - eprintln!("Latest block: {}", sync_summary.block_num); - - // Create the counter account with initial storage and no-auth auth component - let key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); - let value = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); - let counter_cfg = AccountCreationConfig { - storage_slots: vec![miden_client::account::StorageSlot::Map( - StorageMap::with_entries([(key, value)]).unwrap(), - )], - ..Default::default() - }; - - let counter_account = create_account_with_component_and_auth_package( - &mut client, - contract_package.clone(), - no_auth_auth_component.clone(), - counter_cfg, - ) - .await - .unwrap(); - eprintln!("Counter account (no-auth) ID: {:?}", counter_account.id().to_hex()); - - assert_counter_storage( - client - .get_account(counter_account.id()) - .await - .unwrap() - .unwrap() - .account() - .storage(), - 1, - ); - - // Create a separate sender account using only the BasicWallet component - let sender_cfg = AccountCreationConfig::default(); - let sender_account = create_basic_wallet_account(&mut client, keystore.clone(), sender_cfg) - .await - .unwrap(); - eprintln!("Sender account ID: {:?}", sender_account.id().to_hex()); - - // Sender creates the counter note (note script increments counter's storage on consumption) - let counter_note = create_note_from_package( - &mut client, - note_package.clone(), - sender_account.id(), - NoteCreationConfig::default(), - ); - eprintln!("Counter note hash: {:?}", counter_note.id().to_hex()); - - // Submit transaction to create the note from the sender account - let note_request = TransactionRequestBuilder::new() - .own_output_notes(vec![OutputNote::Full(counter_note.clone())]) - .build() - .unwrap(); - - let tx_result = client - .execute_transaction(sender_account.id(), note_request) - .await - .map_err(|e| { - eprintln!("Transaction creation error: {e}"); - e - }) - .unwrap(); - let executed_transaction = tx_result.executed_transaction(); - assert_eq!(executed_transaction.output_notes().num_notes(), 1); - let executed_tx_output_note = executed_transaction.output_notes().get_note(0); - assert_eq!(executed_tx_output_note.id(), counter_note.id()); - let create_note_tx_id = executed_transaction.id(); - let proven_tx = client.prove_transaction(&tx_result).await.unwrap(); - let submission_height = client - .submit_proven_transaction(proven_tx, tx_result.tx_inputs().clone()) - .await - .unwrap(); - client.apply_transaction(&tx_result, submission_height).await.unwrap(); - eprintln!("Created counter note tx: {create_note_tx_id:?}"); - - // Consume the note with the counter account - let consume_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes([(counter_note, None)]) - .build() - .unwrap(); - - let tx_result = client - .execute_transaction(counter_account.id(), consume_request) - .await - .map_err(|e| { - eprintln!("Note consumption transaction error: {e}"); - e - }) - .unwrap(); - eprintln!( - "Consumed counter note tx: https://testnet.midenscan.com/tx/{:?}", - &tx_result.executed_transaction().id() - ); - - let proven_tx = client.prove_transaction(&tx_result).await.unwrap(); - let submission_height = client - .submit_proven_transaction(proven_tx, tx_result.tx_inputs().clone()) - .await - .unwrap(); - client.apply_transaction(&tx_result, submission_height).await.unwrap(); - - let sync_result = client.sync_state().await.unwrap(); - eprintln!("Synced to block: {}", sync_result.block_num); - - // The counter contract storage value should be 2 after the note is consumed - assert_counter_storage( - client - .get_account(counter_account.id()) - .await - .unwrap() - .unwrap() - .account() - .storage(), - 2, - ); - }); -} diff --git a/tests/integration-node/src/node_tests/counter_contract_rust_auth.rs b/tests/integration-node/src/node_tests/counter_contract_rust_auth.rs deleted file mode 100644 index 27fa0d9ae..000000000 --- a/tests/integration-node/src/node_tests/counter_contract_rust_auth.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! Counter contract test using an auth component compiled from Rust (RPO-Falcon512) -//! -//! This test ensures that an account which does not possess the correct -//! RPO-Falcon512 secret key cannot create notes on behalf of the counter -//! contract account that uses the Rust-compiled auth component. - -use miden_client::{ - Client, DebugMode, Word, - account::StorageMap, - auth::{AuthSecretKey, PublicKeyCommitment}, - keystore::FilesystemKeyStore, - transaction::{OutputNote, TransactionRequestBuilder}, - utils::Deserializable, -}; -use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_core::{Felt, FieldElement}; -use miden_mast_package::SectionId; -use miden_objects::crypto::dsa::rpo_falcon512::SecretKey; -use rand::{RngCore, rngs::StdRng}; - -use super::helpers::*; -use crate::local_node::ensure_shared_node; - -fn assert_counter_storage( - counter_account_storage: &miden_client::account::AccountStorage, - expected: u64, -) { - // According to `examples/counter-contract` for inner (slot, key) values - let counter_contract_storage_key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); - - // With RPO-Falcon512 auth component occupying slot 0, the counter component is at slot 1. - let word = counter_account_storage - .get_map_item(1, counter_contract_storage_key) - .expect("Failed to get counter value from storage slot 1"); - - let val = word.last().unwrap(); - assert_eq!( - val.as_int(), - expected, - "Counter value mismatch. Expected: {}, Got: {}", - expected, - val.as_int() - ); -} - -/// Build a counter account from the counter component package and the -/// Rust-compiled RPO-Falcon512 auth component package. -async fn create_counter_account_with_rust_rpo_auth( - client: &mut Client>, - component_package: std::sync::Arc, - auth_component_package: std::sync::Arc, - keystore: std::sync::Arc>, -) -> Result { - use std::collections::BTreeSet; - - use miden_objects::account::{ - AccountBuilder, AccountComponent, AccountComponentMetadata, AccountComponentTemplate, - AccountStorageMode, AccountType, StorageSlot, - }; - - // Build counter component from template/metadata with initial storage - let account_component = match component_package.sections.iter().find_map(|s| { - if s.id == SectionId::ACCOUNT_COMPONENT_METADATA { - Some(s.data.as_ref()) - } else { - None - } - }) { - None => panic!("no account component metadata present"), - Some(bytes) => { - let metadata = AccountComponentMetadata::read_from_bytes(bytes).unwrap(); - let template = AccountComponentTemplate::new( - metadata, - component_package.unwrap_library().as_ref().clone(), - ); - - // Initialize the counter storage to 1 at key [0,0,0,1] - let key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); - let value = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); - let storage = vec![StorageSlot::Map(StorageMap::with_entries([(key, value)]).unwrap())]; - - let component = AccountComponent::new(template.library().clone(), storage).unwrap(); - component.with_supported_types(BTreeSet::from_iter([ - AccountType::RegularAccountUpdatableCode, - ])) - } - }; - - // Build the Rust-compiled auth component with public key commitment in slot 0 - let key_pair = SecretKey::with_rng(client.rng()); - let pk_commitment: Word = PublicKeyCommitment::from(key_pair.public_key()).into(); - let mut auth_component = AccountComponent::new( - auth_component_package.unwrap_library().as_ref().clone(), - vec![StorageSlot::Value(pk_commitment)], - ) - .unwrap(); - auth_component = auth_component - .with_supported_types(BTreeSet::from_iter([AccountType::RegularAccountUpdatableCode])); - - let mut init_seed = [0_u8; 32]; - client.rng().fill_bytes(&mut init_seed); - - let _ = client.sync_state().await?; - - let account = AccountBuilder::new(init_seed) - .account_type(AccountType::RegularAccountUpdatableCode) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(auth_component) - .with_component(miden_client::account::component::BasicWallet) - .with_component(account_component) - .build() - .unwrap(); - - client.add_account(&account, false).await?; - - keystore.add_key(&AuthSecretKey::RpoFalcon512(key_pair)).unwrap(); - - Ok(account) -} - -/// Verify that another client (without the RPO-Falcon512 key) cannot create notes for -/// the counter account which uses the Rust-compiled RPO-Falcon512 authentication component. -#[test] -pub fn test_counter_contract_rust_auth_blocks_unauthorized_note_creation() { - let contract_package = compile_rust_package("../../examples/counter-contract", true); - let note_package = compile_rust_package("../../examples/counter-note", true); - let rpo_auth_package = - compile_rust_package("../../examples/auth-component-rpo-falcon512", true); - - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - let temp_dir = temp_dir::TempDir::with_prefix("test_counter_contract_rust_auth_") - .expect("Failed to create temp directory"); - let node_handle = ensure_shared_node().await.expect("Failed to get shared node"); - - let TestSetup { - mut client, - keystore, - } = setup_test_infrastructure(&temp_dir, &node_handle) - .await - .expect("Failed to setup test infrastructure"); - - let counter_account = create_counter_account_with_rust_rpo_auth( - &mut client, - contract_package.clone(), - rpo_auth_package.clone(), - keystore.clone(), - ) - .await - .unwrap(); - eprintln!( - "Counter account (Rust RPO-Falcon512 auth) ID: {:?}", - counter_account.id().to_hex() - ); - - assert_counter_storage( - client - .get_account(counter_account.id()) - .await - .unwrap() - .unwrap() - .account() - .storage(), - 1, - ); - - // Positive check: original client (with the key) can create a note - let own_note = create_note_from_package( - &mut client, - note_package.clone(), - counter_account.id(), - NoteCreationConfig::default(), - ); - let own_request = TransactionRequestBuilder::new() - .own_output_notes(vec![OutputNote::Full(own_note.clone())]) - .build() - .unwrap(); - let tx_result = client - .execute_transaction(counter_account.id(), own_request.clone()) - .await - .expect("authorized client should be able to create a note"); - assert_eq!(tx_result.executed_transaction().output_notes().num_notes(), 1); - assert_eq!(tx_result.executed_transaction().output_notes().get_note(0).id(), own_note.id()); - - let proven_tx = client.prove_transaction(&tx_result).await.unwrap(); - let submission_height = client - .submit_proven_transaction(proven_tx, tx_result.tx_inputs().clone()) - .await - .unwrap(); - client.apply_transaction(&tx_result, submission_height).await.unwrap(); - - // Create a separate client with its own empty keystore (no key for counter account) - let attacker_dir = temp_dir::TempDir::with_prefix("attacker_client_") - .expect("Failed to create temp directory"); - let rpc_url = node_handle.rpc_url().to_string(); - let endpoint = miden_client::rpc::Endpoint::try_from(rpc_url.as_str()).unwrap(); - let rpc_api = std::sync::Arc::new(miden_client::rpc::GrpcClient::new(&endpoint, 10_000)); - let attacker_store_path = attacker_dir.path().join("store.sqlite3"); - let attacker_keystore_path = attacker_dir.path().join("keystore"); - - let mut attacker_client = miden_client::builder::ClientBuilder::new() - .rpc(rpc_api) - .sqlite_store(attacker_store_path.clone()) - .filesystem_keystore(attacker_keystore_path.to_str().unwrap()) - .in_debug_mode(DebugMode::Enabled) - .build() - .await - .unwrap(); - - // The attacker needs the account record locally to attempt building a tx - // Reuse the same account object; seed is not needed for reading/state queries - attacker_client - .add_account(&counter_account, false) - .await - .expect("failed to add account to attacker client"); - - // Attacker tries to create an output note on behalf of the counter account - // (origin = counter_account.id()), but does not have the required secret key. - let forged_note = create_note_from_package( - &mut attacker_client, - note_package.clone(), - counter_account.id(), - NoteCreationConfig::default(), - ); - - let forged_request = TransactionRequestBuilder::new() - .own_output_notes(vec![OutputNote::Full(forged_note.clone())]) - .build() - .unwrap(); - - let result = - attacker_client.execute_transaction(counter_account.id(), forged_request).await; - - assert!( - result.is_err(), - "Unauthorized client unexpectedly created a transaction for the counter account" - ); - }); -} diff --git a/tests/integration-node/src/node_tests/helpers.rs b/tests/integration-node/src/node_tests/helpers.rs deleted file mode 100644 index a706e13c9..000000000 --- a/tests/integration-node/src/node_tests/helpers.rs +++ /dev/null @@ -1,522 +0,0 @@ -//! Common helper functions for node tests - -use std::{borrow::Borrow, collections::BTreeSet, path::Path, sync::Arc}; - -use miden_client::{ - Client, ClientError, - account::{ - Account, AccountId, AccountStorageMode, AccountType, StorageSlot, - component::{AuthRpoFalcon512, BasicFungibleFaucet, BasicWallet}, - }, - asset::{FungibleAsset, TokenSymbol}, - auth::{AuthSecretKey, PublicKeyCommitment}, - builder::ClientBuilder, - crypto::{FeltRng, RpoRandomCoin, rpo_falcon512::SecretKey}, - keystore::FilesystemKeyStore, - note::{ - Note, NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, NoteScript, NoteTag, - NoteType, - }, - rpc::{Endpoint, GrpcClient}, - transaction::{TransactionRequestBuilder, TransactionScript}, - utils::Deserializable, -}; -use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_core::{Felt, FieldElement, Word}; -use miden_felt_repr_offchain::{AccountIdFeltRepr, ToFeltRepr}; -use miden_integration_tests::CompilerTestBuilder; -use miden_mast_package::{Package, SectionId}; -use miden_objects::{ - account::{ - AccountBuilder, AccountComponent, AccountComponentMetadata, AccountComponentTemplate, - }, - asset::Asset, - transaction::TransactionId, -}; -use midenc_frontend_wasm::WasmTranslationConfig; -use rand::{RngCore, rngs::StdRng}; - -/// Test setup configuration -pub struct TestSetup { - pub client: Client>, - pub keystore: Arc>, -} - -/// Initialize test infrastructure with client, keystore, and temporary directory -pub async fn setup_test_infrastructure( - temp_dir: &temp_dir::TempDir, - node_handle: &crate::local_node::SharedNodeHandle, -) -> Result> { - let rpc_url = node_handle.rpc_url().to_string(); - - // Initialize RPC connection - let endpoint = Endpoint::try_from(rpc_url.as_str()).expect("Failed to create endpoint"); - let timeout_ms = 10_000; - let rpc_api = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); - - // Initialize keystore - let keystore_path = temp_dir.path().join("keystore"); - let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path.clone()).unwrap()); - - // Initialize client - let store_path = temp_dir.path().join("store.sqlite3").to_str().unwrap().to_string(); - let builder = ClientBuilder::new() - .rpc(rpc_api) - .sqlite_store(Path::new(&store_path).to_path_buf()) - .filesystem_keystore(keystore_path.to_str().unwrap()) - .in_debug_mode(miden_client::DebugMode::Enabled); - let client = builder.build().await?; - - Ok(TestSetup { client, keystore }) -} - -/// Configuration for creating an account with a custom component -pub struct AccountCreationConfig { - pub account_type: AccountType, - pub storage_mode: AccountStorageMode, - pub storage_slots: Vec, - pub supported_types: Option>, - pub with_basic_wallet: bool, -} - -impl Default for AccountCreationConfig { - fn default() -> Self { - Self { - account_type: AccountType::RegularAccountUpdatableCode, - storage_mode: AccountStorageMode::Public, - storage_slots: vec![], - supported_types: None, - with_basic_wallet: true, - } - } -} - -/// Helper to create an account with a custom component from a package -pub async fn create_account_with_component( - client: &mut Client>, - keystore: Arc>, - package: Arc, - config: AccountCreationConfig, -) -> Result { - let account_component_metadata = package.sections.iter().find_map(|s| { - if s.id == SectionId::ACCOUNT_COMPONENT_METADATA { - Some(s.data.borrow()) - } else { - None - } - }); - let account_component = match account_component_metadata { - None => panic!("no account component metadata present"), - Some(bytes) => { - let metadata = AccountComponentMetadata::read_from_bytes(bytes).unwrap(); - let template = - AccountComponentTemplate::new(metadata, package.unwrap_library().as_ref().clone()); - - let component = - AccountComponent::new(template.library().clone(), config.storage_slots).unwrap(); - - // Use supported types from config if provided, otherwise default to RegularAccountUpdatableCode - let supported_types = if let Some(types) = config.supported_types { - BTreeSet::from_iter(types) - } else { - BTreeSet::from_iter([AccountType::RegularAccountUpdatableCode]) - }; - - component.with_supported_types(supported_types) - } - }; - - let mut init_seed = [0_u8; 32]; - client.rng().fill_bytes(&mut init_seed); - - let key_pair = SecretKey::with_rng(client.rng()); - - // Sync client state to get latest block info - let _sync_summary = client.sync_state().await.unwrap(); - - let mut builder = AccountBuilder::new(init_seed) - .account_type(config.account_type) - .storage_mode(config.storage_mode) - .with_auth_component(AuthRpoFalcon512::new(PublicKeyCommitment::from( - key_pair.public_key().to_commitment(), - ))); - - if config.with_basic_wallet { - builder = builder.with_component(BasicWallet); - } - - builder = builder.with_component(account_component); - - let account = builder.build().unwrap_or_else(|e| { - eprintln!("failed to build account with custom auth component: {e}"); - panic!("failed to build account with custom auth component") - }); - client.add_account(&account, false).await?; - keystore.add_key(&AuthSecretKey::RpoFalcon512(key_pair)).unwrap(); - - Ok(account) -} - -/// Create a basic wallet account with standard RpoFalcon512 auth. -/// -/// This helper does not require a component package and always adds the `BasicWallet` component. -pub async fn create_basic_wallet_account( - client: &mut Client>, - keystore: Arc>, - config: AccountCreationConfig, -) -> Result { - let mut init_seed = [0_u8; 32]; - client.rng().fill_bytes(&mut init_seed); - - let key_pair = SecretKey::with_rng(client.rng()); - - // Sync client state to get latest block info - let _sync_summary = client.sync_state().await.unwrap(); - - let builder = AccountBuilder::new(init_seed) - .account_type(config.account_type) - .storage_mode(config.storage_mode) - .with_auth_component(AuthRpoFalcon512::new(PublicKeyCommitment::from( - key_pair.public_key().to_commitment(), - ))) - .with_component(BasicWallet); - - let account = builder.build().unwrap(); - client.add_account(&account, false).await?; - keystore.add_key(&AuthSecretKey::RpoFalcon512(key_pair)).unwrap(); - - Ok(account) -} - -/// Helper to create an account with a custom component and a custom authentication component -pub async fn create_account_with_component_and_auth_package( - client: &mut Client>, - component_package: Arc, - auth_component_package: Arc, - config: AccountCreationConfig, -) -> Result { - // Build the main account component from its template metadata - let account_component_metadata_section = component_package - .sections - .iter() - .find(|s| s.id == SectionId::ACCOUNT_COMPONENT_METADATA) - .expect("no account component metadata found"); - let account_component = { - let bytes = account_component_metadata_section.data.as_ref(); - let metadata = AccountComponentMetadata::read_from_bytes(bytes).unwrap(); - let template = AccountComponentTemplate::new( - metadata, - component_package.unwrap_library().as_ref().clone(), - ); - - let component = - AccountComponent::new(template.library().clone(), config.storage_slots.clone()) - .unwrap(); - - // Use supported types from config if provided, otherwise default to RegularAccountUpdatableCode - let supported_types = if let Some(types) = &config.supported_types { - BTreeSet::from_iter(types.iter().cloned()) - } else { - BTreeSet::from_iter([AccountType::RegularAccountUpdatableCode]) - }; - - component.with_supported_types(supported_types) - }; - - // Build the authentication component from the compiled library (no storage) - let mut auth_component = - AccountComponent::new(auth_component_package.unwrap_library().as_ref().clone(), vec![]) - .unwrap(); - - // Ensure auth component supports the intended account type - if let Some(types) = &config.supported_types { - auth_component = - auth_component.with_supported_types(BTreeSet::from_iter(types.iter().cloned())); - } else { - auth_component = auth_component - .with_supported_types(BTreeSet::from_iter([AccountType::RegularAccountUpdatableCode])); - } - - let mut init_seed = [0_u8; 32]; - client.rng().fill_bytes(&mut init_seed); - - // Sync client state to get latest block info - let _sync_summary = client.sync_state().await.unwrap(); - - let mut builder = AccountBuilder::new(init_seed) - .account_type(config.account_type) - .storage_mode(config.storage_mode) - .with_auth_component(auth_component); - - if config.with_basic_wallet { - builder = builder.with_component(BasicWallet); - } - - builder = builder.with_component(account_component); - - let account = builder.build().unwrap(); - client.add_account(&account, false).await?; - // No keystore key needed for no-auth auth component - - Ok(account) -} - -pub async fn create_fungible_faucet_account( - client: &mut Client>, - keystore: Arc>, - token_symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, -) -> Result { - let mut init_seed = [0_u8; 32]; - client.rng().fill_bytes(&mut init_seed); - - let key_pair = SecretKey::with_rng(client.rng()); - // Sync client state to get latest block info - let _sync_summary = client.sync_state().await.unwrap(); - let builder = AccountBuilder::new(init_seed) - .account_type(AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(AuthRpoFalcon512::new(PublicKeyCommitment::from( - key_pair.public_key().to_commitment(), - ))) - .with_component(BasicFungibleFaucet::new(token_symbol, decimals, max_supply).unwrap()); - - let account = builder.build().unwrap(); - client.add_account(&account, false).await?; - keystore.add_key(&AuthSecretKey::RpoFalcon512(key_pair)).unwrap(); - - Ok(account) -} - -/// Helper to compile a Rust package to Miden -pub fn compile_rust_package(package_path: &str, release: bool) -> Arc { - let config = WasmTranslationConfig::default(); - let mut builder = CompilerTestBuilder::rust_source_cargo_miden(package_path, config, []); - - if release { - builder.with_release(true); - } - - let mut test = builder.build(); - test.compiled_package() -} - -/// Configuration for creating a note -pub struct NoteCreationConfig { - pub note_type: NoteType, - pub tag: NoteTag, - pub assets: miden_client::note::NoteAssets, - pub inputs: Vec, - pub execution_hint: NoteExecutionHint, - pub aux: Felt, -} - -impl Default for NoteCreationConfig { - fn default() -> Self { - Self { - note_type: NoteType::Public, - tag: NoteTag::for_local_use_case(0, 0).unwrap(), - assets: Default::default(), - inputs: Default::default(), - execution_hint: NoteExecutionHint::always(), - aux: Felt::ZERO, - } - } -} - -/// Helper to create a note from a compiled package -pub fn create_note_from_package( - client: &mut Client>, - package: Arc, - sender_id: AccountId, - config: NoteCreationConfig, -) -> Note { - let note_program = package.unwrap_program(); - let note_script = - NoteScript::from_parts(note_program.mast_forest().clone(), note_program.entrypoint()); - - let serial_num = client.rng().draw_word(); - let note_inputs = NoteInputs::new(config.inputs).unwrap(); - let recipient = NoteRecipient::new(serial_num, note_script, note_inputs); - - let metadata = NoteMetadata::new( - sender_id, - config.note_type, - config.tag, - config.execution_hint, - config.aux, - ) - .unwrap(); - - Note::new(config.assets, metadata, recipient) -} - -/// Helper function to assert that an account contains a specific fungible asset -/// The account may have other assets as well -pub async fn assert_account_has_fungible_asset( - client: &mut Client>, - account_id: AccountId, - expected_faucet_id: AccountId, - expected_amount: u64, -) { - let account_record = client - .get_account(account_id) - .await - .expect("Failed to get account") - .expect("Account not found"); - - let account_state: miden_objects::account::Account = account_record.into(); - - // Look for the specific fungible asset in the vault - let found_asset = account_state.vault().assets().find_map(|asset| { - if let Asset::Fungible(fungible_asset) = asset { - if fungible_asset.faucet_id() == expected_faucet_id { - Some(fungible_asset) - } else { - None - } - } else { - None - } - }); - - match found_asset { - Some(fungible_asset) => { - assert_eq!( - fungible_asset.amount(), - expected_amount, - "Found asset from faucet {expected_faucet_id} but amount {} doesn't match \ - expected {expected_amount}", - fungible_asset.amount() - ); - } - None => { - panic!("Account does not contain a fungible asset from faucet {expected_faucet_id}"); - } - } -} - -/// Configuration for sending assets between accounts -pub struct AssetTransferConfig { - pub note_type: NoteType, - pub tag: NoteTag, - pub execution_hint: NoteExecutionHint, - pub aux: Felt, -} - -impl Default for AssetTransferConfig { - fn default() -> Self { - Self { - note_type: NoteType::Public, - tag: NoteTag::for_local_use_case(0, 0).unwrap(), - execution_hint: NoteExecutionHint::always(), - aux: Felt::ZERO, - } - } -} - -/// Helper function to send assets from one account to another using a transaction script -/// -/// This function creates a p2id note for the recipient and executes a transaction script -/// to send the specified asset amount. -/// -/// # Arguments -/// * `client` - The client instance -/// * `sender_account_id` - The account ID of the sender -/// * `recipient_account_id` - The account ID of the recipient -/// * `asset` - The fungible asset to transfer -/// * `note_package` - The compiled note package (e.g., p2id-note) -/// * `tx_script_package` - The compiled transaction script package -/// * `config` - Optional configuration for the transfer -/// -/// # Returns -/// A tuple containing the transaction ID and the created Note for the recipient -pub async fn send_asset_to_account( - client: &mut Client>, - sender_account_id: AccountId, - recipient_account_id: AccountId, - asset: FungibleAsset, - note_package: Arc, - tx_script_package: Arc, - config: Option, -) -> Result<(TransactionId, Note), ClientError> { - let config = config.unwrap_or_default(); - - // Create the p2id note for the recipient - let p2id_note = create_note_from_package( - client, - note_package, - sender_account_id, - NoteCreationConfig { - assets: miden_client::note::NoteAssets::new(vec![asset.into()]).unwrap(), - inputs: AccountIdFeltRepr(&recipient_account_id).to_felt_repr(), - note_type: config.note_type, - tag: config.tag, - execution_hint: config.execution_hint, - aux: config.aux, - }, - ); - - let tx_script_program = tx_script_package.unwrap_program(); - let tx_script = TransactionScript::from_parts( - tx_script_program.mast_forest().clone(), - tx_script_program.entrypoint(), - ); - - // Prepare note recipient - let program_hash = tx_script_program.hash(); - let serial_num = RpoRandomCoin::new(program_hash).draw_word(); - let inputs = NoteInputs::new(AccountIdFeltRepr(&recipient_account_id).to_felt_repr()).unwrap(); - let note_recipient = NoteRecipient::new(serial_num, p2id_note.script().clone(), inputs); - - // Prepare commitment data - let mut input: Vec = vec![ - config.tag.into(), - config.aux, - config.note_type.into(), - config.execution_hint.into(), - ]; - let recipient_digest: [Felt; 4] = note_recipient.digest().into(); - input.extend(recipient_digest); - - let asset_arr: Word = asset.into(); - input.extend(asset_arr); - - let mut commitment: [Felt; 4] = miden_core::crypto::hash::Rpo256::hash_elements(&input).into(); - - assert_eq!(input.len() % 4, 0, "input needs to be word-aligned"); - - // Prepare advice map - let mut advice_map = std::collections::BTreeMap::new(); - advice_map.insert(commitment.into(), input.clone()); - - let recipients = vec![note_recipient.clone()]; - - // NOTE: passed on the stack reversed - commitment.reverse(); - - let tx_request = TransactionRequestBuilder::new() - .custom_script(tx_script) - .script_arg(miden_core::Word::new(commitment)) - .expected_output_recipients(recipients) - .extend_advice_map(advice_map) - .build() - .unwrap(); - - let tx_id = client.submit_new_transaction(sender_account_id, tx_request).await?; - - // Create the Note that the recipient will consume - let assets = miden_client::note::NoteAssets::new(vec![asset.into()]).unwrap(); - let metadata = NoteMetadata::new( - sender_account_id, - config.note_type, - config.tag, - config.execution_hint, - config.aux, - ) - .unwrap(); - let recipient_note = Note::new(assets, metadata, note_recipient); - - Ok((tx_id, recipient_note)) -} diff --git a/tests/integration-node/src/node_tests/mod.rs b/tests/integration-node/src/node_tests/mod.rs deleted file mode 100644 index 09f58f362..000000000 --- a/tests/integration-node/src/node_tests/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Integration tests that require a local Miden node instance - -pub mod basic_wallet; -pub mod counter_contract; -pub mod counter_contract_no_auth; -pub mod counter_contract_rust_auth; -pub mod helpers;