diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c29c281..18064c6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -10,10 +10,10 @@ TO COMPLETE TO COMPLETE -[ ] Change ... -[ ] Add ... -[ ] Fix ... -[ ] ... +- [ ] Change ... +- [ ] Add ... +- [ ] Fix ... +- [ ] ... ## ๐Ÿงช How to Test @@ -37,7 +37,7 @@ TO COMPLETE IF APPLICABLE *Mark only completed items. A complete PR should have all boxes ticked.* -[ ] Complete PR description -[ ] Implement the core functionality -[ ] Add/update tests -[ ] Add/update documentation and inline comments +- [ ] Complete PR description +- [ ] Implement the core functionality +- [ ] Add/update tests +- [ ] Add/update documentation and inline comments diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ebfca7..f3644cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,9 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y build-essential clang libclang-dev libssl-dev pkg-config + - name: Install active toolchain run: rustup install diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..6dc622d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,23 @@ +name: Deploy Sequencer + +on: + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Deploy to server + uses: appleboy/ssh-action@v1.2.4 + with: + host: ${{ secrets.DEPLOY_SSH_HOST }} + username: ${{ secrets.DEPLOY_SSH_USERNAME }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + envs: GITHUB_ACTOR + script_path: ci_scripts/deploy.sh diff --git a/Cargo.toml b/Cargo.toml index dd24d98..9b773b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,9 @@ borsh = "1.5.7" base58 = "0.2.0" itertools = "0.14.0" -rocksdb = { version = "0.21.0", default-features = false, features = [ +rocksdb = { version = "0.24.0", default-features = false, features = [ "snappy", + "bindgen-runtime", ] } [workspace.dependencies.rand] diff --git a/README.md b/README.md index ff596b8..cb8628d 100644 --- a/README.md +++ b/README.md @@ -69,16 +69,14 @@ Install build dependencies - On Linux Ubuntu / Debian ```sh -apt install build-essential clang libssl-dev pkg-config +apt install build-essential clang libclang-dev libssl-dev pkg-config ``` Fedora ```sh -sudo dnf install clang openssl-devel pkgconf llvm +sudo dnf install clang clang-devel openssl-devel pkgconf ``` -> **Note for Fedora 41+ users:** GCC 14+ has stricter C++ standard library headers that cause build failures with the bundled RocksDB. You must set `CXXFLAGS="-include cstdint"` when running cargo commands. See the [Run tests](#run-tests) section for examples. - - On Mac ```sh xcode-select --install @@ -110,9 +108,6 @@ The NSSA repository includes both unit and integration test suites. ```bash # RISC0_DEV_MODE=1 is used to skip proof generation and reduce test runtime overhead RISC0_DEV_MODE=1 cargo test --release - -# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build: -CXXFLAGS="-include cstdint" RISC0_DEV_MODE=1 cargo test --release ``` ### Integration tests @@ -122,9 +117,6 @@ export NSSA_WALLET_HOME_DIR=$(pwd)/integration_tests/configs/debug/wallet/ cd integration_tests # RISC0_DEV_MODE=1 skips proof generation; RUST_LOG=info enables runtime logs RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all - -# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build: -CXXFLAGS="-include cstdint" RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all ``` # Run the sequencer @@ -134,9 +126,6 @@ The sequencer can be run locally: ```bash cd sequencer_runner RUST_LOG=info cargo run --release configs/debug - -# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build: -CXXFLAGS="-include cstdint" RUST_LOG=info cargo run --release configs/debug ``` If everything went well you should see an output similar to this: @@ -162,9 +151,6 @@ This repository includes a CLI for interacting with the Nescience sequencer. To ```bash cargo install --path wallet --force - -# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build: -CXXFLAGS="-include cstdint" cargo install --path wallet --force ``` Run `wallet help` to check everything went well. diff --git a/ci_scripts/deploy.sh b/ci_scripts/deploy.sh new file mode 100644 index 0000000..7615df0 --- /dev/null +++ b/ci_scripts/deploy.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -e + +# Base directory for deployment +LSSA_DIR="/home/arjentix/test_deploy/lssa" + +# Expect GITHUB_ACTOR to be passed as first argument or environment variable +GITHUB_ACTOR="${1:-${GITHUB_ACTOR:-unknown}}" + +# Function to log messages with timestamp +log_deploy() { + echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] $1" >> "${LSSA_DIR}/deploy.log" +} + +# Error handler +handle_error() { + echo "โœ— Deployment failed by: ${GITHUB_ACTOR}" + log_deploy "Deployment failed by: ${GITHUB_ACTOR}" + exit 1 +} + +find_sequencer_runner_pids() { + pgrep -f "sequencer_runner" | grep -v $$ +} + +# Set trap to catch any errors +trap 'handle_error' ERR + +# Log deployment info +log_deploy "Deployment initiated by: ${GITHUB_ACTOR}" + +# Navigate to code directory +if [ ! -d "${LSSA_DIR}/code" ]; then + mkdir -p "${LSSA_DIR}/code" +fi +cd "${LSSA_DIR}/code" + +# Stop current sequencer if running +if find_sequencer_runner_pids > /dev/null; then + echo "Stopping current sequencer..." + find_sequencer_runner_pids | xargs -r kill -SIGINT || true + sleep 2 + # Force kill if still running + find_sequencer_runner_pids | grep -v $$ | xargs -r kill -9 || true +fi + +# Clone or update repository +if [ -d ".git" ]; then + echo "Updating existing repository..." + git fetch origin + git checkout main + git reset --hard origin/main +else + echo "Cloning repository..." + git clone https://github.com/vacp2p/nescience-testnet.git . + git checkout main +fi + +# Build sequencer_runner and wallet in release mode +echo "Building sequencer_runner" +# That could be just `cargo build --release --bin sequencer_runner --bin wallet` +# but we have `no_docker` feature bug, see issue #179 +cd sequencer_runner +cargo build --release +cd ../wallet +cargo build --release +cd .. + +# Run sequencer_runner with config +echo "Starting sequencer_runner..." +export RUST_LOG=info +nohup ./target/release/sequencer_runner "${LSSA_DIR}/configs/sequencer" > "${LSSA_DIR}/sequencer.log" 2>&1 & + +# Wait 5 seconds and check health using wallet +sleep 5 +if ./target/release/wallet check-health; then + echo "โœ“ Sequencer started successfully and is healthy" + log_deploy "Deployment completed successfully by: ${GITHUB_ACTOR}" + exit 0 +else + echo "โœ— Sequencer failed health check" + tail -n 50 "${LSSA_DIR}/sequencer.log" + handle_error +fi diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 8012e17..8e42640 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1681,6 +1681,49 @@ pub fn prepare_function_map() -> HashMap { info!("Success!"); } + #[nssa_integration_test] + pub async fn test_authenticated_transfer_initialize_function_private() { + info!("########## test initialize private account for authenticated transfer ##########"); + let command = + Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })); + let SubcommandReturnValue::RegisterAccount { account_id } = + wallet::cli::execute_subcommand(command).await.unwrap() + else { + panic!("Error creating account"); + }; + + let command = Command::AuthTransfer(AuthTransferSubcommand::Init { + account_id: make_private_account_input_from_str(&account_id.to_string()), + }); + wallet::cli::execute_subcommand(command).await.unwrap(); + + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + info!("Checking correct execution"); + let command = Command::Account(AccountSubcommand::SyncPrivate {}); + wallet::cli::execute_subcommand(command).await.unwrap(); + + let wallet_config = fetch_config().await.unwrap(); + let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + let new_commitment1 = wallet_storage + .get_private_account_commitment(&account_id) + .unwrap(); + assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await); + + let account = wallet_storage.get_account_private(&account_id).unwrap(); + + let expected_program_owner = Program::authenticated_transfer_program().id(); + let expected_balance = 0; + + assert_eq!(account.program_owner, expected_program_owner); + assert_eq!(account.balance, expected_balance); + assert!(account.data.is_empty()); + } + #[nssa_integration_test] pub async fn test_pinata_private_receiver() { info!("########## test_pinata_private_receiver ##########"); diff --git a/integration_tests/src/tps_test_utils.rs b/integration_tests/src/tps_test_utils.rs index 6f597e2..154253c 100644 --- a/integration_tests/src/tps_test_utils.rs +++ b/integration_tests/src/tps_test_utils.rs @@ -167,7 +167,8 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { (sender_npk.clone(), sender_ss), (recipient_npk.clone(), recipient_ss), ], - &[(sender_nsk, proof)], + &[sender_nsk], + &[Some(proof)], &program.into(), ) .unwrap(); diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 848fe3e..dedcf78 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -10,11 +10,23 @@ use crate::{ #[derive(Serialize, Deserialize)] pub struct PrivacyPreservingCircuitInput { + /// Outputs of the program execution. pub program_outputs: Vec, + /// Visibility mask for accounts. + /// + /// - `0` - public account + /// - `1` - private account with authentication + /// - `2` - private account without authentication pub visibility_mask: Vec, + /// Nonces of private accounts. pub private_account_nonces: Vec, + /// Public keys of private accounts. pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, - pub private_account_auth: Vec<(NullifierSecretKey, MembershipProof)>, + /// Nullifier secret keys for authorized private accounts. + pub private_account_nsks: Vec, + /// Membership proofs for private accounts. Can be [`None`] for uninitialized accounts. + pub private_account_membership_proofs: Vec>, + /// Program ID. pub program_id: ProgramId, } diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index fe02d06..8a13173 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -17,7 +17,7 @@ fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState { // Continue only if the owner authorized this operation if !is_authorized { - panic!("Invalid input"); + panic!("Account must be authorized"); } account_to_claim @@ -31,12 +31,12 @@ fn transfer( ) -> Vec { // Continue only if the sender has authorized this operation if !sender.is_authorized { - panic!("Invalid input"); + panic!("Sender must be authorized"); } // Continue only if the sender has enough balance if sender.account.balance < balance_to_move { - panic!("Invalid input"); + panic!("Sender has insufficient balance"); } // Create accounts post states, with updated balances diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 4cbc42c..8d1688a 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -16,7 +16,8 @@ fn main() { visibility_mask, private_account_nonces, private_account_keys, - private_account_auth, + private_account_nsks, + private_account_membership_proofs, mut program_id, } = env::read(); @@ -63,7 +64,8 @@ fn main() { for (i, program_output) in program_outputs.iter().enumerate() { let mut program_output = program_output.clone(); - // Check that `program_output` is consistent with the execution of the corresponding program. + // Check that `program_output` is consistent with the execution of the corresponding + // program. let program_output_words = &to_vec(&program_output).expect("program_output must be serializable"); env::verify(program_id, program_output_words) @@ -131,7 +133,8 @@ fn main() { let mut private_nonces_iter = private_account_nonces.iter(); let mut private_keys_iter = private_account_keys.iter(); - let mut private_auth_iter = private_account_auth.iter(); + let mut private_nsks_iter = private_account_nsks.iter(); + let mut private_membership_proofs_iter = private_account_membership_proofs.iter(); let mut output_index = 0; for i in 0..n_accounts { @@ -141,9 +144,7 @@ fn main() { public_pre_states.push(pre_states[i].clone()); let mut post = state_diff.get(&pre_states[i].account_id).unwrap().clone(); - if pre_states[i].is_authorized { - post.nonce += 1; - } + if post.program_owner == DEFAULT_PROGRAM_ID { // Claim account post.program_owner = program_id; @@ -160,8 +161,7 @@ fn main() { if visibility_mask[i] == 1 { // Private account with authentication - let (nsk, membership_proof) = - private_auth_iter.next().expect("Missing private auth"); + let nsk = private_nsks_iter.next().expect("Missing nsk"); // Verify the nullifier public key let expected_npk = NullifierPublicKey::from(nsk); @@ -169,19 +169,38 @@ fn main() { panic!("Nullifier public key mismatch"); } - // Compute commitment set digest associated with provided auth path - let commitment_pre = Commitment::new(npk, &pre_states[i].account); - let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); - // Check pre_state authorization if !pre_states[i].is_authorized { panic!("Pre-state not authorized"); } - // Compute update nullifier - let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); + let membership_proof_opt = private_membership_proofs_iter + .next() + .expect("Missing membership proof"); + let (nullifier, set_digest) = membership_proof_opt + .as_ref() + .map(|membership_proof| { + // Compute commitment set digest associated with provided auth path + let commitment_pre = Commitment::new(npk, &pre_states[i].account); + let set_digest = + compute_digest_for_path(&commitment_pre, membership_proof); + + // Compute update nullifier + let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); + (nullifier, set_digest) + }) + .unwrap_or_else(|| { + if pre_states[i].account != Account::default() { + panic!("Found new private account with non default values."); + } + + // Compute initialization nullifier + let nullifier = Nullifier::for_account_initialization(npk); + (nullifier, DUMMY_COMMITMENT_HASH) + }); new_nullifiers.push((nullifier, set_digest)); } else { + // Private account without authentication if pre_states[i].account != Account::default() { panic!("Found new private account with non default values."); } @@ -190,7 +209,13 @@ fn main() { panic!("Found new private account marked as authorized."); } - // Compute initialization nullifier + let membership_proof_opt = private_membership_proofs_iter + .next() + .expect("Missing membership proof"); + assert!( + membership_proof_opt.is_none(), + "Membership proof must be None for unauthorized accounts" + ); let nullifier = Nullifier::for_account_initialization(npk); new_nullifiers.push((nullifier, DUMMY_COMMITMENT_HASH)); } @@ -225,15 +250,19 @@ fn main() { } if private_nonces_iter.next().is_some() { - panic!("Too many nonces."); + panic!("Too many nonces"); } if private_keys_iter.next().is_some() { - panic!("Too many private account keys."); + panic!("Too many private account keys"); } - if private_auth_iter.next().is_some() { - panic!("Too many private account authentication keys."); + if private_nsks_iter.next().is_some() { + panic!("Too many private account authentication keys"); + } + + if private_membership_proofs_iter.next().is_some() { + panic!("Too many private account membership proofs"); } let output = PrivacyPreservingCircuitOutput { diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index ffe8eb7..739295b 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -1,35 +1,48 @@ use nssa_core::{ - account::{Account, AccountId, AccountWithMetadata, Data, data::DATA_MAX_LENGTH_IN_BYTES}, + account::{Account, AccountId, AccountWithMetadata, Data}, program::{ AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, }, }; // The token program has three functions: -// 1. New token definition. Arguments to this function are: -// * Two **default** accounts: [definition_account, holding_account]. The first default account -// will be initialized with the token definition account values. The second account will be -// initialized to a token holding account for the new token, holding the entire total supply. -// * An instruction data of 23-bytes, indicating the total supply and the token name, with the -// following layout: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] The -// name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -// 2. Token transfer Arguments to this function are: +// 1. New token definition. +// Arguments to this function are: +// * Two **default** accounts: [definition_account, holding_account]. +// The first default account will be initialized with the token definition account values. The second account will +// be initialized to a token holding account for the new token, holding the entire total supply. +// * An instruction data of 23-bytes, indicating the total supply and the token name, with +// the following layout: +// [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] +// The name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] +// 2. Token transfer +// Arguments to this function are: // * Two accounts: [sender_account, recipient_account]. -// * An instruction data byte string of length 23, indicating the total supply with the -// following layout [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 -// || 0x00 || 0x00]. -// 3. Initialize account with zero balance Arguments to this function are: +// * An instruction data byte string of length 23, indicating the total supply with the following layout +// [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. +// 3. Initialize account with zero balance +// Arguments to this function are: // * Two accounts: [definition_account, account_to_initialize]. -// * An dummy byte string of length 23, with the following layout [0x02 || 0x00 || 0x00 || 0x00 -// || ... || 0x00 || 0x00]. +// * An dummy byte string of length 23, with the following layout +// [0x02 || 0x00 || 0x00 || 0x00 || ... || 0x00 || 0x00]. +// 4. Burn tokens from a Token Holding account (thus lowering total supply) +// Arguments to this function are: +// * Two accounts: [definition_account, holding_account]. +// * Authorization required: holding_account +// * An instruction data byte string of length 23, indicating the balance to burn with the folloiwng layout +// [0x03 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. +// 5. Mint additional supply of tokens tokens to a Token Holding account (thus increasing total supply) +// Arguments to this function are: +// * Two accounts: [definition_account, holding_account]. +// * Authorization required: definition_account +// * An instruction data byte string of length 23, indicating the balance to mint with the folloiwng layout +// [0x04 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. const TOKEN_DEFINITION_TYPE: u8 = 0; const TOKEN_DEFINITION_DATA_SIZE: usize = 23; -const _: () = assert!(TOKEN_DEFINITION_DATA_SIZE <= DATA_MAX_LENGTH_IN_BYTES); const TOKEN_HOLDING_TYPE: u8 = 1; const TOKEN_HOLDING_DATA_SIZE: usize = 49; -const _: () = assert!(TOKEN_HOLDING_DATA_SIZE <= DATA_MAX_LENGTH_IN_BYTES); struct TokenDefinition { account_type: u8, @@ -150,7 +163,7 @@ fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec Vec Vec Vec { + if pre_states.len() != 2 { + panic!("Invalid number of accounts"); + } + + let definition = &pre_states[0]; + let user_holding = &pre_states[1]; + + let definition_values = + TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); + let user_values = TokenHolding::parse(&user_holding.account.data) + .expect("Token Holding account must be valid"); + + if definition.account_id != user_values.definition_id { + panic!("Mismatch token definition and token holding"); + } + + if !user_holding.is_authorized { + panic!("Authorization is missing"); + } + + if user_values.balance < balance_to_burn { + panic!("Insufficient balance to burn"); + } + + let mut post_user_holding = user_holding.account.clone(); + let mut post_definition = definition.account.clone(); + + post_user_holding.data = TokenHolding::into_data(TokenHolding { + account_type: user_values.account_type, + definition_id: user_values.definition_id, + balance: user_values + .balance + .checked_sub(balance_to_burn) + .expect("Checked above"), + }); + + post_definition.data = TokenDefinition::into_data(TokenDefinition { + account_type: definition_values.account_type, + name: definition_values.name, + total_supply: definition_values + .total_supply + .checked_sub(balance_to_burn) + .expect("Total supply underflow"), + }); + + vec![ + AccountPostState::new(post_definition), + AccountPostState::new(post_user_holding), + ] +} + +fn mint_additional_supply( + pre_states: &[AccountWithMetadata], + amount_to_mint: u128, +) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of accounts"); + } + + let definition = &pre_states[0]; + let token_holding = &pre_states[1]; + + if !definition.is_authorized { + panic!("Definition authorization is missing"); + } + + let definition_values = + TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); + + let token_holding_values: TokenHolding = if token_holding.account == Account::default() { + TokenHolding::new(&definition.account_id) + } else { + TokenHolding::parse(&token_holding.account.data).expect("Holding account must be valid") + }; + + if definition.account_id != token_holding_values.definition_id { + panic!("Mismatch token definition and token holding"); + } + + let token_holding_post_data = TokenHolding { + account_type: token_holding_values.account_type, + definition_id: token_holding_values.definition_id, + balance: token_holding_values + .balance + .checked_add(amount_to_mint) + .expect("New balance overflow"), + }; + + let post_total_supply = definition_values + .total_supply + .checked_add(amount_to_mint) + .expect("Total supply overflow"); + + let post_definition_data = TokenDefinition { + account_type: definition_values.account_type, + name: definition_values.name, + total_supply: post_total_supply, + }; + + let post_definition = { + let mut this = definition.account.clone(); + this.data = post_definition_data.into_data(); + AccountPostState::new(this) + }; + + let token_holding_post = { + let mut this = token_holding.account.clone(); + this.data = token_holding_post_data.into_data(); + + // Claim the recipient account if it has default program owner + if this.program_owner == DEFAULT_PROGRAM_ID { + AccountPostState::new_claimed(this) + } else { + AccountPostState::new(this) + } + }; + vec![post_definition, token_holding_post] +} + type Instruction = [u8; 23]; fn main() { @@ -295,6 +428,34 @@ fn main() { } initialize_account(&pre_states) } + 3 => { + let balance_to_burn = u128::from_le_bytes( + instruction[1..17] + .try_into() + .expect("Balance to burn must be 16 bytes little-endian"), + ); + let name: [u8; 6] = instruction[17..] + .try_into() + .expect("Name must be 6 bytes long"); + assert_eq!(name, [0; 6]); + + // Execute + burn(&pre_states, balance_to_burn) + } + 4 => { + let balance_to_mint = u128::from_le_bytes( + instruction[1..17] + .try_into() + .expect("Balance to burn must be 16 bytes little-endian"), + ); + let name: [u8; 6] = instruction[17..] + .try_into() + .expect("Name must be 6 bytes long"); + assert_eq!(name, [0; 6]); + + // Execute + mint_additional_supply(&pre_states, balance_to_mint) + } _ => panic!("Invalid instruction"), }; @@ -306,8 +467,9 @@ mod tests { use nssa_core::account::{Account, AccountId, AccountWithMetadata}; use crate::{ - TOKEN_DEFINITION_DATA_SIZE, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE, - initialize_account, new_definition, transfer, + TOKEN_DEFINITION_DATA_SIZE, TOKEN_DEFINITION_TYPE, TOKEN_HOLDING_DATA_SIZE, + TOKEN_HOLDING_TYPE, TokenDefinition, TokenHolding, burn, initialize_account, + mint_additional_supply, new_definition, transfer, }; #[should_panic(expected = "Invalid number of input accounts")] @@ -705,4 +867,455 @@ mod tests { ] ); } + + enum BalanceEnum { + InitSupply, + HoldingBalance, + InitSupplyBurned, + HoldingBalanceBurned, + BurnSuccess, + BurnInsufficient, + MintSuccess, + InitSupplyMint, + HoldingBalanceMint, + MintOverflow, + } + + enum AccountsEnum { + DefinitionAccountAuth, + DefinitionAccountNotAuth, + HoldingDiffDef, + HoldingSameDefAuth, + HoldingSameDefNotAuth, + HoldingSameDefNotAuthOverflow, + DefinitionAccountPostBurn, + HoldingAccountPostBurn, + Uninit, + InitMint, + DefinitionAccountMint, + HoldingSameDefMint, + HoldingSameDefAuthLargeBalance, + } + + enum IdEnum { + PoolDefinitionId, + PoolDefinitionIdDiff, + HoldingId, + } + + fn helper_account_constructor(selection: AccountsEnum) -> AccountWithMetadata { + match selection { + AccountsEnum::DefinitionAccountAuth => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name: [2; 6], + total_supply: helper_balance_constructor(BalanceEnum::InitSupply), + }), + nonce: 0, + }, + is_authorized: true, + account_id: helper_id_constructor(IdEnum::PoolDefinitionId), + }, + AccountsEnum::DefinitionAccountNotAuth => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name: [2; 6], + total_supply: helper_balance_constructor(BalanceEnum::InitSupply), + }), + nonce: 0, + }, + is_authorized: false, + account_id: helper_id_constructor(IdEnum::PoolDefinitionId), + }, + AccountsEnum::HoldingDiffDef => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionIdDiff), + balance: helper_balance_constructor(BalanceEnum::HoldingBalance), + }), + nonce: 0, + }, + is_authorized: true, + account_id: helper_id_constructor(IdEnum::HoldingId), + }, + AccountsEnum::HoldingSameDefAuth => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::HoldingBalance), + }), + nonce: 0, + }, + is_authorized: true, + account_id: helper_id_constructor(IdEnum::HoldingId), + }, + AccountsEnum::HoldingSameDefNotAuth => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::HoldingBalance), + }), + nonce: 0, + }, + is_authorized: false, + account_id: helper_id_constructor(IdEnum::HoldingId), + }, + AccountsEnum::HoldingSameDefNotAuthOverflow => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::InitSupply), + }), + nonce: 0, + }, + is_authorized: false, + account_id: helper_id_constructor(IdEnum::HoldingId), + }, + AccountsEnum::DefinitionAccountPostBurn => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name: [2; 6], + total_supply: helper_balance_constructor(BalanceEnum::InitSupplyBurned), + }), + nonce: 0, + }, + is_authorized: true, + account_id: helper_id_constructor(IdEnum::PoolDefinitionId), + }, + AccountsEnum::HoldingAccountPostBurn => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::HoldingBalanceBurned), + }), + nonce: 0, + }, + is_authorized: false, + account_id: helper_id_constructor(IdEnum::HoldingId), + }, + AccountsEnum::Uninit => AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: helper_id_constructor(IdEnum::HoldingId), + }, + AccountsEnum::InitMint => AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::MintSuccess), + }), + nonce: 0, + }, + is_authorized: false, + account_id: helper_id_constructor(IdEnum::HoldingId), + }, + AccountsEnum::HoldingSameDefMint => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::HoldingBalanceMint), + }), + nonce: 0, + }, + is_authorized: true, + account_id: helper_id_constructor(IdEnum::PoolDefinitionId), + }, + AccountsEnum::DefinitionAccountMint => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name: [2; 6], + total_supply: helper_balance_constructor(BalanceEnum::InitSupplyMint), + }), + nonce: 0, + }, + is_authorized: true, + account_id: helper_id_constructor(IdEnum::PoolDefinitionId), + }, + AccountsEnum::HoldingSameDefAuthLargeBalance => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::MintOverflow), + }), + nonce: 0, + }, + is_authorized: true, + account_id: helper_id_constructor(IdEnum::PoolDefinitionId), + }, + _ => panic!("Invalid selection"), + } + } + + fn helper_balance_constructor(selection: BalanceEnum) -> u128 { + match selection { + BalanceEnum::InitSupply => 100_000, + BalanceEnum::HoldingBalance => 1_000, + BalanceEnum::InitSupplyBurned => 99_500, + BalanceEnum::HoldingBalanceBurned => 500, + BalanceEnum::BurnSuccess => 500, + BalanceEnum::BurnInsufficient => 1_500, + BalanceEnum::MintSuccess => 50_000, + BalanceEnum::InitSupplyMint => 150_000, + BalanceEnum::HoldingBalanceMint => 51_000, + BalanceEnum::MintOverflow => (2 as u128).pow(128) - 40_000, + _ => panic!("Invalid selection"), + } + } + + fn helper_id_constructor(selection: IdEnum) -> AccountId { + match selection { + IdEnum::PoolDefinitionId => AccountId::new([15; 32]), + IdEnum::PoolDefinitionIdDiff => AccountId::new([16; 32]), + IdEnum::HoldingId => AccountId::new([17; 32]), + } + } + + #[test] + #[should_panic(expected = "Invalid number of accounts")] + fn test_burn_invalid_number_of_accounts() { + let pre_states = vec![helper_account_constructor( + AccountsEnum::DefinitionAccountAuth, + )]; + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnSuccess), + ); + } + + #[test] + #[should_panic(expected = "Mismatch token definition and token holding")] + fn test_burn_mismatch_def() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingDiffDef), + ]; + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnSuccess), + ); + } + + #[test] + #[should_panic(expected = "Authorization is missing")] + fn test_burn_missing_authorization() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + ]; + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnSuccess), + ); + } + + #[test] + #[should_panic(expected = "Insufficient balance to burn")] + fn test_burn_insufficient_balance() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefAuth), + ]; + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnInsufficient), + ); + } + + #[test] + #[should_panic(expected = "Total supply underflow")] + fn test_burn_total_supply_underflow() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefAuthLargeBalance), + ]; + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::MintOverflow), + ); + } + + #[test] + fn test_burn_success() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefAuth), + ]; + let post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnSuccess), + ); + + let def_post = post_states[0].clone(); + let holding_post = post_states[1].clone(); + + assert!( + *def_post.account() + == helper_account_constructor(AccountsEnum::DefinitionAccountPostBurn).account + ); + assert!( + *holding_post.account() + == helper_account_constructor(AccountsEnum::HoldingAccountPostBurn).account + ); + } + + #[test] + #[should_panic(expected = "Invalid number of accounts")] + fn test_mint_invalid_number_of_accounts() { + let pre_states = vec![helper_account_constructor( + AccountsEnum::DefinitionAccountAuth, + )]; + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); + } + + #[test] + #[should_panic(expected = "Holding account must be valid")] + fn test_mint_not_valid_holding_account() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::DefinitionAccountNotAuth), + ]; + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); + } + + #[test] + #[should_panic(expected = "Definition authorization is missing")] + fn test_mint_missing_authorization() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountNotAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + ]; + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); + } + + #[test] + #[should_panic(expected = "Mismatch token definition and token holding")] + fn test_mint_mismatched_token_definition() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingDiffDef), + ]; + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); + } + + #[test] + fn test_mint_success() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + ]; + let post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); + + let def_post = post_states[0].clone(); + let holding_post = post_states[1].clone(); + + assert!( + *def_post.account() + == helper_account_constructor(AccountsEnum::DefinitionAccountMint).account + ); + assert!( + *holding_post.account() + == helper_account_constructor(AccountsEnum::HoldingSameDefMint).account + ); + } + + #[test] + fn test_mint_uninit_holding_success() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::Uninit), + ]; + let post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); + + let def_post = post_states[0].clone(); + let holding_post = post_states[1].clone(); + + assert!( + *def_post.account() + == helper_account_constructor(AccountsEnum::DefinitionAccountMint).account + ); + assert!( + *holding_post.account() == helper_account_constructor(AccountsEnum::InitMint).account + ); + assert!(holding_post.requires_claim() == true); + } + + #[test] + #[should_panic(expected = "Total supply overflow")] + fn test_mint_total_supply_overflow() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + ]; + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintOverflow), + ); + } + + #[test] + #[should_panic(expected = "New balance overflow")] + fn test_mint_holding_account_overflow() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuthOverflow), + ]; + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintOverflow), + ); + } } diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index d6fc2c9..c698b1b 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -44,13 +44,15 @@ impl From for ProgramWithDependencies { /// Generates a proof of the execution of a NSSA program inside the privacy preserving execution /// circuit +#[expect(clippy::too_many_arguments, reason = "TODO: fix later")] pub fn execute_and_prove( pre_states: &[AccountWithMetadata], instruction_data: &InstructionData, visibility_mask: &[u8], private_account_nonces: &[u128], private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], - private_account_auth: &[(NullifierSecretKey, MembershipProof)], + private_account_nsks: &[NullifierSecretKey], + private_account_membership_proofs: &[Option], program_with_dependencies: &ProgramWithDependencies, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { let mut program = &program_with_dependencies.program; @@ -105,7 +107,8 @@ pub fn execute_and_prove( visibility_mask: visibility_mask.to_vec(), private_account_nonces: private_account_nonces.to_vec(), private_account_keys: private_account_keys.to_vec(), - private_account_auth: private_account_auth.to_vec(), + private_account_nsks: private_account_nsks.to_vec(), + private_account_membership_proofs: private_account_membership_proofs.to_vec(), program_id: program_with_dependencies.program.id(), }; @@ -195,7 +198,7 @@ mod tests { let expected_sender_post = Account { program_owner: program.id(), balance: 100 - balance_to_move, - nonce: 1, + nonce: 0, data: Data::default(), }; @@ -218,6 +221,7 @@ mod tests { &[0xdeadbeef], &[(recipient_keys.npk(), shared_secret.clone())], &[], + &[None], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -315,10 +319,8 @@ mod tests { (sender_keys.npk(), shared_secret_1.clone()), (recipient_keys.npk(), shared_secret_2.clone()), ], - &[( - sender_keys.nsk, - commitment_set.get_proof_for(&commitment_sender).unwrap(), - )], + &[sender_keys.nsk], + &[commitment_set.get_proof_for(&commitment_sender), None], &program.into(), ) .unwrap(); diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 1865248..335bcb5 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -222,6 +222,15 @@ mod tests { } } + pub fn noop() -> Self { + use test_program_methods::{NOOP_ELF, NOOP_ID}; + + Program { + id: NOOP_ID, + elf: NOOP_ELF.to_vec(), + } + } + pub fn modified_transfer_program() -> Self { use test_program_methods::MODIFIED_TRANSFER_ELF; // This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of diff --git a/nssa/src/state.rs b/nssa/src/state.rs index c3bbc3d..d5c138d 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -154,6 +154,12 @@ impl V02State { *current_account = post; } + // 5. Increment nonces for public signers + for account_id in tx.signer_account_ids() { + let current_account = self.get_account_by_id_mut(account_id); + current_account.nonce += 1; + } + Ok(()) } @@ -862,6 +868,7 @@ pub mod tests { &[0xdeadbeef], &[(recipient_keys.npk(), shared_secret)], &[], + &[None], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -910,10 +917,8 @@ pub mod tests { (sender_keys.npk(), shared_secret_1), (recipient_keys.npk(), shared_secret_2), ], - &[( - sender_keys.nsk, - state.get_proof_for_commitment(&sender_commitment).unwrap(), - )], + &[sender_keys.nsk], + &[state.get_proof_for_commitment(&sender_commitment), None], &program.into(), ) .unwrap(); @@ -962,10 +967,8 @@ pub mod tests { &[1, 0], &[new_nonce], &[(sender_keys.npk(), shared_secret)], - &[( - sender_keys.nsk, - state.get_proof_for_commitment(&sender_commitment).unwrap(), - )], + &[sender_keys.nsk], + &[state.get_proof_for_commitment(&sender_commitment)], &program.into(), ) .unwrap(); @@ -1179,6 +1182,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1205,6 +1209,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1231,6 +1236,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1257,6 +1263,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1285,6 +1292,7 @@ pub mod tests { &[], &[], &[], + &[], &program.to_owned().into(), ); @@ -1311,6 +1319,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1346,6 +1355,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1372,6 +1382,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1407,6 +1418,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1444,6 +1456,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1484,7 +1497,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[(sender_keys.nsk, (0, vec![]))], + &[sender_keys.nsk], + &[Some((0, vec![]))], &program.into(), ); @@ -1518,7 +1532,50 @@ pub mod tests { &[1, 2], &[0xdeadbeef1, 0xdeadbeef2], &private_account_keys, - &[(sender_keys.nsk, (0, vec![]))], + &[sender_keys.nsk], + &[Some((0, vec![]))], + &program.into(), + ); + + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } + + #[test] + fn test_circuit_fails_if_insufficient_commitment_proofs_are_provided() { + let program = Program::simple_balance_transfer(); + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + let private_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + &sender_keys.npk(), + ); + let private_account_2 = + AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + + // Setting no second commitment proof. + let private_account_membership_proofs = [Some((0, vec![]))]; + let result = execute_and_prove( + &[private_account_1, private_account_2], + &Program::serialize_instruction(10u128).unwrap(), + &[1, 2], + &[0xdeadbeef1, 0xdeadbeef2], + &[ + ( + sender_keys.npk(), + SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), + ), + ( + recipient_keys.npk(), + SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), + ), + ], + &[sender_keys.nsk], + &private_account_membership_proofs, &program.into(), ); @@ -1543,7 +1600,7 @@ pub mod tests { AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); // Setting no auth key for an execution with one non default private accounts. - let private_account_auth = []; + let private_account_nsks = []; let result = execute_and_prove( &[private_account_1, private_account_2], &Program::serialize_instruction(10u128).unwrap(), @@ -1559,7 +1616,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &private_account_auth, + &private_account_nsks, + &[], &program.into(), ); @@ -1595,19 +1653,20 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ]; - let private_account_auth = [ - // Setting the recipient key to authorize the sender. - // This should be set to the sender private account in - // a normal circumstance. The recipient can't authorize this. - (recipient_keys.nsk, (0, vec![])), - ]; + + // Setting the recipient key to authorize the sender. + // This should be set to the sender private account in + // a normal circumstance. The recipient can't authorize this. + let private_account_nsks = [recipient_keys.nsk]; + let private_account_membership_proofs = [Some((0, vec![]))]; let result = execute_and_prove( &[private_account_1, private_account_2], &Program::serialize_instruction(10u128).unwrap(), &[1, 2], &[0xdeadbeef1, 0xdeadbeef2], &private_account_keys, - &private_account_auth, + &private_account_nsks, + &private_account_membership_proofs, &program.into(), ); @@ -1653,7 +1712,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[(sender_keys.nsk, (0, vec![]))], + &[sender_keys.nsk], + &[Some((0, vec![]))], &program.into(), ); @@ -1700,7 +1760,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[(sender_keys.nsk, (0, vec![]))], + &[sender_keys.nsk], + &[Some((0, vec![]))], &program.into(), ); @@ -1746,7 +1807,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[(sender_keys.nsk, (0, vec![]))], + &[sender_keys.nsk], + &[Some((0, vec![]))], &program.into(), ); @@ -1792,7 +1854,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[(sender_keys.nsk, (0, vec![]))], + &[sender_keys.nsk], + &[Some((0, vec![]))], &program.into(), ); @@ -1836,7 +1899,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[(sender_keys.nsk, (0, vec![]))], + &[sender_keys.nsk], + &[Some((0, vec![]))], &program.into(), ); @@ -1866,6 +1930,7 @@ pub mod tests { &[], &[], &[], + &[], &program.into(), ); @@ -1907,7 +1972,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[(sender_keys.nsk, (0, vec![]))], + &[sender_keys.nsk], + &[Some((0, vec![]))], &program.into(), ); @@ -1953,7 +2019,8 @@ pub mod tests { &[1, 2], &[0xdeadbeef1, 0xdeadbeef2], &private_account_keys, - &[(sender_keys.nsk, (0, vec![]))], + &[sender_keys.nsk], + &[Some((0, vec![]))], &program.into(), ); @@ -1980,10 +2047,8 @@ pub mod tests { // Setting two private account keys for a circuit execution with only one non default // private account (visibility mask equal to 1 means that auth keys are expected). let visibility_mask = [1, 2]; - let private_account_auth = [ - (sender_keys.nsk, (0, vec![])), - (recipient_keys.nsk, (1, vec![])), - ]; + let private_account_nsks = [sender_keys.nsk, recipient_keys.nsk]; + let private_account_membership_proofs = [Some((0, vec![])), Some((1, vec![]))]; let result = execute_and_prove( &[private_account_1, private_account_2], &Program::serialize_instruction(10u128).unwrap(), @@ -1999,7 +2064,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &private_account_auth, + &private_account_nsks, + &private_account_membership_proofs, &program.into(), ); @@ -2076,10 +2142,8 @@ pub mod tests { ); let visibility_mask = [1, 1]; - let private_account_auth = [ - (sender_keys.nsk, (1, vec![])), - (sender_keys.nsk, (1, vec![])), - ]; + let private_account_nsks = [sender_keys.nsk, sender_keys.nsk]; + let private_account_membership_proofs = [Some((1, vec![])), Some((1, vec![]))]; let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.ivk()); let result = execute_and_prove( &[private_account_1.clone(), private_account_1], @@ -2090,7 +2154,8 @@ pub mod tests { (sender_keys.npk(), shared_secret.clone()), (sender_keys.npk(), shared_secret), ], - &private_account_auth, + &private_account_nsks, + &private_account_membership_proofs, &program.into(), ); @@ -2391,15 +2456,10 @@ pub mod tests { &[1, 1], &[from_new_nonce, to_new_nonce], &[(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)], + &[from_keys.nsk, to_keys.nsk], &[ - ( - from_keys.nsk, - state.get_proof_for_commitment(&from_commitment).unwrap(), - ), - ( - to_keys.nsk, - state.get_proof_for_commitment(&to_commitment).unwrap(), - ), + state.get_proof_for_commitment(&from_commitment), + state.get_proof_for_commitment(&to_commitment), ], &program_with_deps, ) @@ -2606,4 +2666,143 @@ pub mod tests { assert!(expected_sender_post == sender_post); assert!(expected_recipient_post == recipient_post); } + + #[test] + fn test_private_authorized_uninitialized_account() { + let mut state = V02State::new_with_genesis_accounts(&[], &[]); + + // Set up keys for the authorized private account + let private_keys = test_private_account_keys_1(); + + // Create an authorized private account with default values (new account being initialized) + let authorized_account = + AccountWithMetadata::new(Account::default(), true, &private_keys.npk()); + + let program = Program::authenticated_transfer_program(); + + // Set up parameters for the new account + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &private_keys.ivk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + // Balance to initialize the account with (0 for a new account) + let balance: u128 = 0; + + let nonce = 0xdeadbeef1; + + // Execute and prove the circuit with the authorized account but no commitment proof + let (output, proof) = execute_and_prove( + std::slice::from_ref(&authorized_account), + &Program::serialize_instruction(balance).unwrap(), + &[1], + &[nonce], + &[(private_keys.npk(), shared_secret)], + &[private_keys.nsk], + &[None], + &program.into(), + ) + .unwrap(); + + // Create message from circuit output + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![(private_keys.npk(), private_keys.ivk(), epk)], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + + let tx = PrivacyPreservingTransaction::new(message, witness_set); + let result = state.transition_from_privacy_preserving_transaction(&tx); + assert!(result.is_ok()); + + let nullifier = Nullifier::for_account_initialization(&private_keys.npk()); + assert!(state.private_state.1.contains(&nullifier)); + } + + #[test] + fn test_private_account_claimed_then_used_without_init_flag_should_fail() { + let mut state = V02State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + + // Set up keys for the private account + let private_keys = test_private_account_keys_1(); + + // Step 1: Create a new private account with authorization + let authorized_account = + AccountWithMetadata::new(Account::default(), true, &private_keys.npk()); + + let claimer_program = Program::claimer(); + + // Set up parameters for claiming the new account + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &private_keys.ivk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let balance: u128 = 0; + let nonce = 0xdeadbeef1; + + // Step 2: Execute claimer program to claim the account with authentication + let (output, proof) = execute_and_prove( + std::slice::from_ref(&authorized_account), + &Program::serialize_instruction(balance).unwrap(), + &[1], + &[nonce], + &[(private_keys.npk(), shared_secret)], + &[private_keys.nsk], + &[None], + &claimer_program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![(private_keys.npk(), private_keys.ivk(), epk)], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + // Claim should succeed + assert!( + state + .transition_from_privacy_preserving_transaction(&tx) + .is_ok() + ); + + // Verify the account is now initialized (nullifier exists) + let nullifier = Nullifier::for_account_initialization(&private_keys.npk()); + assert!(state.private_state.1.contains(&nullifier)); + + // Prepare new state of account + let account_metadata = { + let mut acc = authorized_account.clone(); + acc.account.program_owner = Program::claimer().id(); + acc + }; + + let noop_program = Program::noop(); + let esk2 = [4; 32]; + let shared_secret2 = SharedSecretKey::new(&esk2, &private_keys.ivk()); + + let nonce2 = 0xdeadbeef2; + + // Step 3: Try to execute noop program with authentication but without initialization + let res = execute_and_prove( + std::slice::from_ref(&account_metadata), + &Program::serialize_instruction(()).unwrap(), + &[1], + &[nonce2], + &[(private_keys.npk(), shared_secret2)], + &[private_keys.nsk], + &[None], + &noop_program.into(), + ); + + assert!(matches!(res, Err(NssaError::CircuitProvingError(_)))); + } } diff --git a/nssa/test_program_methods/guest/src/bin/noop.rs b/nssa/test_program_methods/guest/src/bin/noop.rs new file mode 100644 index 0000000..fb02389 --- /dev/null +++ b/nssa/test_program_methods/guest/src/bin/noop.rs @@ -0,0 +1,12 @@ +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput, AccountPostState}; + +type Instruction = (); + +fn main() { + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); + + let post_states = pre_states.iter().map(|account| { + AccountPostState::new(account.account.clone()) + }).collect(); + write_nssa_outputs(instruction_words, pre_states, post_states); +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index ee2b6dc..7cab837 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -285,7 +285,8 @@ impl WalletCore { .map(|keys| (keys.npk.clone(), keys.ssk.clone())) .collect::>(), &acc_manager.private_account_auth(), - program, + &acc_manager.private_account_membership_proofs(), + &program.to_owned(), ) .unwrap(); @@ -305,7 +306,7 @@ impl WalletCore { nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( &message, proof, - &acc_manager.witness_signing_keys(), + &acc_manager.public_account_auth(), ); let tx = PrivacyPreservingTransaction::new(message, witness_set); diff --git a/wallet/src/pinata_interactions.rs b/wallet/src/pinata_interactions.rs index 65a67b7..5804768 100644 --- a/wallet/src/pinata_interactions.rs +++ b/wallet/src/pinata_interactions.rs @@ -58,7 +58,8 @@ impl WalletCore { &[0, 1], &produce_random_nonces(1), &[(winner_npk.clone(), shared_secret_winner.clone())], - &[(winner_nsk.unwrap(), winner_proof)], + &[(winner_nsk.unwrap())], + &[winner_proof], &program.into(), ) .unwrap(); @@ -125,6 +126,7 @@ impl WalletCore { &produce_random_nonces(1), &[(winner_npk.clone(), shared_secret_winner.clone())], &[], + &[] &program.into(), ) .unwrap(); diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index e79bbac..3bd0c4c 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -133,11 +133,21 @@ impl AccountManager { .collect() } - pub fn private_account_auth(&self) -> Vec<(NullifierSecretKey, MembershipProof)> { + pub fn private_account_auth(&self) -> Vec { self.states .iter() .filter_map(|state| match state { - State::Private(pre) => Some((pre.nsk?, pre.proof.clone()?)), + State::Private(pre) => pre.nsk, + _ => None, + }) + .collect() + } + + pub fn private_account_membership_proofs(&self) -> Vec> { + self.states + .iter() + .filter_map(|state| match state { + State::Private(pre) => Some(pre.proof.clone()), _ => None, }) .collect() @@ -153,7 +163,7 @@ impl AccountManager { .collect() } - pub fn witness_signing_keys(&self) -> Vec<&PrivateKey> { + pub fn public_account_auth(&self) -> Vec<&PrivateKey> { self.states .iter() .filter_map(|state| match state { @@ -185,7 +195,7 @@ async fn private_acc_preparation( return Err(ExecutionFailureKind::KeyNotFoundError); }; - let mut nsk = Some(from_keys.private_key_holder.nullifier_secret_key); + let nsk = from_keys.private_key_holder.nullifier_secret_key; let from_npk = from_keys.nullifer_public_key; let from_ipk = from_keys.incoming_viewing_public_key; @@ -196,14 +206,12 @@ async fn private_acc_preparation( .await .unwrap(); - if proof.is_none() { - nsk = None; - } - - let sender_pre = AccountWithMetadata::new(from_acc.clone(), proof.is_some(), &from_npk); + // TODO: Technically we could allow unauthorized owned accounts, but currently we don't have + // support from that in the wallet. + let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, &from_npk); Ok(AccountPreparedData { - nsk, + nsk: Some(nsk), npk: from_npk, ipk: from_ipk, pre_state: sender_pre,