diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 858a43c9..b327aaae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,10 @@ on: - "**.md" - "!.github/workflows/*.yml" +permissions: + contents: read + pull-requests: read + name: General jobs: @@ -19,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - name: Install nightly toolchain for rustfmt run: rustup install nightly --profile minimal --component rustfmt @@ -32,7 +36,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - name: Install taplo-cli run: cargo install --locked taplo-cli @@ -45,7 +49,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - name: Install active toolchain run: rustup install @@ -61,7 +65,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - name: Install cargo-deny run: cargo install --locked cargo-deny @@ -77,7 +81,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-system-deps @@ -106,7 +110,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-system-deps @@ -134,7 +138,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-system-deps @@ -164,7 +168,7 @@ jobs: # steps: # - uses: actions/checkout@v5 # with: - # ref: ${{ github.head_ref }} + # ref: ${{ github.event.pull_request.head.sha || github.head_ref }} # - uses: ./.github/actions/install-system-deps @@ -192,7 +196,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-system-deps @@ -218,7 +222,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-risc0 diff --git a/Cargo.lock b/Cargo.lock index c3b8c5f8..053c307c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1019,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", ] @@ -1522,6 +1515,7 @@ dependencies = [ "log", "logos-blockchain-common-http-client", "nssa", + "nssa_core", "serde", "serde_with", "sha2", @@ -3977,7 +3971,6 @@ dependencies = [ "nssa", "nssa_core", "rand 0.8.5", - "secp256k1", "serde", "sha2", "thiserror 2.0.18", @@ -5269,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", @@ -7086,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" @@ -8684,6 +8657,7 @@ dependencies = [ "async-stream", "ata_core", "base58", + "bip39", "clap", "common", "env_logger", diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 066fd679..6ff9a2be 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 6aa04d0b..bcd528f1 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 c22c8658..142397f7 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/pinata.bin b/artifacts/program_methods/pinata.bin index 69bc146d..280a834f 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 891a6dfe..4cd06c56 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 17e6cc02..55880e41 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 3387f0c9..b9c82387 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 ffffd04d..a740bdb8 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 e57b9e5f..112ca113 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 3f0b371f..a130510b 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 d08d41e9..41a5cb3b 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 23530a45..3dddebe1 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 65584d71..1d682ec3 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 64a823be..c68496ab 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 7062c8f3..ffd29461 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 dc9f8b3e..a2bbecd8 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 1363a5cf..b44b1233 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 d3b63870..e006fc75 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 5eff41fe..da811f60 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 ef4fd06b..3963873e 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 5525992c..08db47f0 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 161ae6b6..ceb5ae74 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 a0525f93..a7661f03 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/Cargo.toml b/common/Cargo.toml index a3884b70..0ae0b220 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] nssa.workspace = true +nssa_core.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/common/src/block.rs b/common/src/block.rs index 01ba586e..6decc390 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -1,4 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use nssa_core::{BlockId, Timestamp}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256, digest::FixedOutput as _}; @@ -6,8 +7,6 @@ use crate::{HashType, transaction::NSSATransaction}; pub type MantleMsgId = [u8; 32]; pub type BlockHash = HashType; -pub type BlockId = u64; -pub type TimeStamp = u64; #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] pub struct BlockMeta { @@ -35,7 +34,7 @@ pub struct BlockHeader { pub block_id: BlockId, pub prev_block_hash: BlockHash, pub hash: BlockHash, - pub timestamp: TimeStamp, + pub timestamp: Timestamp, pub signature: nssa::Signature, } @@ -75,7 +74,7 @@ impl<'de> Deserialize<'de> for Block { pub struct HashableBlockData { pub block_id: BlockId, pub prev_block_hash: BlockHash, - pub timestamp: TimeStamp, + pub timestamp: Timestamp, pub transactions: Vec, } diff --git a/common/src/transaction.rs b/common/src/transaction.rs index 1862dcc8..ea0b9819 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -1,9 +1,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; use log::warn; use nssa::{AccountId, V03State}; +use nssa_core::{BlockId, Timestamp}; use serde::{Deserialize, Serialize}; -use crate::{HashType, block::BlockId}; +use crate::HashType; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub enum NSSATransaction { @@ -69,11 +70,12 @@ impl NSSATransaction { self, state: &mut V03State, block_id: BlockId, + timestamp: Timestamp, ) -> Result { match &self { - Self::Public(tx) => state.transition_from_public_transaction(tx, block_id), + Self::Public(tx) => state.transition_from_public_transaction(tx, block_id, timestamp), Self::PrivacyPreserving(tx) => { - state.transition_from_privacy_preserving_transaction(tx, block_id) + state.transition_from_privacy_preserving_transaction(tx, block_id, timestamp) } Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx), } 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 381b71c3..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, ProgramOutput, read_nssa_inputs, -}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; // Hello-world example program. // @@ -45,13 +43,7 @@ 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 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 d90c072b..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, ProgramOutput, read_nssa_inputs, -}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; // Hello-world with authorization example program. // @@ -52,13 +50,7 @@ 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 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 0b2885a8..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, ProgramOutput, read_nssa_inputs, - }, + 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] diff --git a/explorer_service/src/pages/transaction_page.rs b/explorer_service/src/pages/transaction_page.rs index ed3d8aac..0a3fc8e2 100644 --- a/explorer_service/src/pages/transaction_page.rs +++ b/explorer_service/src/pages/transaction_page.rs @@ -177,13 +177,13 @@ pub fn TransactionPage() -> impl IntoView { encrypted_private_post_states, new_commitments, new_nullifiers, - validity_window + 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! {
@@ -214,8 +214,12 @@ pub fn TransactionPage() -> impl IntoView { {format!("{proof_len} bytes")}
- "Validity Window:" - {validity_window.to_string()} + "Block Validity Window:" + {block_validity_window.to_string()} +
+
+ "Timestamp Validity Window:" + {timestamp_validity_window.to_string()}
diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index e4534f76..7faf5376 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; @@ -125,7 +126,11 @@ impl IndexerStore { transaction .clone() .transaction_stateless_check()? - .execute_check_on_state(&mut state_guard, block.header.block_id)?; + .execute_check_on_state( + &mut state_guard, + block.header.block_id, + block.header.timestamp, + )?; } } diff --git a/indexer/service/protocol/src/convert.rs b/indexer/service/protocol/src/convert.rs index 2777b512..eb79fa34 100644 --- a/indexer/service/protocol/src/convert.rs +++ b/indexer/service/protocol/src/convert.rs @@ -287,7 +287,8 @@ impl From for PrivacyPre encrypted_private_post_states, new_commitments, new_nullifiers, - validity_window, + block_validity_window, + timestamp_validity_window, } = value; Self { public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), @@ -302,7 +303,8 @@ impl From for PrivacyPre .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), - validity_window: validity_window.into(), + block_validity_window: block_validity_window.into(), + timestamp_validity_window: timestamp_validity_window.into(), } } } @@ -318,7 +320,8 @@ impl TryFrom for nssa::privacy_preserving_transaction: encrypted_private_post_states, new_commitments, new_nullifiers, - validity_window, + block_validity_window, + timestamp_validity_window, } = value; Ok(Self { public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), @@ -340,7 +343,10 @@ impl TryFrom for nssa::privacy_preserving_transaction: .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), - validity_window: validity_window + 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}")))?, }) @@ -692,13 +698,13 @@ impl From for common::HashType { // ValidityWindow conversions // ============================================================================ -impl From for ValidityWindow { - fn from(value: nssa_core::program::ValidityWindow) -> Self { +impl From> for ValidityWindow { + fn from(value: nssa_core::program::ValidityWindow) -> Self { Self((value.start(), value.end())) } } -impl TryFrom for nssa_core::program::ValidityWindow { +impl TryFrom for nssa_core::program::ValidityWindow { type Error = nssa_core::program::InvalidWindow; fn try_from(value: ValidityWindow) -> Result { diff --git a/indexer/service/protocol/src/lib.rs b/indexer/service/protocol/src/lib.rs index 0d8a7e14..59e936bf 100644 --- a/indexer/service/protocol/src/lib.rs +++ b/indexer/service/protocol/src/lib.rs @@ -235,7 +235,8 @@ pub struct PrivacyPreservingMessage { pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, - pub validity_window: ValidityWindow, + pub block_validity_window: ValidityWindow, + pub timestamp_validity_window: ValidityWindow, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index c5891b41..09ae96f5 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -124,7 +124,8 @@ impl MockIndexerService { indexer_service_protocol::Nullifier([tx_idx as u8; 32]), CommitmentSetDigest([0xff; 32]), )], - validity_window: ValidityWindow((None, None)), + 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/src/lib.rs b/integration_tests/src/lib.rs index 08e7cf9f..a4381acf 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -256,11 +256,11 @@ impl TestContext { let config_overrides = WalletConfigOverrides::default(); let wallet_password = "test_pass".to_owned(); - let wallet = WalletCore::new_init_storage( + let (wallet, _mnemonic) = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), - wallet_password.clone(), + &wallet_password, ) .context("Failed to init wallet")?; wallet diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index 42aa5f3f..d9ecb831 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -223,7 +223,7 @@ async fn amm_public() -> Result<()> { // Make swap - let subcommand = AmmProgramAgnosticSubcommand::Swap { + let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput { user_holding_a: format_public_account_id(recipient_account_id_1), user_holding_b: format_public_account_id(recipient_account_id_2), amount_in: 2, @@ -266,7 +266,7 @@ async fn amm_public() -> Result<()> { // Make swap - let subcommand = AmmProgramAgnosticSubcommand::Swap { + let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput { user_holding_a: format_public_account_id(recipient_account_id_1), user_holding_b: format_public_account_id(recipient_account_id_2), amount_in: 2, 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 3d4dd05c..ce5a3447 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -24,7 +24,6 @@ use log::info; use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program}; use nssa_core::program::DEFAULT_PROGRAM_ID; use tempfile::tempdir; -use wallet::WalletCore; use wallet_ffi::{ FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, FfiTransferResult, WalletHandle, error, @@ -211,14 +210,6 @@ fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandl }) } -fn new_wallet_rust_with_default_config(password: &str) -> Result { - let tempdir = tempdir()?; - let config_path = tempdir.path().join("wallet_config.json"); - let storage_path = tempdir.path().join("storage.json"); - - WalletCore::new_init_storage(config_path, storage_path, None, password.to_owned()) -} - fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> { let config_path = home.join("wallet_config.json"); let storage_path = home.join("storage.json"); @@ -232,19 +223,8 @@ fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> { fn wallet_ffi_create_public_accounts() -> Result<()> { let password = "password_for_tests"; let n_accounts = 10; - // First `n_accounts` public accounts created with Rust wallet - let new_public_account_ids_rust = { - let mut account_ids = Vec::new(); - let mut wallet_rust = new_wallet_rust_with_default_config(password)?; - for _ in 0..n_accounts { - let account_id = wallet_rust.create_new_account_public(None).0; - account_ids.push(*account_id.value()); - } - account_ids - }; - - // First `n_accounts` public accounts created with wallet FFI + // Create `n_accounts` public accounts with wallet FFI let new_public_account_ids_ffi = unsafe { let mut account_ids = Vec::new(); @@ -258,7 +238,20 @@ fn wallet_ffi_create_public_accounts() -> Result<()> { account_ids }; - assert_eq!(new_public_account_ids_ffi, new_public_account_ids_rust); + // All returned IDs must be unique and non-zero + assert_eq!(new_public_account_ids_ffi.len(), n_accounts); + let unique: HashSet<_> = new_public_account_ids_ffi.iter().collect(); + assert_eq!( + unique.len(), + n_accounts, + "Duplicate public account IDs returned" + ); + assert!( + new_public_account_ids_ffi + .iter() + .all(|id| *id != [0_u8; 32]), + "Zero account ID returned" + ); Ok(()) } @@ -267,19 +260,7 @@ fn wallet_ffi_create_public_accounts() -> Result<()> { fn wallet_ffi_create_private_accounts() -> Result<()> { let password = "password_for_tests"; let n_accounts = 10; - // First `n_accounts` private accounts created with Rust wallet - let new_private_account_ids_rust = { - let mut account_ids = Vec::new(); - - let mut wallet_rust = new_wallet_rust_with_default_config(password)?; - for _ in 0..n_accounts { - let account_id = wallet_rust.create_new_account_private(None).0; - account_ids.push(*account_id.value()); - } - account_ids - }; - - // First `n_accounts` private accounts created with wallet FFI + // Create `n_accounts` private accounts with wallet FFI let new_private_account_ids_ffi = unsafe { let mut account_ids = Vec::new(); @@ -293,7 +274,20 @@ fn wallet_ffi_create_private_accounts() -> Result<()> { account_ids }; - assert_eq!(new_private_account_ids_ffi, new_private_account_ids_rust); + // All returned IDs must be unique and non-zero + assert_eq!(new_private_account_ids_ffi.len(), n_accounts); + let unique: HashSet<_> = new_private_account_ids_ffi.iter().collect(); + assert_eq!( + unique.len(), + n_accounts, + "Duplicate private account IDs returned" + ); + assert!( + new_private_account_ids_ffi + .iter() + .all(|id| *id != [0_u8; 32]), + "Zero account ID returned" + ); Ok(()) } @@ -349,28 +343,23 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> { fn test_wallet_ffi_list_accounts() -> Result<()> { let password = "password_for_tests"; - // Create the wallet FFI - let wallet_ffi_handle = unsafe { + // Create the wallet FFI and track which account IDs were created as public/private + let (wallet_ffi_handle, created_public_ids, created_private_ids) = unsafe { let handle = new_wallet_ffi_with_default_config(password)?; - // Create 5 public accounts and 5 private accounts + let mut public_ids: Vec<[u8; 32]> = Vec::new(); + let mut private_ids: Vec<[u8; 32]> = Vec::new(); + + // Create 5 public accounts and 5 private accounts, recording their IDs for _ in 0..5 { let mut out_account_id = FfiBytes32::from_bytes([0; 32]); wallet_ffi_create_account_public(handle, &raw mut out_account_id); + public_ids.push(out_account_id.data); + wallet_ffi_create_account_private(handle, &raw mut out_account_id); + private_ids.push(out_account_id.data); } - handle - }; - - // Create the wallet Rust - let wallet_rust = { - let mut wallet = new_wallet_rust_with_default_config(password)?; - // Create 5 public accounts and 5 private accounts - for _ in 0..5 { - wallet.create_new_account_public(None); - wallet.create_new_account_private(None); - } - wallet + (handle, public_ids, private_ids) }; // Get the account list with FFI method @@ -380,15 +369,6 @@ fn test_wallet_ffi_list_accounts() -> Result<()> { out_list }; - let wallet_rust_account_ids = wallet_rust - .storage() - .user_data - .account_ids() - .collect::>(); - - // Assert same number of elements between Rust and FFI result - assert_eq!(wallet_rust_account_ids.len(), wallet_ffi_account_list.count); - let wallet_ffi_account_list_slice = unsafe { core::slice::from_raw_parts( wallet_ffi_account_list.entries, @@ -396,37 +376,38 @@ fn test_wallet_ffi_list_accounts() -> Result<()> { ) }; - // Assert same account ids between Rust and FFI result - assert_eq!( - wallet_rust_account_ids - .iter() - .map(nssa::AccountId::value) - .collect::>(), - wallet_ffi_account_list_slice - .iter() - .map(|entry| &entry.account_id.data) - .collect::>() - ); + // All created accounts must appear in the list + let listed_public_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice + .iter() + .filter(|e| e.is_public) + .map(|e| e.account_id.data) + .collect(); + let listed_private_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice + .iter() + .filter(|e| !e.is_public) + .map(|e| e.account_id.data) + .collect(); - // Assert `is_pub` flag is correct in the FFI result - for entry in wallet_ffi_account_list_slice { - let account_id = AccountId::new(entry.account_id.data); - let is_pub_default_in_rust_wallet = wallet_rust - .storage() - .user_data - .default_pub_account_signing_keys - .contains_key(&account_id); - let is_pub_key_tree_wallet_rust = wallet_rust - .storage() - .user_data - .public_key_tree - .account_id_map - .contains_key(&account_id); - - let is_public_in_rust_wallet = is_pub_default_in_rust_wallet || is_pub_key_tree_wallet_rust; - - assert_eq!(entry.is_public, is_public_in_rust_wallet); + for id in &created_public_ids { + assert!( + listed_public_ids.contains(id), + "Created public account not found in list with is_public=true" + ); } + for id in &created_private_ids { + assert!( + listed_private_ids.contains(id), + "Created private account not found in list with is_public=false" + ); + } + + // Total listed accounts must be at least the number we created + assert!( + wallet_ffi_account_list.count >= created_public_ids.len() + created_private_ids.len(), + "Listed account count ({}) is less than the number of created accounts ({})", + wallet_ffi_account_list.count, + created_public_ids.len() + created_private_ids.len() + ); unsafe { wallet_ffi_free_account_list(&raw mut wallet_ffi_account_list); @@ -924,7 +905,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 +948,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 2c80f3bb..17ae8e2c 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/mod.rs b/key_protocol/src/key_management/mod.rs index dcdaff45..c038c415 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -42,10 +42,10 @@ impl KeyChain { } #[must_use] - pub fn new_mnemonic(passphrase: String) -> Self { + pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) { // Currently dropping SeedHolder at the end of initialization. // Not entirely sure if we need it in the future. - let seed_holder = SeedHolder::new_mnemonic(passphrase); + let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase); let secret_spending_key = seed_holder.produce_top_secret_key_holder(); let private_key_holder = secret_spending_key.produce_private_key_holder(None); @@ -53,12 +53,15 @@ impl KeyChain { let nullifier_public_key = private_key_holder.generate_nullifier_public_key(); let viewing_public_key = private_key_holder.generate_viewing_public_key(); - Self { - secret_spending_key, - private_key_holder, - nullifier_public_key, - viewing_public_key, - } + ( + Self { + secret_spending_key, + private_key_holder, + nullifier_public_key, + viewing_public_key, + }, + mnemonic, + ) } #[must_use] diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index 02890631..9804ba39 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -8,8 +8,6 @@ use rand::{RngCore as _, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, digest::FixedOutput as _}; -const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32]; - /// Seed holder. Non-clonable to ensure that different holders use different seeds. /// Produces `TopSecretKeyHolder` objects. #[derive(Debug)] @@ -48,9 +46,24 @@ impl SeedHolder { } #[must_use] - pub fn new_mnemonic(passphrase: String) -> Self { - let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES) - .expect("Enthropy must be a multiple of 32 bytes"); + pub fn new_mnemonic(passphrase: &str) -> (Self, Mnemonic) { + let mut entropy_bytes: [u8; 32] = [0; 32]; + OsRng.fill_bytes(&mut entropy_bytes); + + let mnemonic = + Mnemonic::from_entropy(&entropy_bytes).expect("Entropy must be a multiple of 32 bytes"); + let seed_wide = mnemonic.to_seed(passphrase); + + ( + Self { + seed: seed_wide.to_vec(), + }, + mnemonic, + ) + } + + #[must_use] + pub fn from_mnemonic(mnemonic: &Mnemonic, passphrase: &str) -> Self { let seed_wide = mnemonic.to_seed(passphrase); Self { @@ -175,12 +188,63 @@ mod tests { } #[test] - fn two_seeds_generated_same_from_same_mnemonic() { - let mnemonic = "test_pass"; + fn two_seeds_recovered_same_from_same_mnemonic() { + let passphrase = "test_pass"; - let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_owned()); - let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_owned()); + // Generate a mnemonic with random entropy + let (original_seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase); - assert_eq!(seed_holder1.seed, seed_holder2.seed); + // Recover from the same mnemonic + let recovered_seed_holder = SeedHolder::from_mnemonic(&mnemonic, passphrase); + + assert_eq!(original_seed_holder.seed, recovered_seed_holder.seed); + } + + #[test] + fn new_mnemonic_generates_different_seeds_each_time() { + let (seed_holder1, mnemonic1) = SeedHolder::new_mnemonic(""); + let (seed_holder2, mnemonic2) = SeedHolder::new_mnemonic(""); + + // Different entropy should produce different mnemonics and seeds + assert_ne!(mnemonic1.to_string(), mnemonic2.to_string()); + assert_ne!(seed_holder1.seed, seed_holder2.seed); + } + + #[test] + fn new_mnemonic_generates_24_word_phrase() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + // 256 bits of entropy produces a 24-word mnemonic + let word_count = mnemonic.to_string().split_whitespace().count(); + assert_eq!(word_count, 24); + } + + #[test] + fn new_mnemonic_produces_valid_seed_length() { + let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic(""); + + assert_eq!(seed_holder.seed.len(), 64); + } + + #[test] + fn different_passphrases_produce_different_seeds() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + let seed_with_pass_a = SeedHolder::from_mnemonic(&mnemonic, "password_a"); + let seed_with_pass_b = SeedHolder::from_mnemonic(&mnemonic, "password_b"); + + // Same mnemonic but different passphrases should produce different seeds + assert_ne!(seed_with_pass_a.seed, seed_with_pass_b.seed); + } + + #[test] + fn empty_passphrase_is_deterministic() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + let seed1 = SeedHolder::from_mnemonic(&mnemonic, ""); + let seed2 = SeedHolder::from_mnemonic(&mnemonic, ""); + + // Same mnemonic and passphrase should always produce the same seed + assert_eq!(seed1.seed, seed2.seed); } } diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 3ec91c99..fec59bed 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -181,11 +181,12 @@ impl NSSAUserData { impl Default for NSSAUserData { fn default() -> Self { + let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic(""); Self::new_with_accounts( BTreeMap::new(), BTreeMap::new(), - KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_owned())), - KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_owned())), + KeyTreePublic::new(&seed_holder), + KeyTreePrivate::new(&seed_holder), ) .unwrap() } 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 bd758f1e..87285a13 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, ValidityWindow}, + program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow}, }; #[derive(Serialize, Deserialize)] @@ -36,7 +36,8 @@ pub struct PrivacyPreservingCircuitOutput { pub ciphertexts: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, - pub validity_window: ValidityWindow, + pub block_validity_window: BlockValidityWindow, + pub timestamp_validity_window: TimestampValidityWindow, } #[cfg(feature = "host")] @@ -107,7 +108,8 @@ mod tests { ), [0xab; 32], )], - validity_window: (Some(1), None).try_into().unwrap(), + 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 8014c7ca..a4fcdee1 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -21,3 +21,7 @@ pub mod program; #[cfg(feature = "host")] pub mod error; + +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 b9fa5de2..673e09b3 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -5,7 +5,10 @@ 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; @@ -22,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 { @@ -91,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 { @@ -105,7 +123,7 @@ impl AccountPostState { pub const fn new(account: Account) -> Self { Self { account, - claim: false, + claim: None, } } @@ -113,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 } @@ -142,6 +162,7 @@ impl AccountPostState { } /// Returns the underlying account. + #[must_use] pub const fn account_mut(&mut self) -> &mut Account { &mut self.account } @@ -153,20 +174,21 @@ impl AccountPostState { } } -pub type BlockId = u64; +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, +pub struct ValidityWindow { + from: Option, + to: Option, } -impl ValidityWindow { - /// Creates a window with no bounds, valid for every block ID. +impl ValidityWindow { + /// Creates a window with no bounds. #[must_use] pub const fn new_unbounded() -> Self { Self { @@ -174,42 +196,42 @@ impl ValidityWindow { to: None, } } +} - /// Returns `true` if `id` falls within the half-open range `[from, to)`. - /// A `None` bound on either side is treated as unbounded in that direction. +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_block_id(&self, id: BlockId) -> bool { - self.from.is_none_or(|start| id >= start) && self.to.is_none_or(|end| id < end) + 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`. - const fn check_window(&self) -> Result<(), InvalidWindow> { - if let (Some(from_id), Some(until_id)) = (self.from, self.to) - && from_id >= until_id + fn check_window(&self) -> Result<(), InvalidWindow> { + if let (Some(from), Some(to)) = (self.from, self.to) + && from >= to { - Err(InvalidWindow) - } else { - Ok(()) + return Err(InvalidWindow); } + Ok(()) } - /// Inclusive lower bound. `None` means the window starts at the beginning of the chain. + /// Inclusive lower bound. `None` means no lower bound. #[must_use] - pub const fn start(&self) -> Option { + pub const fn start(&self) -> Option { self.from } - /// Exclusive upper bound. `None` means the window has no expiry. + /// Exclusive upper bound. `None` means no upper bound. #[must_use] - pub const fn end(&self) -> Option { + pub const fn end(&self) -> Option { self.to } } -impl TryFrom<(Option, Option)> for ValidityWindow { +impl TryFrom<(Option, Option)> for ValidityWindow { type Error = InvalidWindow; - fn try_from(value: (Option, Option)) -> Result { + fn try_from(value: (Option, Option)) -> Result { let this = Self { from: value.0, to: value.1, @@ -219,16 +241,16 @@ impl TryFrom<(Option, Option)> for ValidityWindow { } } -impl TryFrom> for ValidityWindow { +impl TryFrom> for ValidityWindow { type Error = InvalidWindow; - fn try_from(value: std::ops::Range) -> Result { + 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 { +impl From> for ValidityWindow { + fn from(value: std::ops::RangeFrom) -> Self { Self { from: Some(value.start), to: None, @@ -236,8 +258,8 @@ impl From> for ValidityWindow { } } -impl From> for ValidityWindow { - fn from(value: std::ops::RangeTo) -> Self { +impl From> for ValidityWindow { + fn from(value: std::ops::RangeTo) -> Self { Self { from: None, to: Some(value.end), @@ -245,7 +267,7 @@ impl From> for ValidityWindow { } } -impl From for ValidityWindow { +impl From for ValidityWindow { fn from(_: std::ops::RangeFull) -> Self { Self::new_unbounded() } @@ -267,8 +289,10 @@ pub struct ProgramOutput { pub post_states: Vec, /// The list of chained calls to other programs. pub chained_calls: Vec, - /// The window where the program output is valid. - pub validity_window: ValidityWindow, + /// 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 { @@ -282,7 +306,8 @@ impl ProgramOutput { pre_states, post_states, chained_calls: Vec::new(), - validity_window: ValidityWindow::new_unbounded(), + block_validity_window: ValidityWindow::new_unbounded(), + timestamp_validity_window: ValidityWindow::new_unbounded(), } } @@ -295,19 +320,52 @@ impl ProgramOutput { self } - /// Sets the validity window from an infallible range conversion (`1..`, `..5`, `..`). - pub fn with_validity_window>(mut self, window: W) -> Self { - self.validity_window = window.into(); + /// 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 validity window from a fallible range conversion (`1..5`). + /// Sets the block ID validity window from a fallible range conversion (`1..5`). /// Returns `Err` if the range is empty. - pub fn try_with_validity_window>( + pub fn try_with_block_validity_window< + W: TryInto, + >( mut self, window: W, ) -> Result { - self.validity_window = window.try_into()?; + 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) } } @@ -464,128 +522,131 @@ mod tests { use super::*; #[test] - fn validity_window_unbounded_accepts_any_block() { - let w = ValidityWindow::new_unbounded(); - assert!(w.is_valid_for_block_id(0)); - assert!(w.is_valid_for_block_id(u64::MAX)); + 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_block_id(4)); - assert!(w.is_valid_for_block_id(5)); - assert!(w.is_valid_for_block_id(9)); - assert!(!w.is_valid_for_block_id(10)); + 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_block_id(4)); - assert!(w.is_valid_for_block_id(5)); - assert!(w.is_valid_for_block_id(u64::MAX)); + 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_block_id(0)); - assert!(w.is_valid_for_block_id(4)); - assert!(!w.is_valid_for_block_id(5)); + 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()); + 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()); + 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(); + 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::new_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::try_from(5_u64..10).unwrap(); + 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()); + 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()); + assert!(ValidityWindow::::try_from(from..to).is_err()); } #[test] fn validity_window_from_range_from() { - let w: ValidityWindow = (5_u64..).into(); + 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(); + 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(); + let w: ValidityWindow = (..).into(); assert_eq!(w.start(), None); assert_eq!(w.end(), None); } #[test] - fn program_output_try_with_validity_window_range() { + fn program_output_try_with_block_validity_window_range() { let output = ProgramOutput::new(vec![], vec![], vec![]) - .try_with_validity_window(10_u64..100) + .try_with_block_validity_window(10_u64..100) .unwrap(); - assert_eq!(output.validity_window.start(), Some(10)); - assert_eq!(output.validity_window.end(), Some(100)); + assert_eq!(output.block_validity_window.start(), Some(10)); + assert_eq!(output.block_validity_window.end(), Some(100)); } #[test] - fn program_output_with_validity_window_range_from() { - let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(10_u64..); - assert_eq!(output.validity_window.start(), Some(10)); - assert_eq!(output.validity_window.end(), None); + 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_validity_window_range_to() { - let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(..100_u64); - assert_eq!(output.validity_window.start(), None); - assert_eq!(output.validity_window.end(), Some(100)); + 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_validity_window_empty_range_fails() { - let result = ProgramOutput::new(vec![], vec![], vec![]).try_with_validity_window(5_u64..5); + 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()); } @@ -598,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] @@ -616,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 15d4f044..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), diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 1f615946..5596288b 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -3,7 +3,7 @@ use nssa_core::{ Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, account::{Account, Nonce}, encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey}, - program::ValidityWindow, + program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; @@ -56,7 +56,8 @@ pub struct Message { pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, - pub validity_window: ValidityWindow, + pub block_validity_window: BlockValidityWindow, + pub timestamp_validity_window: TimestampValidityWindow, } impl std::fmt::Debug for Message { @@ -82,7 +83,8 @@ impl std::fmt::Debug for Message { ) .field("new_commitments", &self.new_commitments) .field("new_nullifiers", &nullifiers) - .field("validity_window", &self.validity_window) + .field("block_validity_window", &self.block_validity_window) + .field("timestamp_validity_window", &self.timestamp_validity_window) .finish() } } @@ -115,7 +117,8 @@ impl Message { encrypted_private_post_states, new_commitments: output.new_commitments, new_nullifiers: output.new_nullifiers, - validity_window: output.validity_window, + block_validity_window: output.block_validity_window, + timestamp_validity_window: output.timestamp_validity_window, }) } } @@ -126,6 +129,7 @@ pub mod tests { Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey, account::Account, encryption::{EphemeralPublicKey, ViewingPublicKey}, + program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; @@ -170,7 +174,8 @@ pub mod tests { encrypted_private_post_states, new_commitments, new_nullifiers, - validity_window: (None, None).try_into().unwrap(), + 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 b1c30109..977bb0d0 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -5,17 +5,14 @@ use std::{ use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, + BlockId, PrivacyPreservingCircuitOutput, Timestamp, account::{Account, AccountWithMetadata}, - program::{BlockId, ValidityWindow}, }; 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)] @@ -37,6 +34,7 @@ impl PrivacyPreservingTransaction { &self, state: &V03State, block_id: BlockId, + timestamp: Timestamp, ) -> Result, NssaError> { let message = &self.message; let witness_set = &self.witness_set; @@ -94,7 +92,9 @@ impl PrivacyPreservingTransaction { } // Verify validity window - if !message.validity_window.is_valid_for_block_id(block_id) { + if !message.block_validity_window.is_valid_for(block_id) + || !message.timestamp_validity_window.is_valid_for(timestamp) + { return Err(NssaError::OutOfValidityWindow); } @@ -115,11 +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.validity_window, + message, )?; // 5. Commitment freshness @@ -177,23 +173,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)], - validity_window: &ValidityWindow, + 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(), - validity_window: validity_window.to_owned(), + 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/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 8aaf039e..6a27c0a4 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::{BlockId, ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, + program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution}, }; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -71,6 +72,7 @@ impl PublicTransaction { &self, state: &V03State, block_id: BlockId, + timestamp: Timestamp, ) -> Result, NssaError> { let message = self.message(); let witness_set = self.witness_set(); @@ -157,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 @@ -172,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 ); } @@ -193,23 +197,42 @@ impl PublicTransaction { // Verify validity window ensure!( - program_output - .validity_window - .is_valid_for_block_id(block_id), + program_output.block_validity_window.is_valid_for(block_id) + && program_output + .timestamp_validity_window + .is_valid_for(timestamp), NssaError::OutOfValidityWindow ); - for post in program_output - .post_states - .iter_mut() - .filter(|post| post.requires_claim()) - { + 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 @@ -368,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, 1); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -388,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, 1); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -409,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, 1); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -429,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, 1); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -445,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, 1); + 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 021a522e..1940bb2e 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2,9 +2,10 @@ 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::{BlockId, ProgramId}, + program::ProgramId, }; use crate::{ @@ -159,8 +160,9 @@ impl V03State { &mut self, tx: &PublicTransaction, block_id: BlockId, + timestamp: Timestamp, ) -> Result<(), NssaError> { - let state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?; + let state_diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; #[expect( clippy::iter_over_hash_type, @@ -184,9 +186,11 @@ impl V03State { &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, block_id)?; + let public_state_diff = + tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; let message = tx.message(); @@ -338,10 +342,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::{BlockId, PdaSeed, ProgramId, ValidityWindow}, + program::{BlockValidityWindow, PdaSeed, ProgramId, TimestampValidityWindow}, }; use crate::{ @@ -457,16 +462,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) } @@ -568,17 +576,18 @@ pub mod tests { let initial_data = [(account_id, 100)]; let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); 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, 1).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] @@ -589,12 +598,13 @@ pub mod tests { let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); 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, 1); + 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); @@ -614,16 +624,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, 1).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] @@ -634,21 +645,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, &[]); - 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, 1).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, 1).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)); } #[test] @@ -663,7 +691,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -680,7 +708,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -697,7 +725,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -721,7 +749,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -745,7 +773,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -769,7 +797,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -793,7 +821,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -821,7 +849,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -846,7 +874,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -864,7 +892,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -893,7 +921,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -1090,7 +1118,7 @@ pub mod tests { assert!(!state.private_state.0.contains(&expected_new_commitment)); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let sender_post = state.get_account_by_id(sender_keys.account_id()); @@ -1163,7 +1191,7 @@ pub mod tests { assert!(!state.private_state.1.contains(&expected_new_nullifier)); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); assert_eq!(state.public_state, previous_public_state); @@ -1228,7 +1256,7 @@ pub mod tests { assert!(!state.private_state.1.contains(&expected_new_nullifier)); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let recipient_post = state.get_account_by_id(recipient_keys.account_id()); @@ -2180,7 +2208,7 @@ pub mod tests { ); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let sender_private_account = Account { @@ -2198,7 +2226,7 @@ pub mod tests { &state, ); - let result = state.transition_from_privacy_preserving_transaction(&tx, 1); + 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 { @@ -2246,15 +2274,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, &[]).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 @@ -2263,26 +2290,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, 1).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(&[], &[]); + + 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(&[], &[]); + + 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(); @@ -2319,7 +2400,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, 1).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); @@ -2359,7 +2440,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!( result, Err(NssaError::MaxChainedCallsDepthExceeded) @@ -2400,7 +2481,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, 1).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); @@ -2416,15 +2497,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, &[]).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 @@ -2434,6 +2514,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() }; @@ -2449,14 +2530,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, 1).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); @@ -2464,6 +2546,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)); + 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) { @@ -2567,7 +2731,7 @@ pub mod tests { let transaction = PrivacyPreservingTransaction::new(message, witness_set); state - .transition_from_privacy_preserving_transaction(&transaction, 1) + .transition_from_privacy_preserving_transaction(&transaction, 1, 0) .unwrap(); // Assert @@ -2607,36 +2771,47 @@ pub mod tests { let mut state = V03State::new_with_genesis_accounts(&[], &[]); 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, 1).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, 1).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; @@ -2653,7 +2828,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, 1).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!( @@ -2683,7 +2858,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -2729,7 +2904,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, 1); + 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); @@ -2799,13 +2974,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, 1); + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); assert!(result.is_ok()); let nullifier = Nullifier::for_account_initialization(&account_id); 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(&[], &[]).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(&[], &[]).with_test_programs(); @@ -2853,7 +3075,7 @@ pub mod tests { // Claim should succeed assert!( state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .is_ok() ); @@ -2902,7 +3124,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, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); // Should succeed - no changes made, no claim needed assert!(result.is_ok()); @@ -2927,7 +3149,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, 1); + 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))); @@ -3058,7 +3280,7 @@ pub mod tests { validity_window: (Option, Option), block_id: BlockId, ) { - let validity_window: ValidityWindow = validity_window.try_into().unwrap(); + 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()); @@ -3067,21 +3289,76 @@ pub mod tests { let account_ids = vec![pre.account_id]; let nonces = vec![]; let program_id = validity_window_program.id(); - let message = public_transaction::Message::try_new( - program_id, - account_ids, - nonces, - validity_window, - ) - .unwrap(); + 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); - let is_inside_validity_window = match (validity_window.start(), 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, + 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(&[], &[]).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 { @@ -3108,7 +3385,7 @@ pub mod tests { validity_window: (Option, Option), block_id: BlockId, ) { - let validity_window: ValidityWindow = validity_window.try_into().unwrap(); + 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 account_id = AccountId::account_id_without_identifier(&account_keys.npk()); @@ -3119,9 +3396,13 @@ pub mod tests { 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(validity_window).unwrap(), + Program::serialize_instruction(instruction).unwrap(), vec![2], vec![(account_keys.npk(), shared_secret)], vec![], @@ -3141,11 +3422,83 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, proof, &[]); PrivacyPreservingTransaction::new(message, witness_set) }; - let result = state.transition_from_privacy_preserving_transaction(&tx, block_id); - let is_inside_validity_window = match (validity_window.start(), 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, + 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(&[], &[]).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 { diff --git a/program_methods/guest/src/bin/amm.rs b/program_methods/guest/src/bin/amm.rs index 748630d9..3a3e134e 100644 --- a/program_methods/guest/src/bin/amm.rs +++ b/program_methods/guest/src/bin/amm.rs @@ -112,15 +112,15 @@ fn main() { min_amount_to_remove_token_b, ) } - Instruction::Swap { + Instruction::SwapExactInput { swap_amount_in, min_amount_out, token_definition_id_in, } => { let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states .try_into() - .expect("Transfer instruction requires exactly five accounts"); - amm_program::swap::swap( + .expect("SwapExactInput instruction requires exactly five accounts"); + amm_program::swap::swap_exact_input( pool, vault_a, vault_b, @@ -131,6 +131,25 @@ fn main() { token_definition_id_in, ) } + Instruction::SwapExactOutput { + exact_amount_out, + max_amount_in, + token_definition_id_in, + } => { + let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states + .try_into() + .expect("SwapExactOutput instruction requires exactly five accounts"); + amm_program::swap::swap_exact_output( + pool, + vault_a, + vault_b, + user_holding_a, + user_holding_b, + exact_amount_out, + max_amount_in, + token_definition_id_in, + ) + } }; ProgramOutput::new(instruction_words, pre_states_clone, post_states) diff --git a/program_methods/guest/src/bin/authenticated_transfer.rs b/program_methods/guest/src/bin/authenticated_transfer.rs index 20f4dd68..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, ProgramOutput, read_nssa_inputs, + 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) } diff --git a/program_methods/guest/src/bin/pinata.rs b/program_methods/guest/src/bin/pinata.rs index cfe0a7e4..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, ProgramOutput, read_nssa_inputs}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; use risc0_zkvm::sha::{Impl, Sha256 as _}; const PRIZE: u128 = 150; @@ -82,7 +82,7 @@ fn main() { 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), ], ) diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index c6edec0c..b985a2fe 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, ValidityWindow, 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,29 +21,51 @@ use risc0_zkvm::{guest::env, serde::to_vec}; struct ExecutionState { pre_states: Vec, post_states: HashMap, - validity_window: ValidityWindow, + 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 { - let valid_from_id = program_outputs + 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.validity_window.start()) + .filter_map(|output| output.block_validity_window.start()) .max(); - let valid_until_id = program_outputs + let block_valid_until = program_outputs .iter() - .filter_map(|output| output.validity_window.end()) + .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 validity_window = (valid_from_id, valid_until_id).try_into().expect( - "There should be non empty intersection in the program output validity windows", - ); + 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(), - validity_window, + block_validity_window, + timestamp_validity_window, }; let Some(first_output) = program_outputs.first() else { @@ -102,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, @@ -134,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" ); } @@ -144,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, @@ -151,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 @@ -167,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 ); @@ -176,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()); @@ -225,7 +296,8 @@ fn compute_circuit_output( ciphertexts: Vec::new(), new_commitments: Vec::new(), new_nullifiers: Vec::new(), - validity_window: execution_state.validity_window, + block_validity_window: execution_state.block_validity_window, + timestamp_validity_window: execution_state.timestamp_validity_window, }; let states_iter = execution_state.into_states_iter(); @@ -412,7 +484,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/programs/amm/core/src/lib.rs b/programs/amm/core/src/lib.rs index 85efd00d..017f14ff 100644 --- a/programs/amm/core/src/lib.rs +++ b/programs/amm/core/src/lib.rs @@ -68,11 +68,27 @@ pub enum Instruction { /// - User Holding Account for Token A /// - User Holding Account for Token B Either User Holding Account for Token A or Token B is /// authorized. - Swap { + SwapExactInput { swap_amount_in: u128, min_amount_out: u128, token_definition_id_in: AccountId, }, + + /// Swap tokens specifying the exact desired output amount, + /// while maintaining the Pool constant product. + /// + /// Required accounts: + /// - AMM Pool (initialized) + /// - Vault Holding Account for Token A (initialized) + /// - Vault Holding Account for Token B (initialized) + /// - User Holding Account for Token A + /// - User Holding Account for Token B Either User Holding Account for Token A or Token B is + /// authorized. + SwapExactOutput { + exact_amount_out: u128, + max_amount_in: u128, + token_definition_id_in: AccountId, + }, } #[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] 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/swap.rs b/programs/amm/src/swap.rs index cb64f5eb..22f3792a 100644 --- a/programs/amm/src/swap.rs +++ b/programs/amm/src/swap.rs @@ -4,21 +4,14 @@ use nssa_core::{ program::{AccountPostState, ChainedCall}, }; -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] -#[must_use] -pub fn swap( - pool: AccountWithMetadata, - vault_a: AccountWithMetadata, - vault_b: AccountWithMetadata, - user_holding_a: AccountWithMetadata, - user_holding_b: AccountWithMetadata, - swap_amount_in: u128, - min_amount_out: u128, - token_in_id: AccountId, -) -> (Vec, Vec) { - // Verify vaults are in fact vaults +/// Validates swap setup: checks pool is active, vaults match, and reserves are sufficient. +fn validate_swap_setup( + pool: &AccountWithMetadata, + vault_a: &AccountWithMetadata, + vault_b: &AccountWithMetadata, +) -> PoolDefinition { let pool_def_data = PoolDefinition::try_from(&pool.account.data) - .expect("Swap: AMM Program expects a valid Pool Definition Account"); + .expect("AMM Program expects a valid Pool Definition Account"); assert!(pool_def_data.active, "Pool is inactive"); assert_eq!( @@ -30,16 +23,14 @@ pub fn swap( "Vault B was not provided" ); - // fetch pool reserves - // validates reserves is at least the vaults' balances let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data) - .expect("Swap: AMM Program expects a valid Token Holding Account for Vault A"); + .expect("AMM Program expects a valid Token Holding Account for Vault A"); let token_core::TokenHolding::Fungible { definition_id: _, balance: vault_a_balance, } = vault_a_token_holding else { - panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault A"); + panic!("AMM Program expects a valid Fungible Token Holding Account for Vault A"); }; assert!( @@ -48,13 +39,13 @@ pub fn swap( ); let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data) - .expect("Swap: AMM Program expects a valid Token Holding Account for Vault B"); + .expect("AMM Program expects a valid Token Holding Account for Vault B"); let token_core::TokenHolding::Fungible { definition_id: _, balance: vault_b_balance, } = vault_b_token_holding else { - panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault B"); + panic!("AMM Program expects a valid Fungible Token Holding Account for Vault B"); }; assert!( @@ -62,6 +53,59 @@ pub fn swap( "Reserve for Token B exceeds vault balance" ); + pool_def_data +} + +/// Creates post-state and returns reserves after swap. +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::needless_pass_by_value, + reason = "consistent with codebase style" +)] +fn create_swap_post_states( + pool: AccountWithMetadata, + pool_def_data: PoolDefinition, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + deposit_a: u128, + withdraw_a: u128, + deposit_b: u128, + withdraw_b: u128, +) -> Vec { + let mut pool_post = pool.account; + let pool_post_definition = PoolDefinition { + reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a, + reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b, + ..pool_def_data + }; + + pool_post.data = Data::from(&pool_post_definition); + + vec![ + AccountPostState::new(pool_post), + AccountPostState::new(vault_a.account), + AccountPostState::new(vault_b.account), + AccountPostState::new(user_holding_a.account), + AccountPostState::new(user_holding_b.account), + ] +} + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[must_use] +pub fn swap_exact_input( + pool: AccountWithMetadata, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + swap_amount_in: u128, + min_amount_out: u128, + token_in_id: AccountId, +) -> (Vec, Vec) { + let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b); + let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) = if token_in_id == pool_def_data.definition_token_a_id { let (chained_calls, deposit_a, withdraw_b) = swap_logic( @@ -95,23 +139,18 @@ pub fn swap( panic!("AccountId is not a token type for the pool"); }; - // Update pool account - let mut pool_post = pool.account; - let pool_post_definition = PoolDefinition { - reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a, - reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b, - ..pool_def_data - }; - - pool_post.data = Data::from(&pool_post_definition); - - let post_states = vec![ - AccountPostState::new(pool_post), - AccountPostState::new(vault_a.account), - AccountPostState::new(vault_b.account), - AccountPostState::new(user_holding_a.account), - AccountPostState::new(user_holding_b.account), - ]; + let post_states = create_swap_post_states( + pool, + pool_def_data, + vault_a, + vault_b, + user_holding_a, + user_holding_b, + deposit_a, + withdraw_a, + deposit_b, + withdraw_b, + ); (post_states, chained_calls) } @@ -131,7 +170,9 @@ fn swap_logic( // Compute withdraw amount // Maintains pool constant product // k = pool_def_data.reserve_a * pool_def_data.reserve_b; - let withdraw_amount = (reserve_withdraw_vault_amount * swap_amount_in) + let withdraw_amount = reserve_withdraw_vault_amount + .checked_mul(swap_amount_in) + .expect("reserve * amount_in overflows u128") / (reserve_deposit_vault_amount + swap_amount_in); // Slippage check @@ -175,3 +216,135 @@ fn swap_logic( (chained_calls, swap_amount_in, withdraw_amount) } + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[must_use] +pub fn swap_exact_output( + pool: AccountWithMetadata, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + exact_amount_out: u128, + max_amount_in: u128, + token_in_id: AccountId, +) -> (Vec, Vec) { + let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b); + + let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) = + if token_in_id == pool_def_data.definition_token_a_id { + let (chained_calls, deposit_a, withdraw_b) = exact_output_swap_logic( + user_holding_a.clone(), + vault_a.clone(), + vault_b.clone(), + user_holding_b.clone(), + exact_amount_out, + max_amount_in, + pool_def_data.reserve_a, + pool_def_data.reserve_b, + pool.account_id, + ); + + (chained_calls, [deposit_a, 0], [0, withdraw_b]) + } else if token_in_id == pool_def_data.definition_token_b_id { + let (chained_calls, deposit_b, withdraw_a) = exact_output_swap_logic( + user_holding_b.clone(), + vault_b.clone(), + vault_a.clone(), + user_holding_a.clone(), + exact_amount_out, + max_amount_in, + pool_def_data.reserve_b, + pool_def_data.reserve_a, + pool.account_id, + ); + + (chained_calls, [0, withdraw_a], [deposit_b, 0]) + } else { + panic!("AccountId is not a token type for the pool"); + }; + + let post_states = create_swap_post_states( + pool, + pool_def_data, + vault_a, + vault_b, + user_holding_a, + user_holding_b, + deposit_a, + withdraw_a, + deposit_b, + withdraw_b, + ); + + (post_states, chained_calls) +} + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +fn exact_output_swap_logic( + user_deposit: AccountWithMetadata, + vault_deposit: AccountWithMetadata, + vault_withdraw: AccountWithMetadata, + user_withdraw: AccountWithMetadata, + exact_amount_out: u128, + max_amount_in: u128, + reserve_deposit_vault_amount: u128, + reserve_withdraw_vault_amount: u128, + pool_id: AccountId, +) -> (Vec, u128, u128) { + // Guard: exact_amount_out must be nonzero + assert_ne!(exact_amount_out, 0, "Exact amount out must be nonzero"); + + // Guard: exact_amount_out must be less than reserve_withdraw_vault_amount + assert!( + exact_amount_out < reserve_withdraw_vault_amount, + "Exact amount out exceeds reserve" + ); + + // Compute deposit amount using ceiling division + // Formula: amount_in = ceil(reserve_in * exact_amount_out / (reserve_out - exact_amount_out)) + let deposit_amount = reserve_deposit_vault_amount + .checked_mul(exact_amount_out) + .expect("reserve * amount_out overflows u128") + .div_ceil(reserve_withdraw_vault_amount - exact_amount_out); + + // Slippage check + assert!( + deposit_amount <= max_amount_in, + "Required input exceeds maximum amount in" + ); + + let token_program_id = user_deposit.account.program_owner; + + let mut chained_calls = Vec::new(); + chained_calls.push(ChainedCall::new( + token_program_id, + vec![user_deposit, vault_deposit], + &token_core::Instruction::Transfer { + amount_to_transfer: deposit_amount, + }, + )); + + let mut vault_withdraw = vault_withdraw; + vault_withdraw.is_authorized = true; + + let pda_seed = compute_vault_pda_seed( + pool_id, + token_core::TokenHolding::try_from(&vault_withdraw.account.data) + .expect("Exact Output Swap Logic: AMM Program expects valid token data") + .definition_id(), + ); + + chained_calls.push( + ChainedCall::new( + token_program_id, + vec![vault_withdraw, user_withdraw], + &token_core::Instruction::Transfer { + amount_to_transfer: exact_amount_out, + }, + ) + .with_pda_seeds(vec![pda_seed]), + ); + + (chained_calls, deposit_amount, exact_amount_out) +} diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index 86fdb4ff..744b4cb7 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, @@ -14,7 +14,10 @@ use nssa_core::{ use token_core::{TokenDefinition, TokenHolding}; use crate::{ - add::add_liquidity, new_definition::new_definition, remove::remove_liquidity, swap::swap, + add::add_liquidity, + new_definition::new_definition, + remove::remove_liquidity, + swap::{swap_exact_input, swap_exact_output}, }; const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; @@ -153,6 +156,10 @@ impl BalanceForTests { 200 } + fn max_amount_in() -> u128 { + 166 + } + fn vault_a_add_successful() -> u128 { 1_400 } @@ -243,6 +250,74 @@ impl ChainedCallForTests { ) } + fn cc_swap_exact_output_token_a_test_1() -> ChainedCall { + let swap_amount: u128 = 498; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::vault_a_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + } + + fn cc_swap_exact_output_token_b_test_1() -> ChainedCall { + let swap_amount: u128 = 166; + + let mut vault_b_auth = AccountWithMetadataForTests::vault_b_init(); + vault_b_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_b_auth, AccountWithMetadataForTests::user_holding_b()], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + )]) + } + + fn cc_swap_exact_output_token_a_test_2() -> ChainedCall { + let swap_amount: u128 = 285; + + let mut vault_a_auth = AccountWithMetadataForTests::vault_a_init(); + vault_a_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_a_auth, AccountWithMetadataForTests::user_holding_a()], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + )]) + } + + fn cc_swap_exact_output_token_b_test_2() -> ChainedCall { + let swap_amount: u128 = 200; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountWithMetadataForTests::user_holding_b(), + AccountWithMetadataForTests::vault_b_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + } + fn cc_add_token_a() -> ChainedCall { ChainedCall::new( TOKEN_PROGRAM_ID, @@ -829,6 +904,54 @@ impl AccountWithMetadataForTests { } } + fn pool_definition_swap_exact_output_test_1() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0_u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::lp_supply_init(), + reserve_a: 1498_u128, + reserve_b: 334_u128, + fees: 0_u128, + active: true, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_swap_exact_output_test_2() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0_u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::lp_supply_init(), + reserve_a: BalanceForTests::vault_a_swap_test_2(), + reserve_b: BalanceForTests::vault_b_swap_test_2(), + fees: 0_u128, + active: true, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + fn pool_definition_add_zero_lp() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -1756,7 +1879,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 +1924,7 @@ impl AccountsForExeTests { definition_id: IdForExeTests::token_lp_definition_id(), balance: 0, }), - nonce: 0_u128.into(), + nonce: 1.into(), } } } @@ -2400,7 +2523,7 @@ fn call_new_definition_chained_call_successful() { #[should_panic(expected = "AccountId is not a token type for the pool")] #[test] fn call_swap_incorrect_token_type() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2415,7 +2538,7 @@ fn call_swap_incorrect_token_type() { #[should_panic(expected = "Vault A was not provided")] #[test] fn call_swap_vault_a_omitted() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_with_wrong_id(), AccountWithMetadataForTests::vault_b_init(), @@ -2430,7 +2553,7 @@ fn call_swap_vault_a_omitted() { #[should_panic(expected = "Vault B was not provided")] #[test] fn call_swap_vault_b_omitted() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_with_wrong_id(), @@ -2445,7 +2568,7 @@ fn call_swap_vault_b_omitted() { #[should_panic(expected = "Reserve for Token A exceeds vault balance")] #[test] fn call_swap_reserves_vault_mismatch_1() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init_low(), AccountWithMetadataForTests::vault_b_init(), @@ -2460,7 +2583,7 @@ fn call_swap_reserves_vault_mismatch_1() { #[should_panic(expected = "Reserve for Token B exceeds vault balance")] #[test] fn call_swap_reserves_vault_mismatch_2() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init_low(), @@ -2475,7 +2598,7 @@ fn call_swap_reserves_vault_mismatch_2() { #[should_panic(expected = "Pool is inactive")] #[test] fn call_swap_ianctive() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_inactive(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2490,7 +2613,7 @@ fn call_swap_ianctive() { #[should_panic(expected = "Withdraw amount is less than minimal amount out")] #[test] fn call_swap_below_min_out() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2504,7 +2627,7 @@ fn call_swap_below_min_out() { #[test] fn call_swap_chained_call_successful_1() { - let (post_states, chained_calls) = swap( + let (post_states, chained_calls) = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2536,7 +2659,7 @@ fn call_swap_chained_call_successful_1() { #[test] fn call_swap_chained_call_successful_2() { - let (post_states, chained_calls) = swap( + let (post_states, chained_calls) = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2566,6 +2689,281 @@ fn call_swap_chained_call_successful_2() { ); } +#[should_panic(expected = "AccountId is not a token type for the pool")] +#[test] +fn call_swap_exact_output_incorrect_token_type() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_lp_definition_id(), + ); +} + +#[should_panic(expected = "Vault A was not provided")] +#[test] +fn call_swap_exact_output_vault_a_omitted() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_with_wrong_id(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Vault B was not provided")] +#[test] +fn call_swap_exact_output_vault_b_omitted() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_with_wrong_id(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Reserve for Token A exceeds vault balance")] +#[test] +fn call_swap_exact_output_reserves_vault_mismatch_1() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init_low(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Reserve for Token B exceeds vault balance")] +#[test] +fn call_swap_exact_output_reserves_vault_mismatch_2() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init_low(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Pool is inactive")] +#[test] +fn call_swap_exact_output_inactive() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_inactive(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Required input exceeds maximum amount in")] +#[test] +fn call_swap_exact_output_exceeds_max_in() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + 166_u128, + 100_u128, + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Exact amount out must be nonzero")] +#[test] +fn call_swap_exact_output_zero() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + 0_u128, + 500_u128, + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Exact amount out exceeds reserve")] +#[test] +fn call_swap_exact_output_exceeds_reserve() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::vault_b_reserve_init(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[test] +fn call_swap_exact_output_chained_call_successful() { + let (post_states, chained_calls) = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::max_amount_in(), + BalanceForTests::vault_b_reserve_init(), + IdForTests::token_a_definition_id(), + ); + + let pool_post = post_states[0].clone(); + + assert!( + AccountWithMetadataForTests::pool_definition_swap_exact_output_test_1().account + == *pool_post.account() + ); + + let chained_call_a = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + + assert_eq!( + chained_call_a, + ChainedCallForTests::cc_swap_exact_output_token_a_test_1() + ); + assert_eq!( + chained_call_b, + ChainedCallForTests::cc_swap_exact_output_token_b_test_1() + ); +} + +#[test] +fn call_swap_exact_output_chained_call_successful_2() { + let (post_states, chained_calls) = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + 285, + 300, + IdForTests::token_b_definition_id(), + ); + + let pool_post = post_states[0].clone(); + + assert!( + AccountWithMetadataForTests::pool_definition_swap_exact_output_test_2().account + == *pool_post.account() + ); + + let chained_call_a = chained_calls[1].clone(); + let chained_call_b = chained_calls[0].clone(); + + assert_eq!( + chained_call_a, + ChainedCallForTests::cc_swap_exact_output_token_a_test_2() + ); + assert_eq!( + chained_call_b, + ChainedCallForTests::cc_swap_exact_output_token_b_test_2() + ); +} + +// Without the fix, `reserve_a * exact_amount_out` silently wraps to 0 in release mode, +// making `deposit_amount = 0`. The slippage check `0 <= max_amount_in` always passes, +// so an attacker receives `exact_amount_out` tokens while paying nothing. +#[should_panic(expected = "reserve * amount_out overflows u128")] +#[test] +fn swap_exact_output_overflow_protection() { + // reserve_a chosen so that reserve_a * 2 overflows u128: + // (u128::MAX / 2 + 1) * 2 = u128::MAX + 1 → wraps to 0 + let large_reserve: u128 = u128::MAX / 2 + 1; + let reserve_b: u128 = 1_000; + + let pool = AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: 1, + reserve_a: large_reserve, + reserve_b, + fees: 0, + active: true, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + }; + + let vault_a = AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_a_definition_id(), + balance: large_reserve, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + }; + + let vault_b = AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_b_definition_id(), + balance: reserve_b, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + }; + + let _result = swap_exact_output( + pool, + vault_a, + vault_b, + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + 2, // exact_amount_out: small, valid (< reserve_b) + 1, // max_amount_in: tiny — real deposit would be enormous, but + // overflow wraps it to 0, making 0 <= 1 pass silently + IdForTests::token_a_definition_id(), + ); +} + #[test] fn new_definition_lp_asymmetric_amounts() { let (post_states, chained_calls) = new_definition( @@ -2733,7 +3131,7 @@ fn simple_amm_remove() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).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 +3197,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 +3207,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, 1).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 +3296,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, 1).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 +3354,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 +3364,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, 1).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 +3431,7 @@ fn simple_amm_add() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).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()); @@ -3062,7 +3462,7 @@ fn simple_amm_add() { fn simple_amm_swap_1() { let mut state = state_for_amm_tests(); - let instruction = amm_core::Instruction::Swap { + let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in: BalanceForExeTests::swap_amount_in(), min_amount_out: BalanceForExeTests::swap_min_amount_out(), token_definition_id_in: IdForExeTests::token_b_definition_id(), @@ -3088,7 +3488,7 @@ fn simple_amm_swap_1() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).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()); @@ -3113,7 +3513,7 @@ fn simple_amm_swap_1() { fn simple_amm_swap_2() { let mut state = state_for_amm_tests(); - let instruction = amm_core::Instruction::Swap { + let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in: BalanceForExeTests::swap_amount_in(), min_amount_out: BalanceForExeTests::swap_min_amount_out(), token_definition_id_in: IdForExeTests::token_a_definition_id(), @@ -3138,7 +3538,7 @@ fn simple_amm_swap_2() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).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/src/create.rs b/programs/associated_token_account/src/create.rs index 86109952..d44f5d1c 100644 --- a/programs/associated_token_account/src/create.rs +++ b/programs/associated_token_account/src/create.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, - program::{AccountPostState, ChainedCall, ProgramId}, + program::{AccountPostState, ChainedCall, Claim, ProgramId}, }; pub fn create_associated_token_account( @@ -11,7 +11,7 @@ pub fn create_associated_token_account( ) -> (Vec, Vec) { // No authorization check needed: create is idempotent, so anyone can call it safely. let token_program_id = token_definition.account.program_owner; - ata_core::verify_ata_and_get_seed( + let ata_seed = ata_core::verify_ata_and_get_seed( &ata_account, &owner, token_definition.account_id, @@ -22,7 +22,7 @@ pub fn create_associated_token_account( if ata_account.account != Account::default() { return ( vec![ - AccountPostState::new_claimed_if_default(owner.account.clone()), + AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized), AccountPostState::new(token_definition.account.clone()), AccountPostState::new(ata_account.account.clone()), ], @@ -31,14 +31,20 @@ pub fn create_associated_token_account( } let post_states = vec![ - AccountPostState::new_claimed_if_default(owner.account.clone()), + 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.clone()], + 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/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/src/lib.rs b/sequencer/core/src/lib.rs index 931fce45..f6495702 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -16,6 +16,7 @@ use mempool::{MemPool, MemPoolHandle}; #[cfg(feature = "mock")] pub use mock::SequencerCoreWithMockClients; use nssa::{AccountId, V03State}; +use nssa_core::{BlockId, Timestamp}; pub use storage::error::DbError; use testnet_initial_state::initial_state; @@ -165,14 +166,16 @@ impl SequencerCore Result { match &tx { NSSATransaction::Public(tx) => self .state - .transition_from_public_transaction(tx, self.next_block_id()), + .transition_from_public_transaction(tx, block_id, timestamp), NSSATransaction::PrivacyPreserving(tx) => self .state - .transition_from_privacy_preserving_transaction(tx, self.next_block_id()), + .transition_from_privacy_preserving_transaction(tx, block_id, timestamp), NSSATransaction::ProgramDeployment(tx) => self .state .transition_from_program_deployment_transaction(tx), @@ -218,7 +221,7 @@ impl SequencerCore SequencerCore SequencerCore { valid_transactions.push(valid_tx); @@ -272,7 +276,7 @@ impl SequencerCore>, bool); @@ -28,7 +28,7 @@ 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) }; diff --git a/test_program_methods/guest/src/bin/claimer.rs b/test_program_methods/guest/src/bin/claimer.rs index 57e7e4e5..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, ProgramOutput, read_nssa_inputs}; +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); 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 55f4e2a0..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, ProgramOutput, read_nssa_inputs}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = Vec; @@ -25,7 +25,10 @@ fn main() { 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/validity_window.rs b/test_program_methods/guest/src/bin/validity_window.rs index 00e8e5e8..a0ff9f36 100644 --- a/test_program_methods/guest/src/bin/validity_window.rs +++ b/test_program_methods/guest/src/bin/validity_window.rs @@ -1,14 +1,15 @@ use nssa_core::program::{ - AccountPostState, ProgramInput, ProgramOutput, ValidityWindow, read_nssa_inputs, + AccountPostState, BlockValidityWindow, ProgramInput, ProgramOutput, TimestampValidityWindow, + read_nssa_inputs, }; -type Instruction = ValidityWindow; +type Instruction = (BlockValidityWindow, TimestampValidityWindow); fn main() { let ( ProgramInput { pre_states, - instruction: validity_window, + instruction: (block_validity_window, timestamp_validity_window), }, instruction_words, ) = read_nssa_inputs::(); @@ -24,6 +25,7 @@ fn main() { vec![pre], vec![AccountPostState::new(post)], ) - .with_validity_window(validity_window) + .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 index cbd110dd..39f8ad69 100644 --- a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs +++ b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs @@ -1,21 +1,23 @@ use nssa_core::program::{ - AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, ValidityWindow, - read_nssa_inputs, + AccountPostState, BlockValidityWindow, ChainedCall, ProgramId, ProgramInput, ProgramOutput, + TimestampValidityWindow, read_nssa_inputs, }; use risc0_zkvm::serde::to_vec; -/// A program that sets a validity window on its output and chains to another program with a -/// potentially different validity window. +/// 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`. -type Instruction = (ValidityWindow, ProgramId, ValidityWindow); +/// 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: (validity_window, chained_program_id, chained_validity_window), + instruction: (block_validity_window, chained_program_id, chained_block_validity_window), }, instruction_words, ) = read_nssa_inputs::(); @@ -23,7 +25,11 @@ fn main() { 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_validity_window).unwrap(); + 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, @@ -36,7 +42,7 @@ fn main() { vec![pre], vec![AccountPostState::new(post)], ) - .with_validity_window(validity_window) + .with_block_validity_window(block_validity_window) .with_chained_calls(vec![chained_call]) .write(); } diff --git a/wallet-ffi/src/wallet.rs b/wallet-ffi/src/wallet.rs index 9117d0ee..93fc20aa 100644 --- a/wallet-ffi/src/wallet.rs +++ b/wallet-ffi/src/wallet.rs @@ -111,8 +111,8 @@ pub unsafe extern "C" fn wallet_ffi_create_new( return ptr::null_mut(); }; - match WalletCore::new_init_storage(config_path, storage_path, None, password) { - Ok(core) => { + match WalletCore::new_init_storage(config_path, storage_path, None, &password) { + Ok((core, _mnemonic)) => { let wrapper = Box::new(WalletWrapper { core: Mutex::new(core), }); diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index f77988a0..4e98b8ef 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -17,6 +17,7 @@ token_core.workspace = true amm_core.workspace = true testnet_initial_state.workspace = true ata_core.workspace = true +bip39.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index ebfe9896..3699609b 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, HashMap, btree_map::Entry}; use anyhow::Result; +use bip39::Mnemonic; use key_protocol::{ key_management::{ key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, @@ -95,7 +96,7 @@ impl WalletChainStore { }) } - pub fn new_storage(config: WalletConfig, password: String) -> Result { + pub fn new_storage(config: WalletConfig, password: &str) -> Result<(Self, Mnemonic)> { let mut public_init_acc_map = BTreeMap::new(); let mut private_init_acc_map = BTreeMap::new(); @@ -121,13 +122,43 @@ impl WalletChainStore { } } - let public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone())); - let private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password)); + // TODO: Use password for storage encryption + let _ = password; + let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + let public_tree = KeyTreePublic::new(&seed_holder); + let private_tree = KeyTreePrivate::new(&seed_holder); + + Ok(( + Self { + user_data: NSSAUserData::new_with_accounts( + public_init_acc_map, + private_init_acc_map, + public_tree, + private_tree, + )?, + wallet_config: config, + labels: HashMap::new(), + }, + mnemonic, + )) + } + + /// Restore storage from an existing mnemonic phrase. + pub fn restore_storage( + config: WalletConfig, + mnemonic: &Mnemonic, + password: &str, + ) -> Result { + // TODO: Use password for storage encryption + let _ = password; + let seed_holder = SeedHolder::from_mnemonic(mnemonic, ""); + let public_tree = KeyTreePublic::new(&seed_holder); + let private_tree = KeyTreePrivate::new(&seed_holder); Ok(Self { user_data: NSSAUserData::new_with_accounts( - public_init_acc_map, - private_init_acc_map, + BTreeMap::new(), + BTreeMap::new(), public_tree, private_tree, )?, diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 6463dee8..1653e938 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,6 +1,7 @@ -use std::{io::Write as _, path::PathBuf}; +use std::{io::Write as _, path::PathBuf, str::FromStr as _}; use anyhow::{Context as _, Result}; +use bip39::Mnemonic; use clap::{Parser, Subcommand}; use common::{HashType, transaction::NSSATransaction}; use futures::TryFutureExt as _; @@ -167,8 +168,9 @@ pub async fn execute_subcommand( config_subcommand.handle_subcommand(wallet_core).await? } Command::RestoreKeys { depth } => { + let mnemonic = read_mnemonic_from_stdin()?; let password = read_password_from_stdin()?; - wallet_core.reset_storage(password)?; + wallet_core.restore_storage(&mnemonic, &password)?; execute_keys_restoration(wallet_core, depth).await?; SubcommandReturnValue::Empty @@ -212,6 +214,16 @@ pub fn read_password_from_stdin() -> Result { Ok(password.trim().to_owned()) } +pub fn read_mnemonic_from_stdin() -> Result { + let mut phrase = String::new(); + + print!("Input recovery phrase: "); + std::io::stdout().flush()?; + std::io::stdin().read_line(&mut phrase)?; + + Mnemonic::from_str(phrase.trim()).context("Invalid mnemonic phrase") +} + pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) -> Result<()> { wallet_core .storage diff --git a/wallet/src/cli/programs/amm.rs b/wallet/src/cli/programs/amm.rs index 7307569d..0b721d15 100644 --- a/wallet/src/cli/programs/amm.rs +++ b/wallet/src/cli/programs/amm.rs @@ -32,12 +32,12 @@ pub enum AmmProgramAgnosticSubcommand { #[arg(long)] balance_b: u128, }, - /// Swap. + /// Swap specifying exact input amount. /// /// The account associated with swapping token must be owned. /// /// Only public execution allowed. - Swap { + SwapExactInput { /// `user_holding_a` - valid 32 byte base58 string with privacy prefix. #[arg(long)] user_holding_a: String, @@ -52,6 +52,26 @@ pub enum AmmProgramAgnosticSubcommand { #[arg(long)] token_definition: String, }, + /// Swap specifying exact output amount. + /// + /// The account associated with swapping token must be owned. + /// + /// Only public execution allowed. + SwapExactOutput { + /// `user_holding_a` - valid 32 byte base58 string with privacy prefix. + #[arg(long)] + user_holding_a: String, + /// `user_holding_b` - valid 32 byte base58 string with privacy prefix. + #[arg(long)] + user_holding_b: String, + #[arg(long)] + exact_amount_out: u128, + #[arg(long)] + max_amount_in: u128, + /// `token_definition` - valid 32 byte base58 string WITHOUT privacy prefix. + #[arg(long)] + token_definition: String, + }, /// Add liquidity. /// /// `user_holding_a` and `user_holding_b` must be owned. @@ -150,7 +170,7 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { } } } - Self::Swap { + Self::SwapExactInput { user_holding_a, user_holding_b, amount_in, @@ -168,7 +188,7 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { match (user_holding_a_privacy, user_holding_b_privacy) { (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { Amm(wallet_core) - .send_swap( + .send_swap_exact_input( user_holding_a, user_holding_b, amount_in, @@ -185,6 +205,41 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { } } } + Self::SwapExactOutput { + user_holding_a, + user_holding_b, + exact_amount_out, + max_amount_in, + token_definition, + } => { + let (user_holding_a, user_holding_a_privacy) = + parse_addr_with_privacy_prefix(&user_holding_a)?; + let (user_holding_b, user_holding_b_privacy) = + parse_addr_with_privacy_prefix(&user_holding_b)?; + + let user_holding_a: AccountId = user_holding_a.parse()?; + let user_holding_b: AccountId = user_holding_b.parse()?; + + match (user_holding_a_privacy, user_holding_b_privacy) { + (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { + Amm(wallet_core) + .send_swap_exact_output( + user_holding_a, + user_holding_b, + exact_amount_out, + max_amount_in, + token_definition.parse()?, + ) + .await?; + + Ok(SubcommandReturnValue::Empty) + } + _ => { + // ToDo: Implement after private multi-chain calls is available + anyhow::bail!("Only public execution allowed for Amm calls"); + } + } + } Self::AddLiquidity { user_holding_a, user_holding_b, diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 0c46fea8..5b57a515 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -11,6 +11,7 @@ use std::path::PathBuf; use anyhow::{Context as _, Result}; +use bip39::Mnemonic; use chain_storage::WalletChainStore; use common::{HashType, transaction::NSSATransaction}; use config::WalletConfig; @@ -117,15 +118,24 @@ impl WalletCore { config_path: PathBuf, storage_path: PathBuf, config_overrides: Option, - password: String, - ) -> Result { - Self::new( + password: &str, + ) -> Result<(Self, Mnemonic)> { + let mut mnemonic_out = None; + let wallet = Self::new( config_path, storage_path, config_overrides, - |config| WalletChainStore::new_storage(config, password), + |config| { + let (storage, mnemonic) = WalletChainStore::new_storage(config, password)?; + mnemonic_out = Some(mnemonic); + Ok(storage) + }, 0, - ) + )?; + Ok(( + wallet, + mnemonic_out.expect("mnemonic should be set after new_storage"), + )) } fn new( @@ -191,9 +201,13 @@ impl WalletCore { &self.storage } - /// Reset storage. - pub fn reset_storage(&mut self, password: String) -> Result<()> { - self.storage = WalletChainStore::new_storage(self.storage.wallet_config.clone(), password)?; + /// Restore storage from an existing mnemonic phrase. + pub fn restore_storage(&mut self, mnemonic: &Mnemonic, password: &str) -> Result<()> { + self.storage = WalletChainStore::restore_storage( + self.storage.wallet_config.clone(), + mnemonic, + password, + )?; Ok(()) } diff --git a/wallet/src/main.rs b/wallet/src/main.rs index e055bd63..cf8356db 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -46,13 +46,21 @@ async fn main() -> Result<()> { println!("Persistent storage not found, need to execute setup"); let password = read_password_from_stdin()?; - let wallet = WalletCore::new_init_storage( + let (wallet, mnemonic) = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), - password, + &password, )?; + println!(); + println!("IMPORTANT: Write down your recovery phrase and store it securely."); + println!("This is the only way to recover your wallet if you lose access."); + println!(); + println!("Recovery phrase:"); + println!(" {mnemonic}"); + println!(); + wallet.store_persistent_data().await?; wallet }; diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs index f1b94621..b31d0658 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); @@ -100,7 +121,7 @@ impl Amm<'_> { .await?) } - pub async fn send_swap( + pub async fn send_swap_exact_input( &self, user_holding_a: AccountId, user_holding_b: AccountId, @@ -108,7 +129,7 @@ impl Amm<'_> { min_amount_out: u128, token_definition_id_in: AccountId, ) -> Result { - let instruction = amm_core::Instruction::Swap { + let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in, min_amount_out, token_definition_id_in, @@ -147,34 +168,105 @@ impl Amm<'_> { user_holding_b, ]; - let account_id_auth; + let account_id_auth = if definition_token_a_id == token_definition_id_in { + user_holding_a + } else if definition_token_b_id == token_definition_id_in { + user_holding_b + } else { + return Err(ExecutionFailureKind::AccountDataError( + token_definition_id_in, + )); + }; - // Checking, which account are associated with TokenDefinition - let token_holder_acc_a = self + let nonces = self + .0 + .get_accounts_nonces(vec![account_id_auth]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let signing_key = self + .0 + .storage + .user_data + .get_pub_account_signing_key(account_id_auth) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + ) + .unwrap(); + + 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_swap_exact_output( + &self, + user_holding_a: AccountId, + user_holding_b: AccountId, + exact_amount_out: u128, + max_amount_in: u128, + token_definition_id_in: AccountId, + ) -> Result { + let instruction = amm_core::Instruction::SwapExactOutput { + exact_amount_out, + max_amount_in, + token_definition_id_in, + }; + let program = Program::amm(); + let amm_program_id = Program::amm().id(); + + let user_a_acc = self .0 .get_account_public(user_holding_a) .await .map_err(ExecutionFailureKind::SequencerError)?; - let token_holder_acc_b = self + let user_b_acc = self .0 .get_account_public(user_holding_b) .await .map_err(ExecutionFailureKind::SequencerError)?; - let token_holder_a = TokenHolding::try_from(&token_holder_acc_a.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))?; - let token_holder_b = TokenHolding::try_from(&token_holder_acc_b.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))?; + let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) + .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id(); + let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) + .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))? + .definition_id(); - if token_holder_a.definition_id() == token_definition_id_in { - account_id_auth = user_holding_a; - } else if token_holder_b.definition_id() == token_definition_id_in { - account_id_auth = user_holding_b; + let amm_pool = + compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); + let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); + let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); + + let account_ids = vec![ + amm_pool, + vault_holding_a, + vault_holding_b, + user_holding_a, + user_holding_b, + ]; + + let account_id_auth = if definition_token_a_id == token_definition_id_in { + user_holding_a + } else if definition_token_b_id == token_definition_id_in { + user_holding_b } else { return Err(ExecutionFailureKind::AccountDataError( token_definition_id_in, )); - } + }; let nonces = self .0 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);