diff --git a/.dockerignore b/.dockerignore index 0fbe460c..11f1a350 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,11 +26,20 @@ Thumbs.db ci_scripts/ # Documentation +docs/ *.md !README.md -# Configs (copy selectively if needed) +# Non-build project files +completions/ configs/ - -# License +Justfile +clippy.toml +rustfmt.toml +flake.nix +flake.lock LICENSE + +# Docker compose files (not needed inside build) +docker-compose*.yml +**/docker-compose*.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb4cc791..858a43c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,33 +156,35 @@ jobs: RUST_LOG: "info" run: cargo nextest run -p integration_tests -- --skip tps_test --skip indexer - integration-tests-indexer: - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - uses: actions/checkout@v5 - with: - ref: ${{ github.head_ref }} + # # TODO: Bring this back once we find the source of the errors. + # # + # integration-tests-indexer: + # runs-on: ubuntu-latest + # timeout-minutes: 60 + # steps: + # - uses: actions/checkout@v5 + # with: + # ref: ${{ github.head_ref }} - - uses: ./.github/actions/install-system-deps + # - uses: ./.github/actions/install-system-deps - - uses: ./.github/actions/install-risc0 + # - uses: ./.github/actions/install-risc0 - - uses: ./.github/actions/install-logos-blockchain-circuits - with: - github-token: ${{ secrets.GITHUB_TOKEN }} + # - uses: ./.github/actions/install-logos-blockchain-circuits + # with: + # github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install active toolchain - run: rustup install + # - name: Install active toolchain + # run: rustup install - - name: Install nextest - run: cargo install --locked cargo-nextest + # - name: Install nextest + # run: cargo install --locked cargo-nextest - - name: Run tests - env: - RISC0_DEV_MODE: "1" - RUST_LOG: "info" - run: cargo nextest run -p integration_tests indexer -- --skip tps_test + # - name: Run tests + # env: + # RISC0_DEV_MODE: "1" + # RUST_LOG: "info" + # run: cargo nextest run -p integration_tests indexer -- --skip tps_test valid-proof-test: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 6e0461fd..cf582227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -629,9 +629,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "astral-tokio-tar" -version = "0.5.6" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" +checksum = "3c23f3af104b40a3430ccb90ed5f7bd877a8dc5c26fc92fde51a22b40890dcf9" dependencies = [ "filetime", "futures-core", @@ -727,6 +727,24 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "ata_core" +version = "0.1.0" +dependencies = [ + "nssa_core", + "risc0-zkvm", + "serde", +] + +[[package]] +name = "ata_program" +version = "0.1.0" +dependencies = [ + "ata_core", + "nssa_core", + "token_core", +] + [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -1001,19 +1019,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bitcoin-io" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" - [[package]] name = "bitcoin_hashes" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ - "bitcoin-io", "hex-conservative", ] @@ -3442,6 +3453,7 @@ dependencies = [ "serde_json", "storage", "tempfile", + "testnet_initial_state", "tokio", "url", ] @@ -3551,6 +3563,7 @@ name = "integration_tests" version = "0.1.0" dependencies = [ "anyhow", + "ata_core", "bytesize", "common", "env_logger", @@ -3568,6 +3581,7 @@ dependencies = [ "serde_json", "tempfile", "testcontainers", + "testnet_initial_state", "token_core", "tokio", "url", @@ -3957,7 +3971,6 @@ dependencies = [ "nssa", "nssa_core", "rand 0.8.5", - "secp256k1", "serde", "sha2", "thiserror 2.0.18", @@ -5249,13 +5262,13 @@ dependencies = [ "env_logger", "hex", "hex-literal 1.1.0", + "k256", "log", "nssa_core", "rand 0.8.5", "risc0-binfmt", "risc0-build", "risc0-zkvm", - "secp256k1", "serde", "serde_with", "sha2", @@ -5882,6 +5895,8 @@ version = "0.1.0" dependencies = [ "amm_core", "amm_program", + "ata_core", + "ata_program", "nssa_core", "risc0-zkvm", "serde", @@ -5930,7 +5945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.117", @@ -5943,7 +5958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.117", @@ -6024,7 +6039,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6919,7 +6934,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6930,9 +6945,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -7064,26 +7079,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" -dependencies = [ - "bitcoin_hashes", - "rand 0.9.2", - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" -dependencies = [ - "cc", -] - [[package]] name = "security-framework" version = "3.7.0" @@ -7137,7 +7132,6 @@ name = "sequencer_core" version = "0.1.0" dependencies = [ "anyhow", - "base58", "bedrock_client", "borsh", "bytesize", @@ -7157,6 +7151,7 @@ dependencies = [ "serde_json", "storage", "tempfile", + "testnet_initial_state", "tokio", "url", ] @@ -7842,9 +7837,9 @@ dependencies = [ [[package]] name = "testcontainers" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c0624faaa317c56d6d19136580be889677259caf5c897941c6f446b4655068" +checksum = "0bd36b06a2a6c0c3c81a83be1ab05fe86460d054d4d51bf513bc56b3e15bdc22" dependencies = [ "astral-tokio-tar", "async-trait", @@ -7873,6 +7868,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "testnet_initial_state" +version = "0.1.0" +dependencies = [ + "common", + "key_protocol", + "nssa", + "nssa_core", + "serde", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -8649,6 +8655,7 @@ dependencies = [ "amm_core", "anyhow", "async-stream", + "ata_core", "base58", "clap", "common", @@ -8669,6 +8676,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "testnet_initial_state", "thiserror 2.0.18", "token_core", "tokio", @@ -9047,15 +9055,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -9089,30 +9088,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -9125,12 +9107,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -9143,12 +9119,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -9161,24 +9131,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -9191,12 +9149,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -9209,12 +9161,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -9227,12 +9173,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -9245,12 +9185,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.15" diff --git a/Cargo.toml b/Cargo.toml index 829a6539..c2853089 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ members = [ "programs/amm", "programs/token/core", "programs/token", + "programs/associated_token_account/core", + "programs/associated_token_account", "sequencer/core", "sequencer/service", "sequencer/service/protocol", @@ -34,6 +36,7 @@ members = [ "examples/program_deployment/methods", "examples/program_deployment/methods/guest", "bedrock_client", + "testnet_initial_state", ] [workspace.dependencies] @@ -57,8 +60,11 @@ token_core = { path = "programs/token/core" } token_program = { path = "programs/token" } amm_core = { path = "programs/amm/core" } amm_program = { path = "programs/amm" } +ata_core = { path = "programs/associated_token_account/core" } +ata_program = { path = "programs/associated_token_account" } test_program_methods = { path = "test_program_methods" } bedrock_client = { path = "bedrock_client" } +testnet_initial_state = { path = "testnet_initial_state" } tokio = { version = "1.50", features = [ "net", diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 5ca34347..32612069 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index 7b2c40d2..d8cfd622 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index e2a9955c..04183a9a 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index baf61894..189830ce 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index fa67cec8..de89d40a 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index ef8be707..2d91aebc 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index 20eb8196..71f2c5c4 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 1285fb9b..b96d7188 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index f30c6003..b4b961c4 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 7f4b684f..b4a2eef2 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index 3d95f69b..fb94bf44 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index aec2828b..00bc3bc0 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 428f0417..73fe01a7 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index d47081d0..792c1225 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 1a07bf37..e8bfb1ac 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index ba90b184..f0d9674e 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index f828fab7..8cd04a44 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 7054ef8f..64c53aa2 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index f19443de..6a2b96b0 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index c4acea82..d6e3e89e 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 7044c309..6c5bcf06 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index c047253c..2813e34e 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index 6ce96300..0ee7dfeb 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index e4e760ba..f574f82b 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/common/src/block.rs b/common/src/block.rs index 844458e2..f7feb0d1 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -1,5 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use nssa::AccountId; +use nssa_core::BlockId; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256, digest::FixedOutput as _}; @@ -8,7 +8,6 @@ use crate::{HashType, transaction::NSSATransaction}; pub use nssa_core::Timestamp; pub type MantleMsgId = [u8; 32]; pub type BlockHash = HashType; -pub type BlockId = u64; #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] pub struct BlockMeta { @@ -123,20 +122,6 @@ impl From for HashableBlockData { } } -/// Helper struct for account (de-)serialization. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AccountInitialData { - pub account_id: AccountId, - pub balance: u128, -} - -/// Helper struct to (de-)serialize initial commitments. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitmentsInitialData { - pub npk: nssa_core::NullifierPublicKey, - pub account: nssa_core::account::Account, -} - #[cfg(test)] mod tests { use crate::{HashType, block::HashableBlockData, test_utils}; diff --git a/common/src/transaction.rs b/common/src/transaction.rs index dea5a52b..3d08a8e9 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -1,6 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use log::warn; use nssa::{AccountId, V03State}; +use nssa_core::{BlockId, Timestamp}; use serde::{Deserialize, Serialize}; use crate::HashType; @@ -89,10 +90,14 @@ impl NSSATransaction { pub fn execute_check_on_state( self, state: &mut V03State, + block_id: BlockId, + timestamp: Timestamp, ) -> Result { match &self { - Self::Public(tx) => state.transition_from_public_transaction(tx), - Self::PrivacyPreserving(tx) => state.transition_from_privacy_preserving_transaction(tx), + Self::Public(tx) => state.transition_from_public_transaction(tx, block_id, timestamp), + Self::PrivacyPreserving(tx) => { + state.transition_from_privacy_preserving_transaction(tx, block_id, timestamp) + } Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx), } .inspect_err(|err| warn!("Error at transition {err:#?}"))?; diff --git a/configs/docker-all-in-one/indexer/indexer_config.json b/configs/docker-all-in-one/indexer_config.json similarity index 100% rename from configs/docker-all-in-one/indexer/indexer_config.json rename to configs/docker-all-in-one/indexer_config.json diff --git a/configs/docker-all-in-one/sequencer/sequencer_config.json b/configs/docker-all-in-one/sequencer_config.json similarity index 100% rename from configs/docker-all-in-one/sequencer/sequencer_config.json rename to configs/docker-all-in-one/sequencer_config.json diff --git a/docker-compose.override.yml b/docker-compose.override.yml index af70ddd6..db955b23 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -11,17 +11,17 @@ services: depends_on: - logos-blockchain-node-0 - indexer_service - volumes: !override - - ./configs/docker-all-in-one/sequencer:/etc/sequencer_service + volumes: + - ./configs/docker-all-in-one/sequencer_config.json:/etc/sequencer_service/sequencer_config.json indexer_service: depends_on: - logos-blockchain-node-0 volumes: - - ./configs/docker-all-in-one/indexer/indexer_config.json:/etc/indexer_service/indexer_config.json + - ./configs/docker-all-in-one/indexer_config.json:/etc/indexer_service/indexer_config.json explorer_service: depends_on: - indexer_service environment: - - INDEXER_RPC_URL=http://indexer_service:8779 \ No newline at end of file + - INDEXER_RPC_URL=http://indexer_service:8779 diff --git a/docs/LEZ testnet v0.1 tutorials/associated-token-accounts.md b/docs/LEZ testnet v0.1 tutorials/associated-token-accounts.md new file mode 100644 index 00000000..330ae909 --- /dev/null +++ b/docs/LEZ testnet v0.1 tutorials/associated-token-accounts.md @@ -0,0 +1,369 @@ +# Associated Token Accounts (ATAs) + +This tutorial covers Associated Token Accounts (ATAs). An ATA lets you derive a unique token holding address from an owner account and a token definition — no need to create and track holding accounts manually. Given the same inputs, anyone can compute the same ATA address without a network call. By the end, you will have practiced: + +1. Deriving ATA addresses locally. +2. Creating an ATA. +3. Sending tokens via ATAs. +4. Burning tokens from an ATA. +5. Listing ATAs across multiple token definitions. +6. Creating an ATA with a private owner. +7. Sending tokens from a private owner's ATA. +8. Burning tokens from a private owner's ATA. + +> [!Important] +> This tutorial assumes you have completed the [wallet-setup](wallet-setup.md) and [custom-tokens](custom-tokens.md) tutorials. You need a running wallet with accounts and at least one token definition. + +## Prerequisites + +### Deploy the ATA program + +Unlike the Token program (which is built-in), the ATA program must be deployed before you can use it. The pre-built binary is included in the repository: + +```bash +wallet deploy-program artifacts/program_methods/associated_token_account.bin +``` + +> [!Note] +> Program deployment is idempotent — if the ATA program has already been deployed (e.g. by another user on the same network), the command is a no-op. + +You can verify the deployment succeeded by running any `wallet ata` command. If the program is not deployed, commands that submit transactions will fail. + +The CLI provides commands to work with the ATA program. Run `wallet ata` to see the options: + +```bash +Commands: + address Derive and print the Associated Token Account address (local only, no network) + create Create (or idempotently no-op) the Associated Token Account + send Send tokens from owner's ATA to a recipient + burn Burn tokens from holder's ATA + list List all ATAs for a given owner across multiple token definitions + help Print this message or the help of the given subcommand(s) +``` + +## 1. How ATA addresses work + +An ATA address is deterministically derived from two inputs: + +1. The **owner** account ID. +2. The **token definition** account ID. + +The derivation works as follows: + +``` +seed = SHA256(owner_id || definition_id) +ata_address = AccountId::from((ata_program_id, seed)) +``` + +Because the computation is pure, anyone who knows the owner and definition can reproduce the exact same ATA address — no network call required. + +> [!Note] +> All ATA commands that submit transactions accept a privacy prefix on the owner/holder argument — `Public/` for public accounts and `Private/` for private accounts. Using `Private/` generates a zero-knowledge proof locally and submits only the proof to the sequencer, keeping the owner's identity off-chain. + +## 2. Deriving an ATA address (`wallet ata address`) + +The `address` subcommand computes the ATA address locally without submitting a transaction. + +### a. Set up an owner and token definition + +If you already have a public account and a token definition from the custom-tokens tutorial, you can reuse them. Otherwise, create them now: + +```bash +wallet account new public + +# Output: +Generated new account with account_id Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB +``` + +```bash +wallet account new public + +# Output: +Generated new account with account_id Public/3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 +``` + +```bash +wallet token new \ + --name MYTOKEN \ + --total-supply 10000 \ + --definition-account-id Public/3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \ + --supply-account-id Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB +``` + +### b. Derive the ATA address + +```bash +wallet ata address \ + --owner 5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \ + --token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 + +# Output: +7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R +``` + +> [!Note] +> This is a pure computation — no transaction is submitted and no network connection is needed. The same inputs will always produce the same output. + +## 3. Creating an ATA (`wallet ata create`) + +Before an ATA can hold tokens it must be created on-chain. The `create` subcommand submits a transaction that initializes the ATA. If it already exists, the operation is a no-op. + +### a. Create the ATA + +```bash +wallet ata create \ + --owner Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \ + --token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 +``` + +### b. Inspect the ATA + +Use the ATA address derived in the previous section: + +```bash +wallet account get --account-id Public/7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R + +# Output: +Holding account owned by ata program +{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":0} +``` + +> [!Tip] +> Creation is idempotent — running the same command again is a no-op. + +## 4. Sending tokens via ATA (`wallet ata send`) + +The `send` subcommand transfers tokens from the owner's ATA to a recipient account. + +### a. Fund the ATA + +First, move tokens into the ATA from the supply account created earlier: + +```bash +wallet token send \ + --from Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \ + --to Public/7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R \ + --amount 5000 +``` + +### b. Create a recipient account + +```bash +wallet account new public + +# Output: +Generated new account with account_id Public/9Ht4Kv8pYmW2rXjN6dFcQsA7bEoLf3gUZx1wDnR5eTi +``` + +### c. Send tokens from the ATA to the recipient + +```bash +wallet ata send \ + --from Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \ + --token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \ + --to 9Ht4Kv8pYmW2rXjN6dFcQsA7bEoLf3gUZx1wDnR5eTi \ + --amount 2000 +``` + +### d. Verify balances + +```bash +wallet account get --account-id Public/7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R + +# Output: +Holding account owned by ata program +{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":3000} +``` + +```bash +wallet account get --account-id Public/9Ht4Kv8pYmW2rXjN6dFcQsA7bEoLf3gUZx1wDnR5eTi + +# Output: +Holding account owned by token program +{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":2000} +``` + +## 5. Burning tokens from an ATA (`wallet ata burn`) + +The `burn` subcommand destroys tokens held in the owner's ATA, reducing the token's total supply. + +### a. Burn tokens + +```bash +wallet ata burn \ + --holder Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \ + --token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \ + --amount 500 +``` + +### b. Verify the reduced balance + +```bash +wallet account get --account-id Public/7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R + +# Output: +Holding account owned by ata program +{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":2500} +``` + +## 6. Listing ATAs (`wallet ata list`) + +The `list` subcommand queries ATAs for a given owner across one or more token definitions. + +### a. Create a second token and ATA + +Create a second token definition so there are multiple ATAs to list: + +```bash +wallet account new public + +# Output: +Generated new account with account_id Public/BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi +``` + +```bash +wallet account new public + +# Output: +Generated new account with account_id Public/Ck8mVp4YhWn2rXjD6dFsQtA7bEoLf3gUZx1wDnR9eTs +``` + +```bash +wallet token new \ + --name OTHERTOKEN \ + --total-supply 5000 \ + --definition-account-id Public/BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi \ + --supply-account-id Public/Ck8mVp4YhWn2rXjD6dFsQtA7bEoLf3gUZx1wDnR9eTs +``` + +Create an ATA for the second token: + +```bash +wallet ata create \ + --owner Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \ + --token-definition BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi +``` + +### b. List ATAs for both token definitions + +```bash +wallet ata list \ + --owner 5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \ + --token-definition \ + 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \ + BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi + +# Output: +ATA 7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R (definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4): balance 2500 +ATA 4nPxKd8YmW7rVsH2jDfQcA9bEoLf6gUZx3wTnR1eMs5 (definition BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi): balance 0 +``` + +> [!Note] +> The `list` command derives each ATA address locally and fetches its on-chain state. If an ATA has not been created for a given definition, it prints "No ATA for definition ..." instead. + +## 7. Private owner operations + +All three ATA operations — `create`, `send`, and `burn` — support private owner accounts. Passing a `Private/` prefix on the owner argument switches the wallet into privacy-preserving mode: + +1. The wallet builds the transaction locally. +2. The ATA program is executed inside the RISC0 ZK VM to generate a proof. +3. The proof, the updated ATA state (in plaintext), and an encrypted update for the owner's private account are submitted to the sequencer. +4. The sequencer verifies the proof, writes the ATA state change to the public chain, and records the owner's new commitment in the nullifier set. + +The result is that the ATA account and its token balance are **fully public** — anyone can see them. What stays private is the link between the ATA and its owner: the proof demonstrates that someone with the correct private key authorized the operation, but reveals nothing about which account that was. + +> [!Note] +> The ATA address is derived from `SHA256(owner_id || definition_id)`. Because SHA256 is one-way, the ATA address does not reveal the owner's identity. However, if the owner's account ID becomes known for any other reason, all of their ATAs across every token definition can be enumerated by anyone. + +### a. Create a private account + +```bash +wallet account new private + +# Output: +Generated new account with account_id Private/HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi +``` + +### b. Create the ATA for the private owner + +Pass `Private/` on `--owner`. The token definition account has no privacy prefix — it is always a public account. + +```bash +wallet ata create \ + --owner Private/HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi \ + --token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 +``` + +> [!Note] +> Proof generation runs locally in the RISC0 ZK VM and can take up to a minute on first run. + +### c. Verify the ATA was created + +Derive the ATA address using the raw account ID (no privacy prefix): + +```bash +wallet ata address \ + --owner HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi \ + --token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 + +# Output: +2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1 +``` + +```bash +wallet account get --account-id Public/2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1 + +# Output: +Holding account owned by ata program +{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":0} +``` + +### d. Fund the ATA + +The ATA is a public account. Fund it with a direct token transfer from any public holding account: + +```bash +wallet token send \ + --from Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \ + --to Public/2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1 \ + --amount 500 +``` + +### e. Send tokens from the private owner's ATA + +```bash +wallet ata send \ + --from Private/HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi \ + --token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \ + --to 9Ht4Kv8pYmW2rXjN6dFcQsA7bEoLf3gUZx1wDnR5eTi \ + --amount 200 +``` + +Verify the ATA balance decreased: + +```bash +wallet account get --account-id Public/2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1 + +# Output: +Holding account owned by ata program +{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":300} +``` + +### f. Burn tokens from the private owner's ATA + +```bash +wallet ata burn \ + --holder Private/HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi \ + --token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \ + --amount 100 +``` + +Verify the balance and token supply: + +```bash +wallet account get --account-id Public/2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1 + +# Output: +Holding account owned by ata program +{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":200} +``` diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world.rs b/examples/program_deployment/methods/guest/src/bin/hello_world.rs index 3391eb5d..810e83f3 100644 --- a/examples/program_deployment/methods/guest/src/bin/hello_world.rs +++ b/examples/program_deployment/methods/guest/src/bin/hello_world.rs @@ -1,6 +1,4 @@ -use nssa_core::program::{ - AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, -}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; // Hello-world example program. // @@ -45,16 +43,12 @@ fn main() { // Wrap the post state account values inside a `AccountPostState` instance. // This is used to forward the account claiming request if any - let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { - // This produces a claim request - AccountPostState::new_claimed(post_account) - } else { - // This doesn't produce a claim request - AccountPostState::new(post_account) - }; + let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized); // The output is a proposed state difference. It will only succeed if the pre states coincide // with the previous values of the accounts, and the transition to the post states conforms // with the NSSA program rules. - write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); + // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be + // called to commit the output. + ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]).write(); } diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs index e327ca47..62908870 100644 --- a/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs @@ -1,6 +1,4 @@ -use nssa_core::program::{ - AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, -}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; // Hello-world with authorization example program. // @@ -52,16 +50,12 @@ fn main() { // Wrap the post state account values inside a `AccountPostState` instance. // This is used to forward the account claiming request if any - let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { - // This produces a claim request - AccountPostState::new_claimed(post_account) - } else { - // This doesn't produce a claim request - AccountPostState::new(post_account) - }; + let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized); // The output is a proposed state difference. It will only succeed if the pre states coincide // with the previous values of the accounts, and the transition to the post states conforms // with the NSSA program rules. - write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); + // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be + // called to commit the output. + ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]).write(); } diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs index 65f0f9cd..7e29b5de 100644 --- a/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs @@ -1,8 +1,6 @@ use nssa_core::{ - account::{Account, AccountWithMetadata, Data}, - program::{ - AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, - }, + account::{AccountWithMetadata, Data}, + program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}, }; // Hello-world with write + move_data example program. @@ -26,16 +24,6 @@ const MOVE_DATA_FUNCTION_ID: u8 = 1; type Instruction = (u8, Vec); -fn build_post_state(post_account: Account) -> AccountPostState { - if post_account.program_owner == DEFAULT_PROGRAM_ID { - // This produces a claim request - AccountPostState::new_claimed(post_account) - } else { - // This doesn't produce a claim request - AccountPostState::new(post_account) - } -} - fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState { // Construct the post state account values let post_account = { @@ -48,7 +36,7 @@ fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState { this }; - build_post_state(post_account) + AccountPostState::new_claimed_if_default(post_account, Claim::Authorized) } fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec { @@ -58,7 +46,7 @@ fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec< let from_post = { let mut this = from_pre.account; this.data = Data::default(); - build_post_state(this) + AccountPostState::new_claimed_if_default(this, Claim::Authorized) }; let to_post = { @@ -68,7 +56,7 @@ fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec< this.data = bytes .try_into() .expect("Data should fit within the allowed limits"); - build_post_state(this) + AccountPostState::new_claimed_if_default(this, Claim::Authorized) }; vec![from_post, to_post] @@ -95,5 +83,7 @@ fn main() { _ => panic!("invalid params"), }; - write_nssa_outputs(instruction_words, pre_states, post_states); + // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be + // called to commit the output. + ProgramOutput::new(instruction_words, pre_states, post_states).write(); } diff --git a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs index 01389085..d2c04083 100644 --- a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs +++ b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs @@ -1,6 +1,5 @@ use nssa_core::program::{ - AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, - write_nssa_outputs_with_chained_call, + AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs, }; // Tail Call example program. @@ -53,11 +52,10 @@ fn main() { pda_seeds: vec![], }; - // Write the outputs - write_nssa_outputs_with_chained_call( - instruction_data, - vec![pre_state], - vec![post_state], - vec![chained_call], - ); + // Write the outputs. + // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be + // called to commit the output. + ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]) + .with_chained_calls(vec![chained_call]) + .write(); } diff --git a/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs b/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs index 3ebcabd2..564efc2b 100644 --- a/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs +++ b/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs @@ -1,6 +1,6 @@ use nssa_core::program::{ - AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs, - write_nssa_outputs_with_chained_call, + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, }; // Tail Call with PDA example program. @@ -65,11 +65,10 @@ fn main() { pda_seeds: vec![PDA_SEED], }; - // Write the outputs - write_nssa_outputs_with_chained_call( - instruction_data, - vec![pre_state], - vec![post_state], - vec![chained_call], - ); + // Write the outputs. + // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be + // called to commit the output. + ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]) + .with_chained_calls(vec![chained_call]) + .write(); } diff --git a/explorer_service/Dockerfile b/explorer_service/Dockerfile index 238e77e6..6484619f 100644 --- a/explorer_service/Dockerfile +++ b/explorer_service/Dockerfile @@ -22,7 +22,13 @@ WORKDIR /explorer_service COPY . . # Build the app -RUN cargo leptos build --release -vv +RUN --mount=type=cache,target=/usr/local/cargo/registry/index \ + --mount=type=cache,target=/usr/local/cargo/registry/cache \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/explorer_service/target \ + cargo leptos build --release -vv \ + && cp /explorer_service/target/release/explorer_service /usr/local/bin/explorer_service \ + && cp -r /explorer_service/target/site /explorer_service/site_output FROM debian:trixie-slim AS runtime WORKDIR /explorer_service @@ -33,10 +39,10 @@ RUN apt-get update -y \ && rm -rf /var/lib/apt/lists/* # Copy the server binary to the /explorer_service directory -COPY --from=builder /explorer_service/target/release/explorer_service /explorer_service/ +COPY --from=builder /usr/local/bin/explorer_service /explorer_service/ # /target/site contains our JS/WASM/CSS, etc. -COPY --from=builder /explorer_service/target/site /explorer_service/site +COPY --from=builder /explorer_service/site_output /explorer_service/site # Copy Cargo.toml as it’s needed at runtime COPY --from=builder /explorer_service/Cargo.toml /explorer_service/ diff --git a/explorer_service/src/pages/transaction_page.rs b/explorer_service/src/pages/transaction_page.rs index c0fc0296..0a3fc8e2 100644 --- a/explorer_service/src/pages/transaction_page.rs +++ b/explorer_service/src/pages/transaction_page.rs @@ -177,12 +177,13 @@ pub fn TransactionPage() -> impl IntoView { encrypted_private_post_states, new_commitments, new_nullifiers, + block_validity_window, + timestamp_validity_window, } = message; let WitnessSet { signatures_and_public_keys: _, proof, } = witness_set; - let proof_len = proof.map_or(0, |p| p.0.len()); view! {
@@ -212,6 +213,14 @@ pub fn TransactionPage() -> impl IntoView { "Proof Size:" {format!("{proof_len} bytes")}
+
+ "Block Validity Window:" + {block_validity_window.to_string()} +
+
+ "Timestamp Validity Window:" + {timestamp_validity_window.to_string()} +

"Public Accounts"

diff --git a/indexer/core/Cargo.toml b/indexer/core/Cargo.toml index 13e81088..33fe2d9d 100644 --- a/indexer/core/Cargo.toml +++ b/indexer/core/Cargo.toml @@ -13,6 +13,7 @@ bedrock_client.workspace = true nssa.workspace = true nssa_core.workspace = true storage.workspace = true +testnet_initial_state.workspace = true anyhow.workspace = true log.workspace = true diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index fa7413e3..58344030 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -3,10 +3,11 @@ use std::{path::Path, sync::Arc}; use anyhow::Result; use bedrock_client::HeaderId; use common::{ - block::{BedrockStatus, Block, BlockId}, + block::{BedrockStatus, Block}, transaction::NSSATransaction, }; use nssa::{Account, AccountId, V03State}; +use nssa_core::BlockId; use storage::indexer::RocksDBIO; use tokio::sync::RwLock; @@ -148,7 +149,11 @@ impl IndexerStore { transaction .clone() .transaction_stateless_check()? - .execute_check_on_state(&mut state_guard)?; + .execute_check_on_state( + &mut state_guard, + block.header.block_id, + block.header.timestamp, + )?; } } diff --git a/indexer/core/src/config.rs b/indexer/core/src/config.rs index a85284cc..291e54f5 100644 --- a/indexer/core/src/config.rs +++ b/indexer/core/src/config.rs @@ -7,13 +7,11 @@ use std::{ use anyhow::{Context as _, Result}; pub use bedrock_client::BackoffConfig; -use common::{ - block::{AccountInitialData, CommitmentsInitialData}, - config::BasicAuth, -}; +use common::config::BasicAuth; use humantime_serde; pub use logos_blockchain_core::mantle::ops::channel::ChannelId; use serde::{Deserialize, Serialize}; +use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData}; use url::Url; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -29,16 +27,16 @@ pub struct ClientConfig { pub struct IndexerConfig { /// Home dir of sequencer storage. pub home: PathBuf, - /// List of initial accounts data. - pub initial_accounts: Vec, - /// List of initial commitments. - pub initial_commitments: Vec, /// Sequencers signing key. pub signing_key: [u8; 32], #[serde(with = "humantime_serde")] pub consensus_info_polling_interval: Duration, pub bedrock_client_config: ClientConfig, pub channel_id: ChannelId, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_public_accounts: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_private_accounts: Option>, } impl IndexerConfig { diff --git a/indexer/core/src/lib.rs b/indexer/core/src/lib.rs index 882affcc..e68e97c0 100644 --- a/indexer/core/src/lib.rs +++ b/indexer/core/src/lib.rs @@ -2,14 +2,17 @@ use std::collections::VecDeque; use anyhow::Result; use bedrock_client::{BedrockClient, HeaderId}; -use common::block::{Block, HashableBlockData}; -// ToDo: Remove after testnet -use common::{HashType, PINATA_BASE58}; +use common::{ + HashType, PINATA_BASE58, + block::{Block, HashableBlockData}, +}; use log::{debug, error, info}; use logos_blockchain_core::mantle::{ Op, SignedMantleTx, ops::channel::{ChannelId, inscribe::InscriptionOp}, }; +use nssa::V03State; +use testnet_initial_state::initial_state_testnet; use crate::{block_store::IndexerStore, config::IndexerConfig}; @@ -54,40 +57,51 @@ impl IndexerCore { let channel_genesis_msg_id = [0; 32]; let genesis_block = hashable_data.into_pending_block(&signing_key, channel_genesis_msg_id); - // This is a troubling moment, because changes in key protocol can - // affect this. And indexer can not reliably ask this data from sequencer - // because indexer must be independent from it. - // ToDo: move initial state generation into common and use the same method - // for indexer and sequencer. This way both services buit at same version - // could be in sync. - let initial_commitments: Vec = config - .initial_commitments - .iter() - .map(|init_comm_data| { - let npk = &init_comm_data.npk; + let initial_commitments: Option> = config + .initial_private_accounts + .as_ref() + .map(|initial_commitments| { + initial_commitments + .iter() + .map(|init_comm_data| { + let npk = &init_comm_data.npk; - let mut acc = init_comm_data.account.clone(); + let mut acc = init_comm_data.account.clone(); - acc.program_owner = nssa::program::Program::authenticated_transfer_program().id(); + acc.program_owner = + nssa::program::Program::authenticated_transfer_program().id(); - nssa_core::Commitment::new(npk, &acc) - }) - .collect(); + nssa_core::Commitment::new(npk, &acc) + }) + .collect() + }); - let init_accs: Vec<(nssa::AccountId, u128)> = config - .initial_accounts - .iter() - .map(|acc_data| (acc_data.account_id, acc_data.balance)) - .collect(); + let init_accs: Option> = config + .initial_public_accounts + .as_ref() + .map(|initial_accounts| { + initial_accounts + .iter() + .map(|acc_data| (acc_data.account_id, acc_data.balance)) + .collect() + }); - let mut state = nssa::V03State::new_with_genesis_accounts( - &init_accs, - &initial_commitments, - genesis_block.header.timestamp, - ); + // If initial commitments or accounts are present in config, need to construct state from + // them + let state = if initial_commitments.is_some() || init_accs.is_some() { + let mut state = V03State::new_with_genesis_accounts( + &init_accs.unwrap_or_default(), + &initial_commitments.unwrap_or_default(), + genesis_block.header.timestamp, + ); - // ToDo: Remove after testnet - state.add_pinata_program(PINATA_BASE58.parse().unwrap()); + // ToDo: Remove after testnet + state.add_pinata_program(PINATA_BASE58.parse().unwrap()); + + state + } else { + initial_state_testnet() + }; let home = config.home.join("rocksdb"); diff --git a/indexer/service/Dockerfile b/indexer/service/Dockerfile index bb93c2f2..cc7087bb 100644 --- a/indexer/service/Dockerfile +++ b/indexer/service/Dockerfile @@ -51,32 +51,34 @@ RUN cargo chef prepare --bin indexer_service --recipe-path recipe.json FROM chef AS builder COPY --from=planner /indexer_service/recipe.json recipe.json # Build dependencies only (this layer will be cached) -RUN cargo chef cook --bin indexer_service --release --recipe-path recipe.json +RUN --mount=type=cache,target=/usr/local/cargo/registry/index \ + --mount=type=cache,target=/usr/local/cargo/registry/cache \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/indexer_service/target \ + cargo chef cook --bin indexer_service --release --recipe-path recipe.json # Copy source code COPY . . -# Build the actual application -RUN cargo build --release --bin indexer_service - -# Strip debug symbols to reduce binary size -RUN strip /indexer_service/target/release/indexer_service +# Build the actual application and copy the binary out of the cache mount +RUN --mount=type=cache,target=/usr/local/cargo/registry/index \ + --mount=type=cache,target=/usr/local/cargo/registry/cache \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/indexer_service/target \ + cargo build --release --bin indexer_service \ + && strip /indexer_service/target/release/indexer_service \ + && cp /indexer_service/target/release/indexer_service /usr/local/bin/indexer_service # Runtime stage - minimal image FROM debian:trixie-slim -# Install runtime dependencies -RUN apt-get update \ - && apt-get install -y gosu jq \ - && rm -rf /var/lib/apt/lists/* - # Create non-root user for security RUN useradd -m -u 1000 -s /bin/bash indexer_service_user && \ - mkdir -p /indexer_service /etc/indexer_service && \ - chown -R indexer_service_user:indexer_service_user /indexer_service /etc/indexer_service + mkdir -p /indexer_service /etc/indexer_service /var/lib/indexer_service && \ + chown -R indexer_service_user:indexer_service_user /indexer_service /etc/indexer_service /var/lib/indexer_service # Copy binary from builder -COPY --from=builder --chown=indexer_service_user:indexer_service_user /indexer_service/target/release/indexer_service /usr/local/bin/indexer_service +COPY --from=builder --chown=indexer_service_user:indexer_service_user /usr/local/bin/indexer_service /usr/local/bin/indexer_service # Copy r0vm binary from builder COPY --from=builder --chown=indexer_service_user:indexer_service_user /usr/local/bin/r0vm /usr/local/bin/r0vm @@ -84,9 +86,7 @@ COPY --from=builder --chown=indexer_service_user:indexer_service_user /usr/local # Copy logos blockchain circuits from builder COPY --from=builder --chown=indexer_service_user:indexer_service_user /root/.logos-blockchain-circuits /home/indexer_service_user/.logos-blockchain-circuits -# Copy entrypoint script -COPY indexer/service/docker-entrypoint.sh /docker-entrypoint.sh -RUN chmod +x /docker-entrypoint.sh +VOLUME /var/lib/indexer_service # Expose default port EXPOSE 8779 @@ -105,9 +105,7 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ # Run the application ENV RUST_LOG=info -USER root - -ENTRYPOINT ["/docker-entrypoint.sh"] +USER indexer_service_user WORKDIR /indexer_service CMD ["indexer_service", "/etc/indexer_service/indexer_config.json"] diff --git a/indexer/service/docker-compose.yml b/indexer/service/docker-compose.yml index 73ac90ae..b690a180 100644 --- a/indexer/service/docker-compose.yml +++ b/indexer/service/docker-compose.yml @@ -10,5 +10,8 @@ services: volumes: # Mount configuration - ./configs/indexer_config.json:/etc/indexer_service/indexer_config.json - # Mount data folder - - ./data:/var/lib/indexer_service + # Mount data volume + - indexer_data:/var/lib/indexer_service + +volumes: + indexer_data: diff --git a/indexer/service/docker-entrypoint.sh b/indexer/service/docker-entrypoint.sh deleted file mode 100644 index 49a5f891..00000000 --- a/indexer/service/docker-entrypoint.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh - -# This is an entrypoint script for the indexer_service Docker container, -# it's not meant to be executed outside of the container. - -set -e - -CONFIG="/etc/indexer_service/indexer_config.json" - -# Check config file exists -if [ ! -f "$CONFIG" ]; then - echo "Config file not found: $CONFIG" >&2 - exit 1 -fi - -# Parse home dir -HOME_DIR=$(jq -r '.home' "$CONFIG") - -if [ -z "$HOME_DIR" ] || [ "$HOME_DIR" = "null" ]; then - echo "'home' key missing in config" >&2 - exit 1 -fi - -# Give permissions to the data directory and switch to non-root user -if [ "$(id -u)" = "0" ]; then - mkdir -p "$HOME_DIR" - chown -R indexer_service_user:indexer_service_user "$HOME_DIR" - exec gosu indexer_service_user "$@" -fi diff --git a/indexer/service/protocol/src/convert.rs b/indexer/service/protocol/src/convert.rs index 6114620f..eb79fa34 100644 --- a/indexer/service/protocol/src/convert.rs +++ b/indexer/service/protocol/src/convert.rs @@ -7,7 +7,7 @@ use crate::{ CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, MantleMsgId, Nullifier, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, - Signature, Transaction, WitnessSet, + Signature, Transaction, ValidityWindow, WitnessSet, }; // ============================================================================ @@ -287,6 +287,8 @@ impl From for PrivacyPre encrypted_private_post_states, new_commitments, new_nullifiers, + block_validity_window, + timestamp_validity_window, } = value; Self { public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), @@ -301,12 +303,14 @@ impl From for PrivacyPre .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), + block_validity_window: block_validity_window.into(), + timestamp_validity_window: timestamp_validity_window.into(), } } } impl TryFrom for nssa::privacy_preserving_transaction::message::Message { - type Error = nssa_core::account::data::DataTooBigError; + type Error = nssa::error::NssaError; fn try_from(value: PrivacyPreservingMessage) -> Result { let PrivacyPreservingMessage { @@ -316,6 +320,8 @@ impl TryFrom for nssa::privacy_preserving_transaction: encrypted_private_post_states, new_commitments, new_nullifiers, + block_validity_window, + timestamp_validity_window, } = value; Ok(Self { public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), @@ -326,7 +332,8 @@ impl TryFrom for nssa::privacy_preserving_transaction: public_post_states: public_post_states .into_iter() .map(TryInto::try_into) - .collect::, _>>()?, + .collect::, _>>() + .map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?, encrypted_private_post_states: encrypted_private_post_states .into_iter() .map(Into::into) @@ -336,6 +343,12 @@ impl TryFrom for nssa::privacy_preserving_transaction: .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), + block_validity_window: block_validity_window + .try_into() + .map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?, + timestamp_validity_window: timestamp_validity_window + .try_into() + .map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?, }) } } @@ -479,14 +492,7 @@ impl TryFrom for nssa::PrivacyPreservingTransactio witness_set, } = value; - Ok(Self::new( - message - .try_into() - .map_err(|err: nssa_core::account::data::DataTooBigError| { - nssa::error::NssaError::InvalidInput(err.to_string()) - })?, - witness_set.try_into()?, - )) + Ok(Self::new(message.try_into()?, witness_set.try_into()?)) } } @@ -687,3 +693,21 @@ impl From for common::HashType { Self(value.0) } } + +// ============================================================================ +// ValidityWindow conversions +// ============================================================================ + +impl From> for ValidityWindow { + fn from(value: nssa_core::program::ValidityWindow) -> Self { + Self((value.start(), value.end())) + } +} + +impl TryFrom for nssa_core::program::ValidityWindow { + type Error = nssa_core::program::InvalidWindow; + + fn try_from(value: ValidityWindow) -> Result { + value.0.try_into() + } +} diff --git a/indexer/service/protocol/src/lib.rs b/indexer/service/protocol/src/lib.rs index f2dd7a94..d554267e 100644 --- a/indexer/service/protocol/src/lib.rs +++ b/indexer/service/protocol/src/lib.rs @@ -235,6 +235,8 @@ pub struct PrivacyPreservingMessage { pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub block_validity_window: ValidityWindow, + pub timestamp_validity_window: ValidityWindow, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] @@ -300,6 +302,20 @@ pub struct Nullifier( pub [u8; 32], ); +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +pub struct ValidityWindow(pub (Option, Option)); + +impl Display for ValidityWindow { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + (Some(start), Some(end)) => write!(f, "[{start}, {end})"), + (Some(start), None) => write!(f, "[{start}, \u{221e})"), + (None, Some(end)) => write!(f, "(-\u{221e}, {end})"), + (None, None) => write!(f, "(-\u{221e}, \u{221e})"), + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct CommitmentSetDigest( #[serde(with = "base64::arr")] diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index eb6f11f7..09ae96f5 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -13,7 +13,7 @@ use indexer_service_protocol::{ CommitmentSetDigest, Data, EncryptedAccountData, HashType, MantleMsgId, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature, - Transaction, WitnessSet, + Transaction, ValidityWindow, WitnessSet, }; use jsonrpsee::{ core::{SubscriptionResult, async_trait}, @@ -124,6 +124,8 @@ impl MockIndexerService { indexer_service_protocol::Nullifier([tx_idx as u8; 32]), CommitmentSetDigest([0xff; 32]), )], + block_validity_window: ValidityWindow((None, None)), + timestamp_validity_window: ValidityWindow((None, None)), }, witness_set: WitnessSet { signatures_and_public_keys: vec![], diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index c88ec1fd..cb5277d2 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -18,9 +18,11 @@ key_protocol.workspace = true indexer_service.workspace = true serde_json.workspace = true token_core.workspace = true +ata_core.workspace = true indexer_service_rpc.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } wallet-ffi.workspace = true +testnet_initial_state.workspace = true url.workspace = true diff --git a/integration_tests/src/config.rs b/integration_tests/src/config.rs index e7f12fc1..1dd726eb 100644 --- a/integration_tests/src/config.rs +++ b/integration_tests/src/config.rs @@ -2,16 +2,17 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration}; use anyhow::{Context as _, Result}; use bytesize::ByteSize; -use common::block::{AccountInitialData, CommitmentsInitialData}; use indexer_service::{BackoffConfig, ChannelId, ClientConfig, IndexerConfig}; use key_protocol::key_management::KeyChain; use nssa::{Account, AccountId, PrivateKey, PublicKey}; use nssa_core::{account::Data, program::DEFAULT_PROGRAM_ID}; use sequencer_core::config::{BedrockConfig, SequencerConfig}; -use url::Url; -use wallet::config::{ - InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, WalletConfig, +use testnet_initial_state::{ + PrivateAccountPrivateInitialData, PrivateAccountPublicInitialData, + PublicAccountPrivateInitialData, PublicAccountPublicInitialData, }; +use url::Url; +use wallet::config::{InitialAccountData, WalletConfig}; /// Sequencer config options available for custom changes in integration tests. #[derive(Debug, Clone, Copy)] @@ -102,13 +103,13 @@ impl InitialData { } } - fn sequencer_initial_accounts(&self) -> Vec { + fn sequencer_initial_public_accounts(&self) -> Vec { self.public_accounts .iter() .map(|(priv_key, balance)| { let pub_key = PublicKey::new_from_private_key(priv_key); let account_id = AccountId::from(&pub_key); - AccountInitialData { + PublicAccountPublicInitialData { account_id, balance: *balance, } @@ -116,10 +117,10 @@ impl InitialData { .collect() } - fn sequencer_initial_commitments(&self) -> Vec { + fn sequencer_initial_private_accounts(&self) -> Vec { self.private_accounts .iter() - .map(|(key_chain, account)| CommitmentsInitialData { + .map(|(key_chain, account)| PrivateAccountPublicInitialData { npk: key_chain.nullifier_public_key.clone(), account: account.clone(), }) @@ -132,14 +133,14 @@ impl InitialData { .map(|(priv_key, _)| { let pub_key = PublicKey::new_from_private_key(priv_key); let account_id = AccountId::from(&pub_key); - InitialAccountData::Public(InitialAccountDataPublic { + InitialAccountData::Public(PublicAccountPrivateInitialData { account_id, pub_sign_key: priv_key.clone(), }) }) .chain(self.private_accounts.iter().map(|(key_chain, account)| { let account_id = AccountId::from(&key_chain.nullifier_public_key); - InitialAccountData::Private(Box::new(InitialAccountDataPrivate { + InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData { account_id, account: account.clone(), key_chain: key_chain.clone(), @@ -181,8 +182,8 @@ pub fn indexer_config( max_retries: 10, }, }, - initial_accounts: initial_data.sequencer_initial_accounts(), - initial_commitments: initial_data.sequencer_initial_commitments(), + initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()), + initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), signing_key: [37; 32], channel_id: bedrock_channel_id(), }) @@ -211,8 +212,8 @@ pub fn sequencer_config( mempool_max_size, block_create_timeout, retry_pending_blocks_timeout: Duration::from_mins(2), - initial_accounts: initial_data.sequencer_initial_accounts(), - initial_commitments: initial_data.sequencer_initial_commitments(), + initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()), + initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), signing_key: [37; 32], bedrock_config: BedrockConfig { backoff: BackoffConfig { @@ -240,7 +241,7 @@ pub fn wallet_config( seq_tx_poll_max_blocks: 15, seq_poll_max_retries: 10, seq_block_poll_max_amount: 100, - initial_accounts: initial_data.wallet_initial_accounts(), + initial_accounts: Some(initial_data.wallet_initial_accounts()), basic_auth: None, }) } diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs new file mode 100644 index 00000000..94ba98c9 --- /dev/null +++ b/integration_tests/tests/ata.rs @@ -0,0 +1,656 @@ +#![expect( + clippy::shadow_unrelated, + clippy::tests_outside_test_module, + reason = "We don't care about these in tests" +)] + +use std::time::Duration; + +use anyhow::{Context as _, Result}; +use ata_core::{compute_ata_seed, get_associated_token_account_id}; +use integration_tests::{ + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_private_account_id, + format_public_account_id, verify_commitment_is_in_state, +}; +use log::info; +use nssa::program::Program; +use sequencer_service_rpc::RpcClient as _; +use token_core::{TokenDefinition, TokenHolding}; +use tokio::test; +use wallet::cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::{ata::AtaSubcommand, token::TokenProgramAgnosticSubcommand}, +}; + +/// Create a public account and return its ID. +async fn new_public_account(ctx: &mut TestContext) -> Result { + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { account_id } = result else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + Ok(account_id) +} + +/// Create a private account and return its ID. +async fn new_private_account(ctx: &mut TestContext) -> Result { + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { account_id } = result else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + Ok(account_id) +} + +#[test] +async fn create_ata_initializes_holding_account() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let definition_account_id = new_public_account(&mut ctx).await?; + let supply_account_id = new_public_account(&mut ctx).await?; + let owner_account_id = new_public_account(&mut ctx).await?; + + // Create a fungible token + let total_supply = 100_u128; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: format_public_account_id(definition_account_id), + supply_account_id: format_public_account_id(supply_account_id), + name: "TEST".to_owned(), + total_supply, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Create the ATA for owner + definition + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Create { + owner: format_public_account_id(owner_account_id), + token_definition: definition_account_id.to_string(), + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Derive expected ATA address and check on-chain state + let ata_program_id = Program::ata().id(); + let ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(owner_account_id, definition_account_id), + ); + + let ata_acc = ctx + .sequencer_client() + .get_account(ata_id) + .await + .context("ATA account not found")?; + + assert_eq!(ata_acc.program_owner, Program::token().id()); + let holding = TokenHolding::try_from(&ata_acc.data)?; + assert_eq!( + holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: 0, + } + ); + + Ok(()) +} + +#[test] +async fn create_ata_is_idempotent() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let definition_account_id = new_public_account(&mut ctx).await?; + let supply_account_id = new_public_account(&mut ctx).await?; + let owner_account_id = new_public_account(&mut ctx).await?; + + // Create a fungible token + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: format_public_account_id(definition_account_id), + supply_account_id: format_public_account_id(supply_account_id), + name: "TEST".to_owned(), + total_supply: 100, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Create the ATA once + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Create { + owner: format_public_account_id(owner_account_id), + token_definition: definition_account_id.to_string(), + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Create the ATA a second time — must succeed (idempotent) + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Create { + owner: format_public_account_id(owner_account_id), + token_definition: definition_account_id.to_string(), + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // State must be unchanged + let ata_program_id = Program::ata().id(); + let ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(owner_account_id, definition_account_id), + ); + + let ata_acc = ctx + .sequencer_client() + .get_account(ata_id) + .await + .context("ATA account not found")?; + + assert_eq!(ata_acc.program_owner, Program::token().id()); + let holding = TokenHolding::try_from(&ata_acc.data)?; + assert_eq!( + holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: 0, + } + ); + + Ok(()) +} + +#[test] +async fn transfer_and_burn_via_ata() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let definition_account_id = new_public_account(&mut ctx).await?; + let supply_account_id = new_public_account(&mut ctx).await?; + let sender_account_id = new_public_account(&mut ctx).await?; + let recipient_account_id = new_public_account(&mut ctx).await?; + + let total_supply = 1000_u128; + + // Create a fungible token, supply goes to supply_account_id + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: format_public_account_id(definition_account_id), + supply_account_id: format_public_account_id(supply_account_id), + name: "TEST".to_owned(), + total_supply, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Derive ATA addresses + let ata_program_id = Program::ata().id(); + let sender_ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(sender_account_id, definition_account_id), + ); + let recipient_ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(recipient_account_id, definition_account_id), + ); + + // Create ATAs for sender and recipient + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Create { + owner: format_public_account_id(sender_account_id), + token_definition: definition_account_id.to_string(), + }), + ) + .await?; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Create { + owner: format_public_account_id(recipient_account_id), + token_definition: definition_account_id.to_string(), + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Fund sender's ATA from the supply account (direct token transfer) + let fund_amount = 200_u128; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: format_public_account_id(supply_account_id), + to: Some(format_public_account_id(sender_ata_id)), + to_npk: None, + to_vpk: None, + amount: fund_amount, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Transfer from sender's ATA to recipient's ATA via the ATA program + let transfer_amount = 50_u128; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Send { + from: format_public_account_id(sender_account_id), + token_definition: definition_account_id.to_string(), + to: recipient_ata_id.to_string(), + amount: transfer_amount, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Verify sender ATA balance decreased + let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?; + let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?; + assert_eq!( + sender_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: fund_amount - transfer_amount, + } + ); + + // Verify recipient ATA balance increased + let recipient_ata_acc = ctx.sequencer_client().get_account(recipient_ata_id).await?; + let recipient_holding = TokenHolding::try_from(&recipient_ata_acc.data)?; + assert_eq!( + recipient_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount, + } + ); + + // Burn from sender's ATA + let burn_amount = 30_u128; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Burn { + holder: format_public_account_id(sender_account_id), + token_definition: definition_account_id.to_string(), + amount: burn_amount, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Verify sender ATA balance after burn + let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?; + let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?; + assert_eq!( + sender_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: fund_amount - transfer_amount - burn_amount, + } + ); + + // Verify the token definition total_supply decreased by burn_amount + let definition_acc = ctx + .sequencer_client() + .get_account(definition_account_id) + .await?; + let token_definition = TokenDefinition::try_from(&definition_acc.data)?; + assert_eq!( + token_definition, + TokenDefinition::Fungible { + name: "TEST".to_owned(), + total_supply: total_supply - burn_amount, + metadata_id: None, + } + ); + + Ok(()) +} + +#[test] +async fn create_ata_with_private_owner() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let definition_account_id = new_public_account(&mut ctx).await?; + let supply_account_id = new_public_account(&mut ctx).await?; + let owner_account_id = new_private_account(&mut ctx).await?; + + // Create a fungible token + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: format_public_account_id(definition_account_id), + supply_account_id: format_public_account_id(supply_account_id), + name: "TEST".to_owned(), + total_supply: 100, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Create the ATA for the private owner + definition + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Create { + owner: format_private_account_id(owner_account_id), + token_definition: definition_account_id.to_string(), + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Derive expected ATA address and check on-chain state + let ata_program_id = Program::ata().id(); + let ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(owner_account_id, definition_account_id), + ); + + let ata_acc = ctx + .sequencer_client() + .get_account(ata_id) + .await + .context("ATA account not found")?; + + assert_eq!(ata_acc.program_owner, Program::token().id()); + let holding = TokenHolding::try_from(&ata_acc.data)?; + assert_eq!( + holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: 0, + } + ); + + // Verify the private owner's commitment is in state + let commitment = ctx + .wallet() + .get_private_account_commitment(owner_account_id) + .context("Private owner commitment not found")?; + assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); + + Ok(()) +} + +#[test] +async fn transfer_via_ata_private_owner() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let definition_account_id = new_public_account(&mut ctx).await?; + let supply_account_id = new_public_account(&mut ctx).await?; + let sender_account_id = new_private_account(&mut ctx).await?; + let recipient_account_id = new_public_account(&mut ctx).await?; + + let total_supply = 1000_u128; + + // Create a fungible token + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: format_public_account_id(definition_account_id), + supply_account_id: format_public_account_id(supply_account_id), + name: "TEST".to_owned(), + total_supply, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Derive ATA addresses + let ata_program_id = Program::ata().id(); + let sender_ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(sender_account_id, definition_account_id), + ); + let recipient_ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(recipient_account_id, definition_account_id), + ); + + // Create ATAs for sender (private owner) and recipient (public owner) + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Create { + owner: format_private_account_id(sender_account_id), + token_definition: definition_account_id.to_string(), + }), + ) + .await?; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Create { + owner: format_public_account_id(recipient_account_id), + token_definition: definition_account_id.to_string(), + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Fund sender's ATA from the supply account (direct token transfer) + let fund_amount = 200_u128; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: format_public_account_id(supply_account_id), + to: Some(format_public_account_id(sender_ata_id)), + to_npk: None, + to_vpk: None, + amount: fund_amount, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Transfer from sender's ATA (private owner) to recipient's ATA + let transfer_amount = 50_u128; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Send { + from: format_private_account_id(sender_account_id), + token_definition: definition_account_id.to_string(), + to: recipient_ata_id.to_string(), + amount: transfer_amount, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Verify sender ATA balance decreased + let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?; + let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?; + assert_eq!( + sender_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: fund_amount - transfer_amount, + } + ); + + // Verify recipient ATA balance increased + let recipient_ata_acc = ctx.sequencer_client().get_account(recipient_ata_id).await?; + let recipient_holding = TokenHolding::try_from(&recipient_ata_acc.data)?; + assert_eq!( + recipient_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount, + } + ); + + // Verify the private sender's commitment is in state + let commitment = ctx + .wallet() + .get_private_account_commitment(sender_account_id) + .context("Private sender commitment not found")?; + assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); + + Ok(()) +} + +#[test] +async fn burn_via_ata_private_owner() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let definition_account_id = new_public_account(&mut ctx).await?; + let supply_account_id = new_public_account(&mut ctx).await?; + let holder_account_id = new_private_account(&mut ctx).await?; + + let total_supply = 500_u128; + + // Create a fungible token + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: format_public_account_id(definition_account_id), + supply_account_id: format_public_account_id(supply_account_id), + name: "TEST".to_owned(), + total_supply, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Derive holder's ATA address + let ata_program_id = Program::ata().id(); + let holder_ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(holder_account_id, definition_account_id), + ); + + // Create ATA for the private holder + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Create { + owner: format_private_account_id(holder_account_id), + token_definition: definition_account_id.to_string(), + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Fund holder's ATA from the supply account + let fund_amount = 300_u128; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: format_public_account_id(supply_account_id), + to: Some(format_public_account_id(holder_ata_id)), + to_npk: None, + to_vpk: None, + amount: fund_amount, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Burn from holder's ATA (private owner) + let burn_amount = 100_u128; + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Ata(AtaSubcommand::Burn { + holder: format_private_account_id(holder_account_id), + token_definition: definition_account_id.to_string(), + amount: burn_amount, + }), + ) + .await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Verify holder ATA balance after burn + let holder_ata_acc = ctx.sequencer_client().get_account(holder_ata_id).await?; + let holder_holding = TokenHolding::try_from(&holder_ata_acc.data)?; + assert_eq!( + holder_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: fund_amount - burn_amount, + } + ); + + // Verify the token definition total_supply decreased by burn_amount + let definition_acc = ctx + .sequencer_client() + .get_account(definition_account_id) + .await?; + let token_definition = TokenDefinition::try_from(&definition_acc.data)?; + assert_eq!( + token_definition, + TokenDefinition::Fungible { + name: "TEST".to_owned(), + total_supply: total_supply - burn_amount, + metadata_id: None, + } + ); + + // Verify the private holder's commitment is in state + let commitment = ctx + .wallet() + .get_private_account_commitment(holder_account_id) + .context("Private holder commitment not found")?; + assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); + + Ok(()) +} diff --git a/integration_tests/tests/program_deployment.rs b/integration_tests/tests/program_deployment.rs index bb46ba87..64f5a655 100644 --- a/integration_tests/tests/program_deployment.rs +++ b/integration_tests/tests/program_deployment.rs @@ -11,10 +11,13 @@ use integration_tests::{ NSSA_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, }; use log::info; -use nssa::{AccountId, program::Program}; +use nssa::program::Program; use sequencer_service_rpc::RpcClient as _; use tokio::test; -use wallet::cli::Command; +use wallet::cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, +}; #[test] async fn deploy_and_execute_program() -> Result<()> { @@ -40,14 +43,31 @@ async fn deploy_and_execute_program() -> Result<()> { // logic) let bytecode = std::fs::read(binary_filepath)?; let data_changer = Program::new(bytecode)?; - let account_id: AccountId = "11".repeat(16).parse()?; + + let SubcommandReturnValue::RegisterAccount { account_id } = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await? + else { + panic!("Expected RegisterAccount return value"); + }; + + let nonces = ctx.wallet().get_accounts_nonces(vec![account_id]).await?; + let private_key = ctx + .wallet() + .get_account_public_signing_key(account_id) + .unwrap(); let message = nssa::public_transaction::Message::try_new( data_changer.id(), vec![account_id], - vec![], + nonces, vec![0], )?; - let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[private_key]); let transaction = nssa::PublicTransaction::new(message, witness_set); let _response = ctx .sequencer_client() @@ -64,7 +84,7 @@ async fn deploy_and_execute_program() -> Result<()> { assert_eq!(post_state_account.program_owner, data_changer.id()); assert_eq!(post_state_account.balance, 0); assert_eq!(post_state_account.data.as_ref(), &[0]); - assert_eq!(post_state_account.nonce.0, 0); + assert_eq!(post_state_account.nonce.0, 1); info!("Successfully deployed and executed program"); diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index dad4c79e..6e6b190c 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -924,7 +924,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> { let home = tempfile::tempdir()?; let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into(); - let to = FfiBytes32::from_bytes([37; 32]); + let to: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into(); let amount: [u8; 16] = 100_u128.to_le_bytes(); let mut transfer_result = FfiTransferResult::default(); @@ -967,7 +967,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> { }; assert_eq!(from_balance, 9900); - assert_eq!(to_balance, 100); + assert_eq!(to_balance, 10100); unsafe { wallet_ffi_free_transfer_result(&raw mut transfer_result); diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index 5ce7e97c..022f3ccd 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -8,8 +8,6 @@ license = { workspace = true } workspace = true [dependencies] -secp256k1 = "0.31.1" - nssa.workspace = true nssa_core.workspace = true common.workspace = true diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs index 0b20a310..42130b1f 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -137,11 +137,12 @@ impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) { #[cfg(test)] mod tests { - use nssa_core::NullifierSecretKey; + use nssa_core::{NullifierPublicKey, NullifierSecretKey}; use super::*; use crate::key_management::{self, secret_holders::ViewingSecretKey}; + #[expect(clippy::redundant_type_annotations, reason = "TODO: clippy requires")] #[test] fn master_key_generation() { let seed: [u8; 64] = [ @@ -153,7 +154,7 @@ mod tests { let keys = ChildKeysPrivate::root(seed); - let expected_ssk = key_management::secret_holders::SecretSpendingKey([ + let expected_ssk: SecretSpendingKey = key_management::secret_holders::SecretSpendingKey([ 246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139, 222, 42, 195, 105, 33, 115, 97, 186, 0, 97, 14, 218, 191, ]); @@ -168,7 +169,7 @@ mod tests { 34, 234, 19, 222, 2, 22, 12, 163, 252, 88, 11, 0, 163, ]; - let expected_npk = nssa_core::NullifierPublicKey([ + let expected_npk: NullifierPublicKey = nssa_core::NullifierPublicKey([ 7, 123, 125, 191, 233, 183, 201, 4, 20, 214, 155, 210, 45, 234, 27, 240, 194, 111, 97, 247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2, ]); diff --git a/key_protocol/src/key_management/key_tree/keys_public.rs b/key_protocol/src/key_management/key_tree/keys_public.rs index 73ed7bee..d4c32b4a 100644 --- a/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -1,4 +1,4 @@ -use secp256k1::Scalar; +use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _}; use serde::{Deserialize, Serialize}; use crate::key_management::key_tree::traits::KeyNode; @@ -13,7 +13,6 @@ pub struct ChildKeysPublic { } impl ChildKeysPublic { - #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] fn compute_hash_value(&self, cci: u32) -> [u8; 64] { let mut hash_input = vec![]; @@ -21,16 +20,17 @@ impl ChildKeysPublic { // Non-harden. // BIP-032 compatibility requires 1-byte header from the public_key; // Not stored in `self.cpk.value()`. - let sk = secp256k1::SecretKey::from_byte_array(*self.csk.value()) + let sk = k256::SecretKey::from_bytes(self.csk.value().into()) .expect("32 bytes, within curve order"); - let pk = secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &sk); - hash_input.extend_from_slice(&secp256k1::PublicKey::serialize(&pk)); + let pk = sk.public_key(); + hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes()); } else { // Harden. hash_input.extend_from_slice(&[0_u8]); hash_input.extend_from_slice(self.csk.value()); } + #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] hash_input.extend_from_slice(&cci.to_be_bytes()); hmac_sha512::HMAC::mac(hash_input, self.ccc) @@ -41,7 +41,12 @@ impl KeyNode for ChildKeysPublic { fn root(seed: [u8; 64]) -> Self { let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub"); - let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); + let csk = nssa::PrivateKey::try_new( + *hash_value + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"), + ) + .expect("Expect a valid Private Key"); let ccc = *hash_value.last_chunk::<32>().unwrap(); let cpk = nssa::PublicKey::new_from_private_key(&csk); @@ -56,26 +61,20 @@ impl KeyNode for ChildKeysPublic { fn nth_child(&self, cci: u32) -> Self { let hash_value = self.compute_hash_value(cci); - let csk = secp256k1::SecretKey::from_byte_array( - *hash_value - .first_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get first 32"), - ) - .unwrap(); - let csk = nssa::PrivateKey::try_new({ - let scalar = Scalar::from_be_bytes(*self.csk.value()).unwrap(); + let hash_value = hash_value + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"); - csk.add_tweak(&scalar) - .expect("Expect a valid Scalar") - .secret_bytes() + let value_1 = + k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar"); + let value_2 = k256::Scalar::from_repr((*self.csk.value()).into()) + .expect("Expect a valid k256 scalar"); + + let sum = value_1.add(&value_2); + sum.to_bytes().into() }) - .unwrap(); - - assert!( - secp256k1::constants::CURVE_ORDER >= *csk.value(), - "Secret key cannot exceed curve order" - ); + .expect("Expect a valid private key"); let ccc = *hash_value .last_chunk::<32>() diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index 45e640da..02890631 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -20,17 +20,16 @@ pub struct SeedHolder { /// Secret spending key object. Can produce `PrivateKeyHolder` objects. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct SecretSpendingKey(pub(crate) [u8; 32]); +pub struct SecretSpendingKey(pub [u8; 32]); pub type ViewingSecretKey = Scalar; #[derive(Serialize, Deserialize, Debug, Clone)] /// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret /// for recepient. -#[expect(clippy::partial_pub_fields, reason = "TODO: fix later")] pub struct PrivateKeyHolder { pub nullifier_secret_key: NullifierSecretKey, - pub(crate) viewing_secret_key: ViewingSecretKey, + pub viewing_secret_key: ViewingSecretKey, } impl SeedHolder { diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index ceebaa2e..07f5fe53 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -19,7 +19,7 @@ sha2.workspace = true rand.workspace = true borsh.workspace = true hex.workspace = true -secp256k1 = "0.31.1" +k256.workspace = true risc0-binfmt = "3.0.2" log.workspace = true diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 56d63022..998f6d71 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -5,7 +5,7 @@ use crate::{ NullifierSecretKey, SharedSecretKey, account::{Account, AccountWithMetadata}, encryption::Ciphertext, - program::{ProgramId, ProgramOutput}, + program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow}, }; #[derive(Serialize, Deserialize)] @@ -36,6 +36,8 @@ pub struct PrivacyPreservingCircuitOutput { pub ciphertexts: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub block_validity_window: BlockValidityWindow, + pub timestamp_validity_window: TimestampValidityWindow, } #[cfg(feature = "host")] @@ -101,6 +103,8 @@ mod tests { ), [0xab; 32], )], + block_validity_window: (1..).into(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), }; let bytes = output.to_bytes(); let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap(); diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index 46697472..f5b81348 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -23,4 +23,6 @@ pub mod program; #[cfg(feature = "host")] pub mod error; -pub type Timestamp = u64; \ No newline at end of file +pub type BlockId = u64; +/// Unix timestamp in milliseconds. +pub type Timestamp = u64; diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 31b76b0f..673e09b3 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,9 +1,14 @@ use std::collections::HashSet; +#[cfg(any(feature = "host", test))] +use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; -use crate::account::{Account, AccountId, AccountWithMetadata}; +use crate::{ + BlockId, Timestamp, + account::{Account, AccountId, AccountWithMetadata}, +}; pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8]; pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; @@ -20,7 +25,7 @@ pub struct ProgramInput { /// Each program can derive up to `2^256` unique account IDs by choosing different /// seeds. PDAs allow programs to control namespaced account identifiers without /// collisions between programs. -#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] pub struct PdaSeed([u8; 32]); impl PdaSeed { @@ -89,11 +94,26 @@ impl ChainedCall { /// A post state may optionally request that the executing program /// becomes the owner of the account (a “claim”). This is used to signal /// that the program intends to take ownership of the account. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(PartialEq, Eq))] pub struct AccountPostState { account: Account, - claim: bool, + claim: Option, +} + +/// A claim request for an account, indicating that the executing program intends to take ownership +/// of the account. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Claim { + /// The program requests ownership of the account which was authorized by the signer. + /// + /// Note that it's possible to successfully execute program outputting [`AccountPostState`] with + /// `is_authorized == false` and `claim == Some(Claim::Authorized)`. + /// This will give no error if program had authorization in pre state and may be useful + /// if program decides to give up authorization for a chained call. + Authorized, + /// The program requests ownership of the account through a PDA. + Pda(PdaSeed), } impl AccountPostState { @@ -103,7 +123,7 @@ impl AccountPostState { pub const fn new(account: Account) -> Self { Self { account, - claim: false, + claim: None, } } @@ -111,25 +131,27 @@ impl AccountPostState { /// This indicates that the executing program intends to claim the /// account as its own and is allowed to mutate it. #[must_use] - pub const fn new_claimed(account: Account) -> Self { + pub const fn new_claimed(account: Account, claim: Claim) -> Self { Self { account, - claim: true, + claim: Some(claim), } } /// Creates a post state that requests ownership of the account /// if the account's program owner is the default program ID. #[must_use] - pub fn new_claimed_if_default(account: Account) -> Self { - let claim = account.program_owner == DEFAULT_PROGRAM_ID; - Self { account, claim } + pub fn new_claimed_if_default(account: Account, claim: Claim) -> Self { + let is_default_owner = account.program_owner == DEFAULT_PROGRAM_ID; + Self { + account, + claim: is_default_owner.then_some(claim), + } } - /// Returns `true` if this post state requests that the account - /// be claimed (owned) by the executing program. + /// Returns whether this post state requires a claim. #[must_use] - pub const fn requires_claim(&self) -> bool { + pub const fn required_claim(&self) -> Option { self.claim } @@ -140,6 +162,7 @@ impl AccountPostState { } /// Returns the underlying account. + #[must_use] pub const fn account_mut(&mut self) -> &mut Account { &mut self.account } @@ -151,15 +174,200 @@ impl AccountPostState { } } +pub type BlockValidityWindow = ValidityWindow; +pub type TimestampValidityWindow = ValidityWindow; + +#[derive(Clone, Copy, Serialize, Deserialize)] +#[cfg_attr( + any(feature = "host", test), + derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize) +)] +pub struct ValidityWindow { + from: Option, + to: Option, +} + +impl ValidityWindow { + /// Creates a window with no bounds. + #[must_use] + pub const fn new_unbounded() -> Self { + Self { + from: None, + to: None, + } + } +} + +impl ValidityWindow { + /// Valid for values in the range [from, to), where `from` is included and `to` is excluded. + #[must_use] + pub fn is_valid_for(&self, value: T) -> bool { + self.from.is_none_or(|start| value >= start) && self.to.is_none_or(|end| value < end) + } + + /// Returns `Err(InvalidWindow)` if both bounds are set and `from >= to`. + fn check_window(&self) -> Result<(), InvalidWindow> { + if let (Some(from), Some(to)) = (self.from, self.to) + && from >= to + { + return Err(InvalidWindow); + } + Ok(()) + } + + /// Inclusive lower bound. `None` means no lower bound. + #[must_use] + pub const fn start(&self) -> Option { + self.from + } + + /// Exclusive upper bound. `None` means no upper bound. + #[must_use] + pub const fn end(&self) -> Option { + self.to + } +} + +impl TryFrom<(Option, Option)> for ValidityWindow { + type Error = InvalidWindow; + + fn try_from(value: (Option, Option)) -> Result { + let this = Self { + from: value.0, + to: value.1, + }; + this.check_window()?; + Ok(this) + } +} + +impl TryFrom> for ValidityWindow { + type Error = InvalidWindow; + + fn try_from(value: std::ops::Range) -> Result { + (Some(value.start), Some(value.end)).try_into() + } +} + +impl From> for ValidityWindow { + fn from(value: std::ops::RangeFrom) -> Self { + Self { + from: Some(value.start), + to: None, + } + } +} + +impl From> for ValidityWindow { + fn from(value: std::ops::RangeTo) -> Self { + Self { + from: None, + to: Some(value.end), + } + } +} + +impl From for ValidityWindow { + fn from(_: std::ops::RangeFull) -> Self { + Self::new_unbounded() + } +} + +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[error("Invalid window")] +pub struct InvalidWindow; + #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +#[must_use = "ProgramOutput does nothing unless written"] pub struct ProgramOutput { /// The instruction data the program received to produce this output. pub instruction_data: InstructionData, /// The account pre states the program received to produce this output. pub pre_states: Vec, + /// The account post states the program execution produced. pub post_states: Vec, + /// The list of chained calls to other programs. pub chained_calls: Vec, + /// The block ID window where the program output is valid. + pub block_validity_window: BlockValidityWindow, + /// The timestamp window where the program output is valid. + pub timestamp_validity_window: TimestampValidityWindow, +} + +impl ProgramOutput { + pub const fn new( + instruction_data: InstructionData, + pre_states: Vec, + post_states: Vec, + ) -> Self { + Self { + instruction_data, + pre_states, + post_states, + chained_calls: Vec::new(), + block_validity_window: ValidityWindow::new_unbounded(), + timestamp_validity_window: ValidityWindow::new_unbounded(), + } + } + + pub fn write(self) { + env::commit(&self); + } + + pub fn with_chained_calls(mut self, chained_calls: Vec) -> Self { + self.chained_calls = chained_calls; + self + } + + /// Sets the block ID validity window from an infallible range conversion (`1..`, `..5`, `..`). + pub fn with_block_validity_window>(mut self, window: W) -> Self { + self.block_validity_window = window.into(); + self + } + + /// Sets the block ID validity window from a fallible range conversion (`1..5`). + /// Returns `Err` if the range is empty. + pub fn try_with_block_validity_window< + W: TryInto, + >( + mut self, + window: W, + ) -> Result { + self.block_validity_window = window.try_into()?; + Ok(self) + } + + /// Sets the timestamp validity window from an infallible range conversion. + pub fn with_timestamp_validity_window>( + mut self, + window: W, + ) -> Self { + self.timestamp_validity_window = window.into(); + self + } + + /// Sets the timestamp validity window from a fallible range conversion. + /// Returns `Err` if the range is empty. + pub fn try_with_timestamp_validity_window< + W: TryInto, + >( + mut self, + window: W, + ) -> Result { + self.timestamp_validity_window = window.try_into()?; + Ok(self) + } + + pub fn valid_from_timestamp(mut self, ts: Option) -> Result { + self.timestamp_validity_window = (ts, self.timestamp_validity_window.end()).try_into()?; + Ok(self) + } + + pub fn valid_until_timestamp(mut self, ts: Option) -> Result { + self.timestamp_validity_window = (self.timestamp_validity_window.start(), ts).try_into()?; + Ok(self) + } } /// Representation of a number as `lo + hi * 2^128`. @@ -219,35 +427,6 @@ pub fn read_nssa_inputs() -> (ProgramInput, InstructionD ) } -pub fn write_nssa_outputs( - instruction_data: InstructionData, - pre_states: Vec, - post_states: Vec, -) { - let output = ProgramOutput { - instruction_data, - pre_states, - post_states, - chained_calls: Vec::new(), - }; - env::commit(&output); -} - -pub fn write_nssa_outputs_with_chained_call( - instruction_data: InstructionData, - pre_states: Vec, - post_states: Vec, - chained_calls: Vec, -) { - let output = ProgramOutput { - instruction_data, - pre_states, - post_states, - chained_calls, - }; - env::commit(&output); -} - /// Validates well-behaved program execution. /// /// # Parameters @@ -342,6 +521,135 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo mod tests { use super::*; + #[test] + fn validity_window_unbounded_accepts_any_value() { + let w: ValidityWindow = ValidityWindow::new_unbounded(); + assert!(w.is_valid_for(0)); + assert!(w.is_valid_for(u64::MAX)); + } + + #[test] + fn validity_window_bounded_range_includes_from_excludes_to() { + let w: ValidityWindow = (Some(5), Some(10)).try_into().unwrap(); + assert!(!w.is_valid_for(4)); + assert!(w.is_valid_for(5)); + assert!(w.is_valid_for(9)); + assert!(!w.is_valid_for(10)); + } + + #[test] + fn validity_window_only_from_bound() { + let w: ValidityWindow = (Some(5), None).try_into().unwrap(); + assert!(!w.is_valid_for(4)); + assert!(w.is_valid_for(5)); + assert!(w.is_valid_for(u64::MAX)); + } + + #[test] + fn validity_window_only_to_bound() { + let w: ValidityWindow = (None, Some(5)).try_into().unwrap(); + assert!(w.is_valid_for(0)); + assert!(w.is_valid_for(4)); + assert!(!w.is_valid_for(5)); + } + + #[test] + fn validity_window_adjacent_bounds_are_invalid() { + // [5, 5) is an empty range — from == to + assert!(ValidityWindow::::try_from((Some(5), Some(5))).is_err()); + } + + #[test] + fn validity_window_inverted_bounds_are_invalid() { + assert!(ValidityWindow::::try_from((Some(10), Some(5))).is_err()); + } + + #[test] + fn validity_window_getters_match_construction() { + let w: ValidityWindow = (Some(3), Some(7)).try_into().unwrap(); + assert_eq!(w.start(), Some(3)); + assert_eq!(w.end(), Some(7)); + } + + #[test] + fn validity_window_getters_for_unbounded() { + let w: ValidityWindow = ValidityWindow::new_unbounded(); + assert_eq!(w.start(), None); + assert_eq!(w.end(), None); + } + + #[test] + fn validity_window_from_range() { + let w: ValidityWindow = ValidityWindow::try_from(5_u64..10).unwrap(); + assert_eq!(w.start(), Some(5)); + assert_eq!(w.end(), Some(10)); + } + + #[test] + fn validity_window_from_range_empty_is_invalid() { + assert!(ValidityWindow::::try_from(5_u64..5).is_err()); + } + + #[test] + fn validity_window_from_range_inverted_is_invalid() { + let from = 10_u64; + let to = 5_u64; + assert!(ValidityWindow::::try_from(from..to).is_err()); + } + + #[test] + fn validity_window_from_range_from() { + let w: ValidityWindow = (5_u64..).into(); + assert_eq!(w.start(), Some(5)); + assert_eq!(w.end(), None); + } + + #[test] + fn validity_window_from_range_to() { + let w: ValidityWindow = (..10_u64).into(); + assert_eq!(w.start(), None); + assert_eq!(w.end(), Some(10)); + } + + #[test] + fn validity_window_from_range_full() { + let w: ValidityWindow = (..).into(); + assert_eq!(w.start(), None); + assert_eq!(w.end(), None); + } + + #[test] + fn program_output_try_with_block_validity_window_range() { + let output = ProgramOutput::new(vec![], vec![], vec![]) + .try_with_block_validity_window(10_u64..100) + .unwrap(); + assert_eq!(output.block_validity_window.start(), Some(10)); + assert_eq!(output.block_validity_window.end(), Some(100)); + } + + #[test] + fn program_output_with_block_validity_window_range_from() { + let output = + ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(10_u64..); + assert_eq!(output.block_validity_window.start(), Some(10)); + assert_eq!(output.block_validity_window.end(), None); + } + + #[test] + fn program_output_with_block_validity_window_range_to() { + let output = + ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(..100_u64); + assert_eq!(output.block_validity_window.start(), None); + assert_eq!(output.block_validity_window.end(), Some(100)); + } + + #[test] + fn program_output_try_with_block_validity_window_empty_range_fails() { + let result = + ProgramOutput::new(vec![], vec![], vec![]).try_with_block_validity_window(5_u64..5); + assert!(result.is_err()); + } + #[test] fn post_state_new_with_claim_constructor() { let account = Account { @@ -351,10 +659,10 @@ mod tests { nonce: 10_u128.into(), }; - let account_post_state = AccountPostState::new_claimed(account.clone()); + let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized); assert_eq!(account, account_post_state.account); - assert!(account_post_state.requires_claim()); + assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized)); } #[test] @@ -369,7 +677,7 @@ mod tests { let account_post_state = AccountPostState::new(account.clone()); assert_eq!(account, account_post_state.account); - assert!(!account_post_state.requires_claim()); + assert!(account_post_state.required_claim().is_none()); } #[test] diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 3576b366..61966515 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -29,7 +29,10 @@ pub enum NssaError { Io(#[from] io::Error), #[error("Invalid Public Key")] - InvalidPublicKey(#[source] secp256k1::Error), + InvalidPublicKey(#[source] k256::schnorr::Error), + + #[error("Invalid hex for public key")] + InvalidHexPublicKey(hex::FromHexError), #[error("Risc0 error: {0}")] ProgramWriteInputFailed(String), @@ -69,6 +72,9 @@ pub enum NssaError { #[error("Max account nonce reached")] MaxAccountNonceReached, + + #[error("Execution outside of the validity window")] + OutOfValidityWindow, } #[cfg(test)] diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 2ab141a3..0ae7eaac 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -174,12 +174,13 @@ mod tests { #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] use nssa_core::{ - Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, + Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, }; use super::*; use crate::{ + error::NssaError, privacy_preserving_transaction::circuit::execute_and_prove, program::Program, state::{ @@ -364,4 +365,46 @@ mod tests { .unwrap(); assert_eq!(recipient_post, expected_private_account_2); } + + #[test] + fn circuit_fails_when_chained_validity_windows_have_empty_intersection() { + let account_keys = test_private_account_keys_1(); + let pre = AccountWithMetadata::new( + Account::default(), + false, + AccountId::from(&account_keys.npk()), + ); + + let validity_window_chain_caller = Program::validity_window_chain_caller(); + let validity_window = Program::validity_window(); + + let instruction = Program::serialize_instruction(( + Some(1_u64), + Some(4_u64), + validity_window.id(), + Some(4_u64), + Some(7_u64), + )) + .unwrap(); + + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); + + let program_with_deps = ProgramWithDependencies::new( + validity_window_chain_caller, + [(validity_window.id(), validity_window)].into(), + ); + + let result = execute_and_prove( + vec![pre], + instruction, + vec![2], + vec![(account_keys.npk(), shared_secret)], + vec![], + vec![None], + &program_with_deps, + ); + + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } } diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 02c7a614..85f4a202 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -3,6 +3,7 @@ use nssa_core::{ Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput, account::{Account, Nonce}, encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey}, + program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; @@ -52,6 +53,8 @@ pub struct Message { pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub block_validity_window: BlockValidityWindow, + pub timestamp_validity_window: TimestampValidityWindow, } impl std::fmt::Debug for Message { @@ -77,6 +80,8 @@ impl std::fmt::Debug for Message { ) .field("new_commitments", &self.new_commitments) .field("new_nullifiers", &nullifiers) + .field("block_validity_window", &self.block_validity_window) + .field("timestamp_validity_window", &self.timestamp_validity_window) .finish() } } @@ -109,6 +114,8 @@ impl Message { encrypted_private_post_states, new_commitments: output.new_commitments, new_nullifiers: output.new_nullifiers, + block_validity_window: output.block_validity_window, + timestamp_validity_window: output.timestamp_validity_window, }) } } @@ -119,6 +126,7 @@ pub mod tests { Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey, account::Account, encryption::{EphemeralPublicKey, ViewingPublicKey}, + program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; @@ -161,6 +169,8 @@ pub mod tests { encrypted_private_post_states, new_commitments, new_nullifiers, + block_validity_window: BlockValidityWindow::new_unbounded(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), } } diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index dc8670d2..db2bd773 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -5,16 +5,14 @@ use std::{ use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, + BlockId, PrivacyPreservingCircuitOutput, Timestamp, account::{Account, AccountWithMetadata}, }; use sha2::{Digest as _, digest::FixedOutput as _}; use super::{message::Message, witness_set::WitnessSet}; use crate::{ - AccountId, V03State, - error::NssaError, - privacy_preserving_transaction::{circuit::Proof, message::EncryptedAccountData}, + AccountId, V03State, error::NssaError, privacy_preserving_transaction::circuit::Proof, }; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] @@ -35,6 +33,8 @@ impl PrivacyPreservingTransaction { pub(crate) fn validate_and_produce_public_state_diff( &self, state: &V03State, + block_id: BlockId, + timestamp: Timestamp, ) -> Result, NssaError> { let message = &self.message; let witness_set = &self.witness_set; @@ -91,6 +91,13 @@ impl PrivacyPreservingTransaction { } } + // Verify validity window + if !message.block_validity_window.is_valid_for(block_id) + || !message.timestamp_validity_window.is_valid_for(timestamp) + { + return Err(NssaError::OutOfValidityWindow); + } + // Build pre_states for proof verification let public_pre_states: Vec<_> = message .public_account_ids @@ -108,10 +115,7 @@ impl PrivacyPreservingTransaction { check_privacy_preserving_circuit_proof_is_valid( &witness_set.proof, &public_pre_states, - &message.public_post_states, - &message.encrypted_private_post_states, - &message.new_commitments, - &message.new_nullifiers, + message, )?; // 5. Commitment freshness @@ -180,21 +184,21 @@ impl PrivacyPreservingTransaction { fn check_privacy_preserving_circuit_proof_is_valid( proof: &Proof, public_pre_states: &[AccountWithMetadata], - public_post_states: &[Account], - encrypted_private_post_states: &[EncryptedAccountData], - new_commitments: &[Commitment], - new_nullifiers: &[(Nullifier, CommitmentSetDigest)], + message: &Message, ) -> Result<(), NssaError> { let output = PrivacyPreservingCircuitOutput { public_pre_states: public_pre_states.to_vec(), - public_post_states: public_post_states.to_vec(), - ciphertexts: encrypted_private_post_states + public_post_states: message.public_post_states.clone(), + ciphertexts: message + .encrypted_private_post_states .iter() .cloned() .map(|value| value.ciphertext) .collect(), - new_commitments: new_commitments.to_vec(), - new_nullifiers: new_nullifiers.to_vec(), + new_commitments: message.new_commitments.clone(), + new_nullifiers: message.new_nullifiers.clone(), + block_validity_window: message.block_validity_window, + timestamp_validity_window: message.timestamp_validity_window, }; proof .is_valid_for(&output) diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 5353458d..94a2a215 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -8,7 +8,9 @@ use serde::Serialize; use crate::{ error::NssaError, - program_methods::{AMM_ELF, AUTHENTICATED_TRANSFER_ELF, CLOCK_ELF, PINATA_ELF, TOKEN_ELF}, + program_methods::{ + AMM_ELF, ASSOCIATED_TOKEN_ACCOUNT_ELF, AUTHENTICATED_TRANSFER_ELF, CLOCK_ELF, PINATA_ELF, TOKEN_ELF, + }, }; /// Maximum number of cycles for a public execution. @@ -110,6 +112,12 @@ impl Program { pub fn clock() -> Self { Self::new(CLOCK_ELF.to_vec()).expect("The clock program must be a valid Risc0 program") } + + #[must_use] + pub fn ata() -> Self { + Self::new(ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec()) + .expect("The ATA program must be a valid Risc0 program") + } } // TODO: Testnet only. Refactor to prevent compilation on mainnet. @@ -289,6 +297,20 @@ mod tests { // `program_methods` Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap() } + + #[must_use] + pub fn validity_window() -> Self { + use test_program_methods::VALIDITY_WINDOW_ELF; + // This unwrap won't panic since the `VALIDITY_WINDOW_ELF` comes from risc0 build of + // `program_methods` + Self::new(VALIDITY_WINDOW_ELF.to_vec()).unwrap() + } + + #[must_use] + pub fn validity_window_chain_caller() -> Self { + use test_program_methods::VALIDITY_WINDOW_CHAIN_CALLER_ELF; + Self::new(VALIDITY_WINDOW_CHAIN_CALLER_ELF.to_vec()).unwrap() + } } #[test] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 001addb7..0ce52c56 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -3,8 +3,9 @@ use std::collections::{HashMap, HashSet, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use log::debug; use nssa_core::{ + BlockId, Timestamp, account::{Account, AccountId, AccountWithMetadata}, - program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, + program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution}, }; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -70,6 +71,8 @@ impl PublicTransaction { pub(crate) fn validate_and_produce_public_state_diff( &self, state: &V03State, + block_id: BlockId, + timestamp: Timestamp, ) -> Result, NssaError> { let message = self.message(); let witness_set = self.witness_set(); @@ -156,6 +159,10 @@ impl PublicTransaction { &chained_call.pda_seeds, ); + let is_authorized = |account_id: &AccountId| { + signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id) + }; + for pre in &program_output.pre_states { let account_id = pre.account_id; // Check that the program output pre_states coincide with the values in the public @@ -171,10 +178,8 @@ impl PublicTransaction { // Check that authorization flags are consistent with the provided ones or // authorized by program through the PDA mechanism - let is_authorized = signer_account_ids.contains(&account_id) - || authorized_pdas.contains(&account_id); ensure!( - pre.is_authorized == is_authorized, + pre.is_authorized == is_authorized(&account_id), NssaError::InvalidProgramBehavior ); } @@ -190,17 +195,44 @@ impl PublicTransaction { NssaError::InvalidProgramBehavior ); - for post in program_output - .post_states - .iter_mut() - .filter(|post| post.requires_claim()) - { + // Verify validity window + ensure!( + program_output.block_validity_window.is_valid_for(block_id) + && program_output + .timestamp_validity_window + .is_valid_for(timestamp), + NssaError::OutOfValidityWindow + ); + + for (i, post) in program_output.post_states.iter_mut().enumerate() { + let Some(claim) = post.required_claim() else { + continue; + }; // The invoked program can only claim accounts with default program id. - if post.account().program_owner == DEFAULT_PROGRAM_ID { - post.account_mut().program_owner = chained_call.program_id; - } else { - return Err(NssaError::InvalidProgramBehavior); + ensure!( + post.account().program_owner == DEFAULT_PROGRAM_ID, + NssaError::InvalidProgramBehavior + ); + + let account_id = program_output.pre_states[i].account_id; + + match claim { + Claim::Authorized => { + // The program can only claim accounts that were authorized by the signer. + ensure!( + is_authorized(&account_id), + NssaError::InvalidProgramBehavior + ); + } + Claim::Pda(seed) => { + // The program can only claim accounts that correspond to the PDAs it is + // authorized to claim. + let pda = AccountId::from((&chained_call.program_id, &seed)); + ensure!(account_id == pda, NssaError::InvalidProgramBehavior); + } } + + post.account_mut().program_owner = chained_call.program_id; } // Update the state diff @@ -359,7 +391,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -379,7 +411,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -400,7 +432,7 @@ pub mod tests { let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -420,7 +452,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -436,7 +468,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } } diff --git a/nssa/src/signature/mod.rs b/nssa/src/signature/mod.rs index 9dfd47b3..3a594da6 100644 --- a/nssa/src/signature/mod.rs +++ b/nssa/src/signature/mod.rs @@ -49,21 +49,28 @@ impl Signature { aux_random: [u8; 32], ) -> Self { let value = { - let secp = secp256k1::Secp256k1::new(); - let secret_key = secp256k1::SecretKey::from_byte_array(*key.value()).unwrap(); - let keypair = secp256k1::Keypair::from_secret_key(&secp, &secret_key); - let signature = secp.sign_schnorr_with_aux_rand(message, &keypair, &aux_random); - signature.to_byte_array() + let signing_key = k256::schnorr::SigningKey::from_bytes(key.value()) + .expect("Expect valid signing key"); + signing_key + .sign_raw(message, &aux_random) + .expect("Expect to produce a valid signature") + .to_bytes() }; + Self { value } } #[must_use] pub fn is_valid_for(&self, bytes: &[u8], public_key: &PublicKey) -> bool { - let pk = secp256k1::XOnlyPublicKey::from_byte_array(*public_key.value()).unwrap(); - let secp = secp256k1::Secp256k1::new(); - let sig = secp256k1::schnorr::Signature::from_byte_array(self.value); - secp.verify_schnorr(&sig, bytes, &pk).is_ok() + let Ok(pk) = k256::schnorr::VerifyingKey::from_bytes(public_key.value()) else { + return false; + }; + + let Ok(sig) = k256::schnorr::Signature::try_from(self.value.as_slice()) else { + return false; + }; + + pk.verify_raw(bytes, &sig).is_ok() } } diff --git a/nssa/src/signature/private_key.rs b/nssa/src/signature/private_key.rs index e73e0e4f..1bfecf80 100644 --- a/nssa/src/signature/private_key.rs +++ b/nssa/src/signature/private_key.rs @@ -45,7 +45,7 @@ impl PrivateKey { } fn is_valid_key(value: [u8; 32]) -> bool { - secp256k1::SecretKey::from_byte_array(value).is_ok() + k256::SecretKey::from_bytes(&value.into()).is_ok() } pub fn try_new(value: [u8; 32]) -> Result { diff --git a/nssa/src/signature/public_key.rs b/nssa/src/signature/public_key.rs index ee0f5dbc..ebec6b62 100644 --- a/nssa/src/signature/public_key.rs +++ b/nssa/src/signature/public_key.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use k256::elliptic_curve::sec1::ToEncodedPoint as _; use nssa_core::account::AccountId; use serde_with::{DeserializeFromStr, SerializeDisplay}; use sha2::{Digest as _, Sha256}; @@ -27,8 +28,7 @@ impl FromStr for PublicKey { fn from_str(s: &str) -> Result { let mut bytes = [0_u8; 32]; - hex::decode_to_slice(s, &mut bytes) - .map_err(|_err| NssaError::InvalidPublicKey(secp256k1::Error::InvalidPublicKey))?; + hex::decode_to_slice(s, &mut bytes).map_err(NssaError::InvalidHexPublicKey)?; Self::try_new(bytes) } } @@ -46,19 +46,24 @@ impl PublicKey { #[must_use] pub fn new_from_private_key(key: &PrivateKey) -> Self { let value = { - let secret_key = secp256k1::SecretKey::from_byte_array(*key.value()).unwrap(); - let public_key = - secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &secret_key); - let (x_only, _) = public_key.x_only_public_key(); - x_only.serialize() + let secret_key = k256::SecretKey::from_bytes(&(*key.value()).into()) + .expect("Expect a valid private key"); + + let encoded = secret_key.public_key().to_encoded_point(false); + let x_only = encoded + .x() + .expect("Expect k256 point to have a x-coordinate"); + + *x_only.first_chunk().expect("x_only is exactly 32 bytes") }; Self(value) } pub fn try_new(value: [u8; 32]) -> Result { - // Check point is valid - let _ = secp256k1::XOnlyPublicKey::from_byte_array(value) - .map_err(NssaError::InvalidPublicKey)?; + // Check point is a valid x-only public key + let _ = + k256::schnorr::VerifyingKey::from_bytes(&value).map_err(NssaError::InvalidPublicKey)?; + Ok(Self(value)) } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index caf0886d..d162a756 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2,7 +2,8 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, + BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, + Timestamp, account::{Account, AccountId, Nonce}, program::ProgramId, }; @@ -159,6 +160,7 @@ impl V03State { this.insert_program(Program::authenticated_transfer_program()); this.insert_program(Program::token()); this.insert_program(Program::amm()); + this.insert_program(Program::ata()); this } @@ -193,8 +195,10 @@ impl V03State { pub fn transition_from_public_transaction( &mut self, tx: &PublicTransaction, + block_id: BlockId, + timestamp: Timestamp, ) -> Result<(), NssaError> { - let state_diff = tx.validate_and_produce_public_state_diff(self)?; + let state_diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; #[expect( clippy::iter_over_hash_type, @@ -217,9 +221,12 @@ impl V03State { pub fn transition_from_privacy_preserving_transaction( &mut self, tx: &PrivacyPreservingTransaction, + block_id: BlockId, + timestamp: Timestamp, ) -> Result<(), NssaError> { // 1. Verify the transaction satisfies acceptance criteria - let public_state_diff = tx.validate_and_produce_public_state_diff(self)?; + let public_state_diff = + tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; let message = tx.message(); @@ -371,10 +378,11 @@ pub mod tests { use std::collections::HashMap; use nssa_core::{ - Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + BlockId, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, - program::{PdaSeed, ProgramId}, + program::{BlockValidityWindow, PdaSeed, ProgramId, TimestampValidityWindow}, }; use crate::{ @@ -412,6 +420,7 @@ pub mod tests { self.insert_program(Program::amm()); self.insert_program(Program::claimer()); self.insert_program(Program::changer_claimer()); + self.insert_program(Program::validity_window()); self } @@ -491,16 +500,19 @@ pub mod tests { fn transfer_transaction( from: AccountId, from_key: &PrivateKey, - nonce: u128, + from_nonce: u128, to: AccountId, + to_key: &PrivateKey, + to_nonce: u128, balance: u128, ) -> PublicTransaction { let account_ids = vec![from, to]; - let nonces = vec![Nonce(nonce)]; + let nonces = vec![Nonce(from_nonce), Nonce(to_nonce)]; let program_id = Program::authenticated_transfer_program().id(); let message = public_transaction::Message::try_new(program_id, account_ids, nonces, balance).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key]); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[from_key, to_key]); PublicTransaction::new(message, witness_set) } @@ -556,6 +568,7 @@ pub mod tests { this.insert(clock_program.id(), clock_program); this.insert(Program::token().id(), Program::token()); this.insert(Program::amm().id(), Program::amm()); + this.insert(Program::ata().id(), Program::ata()); this }; @@ -617,17 +630,18 @@ pub mod tests { let initial_data = [(account_id, 100)]; let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0); let from = account_id; - let to = AccountId::new([2; 32]); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); assert_eq!(state.get_account_by_id(to), Account::default()); let balance_to_move = 5; - let tx = transfer_transaction(from, &key, 0, to, balance_to_move); - state.transition_from_public_transaction(&tx).unwrap(); + let tx = transfer_transaction(from, &key, 0, to, &to_key, 0, balance_to_move); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); assert_eq!(state.get_account_by_id(from).balance, 95); assert_eq!(state.get_account_by_id(to).balance, 5); assert_eq!(state.get_account_by_id(from).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(to).nonce, Nonce(0)); + assert_eq!(state.get_account_by_id(to).nonce, Nonce(1)); } #[test] @@ -638,12 +652,13 @@ pub mod tests { let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0); let from = account_id; let from_key = key; - let to = AccountId::new([2; 32]); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); let balance_to_move = 101; assert!(state.get_account_by_id(from).balance < balance_to_move); - let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move); - let result = state.transition_from_public_transaction(&tx); + let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); assert_eq!(state.get_account_by_id(from).balance, 100); @@ -663,16 +678,17 @@ pub mod tests { let from = account_id2; let from_key = key2; let to = account_id1; + let to_key = key1; assert_ne!(state.get_account_by_id(to), Account::default()); let balance_to_move = 8; - let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move); - state.transition_from_public_transaction(&tx).unwrap(); + let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); assert_eq!(state.get_account_by_id(from).balance, 192); assert_eq!(state.get_account_by_id(to).balance, 108); assert_eq!(state.get_account_by_id(from).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(to).nonce, Nonce(0)); + assert_eq!(state.get_account_by_id(to).nonce, Nonce(1)); } #[test] @@ -683,21 +699,38 @@ pub mod tests { let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); let initial_data = [(account_id1, 100)]; let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0); - let account_id3 = AccountId::new([3; 32]); + let key3 = PrivateKey::try_new([3; 32]).unwrap(); + let account_id3 = AccountId::from(&PublicKey::new_from_private_key(&key3)); let balance_to_move = 5; - let tx = transfer_transaction(account_id1, &key1, 0, account_id2, balance_to_move); - state.transition_from_public_transaction(&tx).unwrap(); + let tx = transfer_transaction( + account_id1, + &key1, + 0, + account_id2, + &key2, + 0, + balance_to_move, + ); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let balance_to_move = 3; - let tx = transfer_transaction(account_id2, &key2, 0, account_id3, balance_to_move); - state.transition_from_public_transaction(&tx).unwrap(); + let tx = transfer_transaction( + account_id2, + &key2, + 1, + account_id3, + &key3, + 0, + balance_to_move, + ); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); assert_eq!(state.get_account_by_id(account_id1).balance, 95); assert_eq!(state.get_account_by_id(account_id2).balance, 2); assert_eq!(state.get_account_by_id(account_id3).balance, 3); assert_eq!(state.get_account_by_id(account_id1).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(0)); + assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(2)); + assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(1)); } fn clock_transaction(timestamp: nssa_core::Timestamp) -> PublicTransaction { @@ -741,7 +774,7 @@ pub mod tests { let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); let tx = clock_transaction(1234); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); let (block_id, _) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); assert_eq!(block_id, 1); @@ -753,7 +786,7 @@ pub mod tests { let block_timestamp = 1_700_000_000_000_u64; let tx = clock_transaction(block_timestamp); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); let (_, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); assert_eq!(timestamp, block_timestamp); @@ -765,7 +798,7 @@ pub mod tests { for expected_block_id in 1_u64..=5 { let tx = clock_transaction(expected_block_id * 1000); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); let (block_id, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); assert_eq!(block_id, expected_block_id); @@ -781,7 +814,7 @@ pub mod tests { // Run 9 clock ticks (block_ids 1..=9), none of which are multiples of 10. for tick in 1_u64..=9 { let tx = clock_transaction(tick * 1000); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } let (block_id_10, timestamp_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID); @@ -797,7 +830,7 @@ pub mod tests { // Run 10 clock ticks so block_id reaches 10. for tick in 1_u64..=10 { let tx = clock_transaction(tick * 1000); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } let (block_id_1, timestamp_1) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); @@ -814,14 +847,14 @@ pub mod tests { // After 49 ticks the 50-block account should be unchanged. for tick in 1_u64..=49 { let tx = clock_transaction(tick * 1000); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } let (block_id_50, _) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID); assert_eq!(block_id_50, 0); // Tick 50 — now the 50-block account should update. let tx = clock_transaction(50 * 1000); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); let (block_id_50, timestamp_50) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID); assert_eq!(block_id_50, 50); assert_eq!(timestamp_50, 50 * 1000); @@ -834,7 +867,7 @@ pub mod tests { // Advance to block 50 (a multiple of both 10 and 50). for tick in 1_u64..=50 { let tx = clock_transaction(tick * 1000); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } let (block_id_1, ts_1) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); @@ -860,7 +893,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -877,7 +910,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -894,7 +927,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -918,7 +951,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -942,7 +975,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -966,7 +999,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -990,7 +1023,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -1018,7 +1051,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -1043,7 +1076,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -1061,7 +1094,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -1090,7 +1123,7 @@ pub mod tests { .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -1283,7 +1316,7 @@ pub mod tests { assert!(!state.private_state.0.contains(&expected_new_commitment)); state - .transition_from_privacy_preserving_transaction(&tx) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let sender_post = state.get_account_by_id(sender_keys.account_id()); @@ -1353,7 +1386,7 @@ pub mod tests { assert!(!state.private_state.1.contains(&expected_new_nullifier)); state - .transition_from_privacy_preserving_transaction(&tx) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); assert_eq!(state.public_state, previous_public_state); @@ -1418,7 +1451,7 @@ pub mod tests { assert!(!state.private_state.1.contains(&expected_new_nullifier)); state - .transition_from_privacy_preserving_transaction(&tx) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let recipient_post = state.get_account_by_id(recipient_keys.account_id()); @@ -2346,7 +2379,7 @@ pub mod tests { ); state - .transition_from_privacy_preserving_transaction(&tx) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let sender_private_account = Account { @@ -2364,7 +2397,7 @@ pub mod tests { &state, ); - let result = state.transition_from_privacy_preserving_transaction(&tx); + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); let NssaError::InvalidInput(error_message) = result.err().unwrap() else { @@ -2411,15 +2444,14 @@ pub mod tests { #[test] fn claiming_mechanism() { let program = Program::authenticated_transfer_program(); - let key = PrivateKey::try_new([1; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let from_key = PrivateKey::try_new([1; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); let initial_balance = 100; - let initial_data = [(account_id, initial_balance)]; + let initial_data = [(from, initial_balance)]; let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs(); - let from = account_id; - let from_key = key; - let to = AccountId::new([2; 32]); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); let amount: u128 = 37; // Check the recipient is an uninitialized account @@ -2428,26 +2460,80 @@ pub mod tests { let expected_recipient_post = Account { program_owner: program.id(), balance: amount, + nonce: Nonce(1), ..Account::default() }; let message = public_transaction::Message::try_new( program.id(), vec![from, to], - vec![Nonce(0)], + vec![Nonce(0), Nonce(0)], amount, ) .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let recipient_post = state.get_account_by_id(to); assert_eq!(recipient_post, expected_recipient_post); } + #[test] + fn unauthorized_public_account_claiming_fails() { + let program = Program::authenticated_transfer_program(); + let account_key = PrivateKey::try_new([9; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); + + assert_eq!(state.get_account_by_id(account_id), Account::default()); + + let message = + public_transaction::Message::try_new(program.id(), vec![account_id], vec![], 0_u128) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); + assert_eq!(state.get_account_by_id(account_id), Account::default()); + } + + #[test] + fn authorized_public_account_claiming_succeeds() { + let program = Program::authenticated_transfer_program(); + let account_key = PrivateKey::try_new([10; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); + + assert_eq!(state.get_account_by_id(account_id), Account::default()); + + let message = public_transaction::Message::try_new( + program.id(), + vec![account_id], + vec![Nonce(0)], + 0_u128, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&account_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + assert_eq!( + state.get_account_by_id(account_id), + Account { + program_owner: program.id(), + nonce: Nonce(1), + ..Account::default() + } + ); + } + #[test] fn public_chained_call() { let program = Program::chain_caller(); @@ -2484,7 +2570,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let from_post = state.get_account_by_id(from); let to_post = state.get_account_by_id(to); @@ -2524,7 +2610,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!( result, Err(NssaError::MaxChainedCallsDepthExceeded) @@ -2565,7 +2651,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let from_post = state.get_account_by_id(from); let to_post = state.get_account_by_id(to); @@ -2581,15 +2667,14 @@ pub mod tests { // program and not the chained_caller program. let chain_caller = Program::chain_caller(); let auth_transfer = Program::authenticated_transfer_program(); - let key = PrivateKey::try_new([1; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let from_key = PrivateKey::try_new([1; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); let initial_balance = 100; - let initial_data = [(account_id, initial_balance)]; + let initial_data = [(from, initial_balance)]; let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs(); - let from = account_id; - let from_key = key; - let to = AccountId::new([2; 32]); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); let amount: u128 = 37; // Check the recipient is an uninitialized account @@ -2599,6 +2684,7 @@ pub mod tests { // The expected program owner is the authenticated transfer program program_owner: auth_transfer.id(), balance: amount, + nonce: Nonce(1), ..Account::default() }; @@ -2614,14 +2700,15 @@ pub mod tests { chain_caller.id(), vec![to, from], // The chain_caller program permutes the account order in the chain // call - vec![Nonce(0)], + vec![Nonce(0), Nonce(0)], instruction, ) .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let from_post = state.get_account_by_id(from); let to_post = state.get_account_by_id(to); @@ -2629,6 +2716,88 @@ pub mod tests { assert_eq!(to_post, expected_to_post); } + #[test] + fn unauthorized_public_account_claiming_fails_when_executed_privately() { + let program = Program::authenticated_transfer_program(); + let account_id = AccountId::new([11; 32]); + let public_account = AccountWithMetadata::new(Account::default(), false, account_id); + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(0_u128).unwrap(), + vec![0], + vec![], + vec![], + vec![], + &program.into(), + ); + + assert!(matches!(result, Err(NssaError::ProgramProveFailed(_)))); + } + + #[test] + fn authorized_public_account_claiming_succeeds_when_executed_privately() { + let program = Program::authenticated_transfer_program(); + let program_id = program.id(); + let sender_keys = test_private_account_keys_1(); + let sender_private_account = Account { + program_owner: program_id, + balance: 100, + ..Account::default() + }; + let sender_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account); + let mut state = + V03State::new_with_genesis_accounts(&[], std::slice::from_ref(&sender_commitment), 0); + let sender_pre = AccountWithMetadata::new(sender_private_account, true, &sender_keys.npk()); + let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap(); + let recipient_account_id = + AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key)); + let recipient_pre = + AccountWithMetadata::new(Account::default(), true, recipient_account_id); + let esk = [5; 32]; + let shared_secret = SharedSecretKey::new(&esk, &sender_keys.vpk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let (output, proof) = execute_and_prove( + vec![sender_pre, recipient_pre], + Program::serialize_instruction(37_u128).unwrap(), + vec![1, 0], + vec![(sender_keys.npk(), shared_secret)], + vec![sender_keys.nsk], + vec![state.get_proof_for_commitment(&sender_commitment)], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![recipient_account_id], + vec![Nonce(0)], + vec![(sender_keys.npk(), sender_keys.vpk(), epk)], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk); + assert!(state.private_state.1.contains(&nullifier)); + + assert_eq!( + state.get_account_by_id(recipient_account_id), + Account { + program_owner: program_id, + balance: 37, + nonce: Nonce(1), + ..Account::default() + } + ); + } + #[test_case::test_case(1; "single call")] #[test_case::test_case(2; "two calls")] fn private_chained_call(number_of_calls: u32) { @@ -2731,7 +2900,7 @@ pub mod tests { let transaction = PrivacyPreservingTransaction::new(message, witness_set); state - .transition_from_privacy_preserving_transaction(&transaction) + .transition_from_privacy_preserving_transaction(&transaction, 1, 0) .unwrap(); // Assert @@ -2771,36 +2940,47 @@ pub mod tests { let mut state = V03State::new_with_genesis_accounts(&[], &[], 0); state.add_pinata_token_program(pinata_definition_id); - // Execution of the token program to create new token for the pinata token - // definition and supply accounts + // Set up the token accounts directly (bypassing public transactions which + // would require signers for Claim::Authorized). The focus of this test is + // the PDA mechanism in the pinata program's chained call, not token creation. let total_supply: u128 = 10_000_000; - let instruction = token_core::Instruction::NewFungibleDefinition { + let token_definition = token_core::TokenDefinition::Fungible { name: String::from("PINATA"), total_supply, + metadata_id: None, }; - let message = public_transaction::Message::try_new( - token.id(), - vec![pinata_token_definition_id, pinata_token_holding_id], - vec![], - instruction, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); - - // Execution of winner's token holding account initialization - let instruction = token_core::Instruction::InitializeAccount; - let message = public_transaction::Message::try_new( - token.id(), - vec![pinata_token_definition_id, winner_token_holding_id], - vec![], - instruction, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + let token_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: total_supply, + }; + let winner_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: 0, + }; + state.force_insert_account( + pinata_token_definition_id, + Account { + program_owner: token.id(), + data: Data::from(&token_definition), + ..Account::default() + }, + ); + state.force_insert_account( + pinata_token_holding_id, + Account { + program_owner: token.id(), + data: Data::from(&token_holding), + ..Account::default() + }, + ); + state.force_insert_account( + winner_token_holding_id, + Account { + program_owner: token.id(), + data: Data::from(&winner_holding), + ..Account::default() + }, + ); // Submit a solution to the pinata program to claim the prize let solution: u128 = 989_106; @@ -2817,7 +2997,7 @@ pub mod tests { .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let winner_token_holding_post = state.get_account_by_id(winner_token_holding_id); assert_eq!( @@ -2847,7 +3027,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -2894,7 +3074,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); let tx = PublicTransaction::new(message, witness_set); - let res = state.transition_from_public_transaction(&tx); + let res = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(res, Err(NssaError::InvalidProgramBehavior))); let sender_post = state.get_account_by_id(sender_id); @@ -2963,13 +3143,60 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); - let result = state.transition_from_privacy_preserving_transaction(&tx); + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); assert!(result.is_ok()); let nullifier = Nullifier::for_account_initialization(&private_keys.npk()); assert!(state.private_state.1.contains(&nullifier)); } + #[test] + fn private_unauthorized_uninitialized_account_can_still_be_claimed() { + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs(); + + let private_keys = test_private_account_keys_1(); + // This is intentional: claim authorization was introduced to protect public accounts, + // especially PDAs. Private PDAs are not useful in practice because there is no way to + // operate them without the corresponding private keys, so unauthorized private claiming + // remains allowed. + let unauthorized_account = + AccountWithMetadata::new(Account::default(), false, &private_keys.npk()); + + let program = Program::claimer(); + let esk = [5; 32]; + let shared_secret = SharedSecretKey::new(&esk, &private_keys.vpk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let (output, proof) = execute_and_prove( + vec![unauthorized_account], + Program::serialize_instruction(0_u128).unwrap(), + vec![2], + vec![(private_keys.npk(), shared_secret)], + vec![], + vec![None], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![(private_keys.npk(), private_keys.vpk(), epk)], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + let nullifier = Nullifier::for_account_initialization(&private_keys.npk()); + assert!(state.private_state.1.contains(&nullifier)); + } + #[test] fn private_account_claimed_then_used_without_init_flag_should_fail() { let mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs(); @@ -3016,7 +3243,7 @@ pub mod tests { // Claim should succeed assert!( state - .transition_from_privacy_preserving_transaction(&tx) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .is_ok() ); @@ -3065,7 +3292,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); // Should succeed - no changes made, no claim needed assert!(result.is_ok()); @@ -3090,7 +3317,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx); + let result = state.transition_from_public_transaction(&tx, 1, 0); // Should fail - cannot modify data without claiming the account assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); @@ -3203,6 +3430,250 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } + #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] + #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] + #[test_case::test_case((Some(1), Some(3)), 0; "below range")] + #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] + #[test_case::test_case((Some(1), Some(3)), 4; "above range")] + #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] + #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] + #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] + #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] + #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] + #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] + #[test_case::test_case((None, None), 0; "no bounds - always valid")] + #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] + fn validity_window_works_in_public_transactions( + validity_window: (Option, Option), + block_id: BlockId, + ) { + let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap(); + let validity_window_program = Program::validity_window(); + let account_keys = test_public_account_keys_1(); + let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs(); + let tx = { + let account_ids = vec![pre.account_id]; + let nonces = vec![]; + let program_id = validity_window_program.id(); + let instruction = ( + block_validity_window, + TimestampValidityWindow::new_unbounded(), + ); + let message = + public_transaction::Message::try_new(program_id, account_ids, nonces, instruction) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) + }; + let result = state.transition_from_public_transaction(&tx, block_id, 0); + let is_inside_validity_window = + match (block_validity_window.start(), block_validity_window.end()) { + (Some(s), Some(e)) => s <= block_id && block_id < e, + (Some(s), None) => s <= block_id, + (None, Some(e)) => block_id < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(NssaError::OutOfValidityWindow))); + } + } + + #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] + #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] + #[test_case::test_case((Some(1), Some(3)), 0; "below range")] + #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] + #[test_case::test_case((Some(1), Some(3)), 4; "above range")] + #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] + #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] + #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] + #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] + #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] + #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] + #[test_case::test_case((None, None), 0; "no bounds - always valid")] + #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] + fn timestamp_validity_window_works_in_public_transactions( + validity_window: (Option, Option), + timestamp: Timestamp, + ) { + let timestamp_validity_window: TimestampValidityWindow = + validity_window.try_into().unwrap(); + let validity_window_program = Program::validity_window(); + let account_keys = test_public_account_keys_1(); + let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs(); + let tx = { + let account_ids = vec![pre.account_id]; + let nonces = vec![]; + let program_id = validity_window_program.id(); + let instruction = ( + BlockValidityWindow::new_unbounded(), + timestamp_validity_window, + ); + let message = + public_transaction::Message::try_new(program_id, account_ids, nonces, instruction) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) + }; + let result = state.transition_from_public_transaction(&tx, 1, timestamp); + let is_inside_validity_window = match ( + timestamp_validity_window.start(), + timestamp_validity_window.end(), + ) { + (Some(s), Some(e)) => s <= timestamp && timestamp < e, + (Some(s), None) => s <= timestamp, + (None, Some(e)) => timestamp < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(NssaError::OutOfValidityWindow))); + } + } + + #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] + #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] + #[test_case::test_case((Some(1), Some(3)), 0; "below range")] + #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] + #[test_case::test_case((Some(1), Some(3)), 4; "above range")] + #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] + #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] + #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] + #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] + #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] + #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] + #[test_case::test_case((None, None), 0; "no bounds - always valid")] + #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] + fn validity_window_works_in_privacy_preserving_transactions( + validity_window: (Option, Option), + block_id: BlockId, + ) { + let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap(); + let validity_window_program = Program::validity_window(); + let account_keys = test_private_account_keys_1(); + let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk()); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs(); + let tx = { + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let instruction = ( + block_validity_window, + TimestampValidityWindow::new_unbounded(), + ); + let (output, proof) = circuit::execute_and_prove( + vec![pre], + Program::serialize_instruction(instruction).unwrap(), + vec![2], + vec![(account_keys.npk(), shared_secret)], + vec![], + vec![None], + &validity_window_program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![(account_keys.npk(), account_keys.vpk(), epk)], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + PrivacyPreservingTransaction::new(message, witness_set) + }; + let result = state.transition_from_privacy_preserving_transaction(&tx, block_id, 0); + let is_inside_validity_window = + match (block_validity_window.start(), block_validity_window.end()) { + (Some(s), Some(e)) => s <= block_id && block_id < e, + (Some(s), None) => s <= block_id, + (None, Some(e)) => block_id < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(NssaError::OutOfValidityWindow))); + } + } + + #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] + #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] + #[test_case::test_case((Some(1), Some(3)), 0; "below range")] + #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] + #[test_case::test_case((Some(1), Some(3)), 4; "above range")] + #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] + #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] + #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] + #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] + #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] + #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] + #[test_case::test_case((None, None), 0; "no bounds - always valid")] + #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] + fn timestamp_validity_window_works_in_privacy_preserving_transactions( + validity_window: (Option, Option), + timestamp: Timestamp, + ) { + let timestamp_validity_window: TimestampValidityWindow = + validity_window.try_into().unwrap(); + let validity_window_program = Program::validity_window(); + let account_keys = test_private_account_keys_1(); + let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk()); + let mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs(); + let tx = { + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let instruction = ( + BlockValidityWindow::new_unbounded(), + timestamp_validity_window, + ); + let (output, proof) = circuit::execute_and_prove( + vec![pre], + Program::serialize_instruction(instruction).unwrap(), + vec![2], + vec![(account_keys.npk(), shared_secret)], + vec![], + vec![None], + &validity_window_program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![(account_keys.npk(), account_keys.vpk(), epk)], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + PrivacyPreservingTransaction::new(message, witness_set) + }; + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, timestamp); + let is_inside_validity_window = match ( + timestamp_validity_window.start(), + timestamp_validity_window.end(), + ) { + (Some(s), Some(e)) => s <= timestamp && timestamp < e, + (Some(s), None) => s <= timestamp, + (None, Some(e)) => timestamp < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(NssaError::OutOfValidityWindow))); + } + } + #[test] fn state_serialization_roundtrip() { let account_id_1 = AccountId::new([1; 32]); diff --git a/program_methods/guest/Cargo.toml b/program_methods/guest/Cargo.toml index eabcffc8..29ef8304 100644 --- a/program_methods/guest/Cargo.toml +++ b/program_methods/guest/Cargo.toml @@ -13,5 +13,7 @@ token_core.workspace = true token_program.workspace = true amm_core.workspace = true amm_program.workspace = true +ata_core.workspace = true +ata_program.workspace = true risc0-zkvm.workspace = true serde = { workspace = true, default-features = false } diff --git a/program_methods/guest/src/bin/amm.rs b/program_methods/guest/src/bin/amm.rs index 00fd39d3..748630d9 100644 --- a/program_methods/guest/src/bin/amm.rs +++ b/program_methods/guest/src/bin/amm.rs @@ -9,7 +9,7 @@ use std::num::NonZero; use amm_core::Instruction; -use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call}; +use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs}; fn main() { let ( @@ -133,10 +133,7 @@ fn main() { } }; - write_nssa_outputs_with_chained_call( - instruction_words, - pre_states_clone, - post_states, - chained_calls, - ); + ProgramOutput::new(instruction_words, pre_states_clone, post_states) + .with_chained_calls(chained_calls) + .write(); } diff --git a/program_methods/guest/src/bin/associated_token_account.rs b/program_methods/guest/src/bin/associated_token_account.rs new file mode 100644 index 00000000..55d5824b --- /dev/null +++ b/program_methods/guest/src/bin/associated_token_account.rs @@ -0,0 +1,62 @@ +use ata_core::Instruction; +use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs}; + +fn main() { + let ( + ProgramInput { + pre_states, + instruction, + }, + instruction_words, + ) = read_nssa_inputs::(); + + let pre_states_clone = pre_states.clone(); + + let (post_states, chained_calls) = match instruction { + Instruction::Create { ata_program_id } => { + let [owner, token_definition, ata_account] = pre_states + .try_into() + .expect("Create instruction requires exactly three accounts"); + ata_program::create::create_associated_token_account( + owner, + token_definition, + ata_account, + ata_program_id, + ) + } + Instruction::Transfer { + ata_program_id, + amount, + } => { + let [owner, sender_ata, recipient] = pre_states + .try_into() + .expect("Transfer instruction requires exactly three accounts"); + ata_program::transfer::transfer_from_associated_token_account( + owner, + sender_ata, + recipient, + ata_program_id, + amount, + ) + } + Instruction::Burn { + ata_program_id, + amount, + } => { + let [owner, holder_ata, token_definition] = pre_states + .try_into() + .expect("Burn instruction requires exactly three accounts"); + ata_program::burn::burn_from_associated_token_account( + owner, + holder_ata, + token_definition, + ata_program_id, + amount, + ) + } + }; + + ProgramOutput::new(instruction_words, pre_states_clone, post_states) + .with_chained_calls(chained_calls) + .write(); +} diff --git a/program_methods/guest/src/bin/authenticated_transfer.rs b/program_methods/guest/src/bin/authenticated_transfer.rs index 7835f733..2fb0ea8b 100644 --- a/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,13 +1,13 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, program::{ - AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, + AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs, }, }; /// Initializes a default account under the ownership of this program. fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState { - let account_to_claim = AccountPostState::new_claimed(pre_state.account); + let account_to_claim = AccountPostState::new_claimed(pre_state.account, Claim::Authorized); let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values @@ -52,7 +52,7 @@ fn transfer( // Claim recipient account if it has default program owner if recipient_post_account.program_owner == DEFAULT_PROGRAM_ID { - AccountPostState::new_claimed(recipient_post_account) + AccountPostState::new_claimed(recipient_post_account, Claim::Authorized) } else { AccountPostState::new(recipient_post_account) } @@ -84,5 +84,5 @@ fn main() { _ => panic!("invalid params"), }; - write_nssa_outputs(instruction_words, pre_states, post_states); + ProgramOutput::new(instruction_words, pre_states, post_states).write(); } diff --git a/program_methods/guest/src/bin/clock.rs b/program_methods/guest/src/bin/clock.rs index b938d9fc..c74265d1 100644 --- a/program_methods/guest/src/bin/clock.rs +++ b/program_methods/guest/src/bin/clock.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::AccountWithMetadata, - program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}, + program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}, }; type Instruction = nssa_core::Timestamp; @@ -57,9 +57,9 @@ fn main() { let (pre_10, post_10) = update_if_multiple(pre_10, 10, current_block_id, updated_data); let (pre_50, post_50) = update_if_multiple(pre_50, 50, current_block_id, updated_data); - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![pre_01, pre_10, pre_50], vec![post_01, post_10, post_50], - ); + ).write(); } diff --git a/program_methods/guest/src/bin/pinata.rs b/program_methods/guest/src/bin/pinata.rs index c9fc0735..2f85f069 100644 --- a/program_methods/guest/src/bin/pinata.rs +++ b/program_methods/guest/src/bin/pinata.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; use risc0_zkvm::sha::{Impl, Sha256 as _}; const PRIZE: u128 = 150; @@ -78,12 +78,13 @@ fn main() { .checked_add(PRIZE) .expect("Overflow when adding prize to winner"); - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![pinata, winner], vec![ - AccountPostState::new_claimed_if_default(pinata_post), + AccountPostState::new_claimed_if_default(pinata_post, Claim::Authorized), AccountPostState::new(winner_post), ], - ); + ) + .write(); } diff --git a/program_methods/guest/src/bin/pinata_token.rs b/program_methods/guest/src/bin/pinata_token.rs index f1bbdc87..3dee05b7 100644 --- a/program_methods/guest/src/bin/pinata_token.rs +++ b/program_methods/guest/src/bin/pinata_token.rs @@ -1,8 +1,7 @@ use nssa_core::{ account::Data, program::{ - AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs, - write_nssa_outputs_with_chained_call, + AccountPostState, ChainedCall, PdaSeed, ProgramInput, ProgramOutput, read_nssa_inputs, }, }; use risc0_zkvm::sha::{Impl, Sha256 as _}; @@ -97,7 +96,7 @@ fn main() { ) .with_pda_seeds(vec![PdaSeed::new([0; 32])]); - write_nssa_outputs_with_chained_call( + ProgramOutput::new( instruction_words, vec![ pinata_definition, @@ -109,6 +108,7 @@ fn main() { AccountPostState::new(pinata_token_holding_post), AccountPostState::new(winner_token_holding_post), ], - vec![chained_call], - ); + ) + .with_chained_calls(vec![chained_call]) + .write(); } diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 99782d7f..e53334f9 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -10,8 +10,9 @@ use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Nonce}, compute_digest_for_path, program::{ - AccountPostState, ChainedCall, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramId, - ProgramOutput, validate_execution, + AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID, + MAX_NUMBER_CHAINED_CALLS, ProgramId, ProgramOutput, TimestampValidityWindow, + validate_execution, }, }; use risc0_zkvm::{guest::env, serde::to_vec}; @@ -20,11 +21,53 @@ use risc0_zkvm::{guest::env, serde::to_vec}; struct ExecutionState { pre_states: Vec, post_states: HashMap, + block_validity_window: BlockValidityWindow, + timestamp_validity_window: TimestampValidityWindow, } impl ExecutionState { /// Validate program outputs and derive the overall execution state. - pub fn derive_from_outputs(program_id: ProgramId, program_outputs: Vec) -> Self { + pub fn derive_from_outputs( + visibility_mask: &[u8], + program_id: ProgramId, + program_outputs: Vec, + ) -> Self { + let block_valid_from = program_outputs + .iter() + .filter_map(|output| output.block_validity_window.start()) + .max(); + let block_valid_until = program_outputs + .iter() + .filter_map(|output| output.block_validity_window.end()) + .min(); + let ts_valid_from = program_outputs + .iter() + .filter_map(|output| output.timestamp_validity_window.start()) + .max(); + let ts_valid_until = program_outputs + .iter() + .filter_map(|output| output.timestamp_validity_window.end()) + .min(); + + let block_validity_window: BlockValidityWindow = (block_valid_from, block_valid_until) + .try_into() + .expect( + "There should be non empty intersection in the program output block validity windows", + ); + let timestamp_validity_window: TimestampValidityWindow = + (ts_valid_from, ts_valid_until) + .try_into() + .expect( + "There should be non empty intersection in the program output timestamp validity windows", + ); + + let mut execution_state = Self { + pre_states: Vec::new(), + post_states: HashMap::new(), + block_validity_window, + timestamp_validity_window, + }; + let Some(first_output) = program_outputs.first() else { panic!("No program outputs provided"); }; @@ -37,11 +80,6 @@ impl ExecutionState { }; let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); - let mut execution_state = Self { - pre_states: Vec::new(), - post_states: HashMap::new(), - }; - let mut program_outputs_iter = program_outputs.into_iter(); let mut chain_calls_counter = 0; @@ -87,6 +125,7 @@ impl ExecutionState { &chained_call.pda_seeds, ); execution_state.validate_and_sync_states( + visibility_mask, chained_call.program_id, &authorized_pdas, program_output.pre_states, @@ -119,7 +158,7 @@ impl ExecutionState { { assert_ne!( post.program_owner, DEFAULT_PROGRAM_ID, - "Account {account_id:?} was modified but not claimed" + "Account {account_id} was modified but not claimed" ); } @@ -129,6 +168,7 @@ impl ExecutionState { /// Validate program pre and post states and populate the execution state. fn validate_and_sync_states( &mut self, + visibility_mask: &[u8], program_id: ProgramId, authorized_pdas: &HashSet, pre_states: Vec, @@ -136,14 +176,25 @@ impl ExecutionState { ) { for (pre, mut post) in pre_states.into_iter().zip(post_states) { let pre_account_id = pre.account_id; + let pre_is_authorized = pre.is_authorized; let post_states_entry = self.post_states.entry(pre.account_id); match &post_states_entry { Entry::Occupied(occupied) => { + #[expect( + clippy::shadow_unrelated, + reason = "Shadowing is intentional to use all fields" + )] + let AccountWithMetadata { + account: pre_account, + account_id: pre_account_id, + is_authorized: pre_is_authorized, + } = pre; + // Ensure that new pre state is the same as known post state assert_eq!( occupied.get(), - &pre.account, - "Inconsistent pre state for account {pre_account_id:?}", + &pre_account, + "Inconsistent pre state for account {pre_account_id}", ); let previous_is_authorized = self @@ -152,7 +203,7 @@ impl ExecutionState { .find(|acc| acc.account_id == pre_account_id) .map_or_else( || panic!( - "Pre state must exist in execution state for account {pre_account_id:?}", + "Pre state must exist in execution state for account {pre_account_id}", ), |acc| acc.is_authorized ); @@ -161,22 +212,57 @@ impl ExecutionState { previous_is_authorized || authorized_pdas.contains(&pre_account_id); assert_eq!( - pre.is_authorized, is_authorized, - "Inconsistent authorization for account {pre_account_id:?}", + pre_is_authorized, is_authorized, + "Inconsistent authorization for account {pre_account_id}", ); } Entry::Vacant(_) => { + // Pre state for the initial call self.pre_states.push(pre); } } - if post.requires_claim() { + if let Some(claim) = post.required_claim() { // The invoked program can only claim accounts with default program id. - if post.account().program_owner == DEFAULT_PROGRAM_ID { - post.account_mut().program_owner = program_id; + assert_eq!( + post.account().program_owner, + DEFAULT_PROGRAM_ID, + "Cannot claim an initialized account {pre_account_id}" + ); + + let pre_state_position = self + .pre_states + .iter() + .position(|acc| acc.account_id == pre_account_id) + .expect("Pre state must exist at this point"); + + let is_public_account = visibility_mask[pre_state_position] == 0; + if is_public_account { + match claim { + Claim::Authorized => { + // Note: no need to check authorized pdas because we have already + // checked consistency of authorization above. + assert!( + pre_is_authorized, + "Cannot claim unauthorized account {pre_account_id}" + ); + } + Claim::Pda(seed) => { + let pda = AccountId::from((&program_id, &seed)); + assert_eq!( + pre_account_id, pda, + "Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}" + ); + } + } } else { - panic!("Cannot claim an initialized account {pre_account_id:?}"); + // We don't care about the exact claim mechanism for private accounts. + // This is because the main reason to have it is to protect against PDA griefing + // attacks in public execution, while private PDA doesn't make much sense + // anyway. } + + post.account_mut().program_owner = program_id; } post_states_entry.insert_entry(post.into_account()); @@ -210,6 +296,8 @@ fn compute_circuit_output( ciphertexts: Vec::new(), new_commitments: Vec::new(), new_nullifiers: Vec::new(), + block_validity_window: execution_state.block_validity_window, + timestamp_validity_window: execution_state.timestamp_validity_window, }; let states_iter = execution_state.into_states_iter(); @@ -392,7 +480,8 @@ fn main() { program_id, } = env::read(); - let execution_state = ExecutionState::derive_from_outputs(program_id, program_outputs); + let execution_state = + ExecutionState::derive_from_outputs(&visibility_mask, program_id, program_outputs); let output = compute_circuit_output( execution_state, diff --git a/program_methods/guest/src/bin/token.rs b/program_methods/guest/src/bin/token.rs index 0bc3d245..421d43ef 100644 --- a/program_methods/guest/src/bin/token.rs +++ b/program_methods/guest/src/bin/token.rs @@ -6,7 +6,7 @@ //! Token program accepts [`Instruction`] as input, refer to the corresponding documentation //! for more details. -use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs}; use token_program::core::Instruction; fn main() { @@ -81,5 +81,5 @@ fn main() { } }; - write_nssa_outputs(instruction_words, pre_states_clone, post_states); + ProgramOutput::new(instruction_words, pre_states_clone, post_states).write(); } diff --git a/programs/amm/src/new_definition.rs b/programs/amm/src/new_definition.rs index 366eb747..88263b87 100644 --- a/programs/amm/src/new_definition.rs +++ b/programs/amm/src/new_definition.rs @@ -2,11 +2,11 @@ use std::num::NonZeroU128; use amm_core::{ PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, - compute_pool_pda, compute_vault_pda, + compute_pool_pda, compute_pool_pda_seed, compute_vault_pda, compute_vault_pda_seed, }; use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::{AccountPostState, ChainedCall, ProgramId}, + program::{AccountPostState, ChainedCall, Claim, ProgramId}, }; #[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] @@ -108,36 +108,52 @@ pub fn new_definition( }; pool_post.data = Data::from(&pool_post_definition); - let pool_post = AccountPostState::new_claimed_if_default(pool_post); + let pool_pda_seed = compute_pool_pda_seed(definition_token_a_id, definition_token_b_id); + let pool_post = AccountPostState::new_claimed_if_default(pool_post, Claim::Pda(pool_pda_seed)); let token_program_id = user_holding_a.account.program_owner; // Chain call for Token A (user_holding_a -> Vault_A) + let vault_a_seed = compute_vault_pda_seed(pool.account_id, definition_token_a_id); + let vault_a_authorized = AccountWithMetadata { + is_authorized: true, + ..vault_a.clone() + }; let call_token_a = ChainedCall::new( token_program_id, - vec![user_holding_a.clone(), vault_a.clone()], + vec![user_holding_a.clone(), vault_a_authorized], &token_core::Instruction::Transfer { amount_to_transfer: token_a_amount.into(), }, - ); + ) + .with_pda_seeds(vec![vault_a_seed]); + // Chain call for Token B (user_holding_b -> Vault_B) + let vault_b_seed = compute_vault_pda_seed(pool.account_id, definition_token_b_id); + let vault_b_authorized = AccountWithMetadata { + is_authorized: true, + ..vault_b.clone() + }; let call_token_b = ChainedCall::new( token_program_id, - vec![user_holding_b.clone(), vault_b.clone()], + vec![user_holding_b.clone(), vault_b_authorized], &token_core::Instruction::Transfer { amount_to_transfer: token_b_amount.into(), }, - ); - - let mut pool_lp_auth = pool_definition_lp.clone(); - pool_lp_auth.is_authorized = true; + ) + .with_pda_seeds(vec![vault_b_seed]); + let pool_lp_pda_seed = compute_liquidity_token_pda_seed(pool.account_id); + let pool_lp_authorized = AccountWithMetadata { + is_authorized: true, + ..pool_definition_lp.clone() + }; let call_token_lp = ChainedCall::new( token_program_id, - vec![pool_lp_auth, user_holding_lp.clone()], + vec![pool_lp_authorized, user_holding_lp.clone()], &instruction, ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); + .with_pda_seeds(vec![pool_lp_pda_seed]); let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index 01c543ff..13ae7a89 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -1,4 +1,4 @@ -use std::num::NonZero; +use std::{num::NonZero, vec}; use amm_core::{ PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, @@ -1756,7 +1756,7 @@ impl AccountsForExeTests { definition_id: IdForExeTests::token_lp_definition_id(), balance: BalanceForExeTests::lp_supply_init(), }), - nonce: 0_u128.into(), + nonce: 1_u128.into(), } } @@ -1801,7 +1801,7 @@ impl AccountsForExeTests { definition_id: IdForExeTests::token_lp_definition_id(), balance: 0, }), - nonce: 0_u128.into(), + nonce: 1.into(), } } } @@ -2733,7 +2733,7 @@ fn simple_amm_remove() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -2799,7 +2799,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() { IdForExeTests::user_token_b_id(), IdForExeTests::user_token_lp_id(), ], - vec![0_u128.into(), 0_u128.into()], + vec![0_u128.into(), 0_u128.into(), 0_u128.into()], instruction, ) .unwrap(); @@ -2809,11 +2809,12 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() { &[ &PrivateKeysForTests::user_token_a_key(), &PrivateKeysForTests::user_token_b_key(), + &PrivateKeysForTests::user_token_lp_key(), ], ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -2897,7 +2898,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_init_user_lp() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -2955,7 +2956,7 @@ fn simple_amm_new_definition_uninitialized_pool() { IdForExeTests::user_token_b_id(), IdForExeTests::user_token_lp_id(), ], - vec![0_u128.into(), 0_u128.into()], + vec![0_u128.into(), 0_u128.into(), 0_u128.into()], instruction, ) .unwrap(); @@ -2965,11 +2966,12 @@ fn simple_amm_new_definition_uninitialized_pool() { &[ &PrivateKeysForTests::user_token_a_key(), &PrivateKeysForTests::user_token_b_key(), + &PrivateKeysForTests::user_token_lp_key(), ], ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -3031,7 +3033,7 @@ fn simple_amm_add() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -3088,7 +3090,7 @@ fn simple_amm_swap_1() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -3138,7 +3140,7 @@ fn simple_amm_swap_2() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); diff --git a/programs/associated_token_account/Cargo.toml b/programs/associated_token_account/Cargo.toml new file mode 100644 index 00000000..98e0bfd0 --- /dev/null +++ b/programs/associated_token_account/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ata_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +nssa_core.workspace = true +token_core.workspace = true +ata_core.workspace = true diff --git a/programs/associated_token_account/core/Cargo.toml b/programs/associated_token_account/core/Cargo.toml new file mode 100644 index 00000000..7ca8d7fa --- /dev/null +++ b/programs/associated_token_account/core/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ata_core" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +nssa_core.workspace = true +serde.workspace = true +risc0-zkvm.workspace = true diff --git a/programs/associated_token_account/core/src/lib.rs b/programs/associated_token_account/core/src/lib.rs new file mode 100644 index 00000000..994c632b --- /dev/null +++ b/programs/associated_token_account/core/src/lib.rs @@ -0,0 +1,82 @@ +pub use nssa_core::program::PdaSeed; +use nssa_core::{ + account::{AccountId, AccountWithMetadata}, + program::ProgramId, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub enum Instruction { + /// Create the Associated Token Account for (owner, definition). + /// Idempotent: no-op if the account already exists. + /// + /// Required accounts (3): + /// - Owner account + /// - Token definition account + /// - Associated token account (default/uninitialized, or already initialized) + /// + /// `token_program_id` is derived from `token_definition.account.program_owner`. + Create { ata_program_id: ProgramId }, + + /// Transfer tokens FROM owner's ATA to a recipient holding account. + /// Uses PDA seeds to authorize the ATA in the chained Token::Transfer call. + /// + /// Required accounts (3): + /// - Owner account (authorized) + /// - Sender ATA (owner's token holding) + /// - Recipient token holding (any account; auto-created if default) + /// + /// `token_program_id` is derived from `sender_ata.account.program_owner`. + Transfer { + ata_program_id: ProgramId, + amount: u128, + }, + + /// Burn tokens FROM owner's ATA. + /// Uses PDA seeds to authorize the ATA in the chained Token::Burn call. + /// + /// Required accounts (3): + /// - Owner account (authorized) + /// - Owner's ATA (the holding to burn from) + /// - Token definition account + /// + /// `token_program_id` is derived from `holder_ata.account.program_owner`. + Burn { + ata_program_id: ProgramId, + amount: u128, + }, +} + +pub fn compute_ata_seed(owner_id: AccountId, definition_id: AccountId) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + let mut bytes = [0u8; 64]; + bytes[0..32].copy_from_slice(&owner_id.to_bytes()); + bytes[32..64].copy_from_slice(&definition_id.to_bytes()); + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + +pub fn get_associated_token_account_id(ata_program_id: &ProgramId, seed: &PdaSeed) -> AccountId { + AccountId::from((ata_program_id, seed)) +} + +/// Verify the ATA's address matches `(ata_program_id, owner, definition)` and return +/// the [`PdaSeed`] for use in chained calls. +pub fn verify_ata_and_get_seed( + ata_account: &AccountWithMetadata, + owner: &AccountWithMetadata, + definition_id: AccountId, + ata_program_id: ProgramId, +) -> PdaSeed { + let seed = compute_ata_seed(owner.account_id, definition_id); + let expected_id = get_associated_token_account_id(&ata_program_id, &seed); + assert_eq!( + ata_account.account_id, expected_id, + "ATA account ID does not match expected derivation" + ); + seed +} diff --git a/programs/associated_token_account/src/burn.rs b/programs/associated_token_account/src/burn.rs new file mode 100644 index 00000000..4940fdeb --- /dev/null +++ b/programs/associated_token_account/src/burn.rs @@ -0,0 +1,39 @@ +use nssa_core::{ + account::AccountWithMetadata, + program::{AccountPostState, ChainedCall, ProgramId}, +}; +use token_core::TokenHolding; + +pub fn burn_from_associated_token_account( + owner: AccountWithMetadata, + holder_ata: AccountWithMetadata, + token_definition: AccountWithMetadata, + ata_program_id: ProgramId, + amount: u128, +) -> (Vec, Vec) { + let token_program_id = holder_ata.account.program_owner; + assert!(owner.is_authorized, "Owner authorization is missing"); + let definition_id = TokenHolding::try_from(&holder_ata.account.data) + .expect("Holder ATA must hold a valid token") + .definition_id(); + let seed = + ata_core::verify_ata_and_get_seed(&holder_ata, &owner, definition_id, ata_program_id); + + let post_states = vec![ + AccountPostState::new(owner.account.clone()), + AccountPostState::new(holder_ata.account.clone()), + AccountPostState::new(token_definition.account.clone()), + ]; + let mut holder_ata_auth = holder_ata.clone(); + holder_ata_auth.is_authorized = true; + + let chained_call = ChainedCall::new( + token_program_id, + vec![token_definition.clone(), holder_ata_auth], + &token_core::Instruction::Burn { + amount_to_burn: amount, + }, + ) + .with_pda_seeds(vec![seed]); + (post_states, vec![chained_call]) +} diff --git a/programs/associated_token_account/src/create.rs b/programs/associated_token_account/src/create.rs new file mode 100644 index 00000000..d44f5d1c --- /dev/null +++ b/programs/associated_token_account/src/create.rs @@ -0,0 +1,50 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::{AccountPostState, ChainedCall, Claim, ProgramId}, +}; + +pub fn create_associated_token_account( + owner: AccountWithMetadata, + token_definition: AccountWithMetadata, + ata_account: AccountWithMetadata, + ata_program_id: ProgramId, +) -> (Vec, Vec) { + // No authorization check needed: create is idempotent, so anyone can call it safely. + let token_program_id = token_definition.account.program_owner; + let ata_seed = ata_core::verify_ata_and_get_seed( + &ata_account, + &owner, + token_definition.account_id, + ata_program_id, + ); + + // Idempotent: already initialized → no-op + if ata_account.account != Account::default() { + return ( + vec![ + AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized), + AccountPostState::new(token_definition.account.clone()), + AccountPostState::new(ata_account.account.clone()), + ], + vec![], + ); + } + + let post_states = vec![ + AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized), + AccountPostState::new(token_definition.account.clone()), + AccountPostState::new(ata_account.account.clone()), + ]; + let ata_account_auth = AccountWithMetadata { + is_authorized: true, + ..ata_account.clone() + }; + let chained_call = ChainedCall::new( + token_program_id, + vec![token_definition.clone(), ata_account_auth], + &token_core::Instruction::InitializeAccount, + ) + .with_pda_seeds(vec![ata_seed]); + + (post_states, vec![chained_call]) +} diff --git a/programs/associated_token_account/src/lib.rs b/programs/associated_token_account/src/lib.rs new file mode 100644 index 00000000..13740f0a --- /dev/null +++ b/programs/associated_token_account/src/lib.rs @@ -0,0 +1,10 @@ +//! The Associated Token Account Program implementation. + +pub use ata_core as core; + +pub mod burn; +pub mod create; +pub mod transfer; + +#[cfg(test)] +mod tests; diff --git a/programs/associated_token_account/src/tests.rs b/programs/associated_token_account/src/tests.rs new file mode 100644 index 00000000..9835bf37 --- /dev/null +++ b/programs/associated_token_account/src/tests.rs @@ -0,0 +1,153 @@ +#![cfg(test)] + +use ata_core::{compute_ata_seed, get_associated_token_account_id}; +use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data}; +use token_core::{TokenDefinition, TokenHolding}; + +const ATA_PROGRAM_ID: nssa_core::program::ProgramId = [1u32; 8]; +const TOKEN_PROGRAM_ID: nssa_core::program::ProgramId = [2u32; 8]; + +fn owner_id() -> AccountId { + AccountId::new([0x01u8; 32]) +} + +fn definition_id() -> AccountId { + AccountId::new([0x02u8; 32]) +} + +fn ata_id() -> AccountId { + get_associated_token_account_id( + &ATA_PROGRAM_ID, + &compute_ata_seed(owner_id(), definition_id()), + ) +} + +fn owner_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: owner_id(), + } +} + +fn definition_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0, + data: Data::from(&TokenDefinition::Fungible { + name: "TEST".to_string(), + total_supply: 1000, + metadata_id: None, + }), + nonce: nssa_core::account::Nonce(0), + }, + is_authorized: false, + account_id: definition_id(), + } +} + +fn uninitialized_ata_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: ata_id(), + } +} + +fn initialized_ata_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0, + data: Data::from(&TokenHolding::Fungible { + definition_id: definition_id(), + balance: 100, + }), + nonce: nssa_core::account::Nonce(0), + }, + is_authorized: false, + account_id: ata_id(), + } +} + +#[test] +fn create_emits_chained_call_for_uninitialized_ata() { + let (post_states, chained_calls) = crate::create::create_associated_token_account( + owner_account(), + definition_account(), + uninitialized_ata_account(), + ATA_PROGRAM_ID, + ); + + assert_eq!(post_states.len(), 3); + assert_eq!(chained_calls.len(), 1); + assert_eq!(chained_calls[0].program_id, TOKEN_PROGRAM_ID); +} + +#[test] +fn create_is_idempotent_for_initialized_ata() { + let (post_states, chained_calls) = crate::create::create_associated_token_account( + owner_account(), + definition_account(), + initialized_ata_account(), + ATA_PROGRAM_ID, + ); + + assert_eq!(post_states.len(), 3); + assert!( + chained_calls.is_empty(), + "Should emit no chained call for already-initialized ATA" + ); +} + +#[test] +#[should_panic(expected = "ATA account ID does not match expected derivation")] +fn create_panics_on_wrong_ata_address() { + let wrong_ata = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: AccountId::new([0xFFu8; 32]), + }; + + crate::create::create_associated_token_account( + owner_account(), + definition_account(), + wrong_ata, + ATA_PROGRAM_ID, + ); +} + +#[test] +fn get_associated_token_account_id_is_deterministic() { + let seed = compute_ata_seed(owner_id(), definition_id()); + let id1 = get_associated_token_account_id(&ATA_PROGRAM_ID, &seed); + let id2 = get_associated_token_account_id(&ATA_PROGRAM_ID, &seed); + assert_eq!(id1, id2); +} + +#[test] +fn get_associated_token_account_id_differs_by_owner() { + let other_owner = AccountId::new([0x99u8; 32]); + let id1 = get_associated_token_account_id( + &ATA_PROGRAM_ID, + &compute_ata_seed(owner_id(), definition_id()), + ); + let id2 = get_associated_token_account_id( + &ATA_PROGRAM_ID, + &compute_ata_seed(other_owner, definition_id()), + ); + assert_ne!(id1, id2); +} + +#[test] +fn get_associated_token_account_id_differs_by_definition() { + let other_def = AccountId::new([0x99u8; 32]); + let id1 = get_associated_token_account_id( + &ATA_PROGRAM_ID, + &compute_ata_seed(owner_id(), definition_id()), + ); + let id2 = + get_associated_token_account_id(&ATA_PROGRAM_ID, &compute_ata_seed(owner_id(), other_def)); + assert_ne!(id1, id2); +} diff --git a/programs/associated_token_account/src/transfer.rs b/programs/associated_token_account/src/transfer.rs new file mode 100644 index 00000000..89d70135 --- /dev/null +++ b/programs/associated_token_account/src/transfer.rs @@ -0,0 +1,39 @@ +use nssa_core::{ + account::AccountWithMetadata, + program::{AccountPostState, ChainedCall, ProgramId}, +}; +use token_core::TokenHolding; + +pub fn transfer_from_associated_token_account( + owner: AccountWithMetadata, + sender_ata: AccountWithMetadata, + recipient: AccountWithMetadata, + ata_program_id: ProgramId, + amount: u128, +) -> (Vec, Vec) { + let token_program_id = sender_ata.account.program_owner; + assert!(owner.is_authorized, "Owner authorization is missing"); + let definition_id = TokenHolding::try_from(&sender_ata.account.data) + .expect("Sender ATA must hold a valid token") + .definition_id(); + let seed = + ata_core::verify_ata_and_get_seed(&sender_ata, &owner, definition_id, ata_program_id); + + let post_states = vec![ + AccountPostState::new(owner.account.clone()), + AccountPostState::new(sender_ata.account.clone()), + AccountPostState::new(recipient.account.clone()), + ]; + let mut sender_ata_auth = sender_ata.clone(); + sender_ata_auth.is_authorized = true; + + let chained_call = ChainedCall::new( + token_program_id, + vec![sender_ata_auth, recipient.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: amount, + }, + ) + .with_pda_seeds(vec![seed]); + (post_states, vec![chained_call]) +} diff --git a/programs/token/core/src/lib.rs b/programs/token/core/src/lib.rs index 1edbc895..79f49303 100644 --- a/programs/token/core/src/lib.rs +++ b/programs/token/core/src/lib.rs @@ -10,23 +10,23 @@ pub enum Instruction { /// Transfer tokens from sender to recipient. /// /// Required accounts: - /// - Sender's Token Holding account (authorized), - /// - Recipient's Token Holding account. + /// - Sender's Token Holding account (initialized, authorized), + /// - Recipient's Token Holding account (initialized or authorized and uninitialized). Transfer { amount_to_transfer: u128 }, /// Create a new fungible token definition without metadata. /// /// Required accounts: - /// - Token Definition account (uninitialized), - /// - Token Holding account (uninitialized). + /// - Token Definition account (uninitialized, authorized), + /// - Token Holding account (uninitialized, authorized). NewFungibleDefinition { name: String, total_supply: u128 }, /// Create a new fungible or non-fungible token definition with metadata. /// /// Required accounts: - /// - Token Definition account (uninitialized), - /// - Token Holding account (uninitialized), - /// - Token Metadata account (uninitialized). + /// - Token Definition account (uninitialized, authorized), + /// - Token Holding account (uninitialized, authorized), + /// - Token Metadata account (uninitialized, authorized). NewDefinitionWithMetadata { new_definition: NewTokenDefinition, /// Boxed to avoid large enum variant size. @@ -36,29 +36,29 @@ pub enum Instruction { /// Initialize a token holding account for a given token definition. /// /// Required accounts: - /// - Token Definition account (initialized), - /// - Token Holding account (uninitialized), + /// - Token Definition account (initialized, any authorization), + /// - Token Holding account (uninitialized, authorized), InitializeAccount, /// Burn tokens from the holder's account. /// /// Required accounts: - /// - Token Definition account (initialized), - /// - Token Holding account (authorized). + /// - Token Definition account (initialized, any authorization), + /// - Token Holding account (initialized, authorized). Burn { amount_to_burn: u128 }, /// Mint new tokens to the holder's account. /// /// Required accounts: - /// - Token Definition account (authorized), - /// - Token Holding account (uninitialized or initialized). + /// - Token Definition account (initialized, authorized), + /// - Token Holding account (uninitialized or authorized and initialized). Mint { amount_to_mint: u128 }, /// Print a new NFT from the master copy. /// /// Required accounts: /// - NFT Master Token Holding account (authorized), - /// - NFT Printed Copy Token Holding account (uninitialized). + /// - NFT Printed Copy Token Holding account (uninitialized, authorized). PrintNft, } diff --git a/programs/token/src/initialize.rs b/programs/token/src/initialize.rs index dc0b612a..fabb8fd9 100644 --- a/programs/token/src/initialize.rs +++ b/programs/token/src/initialize.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::{TokenDefinition, TokenHolding}; @@ -30,6 +30,6 @@ pub fn initialize_account( vec![ AccountPostState::new(definition_post), - AccountPostState::new_claimed(account_to_initialize), + AccountPostState::new_claimed(account_to_initialize, Claim::Authorized), ] } diff --git a/programs/token/src/mint.rs b/programs/token/src/mint.rs index 8b157340..5a15d81f 100644 --- a/programs/token/src/mint.rs +++ b/programs/token/src/mint.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::{TokenDefinition, TokenHolding}; @@ -67,6 +67,6 @@ pub fn mint( vec![ AccountPostState::new(definition_post), - AccountPostState::new_claimed_if_default(holding_post), + AccountPostState::new_claimed_if_default(holding_post, Claim::Authorized), ] } diff --git a/programs/token/src/new_definition.rs b/programs/token/src/new_definition.rs index 8da55dc1..ba510feb 100644 --- a/programs/token/src/new_definition.rs +++ b/programs/token/src/new_definition.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::{ NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata, @@ -42,8 +42,8 @@ pub fn new_fungible_definition( holding_target_account_post.data = Data::from(&token_holding); vec![ - AccountPostState::new_claimed(definition_target_account_post), - AccountPostState::new_claimed(holding_target_account_post), + AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized), + AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized), ] } @@ -119,8 +119,8 @@ pub fn new_definition_with_metadata( metadata_target_account_post.data = Data::from(&token_metadata); vec![ - AccountPostState::new_claimed(definition_target_account_post), - AccountPostState::new_claimed(holding_target_account_post), - AccountPostState::new_claimed(metadata_target_account_post), + AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized), + AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized), + AccountPostState::new_claimed(metadata_target_account_post, Claim::Authorized), ] } diff --git a/programs/token/src/print_nft.rs b/programs/token/src/print_nft.rs index c7177a43..6bc9612d 100644 --- a/programs/token/src/print_nft.rs +++ b/programs/token/src/print_nft.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::TokenHolding; @@ -50,6 +50,6 @@ pub fn print_nft( vec![ AccountPostState::new(master_account_post), - AccountPostState::new_claimed(printed_account_post), + AccountPostState::new_claimed(printed_account_post, Claim::Authorized), ] } diff --git a/programs/token/src/tests.rs b/programs/token/src/tests.rs index 640d6d76..4c28d769 100644 --- a/programs/token/src/tests.rs +++ b/programs/token/src/tests.rs @@ -5,7 +5,10 @@ reason = "We don't care about it in tests" )] -use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data}; +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data}, + program::Claim, +}; use token_core::{ MetadataStandard, NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, }; @@ -851,7 +854,7 @@ fn mint_uninit_holding_success() { *holding_post.account(), AccountForTests::init_mint().account ); - assert!(holding_post.requires_claim()); + assert_eq!(holding_post.required_claim(), Some(Claim::Authorized)); } #[test] diff --git a/programs/token/src/transfer.rs b/programs/token/src/transfer.rs index 392f630e..2ffd2339 100644 --- a/programs/token/src/transfer.rs +++ b/programs/token/src/transfer.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::TokenHolding; @@ -106,6 +106,6 @@ pub fn transfer( vec![ AccountPostState::new(sender_post), - AccountPostState::new_claimed_if_default(recipient_post), + AccountPostState::new_claimed_if_default(recipient_post, Claim::Authorized), ] } diff --git a/sequencer/core/Cargo.toml b/sequencer/core/Cargo.toml index 334f093c..e1ff0895 100644 --- a/sequencer/core/Cargo.toml +++ b/sequencer/core/Cargo.toml @@ -14,8 +14,8 @@ common.workspace = true storage.workspace = true mempool.workspace = true bedrock_client.workspace = true +testnet_initial_state.workspace = true -base58.workspace = true anyhow.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/sequencer/core/src/config.rs b/sequencer/core/src/config.rs index 9dd10680..2fb101aa 100644 --- a/sequencer/core/src/config.rs +++ b/sequencer/core/src/config.rs @@ -8,13 +8,11 @@ use std::{ use anyhow::Result; use bedrock_client::BackoffConfig; use bytesize::ByteSize; -use common::{ - block::{AccountInitialData, CommitmentsInitialData}, - config::BasicAuth, -}; +use common::config::BasicAuth; use humantime_serde; use logos_blockchain_core::mantle::ops::channel::ChannelId; use serde::{Deserialize, Serialize}; +use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData}; use url::Url; // TODO: Provide default values @@ -39,16 +37,16 @@ pub struct SequencerConfig { /// Interval in which pending blocks are retried. #[serde(with = "humantime_serde")] pub retry_pending_blocks_timeout: Duration, - /// List of initial accounts data. - pub initial_accounts: Vec, - /// List of initial commitments. - pub initial_commitments: Vec, /// Sequencer own signing key. pub signing_key: [u8; 32], /// Bedrock configuration options. pub bedrock_config: BedrockConfig, /// Indexer RPC URL. pub indexer_rpc_url: Url, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_public_accounts: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_private_accounts: Option>, } #[derive(Clone, Serialize, Deserialize)] diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 6f9bf7e1..ef92fa8a 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -15,7 +15,10 @@ use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SI use mempool::{MemPool, MemPoolHandle}; #[cfg(feature = "mock")] pub use mock::SequencerCoreWithMockClients; +use nssa::V03State; +use nssa_core::{BlockId, Timestamp}; pub use storage::error::DbError; +use testnet_initial_state::initial_state; use crate::{ block_settlement_client::{BlockSettlementClient, BlockSettlementClientTrait, MsgId}, @@ -98,34 +101,49 @@ impl SequencerCore = config - .initial_commitments - .iter() - .map(|init_comm_data| { - let npk = &init_comm_data.npk; - let mut acc = init_comm_data.account.clone(); + let initial_commitments: Option> = config + .initial_private_accounts + .clone() + .map(|initial_commitments| { + initial_commitments + .iter() + .map(|init_comm_data| { + let npk = &init_comm_data.npk; - acc.program_owner = - nssa::program::Program::authenticated_transfer_program().id(); + let mut acc = init_comm_data.account.clone(); - nssa_core::Commitment::new(npk, &acc) - }) - .collect(); + acc.program_owner = + nssa::program::Program::authenticated_transfer_program().id(); - let init_accs: Vec<(nssa::AccountId, u128)> = config - .initial_accounts - .iter() - .map(|acc_data| (acc_data.account_id, acc_data.balance)) - .collect(); + nssa_core::Commitment::new(npk, &acc) + }) + .collect() + }); - nssa::V03State::new_with_genesis_accounts( - &init_accs, - &initial_commitments, - genesis_block.header.timestamp, - ) + let init_accs: Option> = config + .initial_public_accounts + .clone() + .map(|initial_accounts| { + initial_accounts + .iter() + .map(|acc_data| (acc_data.account_id, acc_data.balance)) + .collect() + }); + + // If initial commitments or accounts are present in config, need to construct state + // from them + if initial_commitments.is_some() || init_accs.is_some() { + V03State::new_with_genesis_accounts( + &init_accs.unwrap_or_default(), + &initial_commitments.unwrap_or_default(), + genesis_block.header.timestamp, + ) + } else { + initial_state() + } }; #[cfg(feature = "testnet")] @@ -149,12 +167,16 @@ impl SequencerCore Result { match &tx { - NSSATransaction::Public(tx) => self.state.transition_from_public_transaction(tx), + NSSATransaction::Public(tx) => self + .state + .transition_from_public_transaction(tx, block_id, timestamp), NSSATransaction::PrivacyPreserving(tx) => self .state - .transition_from_privacy_preserving_transaction(tx), + .transition_from_privacy_preserving_transaction(tx, block_id, timestamp), NSSATransaction::ProgramDeployment(tx) => self .state .transition_from_program_deployment_transaction(tx), @@ -188,10 +210,7 @@ impl SequencerCore Result<(SignedMantleTx, MsgId)> { let now = Instant::now(); - let new_block_height = self - .chain_height - .checked_add(1) - .with_context(|| format!("Max block height reached: {}", self.chain_height))?; + let new_block_height = self.next_block_id(); let mut valid_transactions = vec![]; @@ -203,7 +222,7 @@ impl SequencerCore SequencerCore SequencerCore { valid_transactions.push(valid_tx); @@ -290,7 +310,7 @@ impl SequencerCore { valid_transactions.push(clock_nssa_tx); @@ -304,7 +324,7 @@ impl SequencerCore SequencerCore IC { self.indexer_client.clone() } + + fn next_block_id(&self) -> u64 { + self.chain_height + .checked_add(1) + .unwrap_or_else(|| panic!("Max block height reached: {}", self.chain_height)) + } } /// Load signing key from file or generate a new one if it doesn't exist. @@ -414,26 +440,20 @@ fn load_or_create_signing_key(path: &Path) -> Result { mod tests { #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] - use std::{pin::pin, str::FromStr as _, time::Duration}; + use std::{pin::pin, time::Duration}; - use base58::ToBase58 as _; use bedrock_client::BackoffConfig; - use common::{ - block::AccountInitialData, test_utils::sequencer_sign_key_for_testing, - transaction::NSSATransaction, - }; + use common::{test_utils::sequencer_sign_key_for_testing, transaction::NSSATransaction}; use logos_blockchain_core::mantle::ops::channel::ChannelId; use mempool::MemPoolHandle; - use nssa::{AccountId, PrivateKey}; + use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys}; use crate::{ config::{BedrockConfig, SequencerConfig}, mock::SequencerCoreWithMockClients, }; - fn setup_sequencer_config_variable_initial_accounts( - initial_accounts: Vec, - ) -> SequencerConfig { + fn setup_sequencer_config() -> SequencerConfig { let tempdir = tempfile::tempdir().unwrap(); let home = tempdir.path().to_path_buf(); @@ -445,8 +465,6 @@ mod tests { max_block_size: bytesize::ByteSize::mib(1), mempool_max_size: 10000, block_create_timeout: Duration::from_secs(1), - initial_accounts, - initial_commitments: vec![], signing_key: *sequencer_sign_key_for_testing().value(), bedrock_config: BedrockConfig { backoff: BackoffConfig { @@ -459,41 +477,17 @@ mod tests { }, retry_pending_blocks_timeout: Duration::from_mins(4), indexer_rpc_url: "ws://localhost:8779".parse().unwrap(), + initial_public_accounts: None, + initial_private_accounts: None, } } - fn setup_sequencer_config() -> SequencerConfig { - let acc1_account_id: Vec = vec![ - 148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112, 83, - 153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102, - ]; - - let acc2_account_id: Vec = vec![ - 30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244, 92, - 94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204, - ]; - - let initial_acc1 = AccountInitialData { - account_id: AccountId::from_str(&acc1_account_id.to_base58()).unwrap(), - balance: 10000, - }; - - let initial_acc2 = AccountInitialData { - account_id: AccountId::from_str(&acc2_account_id.to_base58()).unwrap(), - balance: 20000, - }; - - let initial_accounts = vec![initial_acc1, initial_acc2]; - - setup_sequencer_config_variable_initial_accounts(initial_accounts) - } - fn create_signing_key_for_account1() -> nssa::PrivateKey { - nssa::PrivateKey::try_new([1; 32]).unwrap() + initial_pub_accounts_private_keys()[0].pub_sign_key.clone() } fn create_signing_key_for_account2() -> nssa::PrivateKey { - nssa::PrivateKey::try_new([2; 32]).unwrap() + initial_pub_accounts_private_keys()[1].pub_sign_key.clone() } async fn common_setup() -> (SequencerCoreWithMockClients, MemPoolHandle) { @@ -526,8 +520,8 @@ mod tests { assert_eq!(sequencer.chain_height, config.genesis_id); assert_eq!(sequencer.sequencer_config.max_num_tx_in_block, 10); - let acc1_account_id = config.initial_accounts[0].account_id; - let acc2_account_id = config.initial_accounts[1].account_id; + let acc1_account_id = initial_accounts()[0].account_id; + let acc2_account_id = initial_accounts()[1].account_id; let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance; let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance; @@ -536,47 +530,6 @@ mod tests { assert_eq!(20000, balance_acc_2); } - #[tokio::test] - async fn start_different_intial_accounts_balances() { - let acc1_account_id: Vec = vec![ - 27, 132, 197, 86, 123, 18, 100, 64, 153, 93, 62, 213, 170, 186, 5, 101, 215, 30, 24, - 52, 96, 72, 25, 255, 156, 23, 245, 233, 213, 221, 7, 143, - ]; - - let acc2_account_id: Vec = vec![ - 77, 75, 108, 209, 54, 16, 50, 202, 155, 210, 174, 185, 217, 0, 170, 77, 69, 217, 234, - 216, 10, 201, 66, 51, 116, 196, 81, 167, 37, 77, 7, 102, - ]; - - let initial_acc1 = AccountInitialData { - account_id: AccountId::from_str(&acc1_account_id.to_base58()).unwrap(), - balance: 10000, - }; - - let initial_acc2 = AccountInitialData { - account_id: AccountId::from_str(&acc2_account_id.to_base58()).unwrap(), - balance: 20000, - }; - - let initial_accounts = vec![initial_acc1, initial_acc2]; - - let config = setup_sequencer_config_variable_initial_accounts(initial_accounts); - let (sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - - let acc1_account_id = config.initial_accounts[0].account_id; - let acc2_account_id = config.initial_accounts[1].account_id; - - assert_eq!( - 10000, - sequencer.state.get_account_by_id(acc1_account_id).balance - ); - assert_eq!( - 20000, - sequencer.state.get_account_by_id(acc2_account_id).balance - ); - } - #[test] fn transaction_pre_check_pass() { let tx = common::test_utils::produce_dummy_empty_transaction(); @@ -587,10 +540,10 @@ mod tests { #[tokio::test] async fn transaction_pre_check_native_transfer_valid() { - let (sequencer, _mempool_handle) = common_setup().await; + let (_sequencer, _mempool_handle) = common_setup().await; - let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id; - let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id; + let acc1 = initial_accounts()[0].account_id; + let acc2 = initial_accounts()[1].account_id; let sign_key1 = create_signing_key_for_account1(); @@ -606,8 +559,8 @@ mod tests { async fn transaction_pre_check_native_transfer_other_signature() { let (mut sequencer, _mempool_handle) = common_setup().await; - let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id; - let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id; + let acc1 = initial_accounts()[0].account_id; + let acc2 = initial_accounts()[1].account_id; let sign_key2 = create_signing_key_for_account2(); @@ -619,7 +572,7 @@ mod tests { let tx = tx.transaction_stateless_check().unwrap(); // Signature is not from sender. Execution fails - let result = sequencer.execute_check_transaction_on_state(tx); + let result = sequencer.execute_check_transaction_on_state(tx, 0, 0); assert!(matches!( result, @@ -631,8 +584,8 @@ mod tests { async fn transaction_pre_check_native_transfer_sent_too_much() { let (mut sequencer, _mempool_handle) = common_setup().await; - let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id; - let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id; + let acc1 = initial_accounts()[0].account_id; + let acc2 = initial_accounts()[1].account_id; let sign_key1 = create_signing_key_for_account1(); @@ -645,7 +598,7 @@ mod tests { // Passed pre-check assert!(result.is_ok()); - let result = sequencer.execute_check_transaction_on_state(result.unwrap()); + let result = sequencer.execute_check_transaction_on_state(result.unwrap(), 0, 0); let is_failed_at_balance_mismatch = matches!( result.err().unwrap(), nssa::error::NssaError::ProgramExecutionFailed(_) @@ -658,8 +611,8 @@ mod tests { async fn transaction_execute_native_transfer() { let (mut sequencer, _mempool_handle) = common_setup().await; - let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id; - let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id; + let acc1 = initial_accounts()[0].account_id; + let acc2 = initial_accounts()[1].account_id; let sign_key1 = create_signing_key_for_account1(); @@ -667,7 +620,9 @@ mod tests { acc1, 0, acc2, 100, &sign_key1, ); - sequencer.execute_check_transaction_on_state(tx).unwrap(); + sequencer + .execute_check_transaction_on_state(tx, 0, 0) + .unwrap(); let bal_from = sequencer.state.get_account_by_id(acc1).balance; let bal_to = sequencer.state.get_account_by_id(acc2).balance; @@ -720,8 +675,8 @@ mod tests { async fn replay_transactions_are_rejected_in_the_same_block() { let (mut sequencer, mempool_handle) = common_setup().await; - let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id; - let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id; + let acc1 = initial_accounts()[0].account_id; + let acc2 = initial_accounts()[1].account_id; let sign_key1 = create_signing_key_for_account1(); @@ -759,8 +714,8 @@ mod tests { async fn replay_transactions_are_rejected_in_different_blocks() { let (mut sequencer, mempool_handle) = common_setup().await; - let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id; - let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id; + let acc1 = initial_accounts()[0].account_id; + let acc2 = initial_accounts()[1].account_id; let sign_key1 = create_signing_key_for_account1(); @@ -806,8 +761,8 @@ mod tests { #[tokio::test] async fn restart_from_storage() { let config = setup_sequencer_config(); - let acc1_account_id = config.initial_accounts[0].account_id; - let acc2_account_id = config.initial_accounts[1].account_id; + let acc1_account_id = initial_accounts()[0].account_id; + let acc2_account_id = initial_accounts()[1].account_id; let balance_to_move = 13; // In the following code block a transaction will be processed that moves `balance_to_move` @@ -816,7 +771,7 @@ mod tests { { let (mut sequencer, mempool_handle) = SequencerCoreWithMockClients::start_from_config(config.clone()).await; - let signing_key = PrivateKey::try_new([1; 32]).unwrap(); + let signing_key = create_signing_key_for_account1(); let tx = common::test_utils::create_transaction_native_token_transfer( acc1_account_id, @@ -854,11 +809,11 @@ mod tests { // Balances should be consistent with the stored block assert_eq!( balance_acc_1, - config.initial_accounts[0].balance - balance_to_move + initial_accounts()[0].balance - balance_to_move ); assert_eq!( balance_acc_2, - config.initial_accounts[1].balance + balance_to_move + initial_accounts()[1].balance + balance_to_move ); } @@ -905,15 +860,15 @@ mod tests { #[tokio::test] async fn produce_block_with_correct_prev_meta_after_restart() { let config = setup_sequencer_config(); - let acc1_account_id = config.initial_accounts[0].account_id; - let acc2_account_id = config.initial_accounts[1].account_id; + let acc1_account_id = initial_accounts()[0].account_id; + let acc2_account_id = initial_accounts()[1].account_id; // Step 1: Create initial database with some block metadata let expected_prev_meta = { let (mut sequencer, mempool_handle) = SequencerCoreWithMockClients::start_from_config(config.clone()).await; - let signing_key = PrivateKey::try_new([1; 32]).unwrap(); + let signing_key = create_signing_key_for_account1(); // Add a transaction and produce a block to set up block metadata let tx = common::test_utils::create_transaction_native_token_transfer( @@ -938,7 +893,7 @@ mod tests { SequencerCoreWithMockClients::start_from_config(config.clone()).await; // Step 3: Submit a new transaction - let signing_key = PrivateKey::try_new([1; 32]).unwrap(); + let signing_key = create_signing_key_for_account1(); let tx = common::test_utils::create_transaction_native_token_transfer( acc1_account_id, 1, // Next nonce diff --git a/sequencer/service/Dockerfile b/sequencer/service/Dockerfile index 414d4889..10641e9a 100644 --- a/sequencer/service/Dockerfile +++ b/sequencer/service/Dockerfile @@ -26,7 +26,7 @@ RUN ARCH=$(uname -m); \ else \ echo "Using manual build for $ARCH"; \ git clone --depth 1 --branch release-3.0 https://github.com/risc0/risc0.git; \ - git clone --depth 1 --branch r0.1.94.0 https://github.com/risc0/rust.git; \ + git clone --depth 1 --branch r0.1.91.0 https://github.com/risc0/rust.git; \ cd /risc0; \ cargo install --path rzup; \ rzup build --path /rust rust --verbose; \ @@ -55,7 +55,11 @@ FROM chef AS builder ARG STANDALONE COPY --from=planner /sequencer_service/recipe.json recipe.json # Build dependencies only (this layer will be cached) -RUN if [ "$STANDALONE" = "true" ]; then \ +RUN --mount=type=cache,target=/usr/local/cargo/registry/index \ + --mount=type=cache,target=/usr/local/cargo/registry/cache \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/sequencer_service/target \ + if [ "$STANDALONE" = "true" ]; then \ cargo chef cook --bin sequencer_service --features standalone --release --recipe-path recipe.json; \ else \ cargo chef cook --bin sequencer_service --release --recipe-path recipe.json; \ @@ -64,31 +68,29 @@ RUN if [ "$STANDALONE" = "true" ]; then \ # Copy source code COPY . . -# Build the actual application -RUN if [ "$STANDALONE" = "true" ]; then \ +# Build the actual application and copy the binary out of the cache mount +RUN --mount=type=cache,target=/usr/local/cargo/registry/index \ + --mount=type=cache,target=/usr/local/cargo/registry/cache \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/sequencer_service/target \ + if [ "$STANDALONE" = "true" ]; then \ cargo build --release --features standalone --bin sequencer_service; \ else \ cargo build --release --bin sequencer_service; \ - fi - -# Strip debug symbols to reduce binary size -RUN strip /sequencer_service/target/release/sequencer_service + fi \ + && strip /sequencer_service/target/release/sequencer_service \ + && cp /sequencer_service/target/release/sequencer_service /usr/local/bin/sequencer_service # Runtime stage - minimal image FROM debian:trixie-slim -# Install runtime dependencies -RUN apt-get update \ - && apt-get install -y gosu jq \ - && rm -rf /var/lib/apt/lists/* - # Create non-root user for security RUN useradd -m -u 1000 -s /bin/bash sequencer_user && \ - mkdir -p /sequencer_service /etc/sequencer_service && \ - chown -R sequencer_user:sequencer_user /sequencer_service /etc/sequencer_service + mkdir -p /sequencer_service /etc/sequencer_service /var/lib/sequencer_service && \ + chown -R sequencer_user:sequencer_user /sequencer_service /etc/sequencer_service /var/lib/sequencer_service # Copy binary from builder -COPY --from=builder --chown=sequencer_user:sequencer_user /sequencer_service/target/release/sequencer_service /usr/local/bin/sequencer_service +COPY --from=builder --chown=sequencer_user:sequencer_user /usr/local/bin/sequencer_service /usr/local/bin/sequencer_service # Copy r0vm binary from builder COPY --from=builder --chown=sequencer_user:sequencer_user /usr/local/bin/r0vm /usr/local/bin/r0vm @@ -96,9 +98,7 @@ COPY --from=builder --chown=sequencer_user:sequencer_user /usr/local/bin/r0vm /u # Copy logos blockchain circuits from builder COPY --from=builder --chown=sequencer_user:sequencer_user /root/.logos-blockchain-circuits /home/sequencer_user/.logos-blockchain-circuits -# Copy entrypoint script -COPY sequencer/service/docker-entrypoint.sh /docker-entrypoint.sh -RUN chmod +x /docker-entrypoint.sh +VOLUME /var/lib/sequencer_service # Expose default port EXPOSE 3040 @@ -120,9 +120,7 @@ ENV RUST_LOG=info # Set explicit location for r0vm binary ENV RISC0_SERVER_PATH=/usr/local/bin/r0vm -USER root - -ENTRYPOINT ["/docker-entrypoint.sh"] +USER sequencer_user WORKDIR /sequencer_service CMD ["sequencer_service", "/etc/sequencer_service/sequencer_config.json"] diff --git a/sequencer/service/docker-compose.yml b/sequencer/service/docker-compose.yml index 81520e7b..cede8143 100644 --- a/sequencer/service/docker-compose.yml +++ b/sequencer/service/docker-compose.yml @@ -10,5 +10,8 @@ services: volumes: # Mount configuration file - ./configs/docker/sequencer_config.json:/etc/sequencer_service/sequencer_config.json - # Mount data folder - - ./data:/var/lib/sequencer_service + # Mount data volume + - sequencer_data:/var/lib/sequencer_service + +volumes: + sequencer_data: diff --git a/sequencer/service/docker-entrypoint.sh b/sequencer/service/docker-entrypoint.sh deleted file mode 100644 index 131c83e6..00000000 --- a/sequencer/service/docker-entrypoint.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh - -# This is an entrypoint script for the sequencer_service Docker container, -# it's not meant to be executed outside of the container. - -set -e - -CONFIG="/etc/sequencer/service/sequencer_config.json" - -# Check config file exists -if [ ! -f "$CONFIG" ]; then - echo "Config file not found: $CONFIG" >&2 - exit 1 -fi - -# Parse home dir -HOME_DIR=$(jq -r '.home' "$CONFIG") - -if [ -z "$HOME_DIR" ] || [ "$HOME_DIR" = "null" ]; then - echo "'home' key missing in config" >&2 - exit 1 -fi - -# Give permissions to the data directory and switch to non-root user -if [ "$(id -u)" = "0" ]; then - mkdir -p "$HOME_DIR" - chown -R sequencer_user:sequencer_user "$HOME_DIR" - exec gosu sequencer_user "$@" -fi diff --git a/sequencer/service/protocol/src/lib.rs b/sequencer/service/protocol/src/lib.rs index eb868c0e..ec0020ac 100644 --- a/sequencer/service/protocol/src/lib.rs +++ b/sequencer/service/protocol/src/lib.rs @@ -1,9 +1,5 @@ //! Reexports of types used by sequencer rpc specification. -pub use common::{ - HashType, - block::{Block, BlockId}, - transaction::NSSATransaction, -}; +pub use common::{HashType, block::Block, transaction::NSSATransaction}; pub use nssa::{Account, AccountId, ProgramId}; -pub use nssa_core::{Commitment, MembershipProof, account::Nonce}; +pub use nssa_core::{BlockId, Commitment, MembershipProof, account::Nonce}; diff --git a/storage/src/indexer.rs b/storage/src/indexer.rs deleted file mode 100644 index c398300e..00000000 --- a/storage/src/indexer.rs +++ /dev/null @@ -1,1356 +0,0 @@ -use std::{collections::HashMap, path::Path, sync::Arc}; - -use common::{ - block::{Block, BlockId}, - transaction::NSSATransaction, -}; -use nssa::V03State; -use rocksdb::{ - BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, WriteBatch, -}; - -use crate::error::DbError; - -/// Maximal size of stored blocks in base. -/// -/// Used to control db size. -/// -/// Currently effectively unbounded. -pub const BUFF_SIZE_ROCKSDB: usize = usize::MAX; - -/// Size of stored blocks cache in memory. -/// -/// Keeping small to not run out of memory. -pub const CACHE_SIZE: usize = 1000; - -/// Key base for storing metainformation about id of first block in db. -pub const DB_META_FIRST_BLOCK_IN_DB_KEY: &str = "first_block_in_db"; -/// Key base for storing metainformation about id of last current block in db. -pub const DB_META_LAST_BLOCK_IN_DB_KEY: &str = "last_block_in_db"; -/// Key base for storing metainformation about id of last observed L1 lib header in db. -pub const DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY: &str = - "last_observed_l1_lib_header_in_db"; -/// Key base for storing metainformation which describe if first block has been set. -pub const DB_META_FIRST_BLOCK_SET_KEY: &str = "first_block_set"; -/// Key base for storing metainformation about the last breakpoint. -pub const DB_META_LAST_BREAKPOINT_ID: &str = "last_breakpoint_id"; - -/// Interval between state breakpoints. -pub const BREAKPOINT_INTERVAL: u8 = 100; - -/// Name of block column family. -pub const CF_BLOCK_NAME: &str = "cf_block"; -/// Name of meta column family. -pub const CF_META_NAME: &str = "cf_meta"; -/// Name of breakpoint column family. -pub const CF_BREAKPOINT_NAME: &str = "cf_breakpoint"; -/// Name of hash to id map column family. -pub const CF_HASH_TO_ID: &str = "cf_hash_to_id"; -/// Name of tx hash to id map column family. -pub const CF_TX_TO_ID: &str = "cf_tx_to_id"; -/// Name of account meta column family. -pub const CF_ACC_META: &str = "cf_acc_meta"; -/// Name of account id to tx hash map column family. -pub const CF_ACC_TO_TX: &str = "cf_acc_to_tx"; - -pub type DbResult = Result; - -pub struct RocksDBIO { - pub db: DBWithThreadMode, -} - -impl RocksDBIO { - pub fn open_or_create( - path: &Path, - genesis_block: &Block, - initial_state: &V03State, - ) -> DbResult { - let mut cf_opts = Options::default(); - cf_opts.set_max_write_buffer_number(16); - // ToDo: Add more column families for different data - let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); - let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); - let cfbreakpoint = ColumnFamilyDescriptor::new(CF_BREAKPOINT_NAME, cf_opts.clone()); - let cfhti = ColumnFamilyDescriptor::new(CF_HASH_TO_ID, cf_opts.clone()); - let cftti = ColumnFamilyDescriptor::new(CF_TX_TO_ID, cf_opts.clone()); - let cfameta = ColumnFamilyDescriptor::new(CF_ACC_META, cf_opts.clone()); - let cfatt = ColumnFamilyDescriptor::new(CF_ACC_TO_TX, cf_opts.clone()); - - let mut db_opts = Options::default(); - db_opts.create_missing_column_families(true); - db_opts.create_if_missing(true); - let db = DBWithThreadMode::::open_cf_descriptors( - &db_opts, - path, - vec![cfb, cfmeta, cfbreakpoint, cfhti, cftti, cfameta, cfatt], - ) - .map_err(|err| DbError::RocksDbError { - error: err, - additional_info: Some("Failed to open or create DB".to_owned()), - })?; - - let dbio = Self { db }; - - let is_start_set = dbio.get_meta_is_first_block_set()?; - if !is_start_set { - let block_id = genesis_block.header.block_id; - dbio.put_meta_last_block_in_db(block_id)?; - dbio.put_meta_first_block_in_db(genesis_block)?; - dbio.put_meta_is_first_block_set()?; - - // First breakpoint setup - dbio.put_breakpoint(0, initial_state)?; - dbio.put_meta_last_breakpoint_id(0)?; - } - - Ok(dbio) - } - - pub fn destroy(path: &Path) -> DbResult<()> { - let mut cf_opts = Options::default(); - cf_opts.set_max_write_buffer_number(16); - // ToDo: Add more column families for different data - let _cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); - let _cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); - let _cfsnapshot = ColumnFamilyDescriptor::new(CF_BREAKPOINT_NAME, cf_opts.clone()); - let _cfhti = ColumnFamilyDescriptor::new(CF_HASH_TO_ID, cf_opts.clone()); - let _cftti = ColumnFamilyDescriptor::new(CF_TX_TO_ID, cf_opts.clone()); - let _cfameta = ColumnFamilyDescriptor::new(CF_ACC_META, cf_opts.clone()); - let _cfatt = ColumnFamilyDescriptor::new(CF_ACC_TO_TX, cf_opts.clone()); - - let mut db_opts = Options::default(); - db_opts.create_missing_column_families(true); - db_opts.create_if_missing(true); - DBWithThreadMode::::destroy(&db_opts, path) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None)) - } - - // Columns - - pub fn meta_column(&self) -> Arc> { - self.db.cf_handle(CF_META_NAME).unwrap() - } - - pub fn block_column(&self) -> Arc> { - self.db.cf_handle(CF_BLOCK_NAME).unwrap() - } - - pub fn breakpoint_column(&self) -> Arc> { - self.db.cf_handle(CF_BREAKPOINT_NAME).unwrap() - } - - pub fn hash_to_id_column(&self) -> Arc> { - self.db.cf_handle(CF_HASH_TO_ID).unwrap() - } - - pub fn tx_hash_to_id_column(&self) -> Arc> { - self.db.cf_handle(CF_TX_TO_ID).unwrap() - } - - pub fn account_id_to_tx_hash_column(&self) -> Arc> { - self.db.cf_handle(CF_ACC_TO_TX).unwrap() - } - - pub fn account_meta_column(&self) -> Arc> { - self.db.cf_handle(CF_ACC_META).unwrap() - } - - // Meta - - pub fn get_meta_first_block_in_db(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize first block".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "First block not found".to_owned(), - )) - } - } - - pub fn get_meta_last_block_in_db(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize last block".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "Last block not found".to_owned(), - )) - } - } - - pub fn get_meta_last_observed_l1_lib_header_in_db(&self) -> DbResult> { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY).map_err( - |err| { - DbError::borsh_cast_message( - err, - Some( - "Failed to serialize DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY".to_owned(), - ), - ) - }, - )?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - res.map(|data| { - borsh::from_slice::<[u8; 32]>(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize last l1 lib header".to_owned()), - ) - }) - }) - .transpose() - } - - pub fn get_meta_is_first_block_set(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - Ok(res.is_some()) - } - - pub fn get_meta_last_breakpoint_id(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BREAKPOINT_ID).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BREAKPOINT_ID".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize last breakpoint id".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "Last breakpoint id not found".to_owned(), - )) - } - } - - pub fn put_meta_first_block_in_db(&self, block: &Block) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block.header.block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize first block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - self.put_block(block, [0; 32])?; - Ok(()) - } - - pub fn put_meta_last_block_in_db(&self, block_id: u64) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) - } - - pub fn put_meta_last_observed_l1_lib_header_in_db( - &self, - l1_lib_header: [u8; 32], - ) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY).map_err( - |err| { - DbError::borsh_cast_message( - err, - Some( - "Failed to serialize DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY".to_owned(), - ), - ) - }, - )?, - borsh::to_vec(&l1_lib_header).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last l1 block header".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) - } - - pub fn put_meta_last_breakpoint_id(&self, br_id: u64) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BREAKPOINT_ID).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BREAKPOINT_ID".to_owned()), - ) - })?, - borsh::to_vec(&br_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) - } - - pub fn put_meta_is_first_block_set(&self) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), - ) - })?, - [1_u8; 1], - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) - } - - // Block - - pub fn put_block(&self, block: &Block, l1_lib_header: [u8; 32]) -> DbResult<()> { - let cf_block = self.block_column(); - let cf_hti = self.hash_to_id_column(); - let cf_tti: Arc> = self.tx_hash_to_id_column(); - - // ToDo: rewrite this with write batching - - self.db - .put_cf( - &cf_block, - borsh::to_vec(&block.header.block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - borsh::to_vec(&block).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block data".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - let last_curr_block = self.get_meta_last_block_in_db()?; - - if block.header.block_id > last_curr_block { - self.put_meta_last_block_in_db(block.header.block_id)?; - self.put_meta_last_observed_l1_lib_header_in_db(l1_lib_header)?; - } - - self.db - .put_cf( - &cf_hti, - borsh::to_vec(&block.header.hash).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block hash".to_owned()), - ) - })?, - borsh::to_vec(&block.header.block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - let mut acc_to_tx_map: HashMap<[u8; 32], Vec<[u8; 32]>> = HashMap::new(); - - for tx in &block.body.transactions { - let tx_hash = tx.hash(); - - self.db - .put_cf( - &cf_tti, - borsh::to_vec(&tx_hash).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize tx hash".to_owned()), - ) - })?, - borsh::to_vec(&block.header.block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - let acc_ids = tx - .affected_public_account_ids() - .into_iter() - .map(nssa::AccountId::into_value) - .collect::>(); - - for acc_id in acc_ids { - acc_to_tx_map - .entry(acc_id) - .and_modify(|tx_hashes| tx_hashes.push(tx_hash.into())) - .or_insert_with(|| vec![tx_hash.into()]); - } - } - - #[expect( - clippy::iter_over_hash_type, - reason = "RocksDB will keep ordering persistent" - )] - for (acc_id, tx_hashes) in acc_to_tx_map { - self.put_account_transactions(acc_id, &tx_hashes)?; - } - - if block - .header - .block_id - .is_multiple_of(u64::from(BREAKPOINT_INTERVAL)) - { - self.put_next_breakpoint()?; - } - - Ok(()) - } - - pub fn get_block(&self, block_id: u64) -> DbResult> { - let cf_block = self.block_column(); - let res = self - .db - .get_cf( - &cf_block, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(Some(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message( - serr, - Some("Failed to deserialize block data".to_owned()), - ) - })?)) - } else { - Ok(None) - } - } - - pub fn get_block_batch(&self, before: Option, limit: u64) -> DbResult> { - let cf_block = self.block_column(); - let mut block_batch = vec![]; - - // Determine the starting block ID - let start_block_id = if let Some(before_id) = before { - before_id.saturating_sub(1) - } else { - // Get the latest block ID - self.get_meta_last_block_in_db()? - }; - - // ToDo: Multi get this - - for i in 0..limit { - let block_id = start_block_id.saturating_sub(i); - if block_id == 0 { - break; - } - - let res = self - .db - .get_cf( - &cf_block, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - let block = if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message( - serr, - Some("Failed to deserialize block data".to_owned()), - ) - })?) - } else { - // Block not found, assuming that previous one was the last - break; - }?; - - block_batch.push(block); - } - - Ok(block_batch) - } - - // State - - pub fn put_breakpoint(&self, br_id: u64, breakpoint: &V03State) -> DbResult<()> { - let cf_br = self.breakpoint_column(); - - self.db - .put_cf( - &cf_br, - borsh::to_vec(&br_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize breakpoint id".to_owned()), - ) - })?, - borsh::to_vec(&breakpoint).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize breakpoint data".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None)) - } - - pub fn get_breakpoint(&self, br_id: u64) -> DbResult { - let cf_br = self.breakpoint_column(); - let res = self - .db - .get_cf( - &cf_br, - borsh::to_vec(&br_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize breakpoint id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message( - serr, - Some("Failed to deserialize breakpoint data".to_owned()), - ) - })?) - } else { - // Note: this is not a `DbError::NotFound` case, because we expect that all searched - // breakpoints will be present in db as this is an internal method. - Err(DbError::db_interaction_error( - "Breakpoint on this id not found".to_owned(), - )) - } - } - - pub fn calculate_state_for_id(&self, block_id: u64) -> DbResult { - let last_block = self.get_meta_last_block_in_db()?; - - if block_id <= last_block { - let br_id = closest_breakpoint_id(block_id); - let mut breakpoint = self.get_breakpoint(br_id)?; - - // ToDo: update it to handle any genesis id - // right now works correctly only if genesis_id < BREAKPOINT_INTERVAL - let start = if br_id != 0 { - u64::from(BREAKPOINT_INTERVAL) - .checked_mul(br_id) - .expect("Reached maximum breakpoint id") - } else { - self.get_meta_first_block_in_db()? - }; - - for id in start..=block_id { - let block = self.get_block(id)?.ok_or_else(|| { - DbError::db_interaction_error(format!("Block with id {id} not found")) - })?; - - for transaction in block.body.transactions { - transaction - .transaction_stateless_check() - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction pre check failed with err {err:?}" - )) - })? - .execute_check_on_state(&mut breakpoint) - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction execution failed with err {err:?}" - )) - })?; - } - } - - Ok(breakpoint) - } else { - Err(DbError::db_interaction_error(format!( - "Block with id {block_id} not found" - ))) - } - } - - pub fn final_state(&self) -> DbResult { - self.calculate_state_for_id(self.get_meta_last_block_in_db()?) - } - - pub fn put_next_breakpoint(&self) -> DbResult<()> { - let last_block = self.get_meta_last_block_in_db()?; - let next_breakpoint_id = self - .get_meta_last_breakpoint_id()? - .checked_add(1) - .expect("Reached maximum breakpoint id"); - let block_to_break_id = next_breakpoint_id - .checked_mul(u64::from(BREAKPOINT_INTERVAL)) - .expect("Reached maximum breakpoint id"); - - if block_to_break_id <= last_block { - let next_breakpoint = self.calculate_state_for_id(block_to_break_id)?; - - self.put_breakpoint(next_breakpoint_id, &next_breakpoint)?; - self.put_meta_last_breakpoint_id(next_breakpoint_id) - } else { - Err(DbError::db_interaction_error( - "Breakpoint not yet achieved".to_owned(), - )) - } - } - - // Mappings - - pub fn get_block_id_by_hash(&self, hash: [u8; 32]) -> DbResult> { - let cf_hti = self.hash_to_id_column(); - let res = self - .db - .get_cf( - &cf_hti, - borsh::to_vec(&hash).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block hash".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(Some(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message(serr, Some("Failed to deserialize block id".to_owned())) - })?)) - } else { - Ok(None) - } - } - - pub fn get_block_id_by_tx_hash(&self, tx_hash: [u8; 32]) -> DbResult> { - let cf_tti = self.tx_hash_to_id_column(); - let res = self - .db - .get_cf( - &cf_tti, - borsh::to_vec(&tx_hash).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize transaction hash".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(Some(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message(serr, Some("Failed to deserialize block id".to_owned())) - })?)) - } else { - Ok(None) - } - } - - // Accounts meta - - fn update_acc_meta_batch( - &self, - acc_id: [u8; 32], - num_tx: u64, - write_batch: &mut WriteBatch, - ) -> DbResult<()> { - let cf_ameta = self.account_meta_column(); - - write_batch.put_cf( - &cf_ameta, - borsh::to_vec(&acc_id).map_err(|err| { - DbError::borsh_cast_message(err, Some("Failed to serialize account id".to_owned())) - })?, - borsh::to_vec(&num_tx).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize acc metadata".to_owned()), - ) - })?, - ); - - Ok(()) - } - - fn get_acc_meta_num_tx(&self, acc_id: [u8; 32]) -> DbResult> { - let cf_ameta = self.account_meta_column(); - let res = self.db.get_cf(&cf_ameta, acc_id).map_err(|rerr| { - DbError::rocksdb_cast_message(rerr, Some("Failed to read from acc meta cf".to_owned())) - })?; - - res.map(|data| { - borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message(serr, Some("Failed to deserialize num tx".to_owned())) - }) - }) - .transpose() - } - - // Account - - pub fn put_account_transactions( - &self, - acc_id: [u8; 32], - tx_hashes: &[[u8; 32]], - ) -> DbResult<()> { - let acc_num_tx = self.get_acc_meta_num_tx(acc_id)?.unwrap_or(0); - let cf_att = self.account_id_to_tx_hash_column(); - let mut write_batch = WriteBatch::new(); - - for (tx_id, tx_hash) in tx_hashes.iter().enumerate() { - let put_id = acc_num_tx - .checked_add( - u64::try_from(tx_id) - .expect("Transaction number for account expected to fit in u64"), - ) - .expect("Reached maximum number of transactions for account"); - - let mut prefix = borsh::to_vec(&acc_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize account id".to_owned())) - })?; - let suffix = borsh::to_vec(&put_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize tx id".to_owned())) - })?; - - prefix.extend_from_slice(&suffix); - - write_batch.put_cf( - &cf_att, - prefix, - borsh::to_vec(tx_hash).map_err(|berr| { - DbError::borsh_cast_message( - berr, - Some("Failed to serialize tx hash".to_owned()), - ) - })?, - ); - } - - self.update_acc_meta_batch( - acc_id, - acc_num_tx - .checked_add( - u64::try_from(tx_hashes.len()) - .expect("Number of transactions expected to fit in u64"), - ) - .expect("Reached maximum number of transactions for account"), - &mut write_batch, - )?; - - self.db.write(write_batch).map_err(|rerr| { - DbError::rocksdb_cast_message(rerr, Some("Failed to write batch".to_owned())) - }) - } - - fn get_acc_transaction_hashes( - &self, - acc_id: [u8; 32], - offset: u64, - limit: u64, - ) -> DbResult> { - let cf_att = self.account_id_to_tx_hash_column(); - let mut tx_batch = vec![]; - - // ToDo: Multi get this - - for tx_id in offset..(offset.saturating_add(limit)) { - let mut prefix = borsh::to_vec(&acc_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize account id".to_owned())) - })?; - let suffix = borsh::to_vec(&tx_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize tx id".to_owned())) - })?; - - prefix.extend_from_slice(&suffix); - - let res = self - .db - .get_cf(&cf_att, prefix) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - let tx_hash = if let Some(data) = res { - Ok(borsh::from_slice::<[u8; 32]>(&data).map_err(|serr| { - DbError::borsh_cast_message( - serr, - Some("Failed to deserialize tx_hash".to_owned()), - ) - })?) - } else { - // Tx hash not found, assuming that previous one was the last - break; - }?; - - tx_batch.push(tx_hash); - } - - Ok(tx_batch) - } - - pub fn get_acc_transactions( - &self, - acc_id: [u8; 32], - offset: u64, - limit: u64, - ) -> DbResult> { - let mut tx_batch = vec![]; - - for tx_hash in self.get_acc_transaction_hashes(acc_id, offset, limit)? { - let block_id = self.get_block_id_by_tx_hash(tx_hash)?.ok_or_else(|| { - DbError::db_interaction_error(format!( - "Block id not found for tx hash {tx_hash:#?}" - )) - })?; - let block = self.get_block(block_id)?.ok_or_else(|| { - DbError::db_interaction_error(format!("Block with id {block_id} not found")) - })?; - - let transaction = block - .body - .transactions - .iter() - .find(|tx| tx.hash().0 == tx_hash) - .ok_or_else(|| { - DbError::db_interaction_error(format!( - "Missing transaction in block {} with hash {:#?}", - block.header.block_id, tx_hash - )) - })?; - - tx_batch.push(transaction.clone()); - } - - Ok(tx_batch) - } -} - -fn closest_breakpoint_id(block_id: u64) -> u64 { - block_id - .saturating_sub(1) - .checked_div(u64::from(BREAKPOINT_INTERVAL)) - .expect("Breakpoint interval is not zero") -} - -#[cfg(test)] -mod tests { - #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] - - use nssa::AccountId; - use tempfile::tempdir; - - use super::*; - - fn genesis_block() -> Block { - common::test_utils::produce_dummy_block(1, None, vec![]) - } - - fn acc1() -> AccountId { - AccountId::new([ - 148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112, 83, - 153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102, - ]) - } - - fn acc2() -> AccountId { - AccountId::new([ - 30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244, 92, - 94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204, - ]) - } - - fn acc1_sign_key() -> nssa::PrivateKey { - nssa::PrivateKey::try_new([1; 32]).unwrap() - } - - fn acc2_sign_key() -> nssa::PrivateKey { - nssa::PrivateKey::try_new([2; 32]).unwrap() - } - - fn initial_state() -> V03State { - nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0) - } - - fn transfer(amount: u128, nonce: u128, direction: bool) -> NSSATransaction { - let from; - let to; - let sign_key = if direction { - from = acc1(); - to = acc2(); - acc1_sign_key() - } else { - from = acc2(); - to = acc1(); - acc2_sign_key() - }; - - common::test_utils::create_transaction_native_token_transfer( - from, nonce, to, amount, &sign_key, - ) - } - - #[test] - fn start_db() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = - RocksDBIO::open_or_create(temdir_path, &genesis_block(), &initial_state()).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let first_id = dbio.get_meta_first_block_in_db().unwrap(); - let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); - let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(1).unwrap().unwrap(); - let breakpoint = dbio.get_breakpoint(0).unwrap(); - let final_state = dbio.final_state().unwrap(); - - assert_eq!(last_id, 1); - assert_eq!(first_id, 1); - assert!(is_first_set); - assert_eq!(last_br_id, 0); - assert_eq!(last_block.header.hash, genesis_block().header.hash); - assert_eq!( - breakpoint.get_account_by_id(acc1()), - final_state.get_account_by_id(acc1()) - ); - assert_eq!( - breakpoint.get_account_by_id(acc2()), - final_state.get_account_by_id(acc2()) - ); - } - - #[test] - fn one_block_insertion() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = - RocksDBIO::open_or_create(temdir_path, &genesis_block(), &initial_state()).unwrap(); - - let prev_hash = genesis_block().header.hash; - let transfer_tx = transfer(1, 0, true); - let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let first_id = dbio.get_meta_first_block_in_db().unwrap(); - let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); - let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - let breakpoint = dbio.get_breakpoint(0).unwrap(); - let final_state = dbio.final_state().unwrap(); - - assert_eq!(last_id, 2); - assert_eq!(first_id, 1); - assert!(is_first_set); - assert_eq!(last_br_id, 0); - assert_ne!(last_block.header.hash, genesis_block().header.hash); - assert_eq!( - breakpoint.get_account_by_id(acc1()).balance - - final_state.get_account_by_id(acc1()).balance, - 1 - ); - assert_eq!( - final_state.get_account_by_id(acc2()).balance - - breakpoint.get_account_by_id(acc2()).balance, - 1 - ); - } - - #[test] - fn new_breakpoint() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = - RocksDBIO::open_or_create(temdir_path, &genesis_block(), &initial_state()).unwrap(); - - for i in 1..BREAKPOINT_INTERVAL { - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, u128::from(i - 1), true); - let block = common::test_utils::produce_dummy_block( - u64::from(i + 1), - Some(prev_hash), - vec![transfer_tx], - ); - dbio.put_block(&block, [i; 32]).unwrap(); - } - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let first_id = dbio.get_meta_first_block_in_db().unwrap(); - let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); - let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - let prev_breakpoint = dbio.get_breakpoint(0).unwrap(); - let breakpoint = dbio.get_breakpoint(1).unwrap(); - let final_state = dbio.final_state().unwrap(); - - assert_eq!(last_id, 100); - assert_eq!(first_id, 1); - assert!(is_first_set); - assert_eq!(last_br_id, 1); - assert_ne!(last_block.header.hash, genesis_block().header.hash); - assert_eq!( - prev_breakpoint.get_account_by_id(acc1()).balance - - final_state.get_account_by_id(acc1()).balance, - 99 - ); - assert_eq!( - final_state.get_account_by_id(acc2()).balance - - prev_breakpoint.get_account_by_id(acc2()).balance, - 99 - ); - assert_eq!( - breakpoint.get_account_by_id(acc1()), - final_state.get_account_by_id(acc1()) - ); - assert_eq!( - breakpoint.get_account_by_id(acc2()), - final_state.get_account_by_id(acc2()) - ); - } - - #[test] - fn simple_maps() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = - RocksDBIO::open_or_create(temdir_path, &genesis_block(), &initial_state()).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 0, true); - let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - let control_hash1 = block.header.hash; - - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 1, true); - let block = common::test_utils::produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); - - let control_hash2 = block.header.hash; - - dbio.put_block(&block, [2; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 2, true); - - let control_tx_hash1 = transfer_tx.hash(); - - let block = common::test_utils::produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); - dbio.put_block(&block, [3; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 3, true); - - let control_tx_hash2 = transfer_tx.hash(); - - let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); - dbio.put_block(&block, [4; 32]).unwrap(); - - let control_block_id1 = dbio.get_block_id_by_hash(control_hash1.0).unwrap().unwrap(); - let control_block_id2 = dbio.get_block_id_by_hash(control_hash2.0).unwrap().unwrap(); - let control_block_id3 = dbio - .get_block_id_by_tx_hash(control_tx_hash1.0) - .unwrap() - .unwrap(); - let control_block_id4 = dbio - .get_block_id_by_tx_hash(control_tx_hash2.0) - .unwrap() - .unwrap(); - - assert_eq!(control_block_id1, 2); - assert_eq!(control_block_id2, 3); - assert_eq!(control_block_id3, 4); - assert_eq!(control_block_id4, 5); - } - - #[test] - fn block_batch() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let mut block_res = vec![]; - - let dbio = - RocksDBIO::open_or_create(temdir_path, &genesis_block(), &initial_state()).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 0, true); - let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - block_res.push(block.clone()); - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 1, true); - let block = common::test_utils::produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); - - block_res.push(block.clone()); - dbio.put_block(&block, [2; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 2, true); - - let block = common::test_utils::produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); - block_res.push(block.clone()); - dbio.put_block(&block, [3; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 3, true); - - let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); - block_res.push(block.clone()); - dbio.put_block(&block, [4; 32]).unwrap(); - - let block_hashes_mem: Vec<[u8; 32]> = - block_res.into_iter().map(|bl| bl.header.hash.0).collect(); - - // Get blocks before ID 6 (i.e., starting from 5 going backwards), limit 4 - // This should return blocks 5, 4, 3, 2 in descending order - let mut batch_res = dbio.get_block_batch(Some(6), 4).unwrap(); - batch_res.reverse(); // Reverse to match ascending order for comparison - - let block_hashes_db: Vec<[u8; 32]> = - batch_res.into_iter().map(|bl| bl.header.hash.0).collect(); - - assert_eq!(block_hashes_mem, block_hashes_db); - - let block_hashes_mem_limited = &block_hashes_mem[1..]; - - // Get blocks before ID 6, limit 3 - // This should return blocks 5, 4, 3 in descending order - let mut batch_res_limited = dbio.get_block_batch(Some(6), 3).unwrap(); - batch_res_limited.reverse(); // Reverse to match ascending order for comparison - - let block_hashes_db_limited: Vec<[u8; 32]> = batch_res_limited - .into_iter() - .map(|bl| bl.header.hash.0) - .collect(); - - assert_eq!(block_hashes_mem_limited, block_hashes_db_limited.as_slice()); - } - - #[test] - fn account_map() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let mut tx_hash_res = vec![]; - - let dbio = - RocksDBIO::open_or_create(temdir_path, &genesis_block(), &initial_state()).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 0, true); - - tx_hash_res.push(transfer_tx.hash().0); - - let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 1, true); - - tx_hash_res.push(transfer_tx.hash().0); - - let block = common::test_utils::produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); - - dbio.put_block(&block, [2; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 2, true); - - tx_hash_res.push(transfer_tx.hash().0); - - let block = common::test_utils::produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); - - dbio.put_block(&block, [3; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = transfer(1, 3, true); - - tx_hash_res.push(transfer_tx.hash().0); - - let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); - - dbio.put_block(&block, [4; 32]).unwrap(); - - let acc1_tx = dbio.get_acc_transactions(*acc1().value(), 0, 4).unwrap(); - let acc1_tx_hashes: Vec<[u8; 32]> = acc1_tx.into_iter().map(|tx| tx.hash().0).collect(); - - assert_eq!(acc1_tx_hashes, tx_hash_res); - - let acc1_tx_limited = dbio.get_acc_transactions(*acc1().value(), 1, 4).unwrap(); - let acc1_tx_limited_hashes: Vec<[u8; 32]> = - acc1_tx_limited.into_iter().map(|tx| tx.hash().0).collect(); - - assert_eq!(acc1_tx_limited_hashes.as_slice(), &tx_hash_res[1..]); - } -} diff --git a/storage/src/indexer/mod.rs b/storage/src/indexer/mod.rs new file mode 100644 index 00000000..9ab746d8 --- /dev/null +++ b/storage/src/indexer/mod.rs @@ -0,0 +1,692 @@ +use std::{path::Path, sync::Arc}; + +use common::block::Block; +use nssa::V03State; +use rocksdb::{ + BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, +}; + +use crate::error::DbError; + +pub mod read_multiple; +pub mod read_once; +pub mod write_atomic; +pub mod write_non_atomic; + +/// Maximal size of stored blocks in base. +/// +/// Used to control db size. +/// +/// Currently effectively unbounded. +pub const BUFF_SIZE_ROCKSDB: usize = usize::MAX; + +/// Size of stored blocks cache in memory. +/// +/// Keeping small to not run out of memory. +pub const CACHE_SIZE: usize = 1000; + +/// Key base for storing metainformation about id of first block in db. +pub const DB_META_FIRST_BLOCK_IN_DB_KEY: &str = "first_block_in_db"; +/// Key base for storing metainformation about id of last current block in db. +pub const DB_META_LAST_BLOCK_IN_DB_KEY: &str = "last_block_in_db"; +/// Key base for storing metainformation about id of last observed L1 lib header in db. +pub const DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY: &str = + "last_observed_l1_lib_header_in_db"; +/// Key base for storing metainformation which describe if first block has been set. +pub const DB_META_FIRST_BLOCK_SET_KEY: &str = "first_block_set"; +/// Key base for storing metainformation about the last breakpoint. +pub const DB_META_LAST_BREAKPOINT_ID: &str = "last_breakpoint_id"; + +/// Interval between state breakpoints. +pub const BREAKPOINT_INTERVAL: u8 = 100; + +/// Name of block column family. +pub const CF_BLOCK_NAME: &str = "cf_block"; +/// Name of meta column family. +pub const CF_META_NAME: &str = "cf_meta"; +/// Name of breakpoint column family. +pub const CF_BREAKPOINT_NAME: &str = "cf_breakpoint"; +/// Name of hash to id map column family. +pub const CF_HASH_TO_ID: &str = "cf_hash_to_id"; +/// Name of tx hash to id map column family. +pub const CF_TX_TO_ID: &str = "cf_tx_to_id"; +/// Name of account meta column family. +pub const CF_ACC_META: &str = "cf_acc_meta"; +/// Name of account id to tx hash map column family. +pub const CF_ACC_TO_TX: &str = "cf_acc_to_tx"; + +pub type DbResult = Result; + +pub struct RocksDBIO { + pub db: DBWithThreadMode, +} + +impl RocksDBIO { + pub fn open_or_create( + path: &Path, + genesis_block: &Block, + initial_state: &V03State, + ) -> DbResult { + let mut cf_opts = Options::default(); + cf_opts.set_max_write_buffer_number(16); + // ToDo: Add more column families for different data + let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); + let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); + let cfbreakpoint = ColumnFamilyDescriptor::new(CF_BREAKPOINT_NAME, cf_opts.clone()); + let cfhti = ColumnFamilyDescriptor::new(CF_HASH_TO_ID, cf_opts.clone()); + let cftti = ColumnFamilyDescriptor::new(CF_TX_TO_ID, cf_opts.clone()); + let cfameta = ColumnFamilyDescriptor::new(CF_ACC_META, cf_opts.clone()); + let cfatt = ColumnFamilyDescriptor::new(CF_ACC_TO_TX, cf_opts.clone()); + + let mut db_opts = Options::default(); + db_opts.create_missing_column_families(true); + db_opts.create_if_missing(true); + let db = DBWithThreadMode::::open_cf_descriptors( + &db_opts, + path, + vec![cfb, cfmeta, cfbreakpoint, cfhti, cftti, cfameta, cfatt], + ) + .map_err(|err| DbError::RocksDbError { + error: err, + additional_info: Some("Failed to open or create DB".to_owned()), + })?; + + let dbio = Self { db }; + + let is_start_set = dbio.get_meta_is_first_block_set()?; + if !is_start_set { + let block_id = genesis_block.header.block_id; + dbio.put_meta_last_block_in_db(block_id)?; + dbio.put_meta_first_block_in_db_batch(genesis_block)?; + dbio.put_meta_is_first_block_set()?; + + // First breakpoint setup + dbio.put_breakpoint(0, initial_state)?; + dbio.put_meta_last_breakpoint_id(0)?; + } + + Ok(dbio) + } + + pub fn destroy(path: &Path) -> DbResult<()> { + let db_opts = Options::default(); + DBWithThreadMode::::destroy(&db_opts, path) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None)) + } + + // Columns + + pub fn meta_column(&self) -> Arc> { + self.db + .cf_handle(CF_META_NAME) + .expect("Meta column should exist") + } + + pub fn block_column(&self) -> Arc> { + self.db + .cf_handle(CF_BLOCK_NAME) + .expect("Block column should exist") + } + + pub fn breakpoint_column(&self) -> Arc> { + self.db + .cf_handle(CF_BREAKPOINT_NAME) + .expect("Breakpoint column should exist") + } + + pub fn hash_to_id_column(&self) -> Arc> { + self.db + .cf_handle(CF_HASH_TO_ID) + .expect("Hash to id map column should exist") + } + + pub fn tx_hash_to_id_column(&self) -> Arc> { + self.db + .cf_handle(CF_TX_TO_ID) + .expect("Tx hash to id map column should exist") + } + + pub fn account_id_to_tx_hash_column(&self) -> Arc> { + self.db + .cf_handle(CF_ACC_TO_TX) + .expect("Account id to tx map column should exist") + } + + pub fn account_meta_column(&self) -> Arc> { + self.db + .cf_handle(CF_ACC_META) + .expect("Account meta column should exist") + } + + // State + + pub fn calculate_state_for_id(&self, block_id: u64) -> DbResult { + let last_block = self.get_meta_last_block_in_db()?; + + if block_id <= last_block { + let br_id = closest_breakpoint_id(block_id); + let mut breakpoint = self.get_breakpoint(br_id)?; + + // ToDo: update it to handle any genesis id + // right now works correctly only if genesis_id < BREAKPOINT_INTERVAL + let start = if br_id != 0 { + u64::from(BREAKPOINT_INTERVAL) + .checked_mul(br_id) + .expect("Reached maximum breakpoint id") + } else { + self.get_meta_first_block_in_db()? + }; + + for block in self.get_block_batch_seq( + start.checked_add(1).expect("Will be lesser that u64::MAX")..=block_id, + )? { + for transaction in block.body.transactions { + transaction + .transaction_stateless_check() + .map_err(|err| { + DbError::db_interaction_error(format!( + "transaction pre check failed with err {err:?}" + )) + })? + .execute_check_on_state( + &mut breakpoint, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "transaction execution failed with err {err:?}" + )) + })?; + } + } + + Ok(breakpoint) + } else { + Err(DbError::db_interaction_error( + "Block on this id not found".to_owned(), + )) + } + } + + pub fn final_state(&self) -> DbResult { + self.calculate_state_for_id(self.get_meta_last_block_in_db()?) + } +} + +fn closest_breakpoint_id(block_id: u64) -> u64 { + block_id + .saturating_sub(1) + .checked_div(u64::from(BREAKPOINT_INTERVAL)) + .expect("Breakpoint interval is not zero") +} + +#[expect(clippy::shadow_unrelated, reason = "Fine for tests")] +#[cfg(test)] +mod tests { + use nssa::{AccountId, PublicKey}; + use tempfile::tempdir; + + use super::*; + + fn genesis_block() -> Block { + common::test_utils::produce_dummy_block(1, None, vec![]) + } + + fn acc1_sign_key() -> nssa::PrivateKey { + nssa::PrivateKey::try_new([1; 32]).unwrap() + } + + fn acc2_sign_key() -> nssa::PrivateKey { + nssa::PrivateKey::try_new([2; 32]).unwrap() + } + + fn acc1() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&acc1_sign_key())) + } + + fn acc2() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&acc2_sign_key())) + } + + #[test] + fn start_db() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create( + temdir_path, + &genesis_block(), + &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0), + ) + .unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let first_id = dbio.get_meta_first_block_in_db().unwrap(); + let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); + let last_observed_l1_header = dbio.get_meta_last_observed_l1_lib_header_in_db().unwrap(); + let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); + let last_block = dbio.get_block(1).unwrap().unwrap(); + let breakpoint = dbio.get_breakpoint(0).unwrap(); + let final_state = dbio.final_state().unwrap(); + + assert_eq!(last_id, 1); + assert_eq!(first_id, 1); + assert_eq!(last_observed_l1_header, None); + assert!(is_first_set); + assert_eq!(last_br_id, 0); + assert_eq!(last_block.header.hash, genesis_block().header.hash); + assert_eq!( + breakpoint.get_account_by_id(acc1()), + final_state.get_account_by_id(acc1()) + ); + assert_eq!( + breakpoint.get_account_by_id(acc2()), + final_state.get_account_by_id(acc2()) + ); + } + + #[test] + fn one_block_insertion() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create( + temdir_path, + &genesis_block(), + &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0), + ) + .unwrap(); + + let prev_hash = genesis_block().header.hash; + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let first_id = dbio.get_meta_first_block_in_db().unwrap(); + let last_observed_l1_header = dbio + .get_meta_last_observed_l1_lib_header_in_db() + .unwrap() + .unwrap(); + let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); + let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + let breakpoint = dbio.get_breakpoint(0).unwrap(); + let final_state = dbio.final_state().unwrap(); + + assert_eq!(last_id, 2); + assert_eq!(first_id, 1); + assert_eq!(last_observed_l1_header, [1; 32]); + assert!(is_first_set); + assert_eq!(last_br_id, 0); + assert_ne!(last_block.header.hash, genesis_block().header.hash); + assert_eq!( + breakpoint.get_account_by_id(acc1()).balance + - final_state.get_account_by_id(acc1()).balance, + 1 + ); + assert_eq!( + final_state.get_account_by_id(acc2()).balance + - breakpoint.get_account_by_id(acc2()).balance, + 1 + ); + } + + #[test] + fn new_breakpoint() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create( + temdir_path, + &genesis_block(), + &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0), + ) + .unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + for i in 1..=BREAKPOINT_INTERVAL { + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + + let transfer_tx = common::test_utils::create_transaction_native_token_transfer( + from, + (i - 1).into(), + to, + 1, + &sign_key, + ); + let block = common::test_utils::produce_dummy_block( + (i + 1).into(), + Some(prev_hash), + vec![transfer_tx], + ); + dbio.put_block(&block, [i; 32]).unwrap(); + } + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let first_id = dbio.get_meta_first_block_in_db().unwrap(); + let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); + let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + let prev_breakpoint = dbio.get_breakpoint(0).unwrap(); + let breakpoint = dbio.get_breakpoint(1).unwrap(); + let final_state = dbio.final_state().unwrap(); + + assert_eq!(last_id, 101); + assert_eq!(first_id, 1); + assert!(is_first_set); + assert_eq!(last_br_id, 1); + assert_ne!(last_block.header.hash, genesis_block().header.hash); + assert_eq!( + prev_breakpoint.get_account_by_id(acc1()).balance + - final_state.get_account_by_id(acc1()).balance, + 100 + ); + assert_eq!( + final_state.get_account_by_id(acc2()).balance + - prev_breakpoint.get_account_by_id(acc2()).balance, + 100 + ); + assert_eq!( + breakpoint.get_account_by_id(acc1()).balance + - final_state.get_account_by_id(acc1()).balance, + 1 + ); + assert_eq!( + final_state.get_account_by_id(acc2()).balance + - breakpoint.get_account_by_id(acc2()).balance, + 1 + ); + } + + #[test] + fn simple_maps() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create( + temdir_path, + &genesis_block(), + &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0), + ) + .unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + + let control_hash1 = block.header.hash; + + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); + let block = common::test_utils::produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); + + let control_hash2 = block.header.hash; + + dbio.put_block(&block, [2; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); + + let control_tx_hash1 = transfer_tx.hash(); + + let block = common::test_utils::produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + dbio.put_block(&block, [3; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); + + let control_tx_hash2 = transfer_tx.hash(); + + let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); + dbio.put_block(&block, [4; 32]).unwrap(); + + let control_block_id1 = dbio.get_block_id_by_hash(control_hash1.0).unwrap().unwrap(); + let control_block_id2 = dbio.get_block_id_by_hash(control_hash2.0).unwrap().unwrap(); + let control_block_id3 = dbio + .get_block_id_by_tx_hash(control_tx_hash1.0) + .unwrap() + .unwrap(); + let control_block_id4 = dbio + .get_block_id_by_tx_hash(control_tx_hash2.0) + .unwrap() + .unwrap(); + + assert_eq!(control_block_id1, 2); + assert_eq!(control_block_id2, 3); + assert_eq!(control_block_id3, 4); + assert_eq!(control_block_id4, 5); + } + + #[test] + fn block_batch() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let mut block_res = vec![]; + + let dbio = RocksDBIO::open_or_create( + temdir_path, + &genesis_block(), + &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0), + ) + .unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + + block_res.push(block.clone()); + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); + let block = common::test_utils::produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); + + block_res.push(block.clone()); + dbio.put_block(&block, [2; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); + + let block = common::test_utils::produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + block_res.push(block.clone()); + dbio.put_block(&block, [3; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); + + let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); + block_res.push(block.clone()); + dbio.put_block(&block, [4; 32]).unwrap(); + + let block_hashes_mem: Vec<[u8; 32]> = + block_res.into_iter().map(|bl| bl.header.hash.0).collect(); + + // Get blocks before ID 6 (i.e., starting from 5 going backwards), limit 4 + // This should return blocks 5, 4, 3, 2 in descending order + let mut batch_res = dbio.get_block_batch(Some(6), 4).unwrap(); + batch_res.reverse(); // Reverse to match ascending order for comparison + + let block_hashes_db: Vec<[u8; 32]> = + batch_res.into_iter().map(|bl| bl.header.hash.0).collect(); + + assert_eq!(block_hashes_mem, block_hashes_db); + + let block_hashes_mem_limited = &block_hashes_mem[1..]; + + // Get blocks before ID 6, limit 3 + // This should return blocks 5, 4, 3 in descending order + let mut batch_res_limited = dbio.get_block_batch(Some(6), 3).unwrap(); + batch_res_limited.reverse(); // Reverse to match ascending order for comparison + + let block_hashes_db_limited: Vec<[u8; 32]> = batch_res_limited + .into_iter() + .map(|bl| bl.header.hash.0) + .collect(); + + assert_eq!(block_hashes_mem_limited, block_hashes_db_limited.as_slice()); + + let block_batch_seq = dbio.get_block_batch_seq(1..=5).unwrap(); + let block_batch_ids = block_batch_seq + .into_iter() + .map(|block| block.header.block_id) + .collect::>(); + + assert_eq!(block_batch_ids, vec![1, 2, 3, 4, 5]); + } + + #[test] + fn account_map() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create( + temdir_path, + &genesis_block(), + &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0), + ) + .unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let mut tx_hash_res = vec![]; + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx1 = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let transfer_tx2 = + common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); + tx_hash_res.push(transfer_tx1.hash().0); + tx_hash_res.push(transfer_tx2.hash().0); + + let block = common::test_utils::produce_dummy_block( + 2, + Some(prev_hash), + vec![transfer_tx1, transfer_tx2], + ); + + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx1 = + common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); + let transfer_tx2 = + common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); + tx_hash_res.push(transfer_tx1.hash().0); + tx_hash_res.push(transfer_tx2.hash().0); + + let block = common::test_utils::produce_dummy_block( + 3, + Some(prev_hash), + vec![transfer_tx1, transfer_tx2], + ); + + dbio.put_block(&block, [2; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx1 = + common::test_utils::create_transaction_native_token_transfer(from, 4, to, 1, &sign_key); + let transfer_tx2 = + common::test_utils::create_transaction_native_token_transfer(from, 5, to, 1, &sign_key); + tx_hash_res.push(transfer_tx1.hash().0); + tx_hash_res.push(transfer_tx2.hash().0); + + let block = common::test_utils::produce_dummy_block( + 4, + Some(prev_hash), + vec![transfer_tx1, transfer_tx2], + ); + + dbio.put_block(&block, [3; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 6, to, 1, &sign_key); + tx_hash_res.push(transfer_tx.hash().0); + + let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); + + dbio.put_block(&block, [4; 32]).unwrap(); + + let acc1_tx = dbio.get_acc_transactions(*acc1().value(), 0, 7).unwrap(); + let acc1_tx_hashes: Vec<[u8; 32]> = acc1_tx.into_iter().map(|tx| tx.hash().0).collect(); + + assert_eq!(acc1_tx_hashes, tx_hash_res); + + let acc1_tx_limited = dbio.get_acc_transactions(*acc1().value(), 1, 4).unwrap(); + let acc1_tx_limited_hashes: Vec<[u8; 32]> = + acc1_tx_limited.into_iter().map(|tx| tx.hash().0).collect(); + + assert_eq!(acc1_tx_limited_hashes.as_slice(), &tx_hash_res[1..5]); + } +} diff --git a/storage/src/indexer/read_multiple.rs b/storage/src/indexer/read_multiple.rs new file mode 100644 index 00000000..866fc7b0 --- /dev/null +++ b/storage/src/indexer/read_multiple.rs @@ -0,0 +1,209 @@ +use common::transaction::NSSATransaction; + +use super::{Block, DbError, DbResult, RocksDBIO}; + +#[expect(clippy::multiple_inherent_impl, reason = "Readability")] +impl RocksDBIO { + pub fn get_block_batch(&self, before: Option, limit: u64) -> DbResult> { + let mut seq = vec![]; + + // Determine the starting block ID + let start_block_id = if let Some(before_id) = before { + before_id.saturating_sub(1) + } else { + // Get the latest block ID + self.get_meta_last_block_in_db()? + }; + + for i in 0..limit { + let block_id = start_block_id.saturating_sub(i); + if block_id == 0 { + break; + } + seq.push(block_id); + } + + self.get_block_batch_seq(seq.into_iter()) + } + + /// Get block batch from a sequence. + /// + /// Currently assumes non-decreasing sequence. + /// + /// `ToDo`: Add suport of arbitrary sequences. + pub fn get_block_batch_seq(&self, seq: impl Iterator) -> DbResult> { + let cf_block = self.block_column(); + + // Keys setup + let mut keys = vec![]; + for block_id in seq { + keys.push(( + &cf_block, + borsh::to_vec(&block_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize block id".to_owned()), + ) + })?, + )); + } + + let multi_get_res = self.db.multi_get_cf(keys); + + // Keys parsing + let mut block_batch = vec![]; + for res in multi_get_res { + let res = res.map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + let block = if let Some(data) = res { + Ok(borsh::from_slice::(&data).map_err(|serr| { + DbError::borsh_cast_message( + serr, + Some("Failed to deserialize block data".to_owned()), + ) + })?) + } else { + // Block not found, assuming that previous one was the last + break; + }?; + + block_batch.push(block); + } + + Ok(block_batch) + } + + /// Get block ids by txs. + /// + /// `ToDo`: There may be multiple transactions in one block + /// so this method can take redundant reads. + /// Need to update signature and implementation. + fn get_block_ids_by_tx_vec(&self, tx_vec: &[[u8; 32]]) -> DbResult> { + let cf_tti = self.tx_hash_to_id_column(); + + // Keys setup + let mut keys = vec![]; + for tx_hash in tx_vec { + keys.push(( + &cf_tti, + borsh::to_vec(tx_hash).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize tx_hash".to_owned())) + })?, + )); + } + + let multi_get_res = self.db.multi_get_cf(keys); + + // Keys parsing + let mut block_id_batch = vec![]; + for res in multi_get_res { + let res = res + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))? + .ok_or_else(|| { + DbError::db_interaction_error( + "Tx to block id mapping do not contain transaction from vec".to_owned(), + ) + })?; + + let block_id = { + Ok(borsh::from_slice::(&res).map_err(|serr| { + DbError::borsh_cast_message( + serr, + Some("Failed to deserialize block id".to_owned()), + ) + })?) + }?; + + block_id_batch.push(block_id); + } + + Ok(block_id_batch) + } + + // Account + + pub(crate) fn get_acc_transaction_hashes( + &self, + acc_id: [u8; 32], + offset: u64, + limit: u64, + ) -> DbResult> { + let cf_att = self.account_id_to_tx_hash_column(); + let mut tx_batch = vec![]; + + // Keys preparation + let mut keys = vec![]; + for tx_id in offset + ..offset + .checked_add(limit) + .expect("Transaction limit should be lesser than u64::MAX") + { + let mut prefix = borsh::to_vec(&acc_id).map_err(|berr| { + DbError::borsh_cast_message(berr, Some("Failed to serialize account id".to_owned())) + })?; + let suffix = borsh::to_vec(&tx_id).map_err(|berr| { + DbError::borsh_cast_message(berr, Some("Failed to serialize tx id".to_owned())) + })?; + + prefix.extend_from_slice(&suffix); + + keys.push((&cf_att, prefix)); + } + + let multi_get_res = self.db.multi_get_cf(keys); + + for res in multi_get_res { + let res = res.map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + let tx_hash = if let Some(data) = res { + Ok(borsh::from_slice::<[u8; 32]>(&data).map_err(|serr| { + DbError::borsh_cast_message( + serr, + Some("Failed to deserialize tx_hash".to_owned()), + ) + })?) + } else { + // Tx hash not found, assuming that previous one was the last + break; + }?; + + tx_batch.push(tx_hash); + } + + Ok(tx_batch) + } + + pub fn get_acc_transactions( + &self, + acc_id: [u8; 32], + offset: u64, + limit: u64, + ) -> DbResult> { + let mut tx_batch = vec![]; + + let tx_hashes = self.get_acc_transaction_hashes(acc_id, offset, limit)?; + + let associated_blocks_multi_get = self + .get_block_batch_seq(self.get_block_ids_by_tx_vec(&tx_hashes)?.into_iter())? + .into_iter() + .zip(tx_hashes); + + for (block, tx_hash) in associated_blocks_multi_get { + let transaction = block + .body + .transactions + .iter() + .find(|tx| tx.hash().0 == tx_hash) + .ok_or_else(|| { + DbError::db_interaction_error(format!( + "Missing transaction in block {} with hash {:#?}", + block.header.block_id, tx_hash + )) + })?; + + tx_batch.push(transaction.clone()); + } + + Ok(tx_batch) + } +} diff --git a/storage/src/indexer/read_once.rs b/storage/src/indexer/read_once.rs new file mode 100644 index 00000000..74d1afe9 --- /dev/null +++ b/storage/src/indexer/read_once.rs @@ -0,0 +1,272 @@ +use super::{ + Block, DB_META_FIRST_BLOCK_IN_DB_KEY, DB_META_FIRST_BLOCK_SET_KEY, + DB_META_LAST_BLOCK_IN_DB_KEY, DB_META_LAST_BREAKPOINT_ID, + DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY, DbError, DbResult, RocksDBIO, V03State, +}; + +#[expect(clippy::multiple_inherent_impl, reason = "Readability")] +impl RocksDBIO { + // Meta + + pub fn get_meta_first_block_in_db(&self) -> DbResult { + let cf_meta = self.meta_column(); + let res = self + .db + .get_cf( + &cf_meta, + borsh::to_vec(&DB_META_FIRST_BLOCK_IN_DB_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_FIRST_BLOCK_IN_DB_KEY".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + if let Some(data) = res { + Ok(borsh::from_slice::(&data).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to deserialize first block".to_owned()), + ) + })?) + } else { + Err(DbError::db_interaction_error( + "First block not found".to_owned(), + )) + } + } + + pub fn get_meta_last_block_in_db(&self) -> DbResult { + let cf_meta = self.meta_column(); + let res = self + .db + .get_cf( + &cf_meta, + borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + if let Some(data) = res { + Ok(borsh::from_slice::(&data).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to deserialize last block".to_owned()), + ) + })?) + } else { + Err(DbError::db_interaction_error( + "Last block not found".to_owned(), + )) + } + } + + pub fn get_meta_last_observed_l1_lib_header_in_db(&self) -> DbResult> { + let cf_meta = self.meta_column(); + let res = self + .db + .get_cf( + &cf_meta, + borsh::to_vec(&DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY).map_err( + |err| { + DbError::borsh_cast_message( + err, + Some( + "Failed to serialize DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY" + .to_owned(), + ), + ) + }, + )?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + res.map(|data| { + borsh::from_slice::<[u8; 32]>(&data).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to deserialize last l1 lib header".to_owned()), + ) + }) + }) + .transpose() + } + + pub fn get_meta_is_first_block_set(&self) -> DbResult { + let cf_meta = self.meta_column(); + let res = self + .db + .get_cf( + &cf_meta, + borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + Ok(res.is_some()) + } + + pub fn get_meta_last_breakpoint_id(&self) -> DbResult { + let cf_meta = self.meta_column(); + let res = self + .db + .get_cf( + &cf_meta, + borsh::to_vec(&DB_META_LAST_BREAKPOINT_ID).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_LAST_BREAKPOINT_ID".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + if let Some(data) = res { + Ok(borsh::from_slice::(&data).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to deserialize last breakpoint id".to_owned()), + ) + })?) + } else { + Err(DbError::db_interaction_error( + "Last breakpoint id not found".to_owned(), + )) + } + } + + // Block + + pub fn get_block(&self, block_id: u64) -> DbResult> { + let cf_block = self.block_column(); + let res = self + .db + .get_cf( + &cf_block, + borsh::to_vec(&block_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize block id".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + if let Some(data) = res { + Ok(Some(borsh::from_slice::(&data).map_err(|serr| { + DbError::borsh_cast_message( + serr, + Some("Failed to deserialize block data".to_owned()), + ) + })?)) + } else { + Ok(None) + } + } + + // State + + pub fn get_breakpoint(&self, br_id: u64) -> DbResult { + let cf_br = self.breakpoint_column(); + let res = self + .db + .get_cf( + &cf_br, + borsh::to_vec(&br_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize breakpoint id".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + if let Some(data) = res { + Ok(borsh::from_slice::(&data).map_err(|serr| { + DbError::borsh_cast_message( + serr, + Some("Failed to deserialize breakpoint data".to_owned()), + ) + })?) + } else { + Err(DbError::db_interaction_error( + "Breakpoint on this id not found".to_owned(), + )) + } + } + + // Mappings + + pub fn get_block_id_by_hash(&self, hash: [u8; 32]) -> DbResult> { + let cf_hti = self.hash_to_id_column(); + let res = self + .db + .get_cf( + &cf_hti, + borsh::to_vec(&hash).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize block hash".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + if let Some(data) = res { + Ok(Some(borsh::from_slice::(&data).map_err(|serr| { + DbError::borsh_cast_message(serr, Some("Failed to deserialize block id".to_owned())) + })?)) + } else { + Ok(None) + } + } + + pub fn get_block_id_by_tx_hash(&self, tx_hash: [u8; 32]) -> DbResult> { + let cf_tti = self.tx_hash_to_id_column(); + let res = self + .db + .get_cf( + &cf_tti, + borsh::to_vec(&tx_hash).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize transaction hash".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + if let Some(data) = res { + Ok(Some(borsh::from_slice::(&data).map_err(|serr| { + DbError::borsh_cast_message(serr, Some("Failed to deserialize block id".to_owned())) + })?)) + } else { + Ok(None) + } + } + + // Accounts meta + + pub(crate) fn get_acc_meta_num_tx(&self, acc_id: [u8; 32]) -> DbResult> { + let cf_ameta = self.account_meta_column(); + let res = self.db.get_cf(&cf_ameta, acc_id).map_err(|rerr| { + DbError::rocksdb_cast_message(rerr, Some("Failed to read from acc meta cf".to_owned())) + })?; + + res.map(|data| { + borsh::from_slice::(&data).map_err(|serr| { + DbError::borsh_cast_message(serr, Some("Failed to deserialize num tx".to_owned())) + }) + }) + .transpose() + } +} diff --git a/storage/src/indexer/write_atomic.rs b/storage/src/indexer/write_atomic.rs new file mode 100644 index 00000000..161d763a --- /dev/null +++ b/storage/src/indexer/write_atomic.rs @@ -0,0 +1,339 @@ +use std::collections::HashMap; + +use rocksdb::WriteBatch; + +use super::{ + Arc, BREAKPOINT_INTERVAL, Block, BoundColumnFamily, DB_META_FIRST_BLOCK_IN_DB_KEY, + DB_META_FIRST_BLOCK_SET_KEY, DB_META_LAST_BLOCK_IN_DB_KEY, DB_META_LAST_BREAKPOINT_ID, + DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY, DbError, DbResult, RocksDBIO, +}; + +#[expect(clippy::multiple_inherent_impl, reason = "Readability")] +impl RocksDBIO { + // Accounts meta + + pub(crate) fn update_acc_meta_batch( + &self, + acc_id: [u8; 32], + num_tx: u64, + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + let cf_ameta = self.account_meta_column(); + + write_batch.put_cf( + &cf_ameta, + borsh::to_vec(&acc_id).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize account id".to_owned())) + })?, + borsh::to_vec(&num_tx).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize acc metadata".to_owned()), + ) + })?, + ); + + Ok(()) + } + + // Account + + pub fn put_account_transactions( + &self, + acc_id: [u8; 32], + tx_hashes: &[[u8; 32]], + ) -> DbResult<()> { + let acc_num_tx = self.get_acc_meta_num_tx(acc_id)?.unwrap_or(0); + let cf_att = self.account_id_to_tx_hash_column(); + let mut write_batch = WriteBatch::new(); + + for (tx_id, tx_hash) in tx_hashes.iter().enumerate() { + let put_id = acc_num_tx + .checked_add(tx_id.try_into().expect("Must fit into u64")) + .expect("Tx count should be lesser that u64::MAX"); + + let mut prefix = borsh::to_vec(&acc_id).map_err(|berr| { + DbError::borsh_cast_message(berr, Some("Failed to serialize account id".to_owned())) + })?; + let suffix = borsh::to_vec(&put_id).map_err(|berr| { + DbError::borsh_cast_message(berr, Some("Failed to serialize tx id".to_owned())) + })?; + + prefix.extend_from_slice(&suffix); + + write_batch.put_cf( + &cf_att, + prefix, + borsh::to_vec(tx_hash).map_err(|berr| { + DbError::borsh_cast_message( + berr, + Some("Failed to serialize tx hash".to_owned()), + ) + })?, + ); + } + + self.update_acc_meta_batch( + acc_id, + acc_num_tx + .checked_add(tx_hashes.len().try_into().expect("Must fit into u64")) + .expect("Tx count should be lesser that u64::MAX"), + &mut write_batch, + )?; + + self.db.write(write_batch).map_err(|rerr| { + DbError::rocksdb_cast_message(rerr, Some("Failed to write batch".to_owned())) + }) + } + + pub fn put_account_transactions_dependant( + &self, + acc_id: [u8; 32], + tx_hashes: &[[u8; 32]], + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + let acc_num_tx = self.get_acc_meta_num_tx(acc_id)?.unwrap_or(0); + let cf_att = self.account_id_to_tx_hash_column(); + + for (tx_id, tx_hash) in tx_hashes.iter().enumerate() { + let put_id = acc_num_tx + .checked_add(tx_id.try_into().expect("Must fit into u64")) + .expect("Tx count should be lesser that u64::MAX"); + + let mut prefix = borsh::to_vec(&acc_id).map_err(|berr| { + DbError::borsh_cast_message(berr, Some("Failed to serialize account id".to_owned())) + })?; + let suffix = borsh::to_vec(&put_id).map_err(|berr| { + DbError::borsh_cast_message(berr, Some("Failed to serialize tx id".to_owned())) + })?; + + prefix.extend_from_slice(&suffix); + + write_batch.put_cf( + &cf_att, + prefix, + borsh::to_vec(tx_hash).map_err(|berr| { + DbError::borsh_cast_message( + berr, + Some("Failed to serialize tx hash".to_owned()), + ) + })?, + ); + } + + self.update_acc_meta_batch( + acc_id, + acc_num_tx + .checked_add(tx_hashes.len().try_into().expect("Must fit into u64")) + .expect("Tx count should be lesser that u64::MAX"), + write_batch, + )?; + + Ok(()) + } + + // Meta + + pub fn put_meta_first_block_in_db_batch(&self, block: &Block) -> DbResult<()> { + let cf_meta = self.meta_column(); + self.db + .put_cf( + &cf_meta, + borsh::to_vec(&DB_META_FIRST_BLOCK_IN_DB_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_FIRST_BLOCK_IN_DB_KEY".to_owned()), + ) + })?, + borsh::to_vec(&block.header.block_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize first block id".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + self.put_block(block, [0; 32])?; + Ok(()) + } + + pub fn put_meta_last_block_in_db_batch( + &self, + block_id: u64, + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + let cf_meta = self.meta_column(); + write_batch.put_cf( + &cf_meta, + borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), + ) + })?, + borsh::to_vec(&block_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize last block id".to_owned()), + ) + })?, + ); + Ok(()) + } + + pub fn put_meta_last_observed_l1_lib_header_in_db_batch( + &self, + l1_lib_header: [u8; 32], + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + let cf_meta = self.meta_column(); + write_batch.put_cf( + &cf_meta, + borsh::to_vec(&DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some( + "Failed to serialize DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY" + .to_owned(), + ), + ) + })?, + borsh::to_vec(&l1_lib_header).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize last l1 block header".to_owned()), + ) + })?, + ); + Ok(()) + } + + pub fn put_meta_last_breakpoint_id_batch( + &self, + br_id: u64, + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + let cf_meta = self.meta_column(); + write_batch.put_cf( + &cf_meta, + borsh::to_vec(&DB_META_LAST_BREAKPOINT_ID).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_LAST_BREAKPOINT_ID".to_owned()), + ) + })?, + borsh::to_vec(&br_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize last block id".to_owned()), + ) + })?, + ); + Ok(()) + } + + pub fn put_meta_is_first_block_set_batch(&self, write_batch: &mut WriteBatch) -> DbResult<()> { + let cf_meta = self.meta_column(); + write_batch.put_cf( + &cf_meta, + borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), + ) + })?, + [1_u8; 1], + ); + Ok(()) + } + + // Block + + pub fn put_block(&self, block: &Block, l1_lib_header: [u8; 32]) -> DbResult<()> { + let cf_block = self.block_column(); + let cf_hti = self.hash_to_id_column(); + let cf_tti: Arc> = self.tx_hash_to_id_column(); + let last_curr_block = self.get_meta_last_block_in_db()?; + let mut write_batch = WriteBatch::default(); + + write_batch.put_cf( + &cf_block, + borsh::to_vec(&block.header.block_id).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize block id".to_owned())) + })?, + borsh::to_vec(block).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize block data".to_owned())) + })?, + ); + + if block.header.block_id > last_curr_block { + self.put_meta_last_block_in_db_batch(block.header.block_id, &mut write_batch)?; + self.put_meta_last_observed_l1_lib_header_in_db_batch(l1_lib_header, &mut write_batch)?; + } + + write_batch.put_cf( + &cf_hti, + borsh::to_vec(&block.header.hash).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize block hash".to_owned())) + })?, + borsh::to_vec(&block.header.block_id).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize block id".to_owned())) + })?, + ); + + let mut acc_to_tx_map: HashMap<[u8; 32], Vec<[u8; 32]>> = HashMap::new(); + + for tx in &block.body.transactions { + let tx_hash = tx.hash(); + + write_batch.put_cf( + &cf_tti, + borsh::to_vec(&tx_hash).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize tx hash".to_owned())) + })?, + borsh::to_vec(&block.header.block_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize block id".to_owned()), + ) + })?, + ); + + let acc_ids = tx + .affected_public_account_ids() + .into_iter() + .map(nssa::AccountId::into_value) + .collect::>(); + + for acc_id in acc_ids { + acc_to_tx_map + .entry(acc_id) + .and_modify(|tx_hashes| tx_hashes.push(tx_hash.into())) + .or_insert_with(|| vec![tx_hash.into()]); + } + } + + #[expect( + clippy::iter_over_hash_type, + reason = "RocksDB will keep ordering persistent" + )] + for (acc_id, tx_hashes) in acc_to_tx_map { + self.put_account_transactions_dependant(acc_id, &tx_hashes, &mut write_batch)?; + } + + self.db.write(write_batch).map_err(|rerr| { + DbError::rocksdb_cast_message(rerr, Some("Failed to write batch".to_owned())) + })?; + + if block + .header + .block_id + .is_multiple_of(BREAKPOINT_INTERVAL.into()) + { + self.put_next_breakpoint()?; + } + + Ok(()) + } +} diff --git a/storage/src/indexer/write_non_atomic.rs b/storage/src/indexer/write_non_atomic.rs new file mode 100644 index 00000000..17c1be18 --- /dev/null +++ b/storage/src/indexer/write_non_atomic.rs @@ -0,0 +1,147 @@ +use super::{ + BREAKPOINT_INTERVAL, DB_META_FIRST_BLOCK_SET_KEY, DB_META_LAST_BLOCK_IN_DB_KEY, + DB_META_LAST_BREAKPOINT_ID, DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY, DbError, + DbResult, RocksDBIO, V03State, +}; + +#[expect(clippy::multiple_inherent_impl, reason = "Readability")] +impl RocksDBIO { + // Meta + + pub fn put_meta_last_block_in_db(&self, block_id: u64) -> DbResult<()> { + let cf_meta = self.meta_column(); + self.db + .put_cf( + &cf_meta, + borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), + ) + })?, + borsh::to_vec(&block_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize last block id".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + Ok(()) + } + + pub fn put_meta_last_observed_l1_lib_header_in_db( + &self, + l1_lib_header: [u8; 32], + ) -> DbResult<()> { + let cf_meta = self.meta_column(); + self.db + .put_cf( + &cf_meta, + borsh::to_vec(&DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY).map_err( + |err| { + DbError::borsh_cast_message( + err, + Some( + "Failed to serialize DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY" + .to_owned(), + ), + ) + }, + )?, + borsh::to_vec(&l1_lib_header).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize last l1 block header".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + Ok(()) + } + + pub fn put_meta_last_breakpoint_id(&self, br_id: u64) -> DbResult<()> { + let cf_meta = self.meta_column(); + self.db + .put_cf( + &cf_meta, + borsh::to_vec(&DB_META_LAST_BREAKPOINT_ID).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_LAST_BREAKPOINT_ID".to_owned()), + ) + })?, + borsh::to_vec(&br_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize last block id".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + Ok(()) + } + + pub fn put_meta_is_first_block_set(&self) -> DbResult<()> { + let cf_meta = self.meta_column(); + self.db + .put_cf( + &cf_meta, + borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), + ) + })?, + [1_u8; 1], + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + Ok(()) + } + + // State + + pub fn put_breakpoint(&self, br_id: u64, breakpoint: &V03State) -> DbResult<()> { + let cf_br = self.breakpoint_column(); + + self.db + .put_cf( + &cf_br, + borsh::to_vec(&br_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize breakpoint id".to_owned()), + ) + })?, + borsh::to_vec(breakpoint).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize breakpoint data".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None)) + } + + pub fn put_next_breakpoint(&self) -> DbResult<()> { + let last_block = self.get_meta_last_block_in_db()?; + let next_breakpoint_id = self + .get_meta_last_breakpoint_id()? + .checked_add(1) + .expect("Breakpoint Id will be lesser than u64::MAX"); + let block_to_break_id = next_breakpoint_id + .checked_mul(u64::from(BREAKPOINT_INTERVAL)) + .expect("Reached maximum breakpoint id"); + + if block_to_break_id <= last_block { + let next_breakpoint = self.calculate_state_for_id(block_to_break_id)?; + + self.put_breakpoint(next_breakpoint_id, &next_breakpoint)?; + self.put_meta_last_breakpoint_id(next_breakpoint_id) + } else { + Err(DbError::db_interaction_error( + "Breakpoint not yet achieved".to_owned(), + )) + } + } +} diff --git a/test_program_methods/guest/src/bin/burner.rs b/test_program_methods/guest/src/bin/burner.rs index a2256aa3..991091c0 100644 --- a/test_program_methods/guest/src/bin/burner.rs +++ b/test_program_methods/guest/src/bin/burner.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = u128; @@ -19,9 +19,10 @@ fn main() { let mut account_post = account_pre.clone(); account_post.balance = account_post.balance.saturating_sub(balance_to_burn); - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![pre], vec![AccountPostState::new(account_post)], - ); + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/chain_caller.rs b/test_program_methods/guest/src/bin/chain_caller.rs index 7e67fa9b..c5780665 100644 --- a/test_program_methods/guest/src/bin/chain_caller.rs +++ b/test_program_methods/guest/src/bin/chain_caller.rs @@ -1,6 +1,6 @@ use nssa_core::program::{ - AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs, - write_nssa_outputs_with_chained_call, + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, }; use risc0_zkvm::serde::to_vec; @@ -38,7 +38,7 @@ fn main() { program_id: auth_transfer_id, instruction_data: instruction_data.clone(), pre_states: vec![running_sender_pre.clone(), running_recipient_pre.clone()], /* <- Account order permutation here */ - pda_seeds: pda_seed.iter().cloned().collect(), + pda_seeds: pda_seed.iter().copied().collect(), }; chained_calls.push(new_chained_call); @@ -54,13 +54,14 @@ fn main() { }; } - write_nssa_outputs_with_chained_call( + ProgramOutput::new( instruction_words, vec![sender_pre.clone(), recipient_pre.clone()], vec![ AccountPostState::new(sender_pre.account), AccountPostState::new(recipient_pre.account), ], - chained_calls, - ); + ) + .with_chained_calls(chained_calls) + .write(); } diff --git a/test_program_methods/guest/src/bin/changer_claimer.rs b/test_program_methods/guest/src/bin/changer_claimer.rs index 37079737..ee82ec16 100644 --- a/test_program_methods/guest/src/bin/changer_claimer.rs +++ b/test_program_methods/guest/src/bin/changer_claimer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = (Option>, bool); @@ -28,10 +28,10 @@ fn main() { // Claim or not based on the boolean flag let post_state = if should_claim { - AccountPostState::new_claimed(account_post) + AccountPostState::new_claimed(account_post, Claim::Authorized) } else { AccountPostState::new(account_post) }; - write_nssa_outputs(instruction_words, vec![pre], vec![post_state]); + ProgramOutput::new(instruction_words, vec![pre], vec![post_state]).write(); } diff --git a/test_program_methods/guest/src/bin/claimer.rs b/test_program_methods/guest/src/bin/claimer.rs index 897ca6a6..e6239381 100644 --- a/test_program_methods/guest/src/bin/claimer.rs +++ b/test_program_methods/guest/src/bin/claimer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = (); @@ -15,7 +15,7 @@ fn main() { return; }; - let account_post = AccountPostState::new_claimed(pre.account.clone()); + let account_post = AccountPostState::new_claimed(pre.account.clone(), Claim::Authorized); - write_nssa_outputs(instruction_words, vec![pre], vec![account_post]); + ProgramOutput::new(instruction_words, vec![pre], vec![account_post]).write(); } diff --git a/test_program_methods/guest/src/bin/data_changer.rs b/test_program_methods/guest/src/bin/data_changer.rs index c689dce5..730a7180 100644 --- a/test_program_methods/guest/src/bin/data_changer.rs +++ b/test_program_methods/guest/src/bin/data_changer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = Vec; @@ -22,9 +22,13 @@ fn main() { .try_into() .expect("provided data should fit into data limit"); - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![pre], - vec![AccountPostState::new_claimed(account_post)], - ); + vec![AccountPostState::new_claimed( + account_post, + Claim::Authorized, + )], + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/extra_output.rs b/test_program_methods/guest/src/bin/extra_output.rs index 4d67df6e..3adc591c 100644 --- a/test_program_methods/guest/src/bin/extra_output.rs +++ b/test_program_methods/guest/src/bin/extra_output.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::Account, - program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}, + program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}, }; type Instruction = (); @@ -14,12 +14,13 @@ fn main() { let account_pre = pre.account.clone(); - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![pre], vec![ AccountPostState::new(account_pre), AccountPostState::new(Account::default()), ], - ); + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs index 56ba7e72..7452d337 100644 --- a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs +++ b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs @@ -1,8 +1,7 @@ use nssa_core::{ account::AccountWithMetadata, program::{ - AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, - write_nssa_outputs_with_chained_call, + AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs, }, }; use risc0_zkvm::serde::to_vec; @@ -40,13 +39,14 @@ fn main() { pda_seeds: vec![], }; - write_nssa_outputs_with_chained_call( + ProgramOutput::new( instruction_words, vec![sender.clone(), receiver.clone()], vec![ AccountPostState::new(sender.account), AccountPostState::new(receiver.account), ], - vec![chained_call], - ); + ) + .with_chained_calls(vec![chained_call]) + .write(); } diff --git a/test_program_methods/guest/src/bin/minter.rs b/test_program_methods/guest/src/bin/minter.rs index a602df56..ac29e4d3 100644 --- a/test_program_methods/guest/src/bin/minter.rs +++ b/test_program_methods/guest/src/bin/minter.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = (); @@ -16,9 +16,10 @@ fn main() { .checked_add(1) .expect("Balance overflow"); - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![pre], vec![AccountPostState::new(account_post)], - ); + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/missing_output.rs b/test_program_methods/guest/src/bin/missing_output.rs index 52ca6e2f..b485e87a 100644 --- a/test_program_methods/guest/src/bin/missing_output.rs +++ b/test_program_methods/guest/src/bin/missing_output.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = (); @@ -11,9 +11,10 @@ fn main() { let account_pre1 = pre1.account.clone(); - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![pre1, pre2], vec![AccountPostState::new(account_pre1)], - ); + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/modified_transfer.rs b/test_program_methods/guest/src/bin/modified_transfer.rs index 3aee3816..a89c72fb 100644 --- a/test_program_methods/guest/src/bin/modified_transfer.rs +++ b/test_program_methods/guest/src/bin/modified_transfer.rs @@ -5,7 +5,7 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, - program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}, + program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}, }; /// Initializes a default account under the ownership of this program. @@ -80,5 +80,5 @@ fn main() { } _ => panic!("invalid params"), }; - write_nssa_outputs(instruction_data, pre_states, post_states); + ProgramOutput::new(instruction_data, pre_states, post_states).write(); } diff --git a/test_program_methods/guest/src/bin/nonce_changer.rs b/test_program_methods/guest/src/bin/nonce_changer.rs index 52d2e392..0cecdc81 100644 --- a/test_program_methods/guest/src/bin/nonce_changer.rs +++ b/test_program_methods/guest/src/bin/nonce_changer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = (); @@ -13,9 +13,10 @@ fn main() { let mut account_post = account_pre.clone(); account_post.nonce.public_account_nonce_increment(); - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![pre], vec![AccountPostState::new(account_post)], - ); + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/noop.rs b/test_program_methods/guest/src/bin/noop.rs index 79dd1dec..35a07765 100644 --- a/test_program_methods/guest/src/bin/noop.rs +++ b/test_program_methods/guest/src/bin/noop.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = (); @@ -9,5 +9,5 @@ fn main() { .iter() .map(|account| AccountPostState::new(account.account.clone())) .collect(); - write_nssa_outputs(instruction_words, pre_states, post_states); + ProgramOutput::new(instruction_words, pre_states, post_states).write(); } diff --git a/test_program_methods/guest/src/bin/program_owner_changer.rs b/test_program_methods/guest/src/bin/program_owner_changer.rs index 4b7de0f7..7e421351 100644 --- a/test_program_methods/guest/src/bin/program_owner_changer.rs +++ b/test_program_methods/guest/src/bin/program_owner_changer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = (); @@ -13,9 +13,10 @@ fn main() { let mut account_post = account_pre.clone(); account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7]; - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![pre], vec![AccountPostState::new(account_post)], - ); + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/simple_balance_transfer.rs b/test_program_methods/guest/src/bin/simple_balance_transfer.rs index 55bbfcef..9ee715e8 100644 --- a/test_program_methods/guest/src/bin/simple_balance_transfer.rs +++ b/test_program_methods/guest/src/bin/simple_balance_transfer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = u128; @@ -26,12 +26,13 @@ fn main() { .checked_add(balance) .expect("Overflow when adding balance"); - write_nssa_outputs( + ProgramOutput::new( instruction_words, vec![sender_pre, receiver_pre], vec![ AccountPostState::new(sender_post), AccountPostState::new(receiver_post), ], - ); + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/validity_window.rs b/test_program_methods/guest/src/bin/validity_window.rs new file mode 100644 index 00000000..a0ff9f36 --- /dev/null +++ b/test_program_methods/guest/src/bin/validity_window.rs @@ -0,0 +1,31 @@ +use nssa_core::program::{ + AccountPostState, BlockValidityWindow, ProgramInput, ProgramOutput, TimestampValidityWindow, + read_nssa_inputs, +}; + +type Instruction = (BlockValidityWindow, TimestampValidityWindow); + +fn main() { + let ( + ProgramInput { + pre_states, + instruction: (block_validity_window, timestamp_validity_window), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { + return; + }; + + let post = pre.account.clone(); + + ProgramOutput::new( + instruction_words, + vec![pre], + vec![AccountPostState::new(post)], + ) + .with_block_validity_window(block_validity_window) + .with_timestamp_validity_window(timestamp_validity_window) + .write(); +} diff --git a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs new file mode 100644 index 00000000..39f8ad69 --- /dev/null +++ b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs @@ -0,0 +1,48 @@ +use nssa_core::program::{ + AccountPostState, BlockValidityWindow, ChainedCall, ProgramId, ProgramInput, ProgramOutput, + TimestampValidityWindow, read_nssa_inputs, +}; +use risc0_zkvm::serde::to_vec; + +/// A program that sets a block validity window on its output and chains to another program with a +/// potentially different block validity window. +/// +/// Instruction: (`window`, `chained_program_id`, `chained_window`) +/// The initial output uses `window` and chains to `chained_program_id` with `chained_window`. +/// The chained program (`validity_window`) expects `(BlockValidityWindow, TimestampValidityWindow)` +/// so an unbounded timestamp window is appended automatically. +type Instruction = (BlockValidityWindow, ProgramId, BlockValidityWindow); + +fn main() { + let ( + ProgramInput { + pre_states, + instruction: (block_validity_window, chained_program_id, chained_block_validity_window), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let [pre] = <[_; 1]>::try_from(pre_states.clone()).expect("Expected exactly one pre state"); + let post = pre.account.clone(); + + let chained_instruction = to_vec(&( + chained_block_validity_window, + TimestampValidityWindow::new_unbounded(), + )) + .unwrap(); + let chained_call = ChainedCall { + program_id: chained_program_id, + instruction_data: chained_instruction, + pre_states, + pda_seeds: vec![], + }; + + ProgramOutput::new( + instruction_words, + vec![pre], + vec![AccountPostState::new(post)], + ) + .with_block_validity_window(block_validity_window) + .with_chained_calls(vec![chained_call]) + .write(); +} diff --git a/testnet_initial_state/Cargo.toml b/testnet_initial_state/Cargo.toml new file mode 100644 index 00000000..2b73f479 --- /dev/null +++ b/testnet_initial_state/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "testnet_initial_state" +version = "0.1.0" +edition = "2024" +license.workspace = true + +[dependencies] +key_protocol.workspace = true +nssa.workspace = true +nssa_core.workspace = true +common.workspace = true + +serde.workspace = true + +[lints] +workspace = true diff --git a/testnet_initial_state/src/lib.rs b/testnet_initial_state/src/lib.rs new file mode 100644 index 00000000..1d75d6a1 --- /dev/null +++ b/testnet_initial_state/src/lib.rs @@ -0,0 +1,396 @@ +use common::PINATA_BASE58; +use key_protocol::key_management::{ + KeyChain, + secret_holders::{PrivateKeyHolder, SecretSpendingKey}, +}; +use nssa::{Account, AccountId, Data, PrivateKey, PublicKey, V03State}; +use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point}; +use serde::{Deserialize, Serialize}; + +const PRIVATE_KEY_PUB_ACC_A: [u8; 32] = [ + 16, 162, 106, 154, 236, 125, 52, 184, 35, 100, 238, 174, 69, 197, 41, 77, 187, 10, 118, 75, 0, + 11, 148, 238, 185, 181, 133, 17, 220, 72, 124, 77, +]; + +const PRIVATE_KEY_PUB_ACC_B: [u8; 32] = [ + 113, 121, 64, 177, 204, 85, 229, 214, 178, 6, 109, 191, 29, 154, 63, 38, 242, 18, 244, 219, 8, + 208, 35, 136, 23, 127, 207, 237, 216, 169, 190, 27, +]; + +const SSK_PRIV_ACC_A: [u8; 32] = [ + 93, 13, 190, 240, 250, 33, 108, 195, 176, 40, 144, 61, 4, 28, 58, 112, 53, 161, 42, 238, 155, + 27, 23, 176, 208, 121, 15, 229, 165, 180, 99, 143, +]; + +const SSK_PRIV_ACC_B: [u8; 32] = [ + 48, 175, 124, 10, 230, 240, 166, 14, 249, 254, 157, 226, 208, 124, 122, 177, 203, 139, 192, + 180, 43, 120, 55, 151, 50, 21, 113, 22, 254, 83, 148, 56, +]; + +const NSK_PRIV_ACC_A: [u8; 32] = [ + 25, 21, 186, 59, 180, 224, 101, 64, 163, 208, 228, 43, 13, 185, 100, 123, 156, 47, 80, 179, 72, + 51, 115, 11, 180, 99, 21, 201, 48, 194, 118, 144, +]; + +const NSK_PRIV_ACC_B: [u8; 32] = [ + 99, 82, 190, 140, 234, 10, 61, 163, 15, 211, 179, 54, 70, 166, 87, 5, 182, 68, 117, 244, 217, + 23, 99, 9, 4, 177, 230, 125, 109, 91, 160, 30, +]; + +const VSK_PRIV_ACC_A: [u8; 32] = [ + 5, 85, 114, 119, 141, 187, 202, 170, 122, 253, 198, 81, 150, 8, 155, 21, 192, 65, 24, 124, 116, + 98, 110, 106, 137, 90, 165, 239, 80, 13, 222, 30, +]; + +const VSK_PRIV_ACC_B: [u8; 32] = [ + 205, 32, 76, 251, 255, 236, 96, 119, 61, 111, 65, 100, 75, 218, 12, 22, 17, 170, 55, 226, 21, + 154, 161, 34, 208, 74, 27, 1, 119, 13, 88, 128, +]; + +const VPK_PRIV_ACC_A: [u8; 33] = [ + 2, 210, 206, 38, 213, 4, 182, 198, 220, 47, 93, 148, 61, 84, 148, 250, 158, 45, 8, 81, 48, 80, + 46, 230, 87, 210, 47, 204, 76, 58, 214, 167, 81, +]; + +const VPK_PRIV_ACC_B: [u8; 33] = [ + 2, 79, 110, 46, 203, 29, 206, 205, 18, 86, 27, 189, 104, 103, 113, 181, 110, 53, 78, 172, 11, + 171, 190, 18, 126, 214, 81, 77, 192, 154, 58, 195, 238, +]; + +const NPK_PRIV_ACC_A: [u8; 32] = [ + 167, 108, 50, 153, 74, 47, 151, 188, 140, 79, 195, 31, 181, 9, 40, 167, 201, 32, 175, 129, 45, + 245, 223, 193, 210, 170, 247, 128, 167, 140, 155, 129, +]; + +const NPK_PRIV_ACC_B: [u8; 32] = [ + 32, 67, 72, 164, 106, 53, 66, 239, 141, 15, 52, 230, 136, 177, 2, 236, 207, 243, 134, 135, 210, + 143, 87, 232, 215, 128, 194, 120, 113, 224, 4, 165, +]; + +const DEFAULT_PROGRAM_OWNER: [u32; 8] = [0, 0, 0, 0, 0, 0, 0, 0]; + +const PUB_ACC_A_INITIAL_BALANCE: u128 = 10000; +const PUB_ACC_B_INITIAL_BALANCE: u128 = 20000; + +const PRIV_ACC_A_INITIAL_BALANCE: u128 = 10000; +const PRIV_ACC_B_INITIAL_BALANCE: u128 = 20000; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PublicAccountPublicInitialData { + pub account_id: AccountId, + pub balance: u128, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PrivateAccountPublicInitialData { + pub npk: nssa_core::NullifierPublicKey, + pub account: nssa_core::account::Account, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PublicAccountPrivateInitialData { + pub account_id: nssa::AccountId, + pub pub_sign_key: nssa::PrivateKey, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrivateAccountPrivateInitialData { + pub account_id: nssa::AccountId, + pub account: nssa_core::account::Account, + pub key_chain: KeyChain, +} + +#[must_use] +pub fn initial_pub_accounts_private_keys() -> Vec { + let acc1_pub_sign_key = PrivateKey::try_new(PRIVATE_KEY_PUB_ACC_A).unwrap(); + + let acc2_pub_sign_key = PrivateKey::try_new(PRIVATE_KEY_PUB_ACC_B).unwrap(); + + vec![ + PublicAccountPrivateInitialData { + account_id: AccountId::from(&PublicKey::new_from_private_key(&acc1_pub_sign_key)), + pub_sign_key: acc1_pub_sign_key, + }, + PublicAccountPrivateInitialData { + account_id: AccountId::from(&PublicKey::new_from_private_key(&acc2_pub_sign_key)), + pub_sign_key: acc2_pub_sign_key, + }, + ] +} + +#[must_use] +pub fn initial_priv_accounts_private_keys() -> Vec { + let key_chain_1 = KeyChain { + secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_A), + private_key_holder: PrivateKeyHolder { + nullifier_secret_key: NSK_PRIV_ACC_A, + viewing_secret_key: VSK_PRIV_ACC_A, + }, + nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_A), + viewing_public_key: Secp256k1Point(VPK_PRIV_ACC_A.to_vec()), + }; + + let key_chain_2 = KeyChain { + secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_B), + private_key_holder: PrivateKeyHolder { + nullifier_secret_key: NSK_PRIV_ACC_B, + viewing_secret_key: VSK_PRIV_ACC_B, + }, + nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_B), + viewing_public_key: Secp256k1Point(VPK_PRIV_ACC_B.to_vec()), + }; + + vec![ + PrivateAccountPrivateInitialData { + account_id: AccountId::from(&key_chain_1.nullifier_public_key), + account: Account { + program_owner: DEFAULT_PROGRAM_OWNER, + balance: PRIV_ACC_A_INITIAL_BALANCE, + data: Data::default(), + nonce: 0.into(), + }, + key_chain: key_chain_1, + }, + PrivateAccountPrivateInitialData { + account_id: AccountId::from(&key_chain_2.nullifier_public_key), + account: Account { + program_owner: DEFAULT_PROGRAM_OWNER, + balance: PRIV_ACC_B_INITIAL_BALANCE, + data: Data::default(), + nonce: 0.into(), + }, + key_chain: key_chain_2, + }, + ] +} + +#[must_use] +pub fn initial_commitments() -> Vec { + initial_priv_accounts_private_keys() + .into_iter() + .map(|data| PrivateAccountPublicInitialData { + npk: data.key_chain.nullifier_public_key.clone(), + account: data.account, + }) + .collect() +} + +#[must_use] +pub fn initial_accounts() -> Vec { + let initial_account_ids = initial_pub_accounts_private_keys() + .into_iter() + .map(|data| data.account_id) + .collect::>(); + + vec![ + PublicAccountPublicInitialData { + account_id: initial_account_ids[0], + balance: PUB_ACC_A_INITIAL_BALANCE, + }, + PublicAccountPublicInitialData { + account_id: initial_account_ids[1], + balance: PUB_ACC_B_INITIAL_BALANCE, + }, + ] +} + +#[must_use] +pub fn initial_state() -> V03State { + let initial_commitments: Vec = initial_commitments() + .iter() + .map(|init_comm_data| { + let npk = &init_comm_data.npk; + + let mut acc = init_comm_data.account.clone(); + + acc.program_owner = nssa::program::Program::authenticated_transfer_program().id(); + + nssa_core::Commitment::new(npk, &acc) + }) + .collect(); + + let init_accs: Vec<(nssa::AccountId, u128)> = initial_accounts() + .iter() + .map(|acc_data| (acc_data.account_id, acc_data.balance)) + .collect(); + + nssa::V03State::new_with_genesis_accounts(&init_accs, &initial_commitments, 0) +} + +#[must_use] +pub fn initial_state_testnet() -> V03State { + let mut state = initial_state(); + + state.add_pinata_program(PINATA_BASE58.parse().unwrap()); + + state +} + +#[cfg(test)] +mod tests { + use std::str::FromStr as _; + + use super::*; + + const PUB_ACC_A_TEXT_ADDR: &str = "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV"; + const PUB_ACC_B_TEXT_ADDR: &str = "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"; + + const PRIV_ACC_A_TEXT_ADDR: &str = "5ya25h4Xc9GAmrGB2WrTEnEWtQKJwRwQx3Xfo2tucNcE"; + const PRIV_ACC_B_TEXT_ADDR: &str = "E8HwiTyQe4H9HK7icTvn95HQMnzx49mP9A2ddtMLpNaN"; + + #[test] + fn pub_state_consistency() { + let init_accs_private_data = initial_pub_accounts_private_keys(); + let init_accs_pub_data = initial_accounts(); + + assert_eq!( + init_accs_private_data[0].account_id, + init_accs_pub_data[0].account_id + ); + + assert_eq!( + init_accs_private_data[1].account_id, + init_accs_pub_data[1].account_id + ); + + assert_eq!( + init_accs_pub_data[0], + PublicAccountPublicInitialData { + account_id: AccountId::from_str(PUB_ACC_A_TEXT_ADDR).unwrap(), + balance: PUB_ACC_A_INITIAL_BALANCE, + } + ); + + assert_eq!( + init_accs_pub_data[1], + PublicAccountPublicInitialData { + account_id: AccountId::from_str(PUB_ACC_B_TEXT_ADDR).unwrap(), + balance: PUB_ACC_B_INITIAL_BALANCE, + } + ); + } + + #[test] + fn private_state_consistency() { + let init_private_accs_keys = initial_priv_accounts_private_keys(); + let init_comms = initial_commitments(); + + assert_eq!( + init_private_accs_keys[0] + .key_chain + .secret_spending_key + .produce_private_key_holder(None) + .nullifier_secret_key, + init_private_accs_keys[0] + .key_chain + .private_key_holder + .nullifier_secret_key + ); + assert_eq!( + init_private_accs_keys[0] + .key_chain + .secret_spending_key + .produce_private_key_holder(None) + .viewing_secret_key, + init_private_accs_keys[0] + .key_chain + .private_key_holder + .viewing_secret_key + ); + assert_eq!( + init_private_accs_keys[0] + .key_chain + .private_key_holder + .generate_nullifier_public_key(), + init_private_accs_keys[0].key_chain.nullifier_public_key + ); + assert_eq!( + init_private_accs_keys[0] + .key_chain + .private_key_holder + .generate_viewing_public_key(), + init_private_accs_keys[0].key_chain.viewing_public_key + ); + + assert_eq!( + init_private_accs_keys[1] + .key_chain + .secret_spending_key + .produce_private_key_holder(None) + .nullifier_secret_key, + init_private_accs_keys[1] + .key_chain + .private_key_holder + .nullifier_secret_key + ); + assert_eq!( + init_private_accs_keys[1] + .key_chain + .secret_spending_key + .produce_private_key_holder(None) + .viewing_secret_key, + init_private_accs_keys[1] + .key_chain + .private_key_holder + .viewing_secret_key + ); + assert_eq!( + init_private_accs_keys[1] + .key_chain + .private_key_holder + .generate_nullifier_public_key(), + init_private_accs_keys[1].key_chain.nullifier_public_key + ); + assert_eq!( + init_private_accs_keys[1] + .key_chain + .private_key_holder + .generate_viewing_public_key(), + init_private_accs_keys[1].key_chain.viewing_public_key + ); + + assert_eq!( + init_private_accs_keys[0].account_id.to_string(), + PRIV_ACC_A_TEXT_ADDR + ); + assert_eq!( + init_private_accs_keys[1].account_id.to_string(), + PRIV_ACC_B_TEXT_ADDR + ); + + assert_eq!( + init_private_accs_keys[0].key_chain.nullifier_public_key, + init_comms[0].npk + ); + assert_eq!( + init_private_accs_keys[1].key_chain.nullifier_public_key, + init_comms[1].npk + ); + + assert_eq!( + init_comms[0], + PrivateAccountPublicInitialData { + npk: NullifierPublicKey(NPK_PRIV_ACC_A), + account: Account { + program_owner: DEFAULT_PROGRAM_OWNER, + balance: PRIV_ACC_A_INITIAL_BALANCE, + data: Data::default(), + nonce: 0.into(), + }, + } + ); + + assert_eq!( + init_comms[1], + PrivateAccountPublicInitialData { + npk: NullifierPublicKey(NPK_PRIV_ACC_B), + account: Account { + program_owner: DEFAULT_PROGRAM_OWNER, + balance: PRIV_ACC_B_INITIAL_BALANCE, + data: Data::default(), + nonce: 0.into(), + }, + } + ); + } +} diff --git a/wallet-ffi/src/pinata.rs b/wallet-ffi/src/pinata.rs index a8e8a688..5807db7b 100644 --- a/wallet-ffi/src/pinata.rs +++ b/wallet-ffi/src/pinata.rs @@ -78,7 +78,6 @@ pub unsafe extern "C" fn wallet_ffi_claim_pinata( Ok(tx_hash) => { let tx_hash = CString::new(tx_hash.to_string()) .map_or(ptr::null_mut(), std::ffi::CString::into_raw); - unsafe { (*out_result).tx_hash = tx_hash; (*out_result).success = true; diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 2e628790..f77988a0 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -15,6 +15,8 @@ key_protocol.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } token_core.workspace = true amm_core.workspace = true +testnet_initial_state.workspace = true +ata_core.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index a88e0b8b..ebfe9896 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -99,16 +99,22 @@ impl WalletChainStore { let mut public_init_acc_map = BTreeMap::new(); let mut private_init_acc_map = BTreeMap::new(); - for init_acc_data in config.initial_accounts.clone() { + let initial_accounts = config + .initial_accounts + .clone() + .unwrap_or_else(InitialAccountData::create_initial_accounts_data); + + for init_acc_data in initial_accounts { match init_acc_data { InitialAccountData::Public(data) => { public_init_acc_map.insert(data.account_id, data.pub_sign_key); } InitialAccountData::Private(data) => { let mut account = data.account; - // TODO: Program owner is only known after code is compiled and can't be set in - // the config. Therefore we overwrite it here on startup. Fix this when program - // id can be fetched from the node and queried from the wallet. + // TODO: Program owner is only known after code is compiled and can't be set + // in the config. Therefore we overwrite it here on + // startup. Fix this when program id can be fetched + // from the node and queried from the wallet. account.program_owner = Program::authenticated_transfer_program().id(); private_init_acc_map.insert(data.account_id, (data.key_chain, account)); } @@ -161,45 +167,12 @@ impl WalletChainStore { #[cfg(test)] mod tests { - use std::str::FromStr as _; - use key_protocol::key_management::key_tree::{ keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, traits::KeyNode as _, }; - use nssa::PrivateKey; use super::*; - use crate::config::{ - InitialAccountData, InitialAccountDataPublic, PersistentAccountDataPrivate, - PersistentAccountDataPublic, - }; - - fn create_initial_accounts() -> Vec { - vec![ - InitialAccountData::Public(InitialAccountDataPublic { - account_id: nssa::AccountId::from_str( - "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r", - ) - .unwrap(), - pub_sign_key: PrivateKey::try_new([ - 127, 39, 48, 152, 242, 91, 113, 230, 192, 5, 169, 81, 159, 38, 120, 218, 141, - 28, 127, 1, 246, 162, 119, 120, 226, 217, 148, 138, 189, 249, 1, 251, - ]) - .unwrap(), - }), - InitialAccountData::Public(InitialAccountDataPublic { - account_id: nssa::AccountId::from_str( - "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2", - ) - .unwrap(), - pub_sign_key: PrivateKey::try_new([ - 244, 52, 248, 116, 23, 32, 1, 69, 134, 174, 67, 53, 109, 42, 236, 98, 87, 218, - 8, 98, 34, 246, 4, 221, 183, 93, 105, 115, 59, 134, 252, 76, - ]) - .unwrap(), - }), - ] - } + use crate::config::{PersistentAccountDataPrivate, PersistentAccountDataPublic}; fn create_sample_wallet_config() -> WalletConfig { WalletConfig { @@ -208,8 +181,8 @@ mod tests { seq_tx_poll_max_blocks: 5, seq_poll_max_retries: 10, seq_block_poll_max_amount: 100, - initial_accounts: create_initial_accounts(), basic_auth: None, + initial_accounts: None, } } diff --git a/wallet/src/cli/config.rs b/wallet/src/cli/config.rs index c9a5796f..7f0ba952 100644 --- a/wallet/src/cli/config.rs +++ b/wallet/src/cli/config.rs @@ -4,6 +4,7 @@ use clap::Subcommand; use crate::{ WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, + config::InitialAccountData, }; /// Represents generic config CLI subcommand. @@ -59,7 +60,17 @@ impl WalletSubcommand for ConfigSubcommand { ); } "initial_accounts" => { - println!("{:#?}", wallet_core.storage.wallet_config.initial_accounts); + println!( + "{:#?}", + wallet_core + .storage + .wallet_config + .initial_accounts + .clone() + .unwrap_or_else( + InitialAccountData::create_initial_accounts_data + ) + ); } "basic_auth" => { if let Some(basic_auth) = &wallet_core.storage.wallet_config.basic_auth diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 85e792cc..6463dee8 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -14,8 +14,9 @@ use crate::{ chain::ChainSubcommand, config::ConfigSubcommand, programs::{ - amm::AmmProgramAgnosticSubcommand, native_token_transfer::AuthTransferSubcommand, - pinata::PinataProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand, + amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand, + native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, + token::TokenProgramAgnosticSubcommand, }, }, }; @@ -52,6 +53,9 @@ pub enum Command { /// AMM program interaction subcommand. #[command(subcommand)] AMM(AmmProgramAgnosticSubcommand), + /// Associated Token Account program interaction subcommand. + #[command(subcommand)] + Ata(AtaSubcommand), /// Check the wallet can connect to the node and builtin local programs /// match the remote versions. CheckHealth, @@ -158,6 +162,7 @@ pub async fn execute_subcommand( } Command::Token(token_subcommand) => token_subcommand.handle_subcommand(wallet_core).await?, Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(wallet_core).await?, + Command::Ata(ata_subcommand) => ata_subcommand.handle_subcommand(wallet_core).await?, Command::Config(config_subcommand) => { config_subcommand.handle_subcommand(wallet_core).await? } diff --git a/wallet/src/cli/programs/ata.rs b/wallet/src/cli/programs/ata.rs new file mode 100644 index 00000000..1a63fa67 --- /dev/null +++ b/wallet/src/cli/programs/ata.rs @@ -0,0 +1,240 @@ +use anyhow::Result; +use clap::Subcommand; +use common::transaction::NSSATransaction; +use nssa::{Account, AccountId, program::Program}; +use token_core::TokenHolding; + +use crate::{ + AccDecodeData::Decode, + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, + helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, + program_facades::ata::Ata, +}; + +/// Represents generic CLI subcommand for a wallet working with the ATA program. +#[derive(Subcommand, Debug, Clone)] +pub enum AtaSubcommand { + /// Derive and print the Associated Token Account address (local only, no network). + Address { + /// Owner account - valid 32 byte base58 string (no privacy prefix). + #[arg(long)] + owner: String, + /// Token definition account - valid 32 byte base58 string (no privacy prefix). + #[arg(long)] + token_definition: String, + }, + /// Create (or idempotently no-op) the Associated Token Account. + Create { + /// Owner account - valid 32 byte base58 string with privacy prefix. + #[arg(long)] + owner: String, + /// Token definition account - valid 32 byte base58 string WITHOUT privacy prefix. + #[arg(long)] + token_definition: String, + }, + /// Send tokens from owner's ATA to a recipient token holding account. + Send { + /// Sender account - valid 32 byte base58 string with privacy prefix. + #[arg(long)] + from: String, + /// Token definition account - valid 32 byte base58 string WITHOUT privacy prefix. + #[arg(long)] + token_definition: String, + /// Recipient account - valid 32 byte base58 string WITHOUT privacy prefix. + #[arg(long)] + to: String, + #[arg(long)] + amount: u128, + }, + /// Burn tokens from holder's ATA. + Burn { + /// Holder account - valid 32 byte base58 string with privacy prefix. + #[arg(long)] + holder: String, + /// Token definition account - valid 32 byte base58 string WITHOUT privacy prefix. + #[arg(long)] + token_definition: String, + #[arg(long)] + amount: u128, + }, + /// List all ATAs for a given owner across multiple token definitions. + List { + /// Owner account - valid 32 byte base58 string (no privacy prefix). + #[arg(long)] + owner: String, + /// Token definition accounts - valid 32 byte base58 strings (no privacy prefix). + #[arg(long, num_args = 1..)] + token_definition: Vec, + }, +} + +impl WalletSubcommand for AtaSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + Self::Address { + owner, + token_definition, + } => { + let owner_id: AccountId = owner.parse()?; + let definition_id: AccountId = token_definition.parse()?; + let ata_program_id = Program::ata().id(); + let ata_id = ata_core::get_associated_token_account_id( + &ata_program_id, + &ata_core::compute_ata_seed(owner_id, definition_id), + ); + println!("{ata_id}"); + Ok(SubcommandReturnValue::Empty) + } + Self::Create { + owner, + token_definition, + } => { + let (owner_str, owner_privacy) = parse_addr_with_privacy_prefix(&owner)?; + let owner_id: AccountId = owner_str.parse()?; + let definition_id: AccountId = token_definition.parse()?; + + match owner_privacy { + AccountPrivacyKind::Public => { + Ata(wallet_core) + .send_create(owner_id, definition_id) + .await?; + Ok(SubcommandReturnValue::Empty) + } + AccountPrivacyKind::Private => { + let (tx_hash, secret) = Ata(wallet_core) + .send_create_private_owner(owner_id, definition_id) + .await?; + + println!("Transaction hash is {tx_hash}"); + + let tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + if let NSSATransaction::PrivacyPreserving(tx) = tx { + wallet_core.decode_insert_privacy_preserving_transaction_results( + &tx, + &[Decode(secret, owner_id)], + )?; + } + + wallet_core.store_persistent_data().await?; + Ok(SubcommandReturnValue::Empty) + } + } + } + Self::Send { + from, + token_definition, + to, + amount, + } => { + let (from_str, from_privacy) = parse_addr_with_privacy_prefix(&from)?; + let from_id: AccountId = from_str.parse()?; + let definition_id: AccountId = token_definition.parse()?; + let to_id: AccountId = to.parse()?; + + match from_privacy { + AccountPrivacyKind::Public => { + Ata(wallet_core) + .send_transfer(from_id, definition_id, to_id, amount) + .await?; + Ok(SubcommandReturnValue::Empty) + } + AccountPrivacyKind::Private => { + let (tx_hash, secret) = Ata(wallet_core) + .send_transfer_private_owner(from_id, definition_id, to_id, amount) + .await?; + + println!("Transaction hash is {tx_hash}"); + + let tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + if let NSSATransaction::PrivacyPreserving(tx) = tx { + wallet_core.decode_insert_privacy_preserving_transaction_results( + &tx, + &[Decode(secret, from_id)], + )?; + } + + wallet_core.store_persistent_data().await?; + Ok(SubcommandReturnValue::Empty) + } + } + } + Self::Burn { + holder, + token_definition, + amount, + } => { + let (holder_str, holder_privacy) = parse_addr_with_privacy_prefix(&holder)?; + let holder_id: AccountId = holder_str.parse()?; + let definition_id: AccountId = token_definition.parse()?; + + match holder_privacy { + AccountPrivacyKind::Public => { + Ata(wallet_core) + .send_burn(holder_id, definition_id, amount) + .await?; + Ok(SubcommandReturnValue::Empty) + } + AccountPrivacyKind::Private => { + let (tx_hash, secret) = Ata(wallet_core) + .send_burn_private_owner(holder_id, definition_id, amount) + .await?; + + println!("Transaction hash is {tx_hash}"); + + let tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + if let NSSATransaction::PrivacyPreserving(tx) = tx { + wallet_core.decode_insert_privacy_preserving_transaction_results( + &tx, + &[Decode(secret, holder_id)], + )?; + } + + wallet_core.store_persistent_data().await?; + Ok(SubcommandReturnValue::Empty) + } + } + } + Self::List { + owner, + token_definition, + } => { + let owner_id: AccountId = owner.parse()?; + let ata_program_id = Program::ata().id(); + + for def in &token_definition { + let definition_id: AccountId = def.parse()?; + let ata_id = ata_core::get_associated_token_account_id( + &ata_program_id, + &ata_core::compute_ata_seed(owner_id, definition_id), + ); + let account = wallet_core.get_account_public(ata_id).await?; + + if account == Account::default() { + println!("No ATA for definition {definition_id}"); + } else { + let holding = TokenHolding::try_from(&account.data)?; + match holding { + TokenHolding::Fungible { balance, .. } => { + println!( + "ATA {ata_id} (definition {definition_id}): balance {balance}" + ); + } + TokenHolding::NftMaster { .. } + | TokenHolding::NftPrintedCopy { .. } => { + println!( + "ATA {ata_id} (definition {definition_id}): unsupported token type" + ); + } + } + } + } + + Ok(SubcommandReturnValue::Empty) + } + } + } +} diff --git a/wallet/src/cli/programs/mod.rs b/wallet/src/cli/programs/mod.rs index 96a4e766..f6e4b5dc 100644 --- a/wallet/src/cli/programs/mod.rs +++ b/wallet/src/cli/programs/mod.rs @@ -1,4 +1,5 @@ pub mod amm; +pub mod ata; pub mod native_token_transfer; pub mod pinata; pub mod token; diff --git a/wallet/src/config.rs b/wallet/src/config.rs index 60164e86..33527009 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -8,22 +8,17 @@ use std::{ use anyhow::{Context as _, Result}; use common::config::BasicAuth; use humantime_serde; -use key_protocol::key_management::{ - KeyChain, - key_tree::{ - chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, - }, +use key_protocol::key_management::key_tree::{ + chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, }; use log::warn; use serde::{Deserialize, Serialize}; +use testnet_initial_state::{ + PrivateAccountPrivateInitialData, PublicAccountPrivateInitialData, + initial_priv_accounts_private_keys, initial_pub_accounts_private_keys, +}; use url::Url; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InitialAccountDataPublic { - pub account_id: nssa::AccountId, - pub pub_sign_key: nssa::PrivateKey, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersistentAccountDataPublic { pub account_id: nssa::AccountId, @@ -31,13 +26,6 @@ pub struct PersistentAccountDataPublic { pub data: ChildKeysPublic, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InitialAccountDataPrivate { - pub account_id: nssa::AccountId, - pub account: nssa_core::account::Account, - pub key_chain: KeyChain, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersistentAccountDataPrivate { pub account_id: nssa::AccountId, @@ -50,8 +38,29 @@ pub struct PersistentAccountDataPrivate { // memory #[derive(Debug, Clone, Serialize, Deserialize)] pub enum InitialAccountData { - Public(InitialAccountDataPublic), - Private(Box), + Public(PublicAccountPrivateInitialData), + Private(Box), +} + +impl InitialAccountData { + #[must_use] + pub const fn account_id(&self) -> nssa::AccountId { + match &self { + Self::Public(acc) => acc.account_id, + Self::Private(acc) => acc.account_id, + } + } + + pub(crate) fn create_initial_accounts_data() -> Vec { + let pub_data = initial_pub_accounts_private_keys(); + let priv_data = initial_priv_accounts_private_keys(); + + pub_data + .into_iter() + .map(Into::into) + .chain(priv_data.into_iter().map(Into::into)) + .collect() + } } // Big difference in enum variants sizes @@ -114,16 +123,6 @@ impl PersistentStorage { } } -impl InitialAccountData { - #[must_use] - pub fn account_id(&self) -> nssa::AccountId { - match &self { - Self::Public(acc) => acc.account_id, - Self::Private(acc) => acc.account_id, - } - } -} - impl PersistentAccountData { #[must_use] pub fn account_id(&self) -> nssa::AccountId { @@ -135,14 +134,14 @@ impl PersistentAccountData { } } -impl From for InitialAccountData { - fn from(value: InitialAccountDataPublic) -> Self { +impl From for InitialAccountData { + fn from(value: PublicAccountPrivateInitialData) -> Self { Self::Public(value) } } -impl From for InitialAccountData { - fn from(value: InitialAccountDataPrivate) -> Self { +impl From for InitialAccountData { + fn from(value: PrivateAccountPrivateInitialData) -> Self { Self::Private(Box::new(value)) } } @@ -197,37 +196,15 @@ pub struct WalletConfig { pub seq_poll_max_retries: u64, /// Max amount of blocks to poll in one request. pub seq_block_poll_max_amount: u64, - /// Initial accounts for wallet. - pub initial_accounts: Vec, - /// Basic authentication credentials. + /// Basic authentication credentials #[serde(skip_serializing_if = "Option::is_none")] pub basic_auth: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_accounts: Option>, } impl Default for WalletConfig { fn default() -> Self { - let pub_sign_key1 = nssa::PrivateKey::try_new([ - 127, 39, 48, 152, 242, 91, 113, 230, 192, 5, 169, 81, 159, 38, 120, 218, 141, 28, 127, - 1, 246, 162, 119, 120, 226, 217, 148, 138, 189, 249, 1, 251, - ]) - .unwrap(); - let public_key1 = nssa::PublicKey::new_from_private_key(&pub_sign_key1); - let public_account_id1 = nssa::AccountId::from(&public_key1); - - let pub_sign_key2 = nssa::PrivateKey::try_new([ - 244, 52, 248, 116, 23, 32, 1, 69, 134, 174, 67, 53, 109, 42, 236, 98, 87, 218, 8, 98, - 34, 246, 4, 221, 183, 93, 105, 115, 59, 134, 252, 76, - ]) - .unwrap(); - let public_key2 = nssa::PublicKey::new_from_private_key(&pub_sign_key2); - let public_account_id2 = nssa::AccountId::from(&public_key2); - - let key_chain1 = KeyChain::new_mnemonic("default_private_account_1".to_owned()); - let private_account_id1 = nssa::AccountId::from(&key_chain1.nullifier_public_key); - - let key_chain2 = KeyChain::new_mnemonic("default_private_account_2".to_owned()); - let private_account_id2 = nssa::AccountId::from(&key_chain2.nullifier_public_key); - Self { sequencer_addr: "http://127.0.0.1:3040".parse().unwrap(), seq_poll_timeout: Duration::from_secs(12), @@ -235,32 +212,7 @@ impl Default for WalletConfig { seq_poll_max_retries: 5, seq_block_poll_max_amount: 100, basic_auth: None, - initial_accounts: vec![ - InitialAccountData::Public(InitialAccountDataPublic { - account_id: public_account_id1, - pub_sign_key: pub_sign_key1, - }), - InitialAccountData::Public(InitialAccountDataPublic { - account_id: public_account_id2, - pub_sign_key: pub_sign_key2, - }), - InitialAccountData::Private(Box::new(InitialAccountDataPrivate { - account_id: private_account_id1, - account: nssa::Account { - balance: 10_000, - ..Default::default() - }, - key_chain: key_chain1, - })), - InitialAccountData::Private(Box::new(InitialAccountDataPrivate { - account_id: private_account_id2, - account: nssa::Account { - balance: 20_000, - ..Default::default() - }, - key_chain: key_chain2, - })), - ], + initial_accounts: None, } } } @@ -310,8 +262,8 @@ impl WalletConfig { seq_tx_poll_max_blocks, seq_poll_max_retries, seq_block_poll_max_amount, - initial_accounts, basic_auth, + initial_accounts, } = self; let WalletConfigOverrides { @@ -320,8 +272,8 @@ impl WalletConfig { seq_tx_poll_max_blocks: o_seq_tx_poll_max_blocks, seq_poll_max_retries: o_seq_poll_max_retries, seq_block_poll_max_amount: o_seq_block_poll_max_amount, - initial_accounts: o_initial_accounts, basic_auth: o_basic_auth, + initial_accounts: o_initial_accounts, } = overrides; if let Some(v) = o_sequencer_addr { @@ -344,13 +296,13 @@ impl WalletConfig { warn!("Overriding wallet config 'seq_block_poll_max_amount' to {v}"); *seq_block_poll_max_amount = v; } - if let Some(v) = o_initial_accounts { - warn!("Overriding wallet config 'initial_accounts' to {v:#?}"); - *initial_accounts = v; - } if let Some(v) = o_basic_auth { warn!("Overriding wallet config 'basic_auth' to {v:#?}"); *basic_auth = v; } + if let Some(v) = o_initial_accounts { + warn!("Overriding wallet config 'initial_accounts' to {v:#?}"); + *initial_accounts = v; + } } } diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 74f7bab3..d82dedaf 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -7,12 +7,13 @@ use nssa::Account; use nssa_core::account::Nonce; use rand::{RngCore as _, rngs::OsRng}; use serde::Serialize; +use testnet_initial_state::{PrivateAccountPrivateInitialData, PublicAccountPrivateInitialData}; use crate::{ HOME_DIR_ENV_VAR, config::{ - InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, Label, - PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, + InitialAccountData, Label, PersistentAccountDataPrivate, PersistentAccountDataPublic, + PersistentStorage, }, }; @@ -119,7 +120,7 @@ pub fn produce_data_for_storage( for (account_id, key) in &user_data.default_pub_account_signing_keys { vec_for_storage.push( - InitialAccountData::Public(InitialAccountDataPublic { + InitialAccountData::Public(PublicAccountPrivateInitialData { account_id: *account_id, pub_sign_key: key.clone(), }) @@ -129,7 +130,7 @@ pub fn produce_data_for_storage( for (account_id, (key_chain, account)) in &user_data.default_user_private_accounts { vec_for_storage.push( - InitialAccountData::Private(Box::new(InitialAccountDataPrivate { + InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData { account_id: *account_id, account: account.clone(), key_chain: key_chain.clone(), diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index bf82a1bd..a09d477e 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -64,6 +64,8 @@ pub enum ExecutionFailureKind { InsufficientFundsError, #[error("Account {0} data is invalid")] AccountDataError(AccountId), + #[error("Failed to build transaction: {0}")] + TransactionBuildError(#[from] nssa::error::NssaError), } #[expect(clippy::partial_pub_fields, reason = "TODO: make all fields private")] diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs index f1b94621..d68de7a5 100644 --- a/wallet/src/program_facades/amm.rs +++ b/wallet/src/program_facades/amm.rs @@ -58,18 +58,21 @@ impl Amm<'_> { user_holding_lp, ]; - let nonces = self + let mut nonces = self .0 .get_accounts_nonces(vec![user_holding_a, user_holding_b]) .await .map_err(ExecutionFailureKind::SequencerError)?; + let mut private_keys = Vec::new(); + let signing_key_a = self .0 .storage .user_data .get_pub_account_signing_key(user_holding_a) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + private_keys.push(signing_key_a); let signing_key_b = self .0 @@ -77,6 +80,26 @@ impl Amm<'_> { .user_data .get_pub_account_signing_key(user_holding_b) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + private_keys.push(signing_key_b); + + if let Some(signing_key_lp) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(user_holding_lp) + { + private_keys.push(signing_key_lp); + let lp_nonces = self + .0 + .get_accounts_nonces(vec![user_holding_lp]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.extend(lp_nonces); + } else { + println!( + "Liquidity pool tokens receiver's account ({user_holding_lp}) private key not found in wallet. Proceeding with only liquidity provider's keys." + ); + } let message = nssa::public_transaction::Message::try_new( program.id(), @@ -86,10 +109,8 @@ impl Amm<'_> { ) .unwrap(); - let witness_set = nssa::public_transaction::WitnessSet::for_message( - &message, - &[signing_key_a, signing_key_b], - ); + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &private_keys); let tx = nssa::PublicTransaction::new(message, witness_set); diff --git a/wallet/src/program_facades/ata.rs b/wallet/src/program_facades/ata.rs new file mode 100644 index 00000000..ac60fb63 --- /dev/null +++ b/wallet/src/program_facades/ata.rs @@ -0,0 +1,280 @@ +use std::collections::HashMap; + +use ata_core::{compute_ata_seed, get_associated_token_account_id}; +use common::{HashType, transaction::NSSATransaction}; +use nssa::{ + AccountId, privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, +}; +use nssa_core::SharedSecretKey; +use sequencer_service_rpc::RpcClient as _; + +use crate::{ExecutionFailureKind, PrivacyPreservingAccount, WalletCore}; + +pub struct Ata<'wallet>(pub &'wallet WalletCore); + +impl Ata<'_> { + pub async fn send_create( + &self, + owner_id: AccountId, + definition_id: AccountId, + ) -> Result { + let program = Program::ata(); + let ata_program_id = program.id(); + let ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(owner_id, definition_id), + ); + + let account_ids = vec![owner_id, definition_id, ata_id]; + + let nonces = self + .0 + .get_accounts_nonces(vec![owner_id]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let Some(signing_key) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(owner_id) + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let instruction = ata_core::Instruction::Create { ata_program_id }; + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + )?; + + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self + .0 + .sequencer_client + .send_transaction(NSSATransaction::Public(tx)) + .await?) + } + + pub async fn send_transfer( + &self, + owner_id: AccountId, + definition_id: AccountId, + recipient_id: AccountId, + amount: u128, + ) -> Result { + let program = Program::ata(); + let ata_program_id = program.id(); + let sender_ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(owner_id, definition_id), + ); + + let account_ids = vec![owner_id, sender_ata_id, recipient_id]; + + let nonces = self + .0 + .get_accounts_nonces(vec![owner_id]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let Some(signing_key) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(owner_id) + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let instruction = ata_core::Instruction::Transfer { + ata_program_id, + amount, + }; + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + )?; + + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self + .0 + .sequencer_client + .send_transaction(NSSATransaction::Public(tx)) + .await?) + } + + pub async fn send_burn( + &self, + owner_id: AccountId, + definition_id: AccountId, + amount: u128, + ) -> Result { + let program = Program::ata(); + let ata_program_id = program.id(); + let holder_ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(owner_id, definition_id), + ); + + let account_ids = vec![owner_id, holder_ata_id, definition_id]; + + let nonces = self + .0 + .get_accounts_nonces(vec![owner_id]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let Some(signing_key) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(owner_id) + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let instruction = ata_core::Instruction::Burn { + ata_program_id, + amount, + }; + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + )?; + + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self + .0 + .sequencer_client + .send_transaction(NSSATransaction::Public(tx)) + .await?) + } + + pub async fn send_create_private_owner( + &self, + owner_id: AccountId, + definition_id: AccountId, + ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { + let ata_program_id = Program::ata().id(); + let ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(owner_id, definition_id), + ); + + let instruction = ata_core::Instruction::Create { ata_program_id }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); + + let accounts = vec![ + PrivacyPreservingAccount::PrivateOwned(owner_id), + PrivacyPreservingAccount::Public(definition_id), + PrivacyPreservingAccount::Public(ata_id), + ]; + + self.0 + .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency()) + .await + .map(|(hash, mut secrets)| { + let secret = secrets.pop().expect("expected owner's secret"); + (hash, secret) + }) + } + + pub async fn send_transfer_private_owner( + &self, + owner_id: AccountId, + definition_id: AccountId, + recipient_id: AccountId, + amount: u128, + ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { + let ata_program_id = Program::ata().id(); + let sender_ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(owner_id, definition_id), + ); + + let instruction = ata_core::Instruction::Transfer { + ata_program_id, + amount, + }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); + + let accounts = vec![ + PrivacyPreservingAccount::PrivateOwned(owner_id), + PrivacyPreservingAccount::Public(sender_ata_id), + PrivacyPreservingAccount::Public(recipient_id), + ]; + + self.0 + .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency()) + .await + .map(|(hash, mut secrets)| { + let secret = secrets.pop().expect("expected owner's secret"); + (hash, secret) + }) + } + + pub async fn send_burn_private_owner( + &self, + owner_id: AccountId, + definition_id: AccountId, + amount: u128, + ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { + let ata_program_id = Program::ata().id(); + let holder_ata_id = get_associated_token_account_id( + &ata_program_id, + &compute_ata_seed(owner_id, definition_id), + ); + + let instruction = ata_core::Instruction::Burn { + ata_program_id, + amount, + }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); + + let accounts = vec![ + PrivacyPreservingAccount::PrivateOwned(owner_id), + PrivacyPreservingAccount::Public(holder_ata_id), + PrivacyPreservingAccount::Public(definition_id), + ]; + + self.0 + .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency()) + .await + .map(|(hash, mut secrets)| { + let secret = secrets.pop().expect("expected owner's secret"); + (hash, secret) + }) + } +} + +fn ata_with_token_dependency() -> ProgramWithDependencies { + let token = Program::token(); + let mut deps = HashMap::new(); + deps.insert(token.id(), token); + ProgramWithDependencies::new(Program::ata(), deps) +} diff --git a/wallet/src/program_facades/mod.rs b/wallet/src/program_facades/mod.rs index 5fdcdb39..a0f8189c 100644 --- a/wallet/src/program_facades/mod.rs +++ b/wallet/src/program_facades/mod.rs @@ -2,6 +2,7 @@ //! on-chain programs. pub mod amm; +pub mod ata; pub mod native_token_transfer; pub mod pinata; pub mod token; diff --git a/wallet/src/program_facades/native_token_transfer/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs index f91171db..2d936d3f 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -23,24 +23,40 @@ impl NativeTokenTransfer<'_> { .map_err(ExecutionFailureKind::SequencerError)?; if balance >= balance_to_move { - let nonces = self + let account_ids = vec![from, to]; + let program_id = Program::authenticated_transfer_program().id(); + + let mut nonces = self .0 .get_accounts_nonces(vec![from]) .await .map_err(ExecutionFailureKind::SequencerError)?; - let account_ids = vec![from, to]; - let program_id = Program::authenticated_transfer_program().id(); - let message = - Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap(); - - let signing_key = self.0.storage.user_data.get_pub_account_signing_key(from); - - let Some(signing_key) = signing_key else { + let mut private_keys = Vec::new(); + let from_signing_key = self.0.storage.user_data.get_pub_account_signing_key(from); + let Some(from_signing_key) = from_signing_key else { return Err(ExecutionFailureKind::KeyNotFoundError); }; + private_keys.push(from_signing_key); - let witness_set = WitnessSet::for_message(&message, &[signing_key]); + let to_signing_key = self.0.storage.user_data.get_pub_account_signing_key(to); + if let Some(to_signing_key) = to_signing_key { + private_keys.push(to_signing_key); + let to_nonces = self + .0 + .get_accounts_nonces(vec![to]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.extend(to_nonces); + } else { + println!( + "Receiver's account ({to}) private key not found in wallet. Proceeding with only sender's key." + ); + } + + let message = + Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap(); + let witness_set = WitnessSet::for_message(&message, &private_keys); let tx = PublicTransaction::new(message, witness_set); diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 3aa6891f..1f941c8c 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -19,15 +19,36 @@ impl Token<'_> { let account_ids = vec![definition_account_id, supply_account_id]; let program_id = nssa::program::Program::token().id(); let instruction = Instruction::NewFungibleDefinition { name, total_supply }; + let nonces = self + .0 + .get_accounts_nonces(account_ids.clone()) + .await + .map_err(ExecutionFailureKind::SequencerError)?; let message = nssa::public_transaction::Message::try_new( program_id, account_ids, - vec![], + nonces, instruction, ) .unwrap(); - let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + let def_private_key = self + .0 + .storage + .user_data + .get_pub_account_signing_key(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let supply_private_key = self + .0 + .storage + .user_data + .get_pub_account_signing_key(supply_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let witness_set = nssa::public_transaction::WitnessSet::for_message( + &message, + &[def_private_key, supply_private_key], + ); let tx = nssa::PublicTransaction::new(message, witness_set); @@ -138,11 +159,40 @@ impl Token<'_> { let instruction = Instruction::Transfer { amount_to_transfer: amount, }; - let nonces = self + let mut nonces = self .0 .get_accounts_nonces(vec![sender_account_id]) .await .map_err(ExecutionFailureKind::SequencerError)?; + + let mut private_keys = Vec::new(); + let sender_sk = self + .0 + .storage + .user_data + .get_pub_account_signing_key(sender_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + private_keys.push(sender_sk); + + if let Some(recipient_sk) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(recipient_account_id) + { + private_keys.push(recipient_sk); + let recipient_nonces = self + .0 + .get_accounts_nonces(vec![recipient_account_id]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.extend(recipient_nonces); + } else { + println!( + "Receiver's account ({recipient_account_id}) private key not found in wallet. Proceeding with only sender's key." + ); + } + let message = nssa::public_transaction::Message::try_new( program_id, account_ids, @@ -150,17 +200,8 @@ impl Token<'_> { instruction, ) .unwrap(); - - let Some(signing_key) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(sender_account_id) - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; let witness_set = - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + nssa::public_transaction::WitnessSet::for_message(&message, &private_keys); let tx = nssa::PublicTransaction::new(message, witness_set); @@ -477,11 +518,40 @@ impl Token<'_> { amount_to_mint: amount, }; - let nonces = self + let mut nonces = self .0 .get_accounts_nonces(vec![definition_account_id]) .await .map_err(ExecutionFailureKind::SequencerError)?; + + let mut private_keys = Vec::new(); + let definition_sk = self + .0 + .storage + .user_data + .get_pub_account_signing_key(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + private_keys.push(definition_sk); + + if let Some(holder_sk) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(holder_account_id) + { + private_keys.push(holder_sk); + let recipient_nonces = self + .0 + .get_accounts_nonces(vec![holder_account_id]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.extend(recipient_nonces); + } else { + println!( + "Holder's account ({holder_account_id}) private key not found in wallet. Proceeding with only definition's key." + ); + } + let message = nssa::public_transaction::Message::try_new( Program::token().id(), account_ids, @@ -489,17 +559,8 @@ impl Token<'_> { instruction, ) .unwrap(); - - let Some(signing_key) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(definition_account_id) - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; let witness_set = - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + nssa::public_transaction::WitnessSet::for_message(&message, &private_keys); let tx = nssa::PublicTransaction::new(message, witness_set);