diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh old mode 100644 new mode 100755 index ebc50aa..5c19b36 --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -5,10 +5,15 @@ curl -L https://risczero.com/install | bash source env.sh RISC0_DEV_MODE=1 cargo test --release + cd integration_tests export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ export RUST_LOG=info +cargo run $(pwd)/configs/debug all echo "Try test valid proof at least once" cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account echo "Continuing in dev mode" RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all +cd .. + +cd nssa/program_methods/guest && cargo test --release diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index a5f975b..6d64f98 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -8,6 +8,7 @@ anyhow.workspace = true env_logger.workspace = true log.workspace = true actix.workspace = true +bytemuck = "1.23.2" actix-web.workspace = true tokio.workspace = true diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 071b5ae..da4f7c5 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -13,6 +13,7 @@ use tempfile::TempDir; use tokio::task::JoinHandle; use wallet::{ Command, SubcommandReturnValue, WalletCore, + config::PersistentAccountData, helperfunctions::{fetch_config, fetch_persistent_accounts, produce_account_addr_from_hex}, }; @@ -303,6 +304,152 @@ pub async fn test_get_account() { assert_eq!(account.nonce, 0); } +/// This test creates a new token using the token program. After creating the token, the test executes a +/// token transfer to a new account. +pub async fn test_success_token_program() { + let wallet_config = fetch_config().unwrap(); + + // Create new account for the token definition + wallet::execute_subcommand(Command::RegisterAccountPublic {}) + .await + .unwrap(); + // Create new account for the token supply holder + wallet::execute_subcommand(Command::RegisterAccountPublic {}) + .await + .unwrap(); + // Create new account for receiving a token transaction + wallet::execute_subcommand(Command::RegisterAccountPublic {}) + .await + .unwrap(); + + let persistent_accounts = fetch_persistent_accounts().unwrap(); + + let mut new_persistent_accounts_addr = Vec::new(); + + for per_acc in persistent_accounts { + match per_acc { + PersistentAccountData::Public(per_acc) => { + if (per_acc.address.to_string() != ACC_RECEIVER) + && (per_acc.address.to_string() != ACC_SENDER) + { + new_persistent_accounts_addr.push(per_acc.address); + } + } + _ => continue, + } + } + + let [definition_addr, supply_addr, recipient_addr] = new_persistent_accounts_addr + .try_into() + .expect("Failed to produce new account, not present in persistent accounts"); + + // Create new token + let command = Command::CreateNewToken { + definition_addr: definition_addr.to_string(), + supply_addr: supply_addr.to_string(), + name: "A NAME".to_string(), + total_supply: 37, + }; + wallet::execute_subcommand(command).await.unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); + + // Check the status of the token definition account is the expected after the execution + let definition_acc = seq_client + .get_account(definition_addr.to_string()) + .await + .unwrap() + .account; + + assert_eq!(definition_acc.program_owner, Program::token().id()); + // The data of a token definition account has the following layout: + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ] + assert_eq!( + definition_acc.data, + vec![ + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + // Check the status of the token holding account with the total supply is the expected after the execution + let supply_acc = seq_client + .get_account(supply_addr.to_string()) + .await + .unwrap() + .account; + + // The account must be owned by the token program + assert_eq!(supply_acc.program_owner, Program::token().id()); + // The data of a token definition account has the following layout: + // [ 0x01 || corresponding_token_definition_id (32 bytes) || balance (little endian 16 bytes) ] + // First byte of the data equal to 1 means it's a token holding account + assert_eq!(supply_acc.data[0], 1); + // Bytes from 1 to 33 represent the id of the token this account is associated with. + // In this example, this is a token account of the newly created token, so it is expected + // to be equal to the address of the token definition account. + assert_eq!( + &supply_acc.data[1..33], + nssa::AccountId::from(&definition_addr).to_bytes() + ); + assert_eq!( + u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()), + 37 + ); + + // Transfer 7 tokens from `supply_acc` to the account at address `recipient_addr` + let command = Command::TransferToken { + sender_addr: supply_addr.to_string(), + recipient_addr: recipient_addr.to_string(), + balance_to_move: 7, + }; + wallet::execute_subcommand(command).await.unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Check the status of the account at `supply_addr` is the expected after the execution + let supply_acc = seq_client + .get_account(supply_addr.to_string()) + .await + .unwrap() + .account; + // The account must be owned by the token program + assert_eq!(supply_acc.program_owner, Program::token().id()); + // First byte equal to 1 means it's a token holding account + assert_eq!(supply_acc.data[0], 1); + // Bytes from 1 to 33 represent the id of the token this account is associated with. + assert_eq!( + &supply_acc.data[1..33], + nssa::AccountId::from(&definition_addr).to_bytes() + ); + assert_eq!( + u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()), + 30 + ); + + // Check the status of the account at `recipient_addr` is the expected after the execution + let recipient_acc = seq_client + .get_account(recipient_addr.to_string()) + .await + .unwrap() + .account; + + // The account must be owned by the token program + assert_eq!(recipient_acc.program_owner, Program::token().id()); + // First byte equal to 1 means it's a token holding account + assert_eq!(recipient_acc.data[0], 1); + // Bytes from 1 to 33 represent the id of the token this account is associated with. + assert_eq!( + &recipient_acc.data[1..33], + nssa::AccountId::from(&definition_addr).to_bytes() + ); + assert_eq!( + u128::from_le_bytes(recipient_acc.data[33..].try_into().unwrap()), + 7 + ); +} + pub async fn test_success_private_transfer_to_another_owned_account() { info!("test_success_private_transfer_to_another_owned_account"); let command = Command::SendNativeTokenTransferPrivate { @@ -846,6 +993,9 @@ pub async fn main_tests_runner() -> Result<()> { } = args; match test_name.as_str() { + "test_success_token_program" => { + test_cleanup_wrap!(home_dir, test_success_token_program); + } "test_success_move_to_another_account" => { test_cleanup_wrap!(home_dir, test_success_move_to_another_account); } @@ -855,7 +1005,7 @@ pub async fn main_tests_runner() -> Result<()> { "test_failure" => { test_cleanup_wrap!(home_dir, test_failure); } - "test_get_account_wallet_command" => { + "test_get_account" => { test_cleanup_wrap!(home_dir, test_get_account); } "test_success_two_transactions" => { @@ -911,6 +1061,7 @@ pub async fn main_tests_runner() -> Result<()> { test_cleanup_wrap!(home_dir, test_success); test_cleanup_wrap!(home_dir, test_failure); test_cleanup_wrap!(home_dir, test_success_two_transactions); + test_cleanup_wrap!(home_dir, test_success_token_program); test_cleanup_wrap!( home_dir, test_success_private_transfer_to_another_owned_account diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index e7f8558..ddae797 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -2,7 +2,7 @@ use crate::program::ProgramId; use serde::{Deserialize, Serialize}; pub type Nonce = u128; -type Data = Vec; +pub type Data = Vec; /// Account to be used both in public and private contexts #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)] @@ -18,7 +18,7 @@ pub struct Account { /// is public, or a `NullifierPublicKey` in case the account is private. #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] #[cfg_attr(any(feature = "host", test), derive(Debug))] -pub struct AccountId([u8; 32]); +pub struct AccountId(pub(super) [u8; 32]); impl AccountId { pub fn new(value: [u8; 32]) -> Self { Self(value) diff --git a/nssa/core/src/encoding.rs b/nssa/core/src/encoding.rs index dd586de..d7fc8b8 100644 --- a/nssa/core/src/encoding.rs +++ b/nssa/core/src/encoding.rs @@ -7,6 +7,7 @@ use std::io::Read; use crate::account::Account; +use crate::account::AccountId; #[cfg(feature = "host")] use crate::encryption::shared_key_derivation::Secp256k1Point; @@ -137,6 +138,12 @@ impl Secp256k1Point { } } +impl AccountId { + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index a4e6722..82023f3 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -59,20 +59,23 @@ pub fn validate_execution( return false; } - // 3. Ownership change only allowed from default accounts - if pre.account.program_owner != post.program_owner && pre.account != Account::default() { + // 3. Program ownership changes are not allowed + if pre.account.program_owner != post.program_owner { return false; } + let account_program_owner = pre.account.program_owner; + // 4. Decreasing balance only allowed if owned by executing program - if post.balance < pre.account.balance && pre.account.program_owner != executing_program_id { + if post.balance < pre.account.balance && account_program_owner != executing_program_id { return false; } - // 5. Data changes only allowed if owned by executing program + // 5. Data changes only allowed if owned by executing program or if account pre state has + // default values if pre.account.data != post.data - && (executing_program_id != pre.account.program_owner - || executing_program_id != post.program_owner) + && pre.account != Account::default() + && account_program_owner != executing_program_id { return false; } diff --git a/nssa/program_methods/guest/Cargo.toml b/nssa/program_methods/guest/Cargo.toml index f069635..9e5f543 100644 --- a/nssa/program_methods/guest/Cargo.toml +++ b/nssa/program_methods/guest/Cargo.toml @@ -8,3 +8,4 @@ edition = "2024" [dependencies] risc0-zkvm = { version = "3.0.3", features = ['std'] } nssa-core = { path = "../../core" } +serde = { version = "1.0.219", default-features = false } diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index 89b84fd..210e3f0 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; /// A transfer of balance program. /// To be used both in public and private contexts. 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 158a660..235b5c8 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,7 +1,12 @@ use risc0_zkvm::{guest::env, serde::to_vec}; use nssa_core::{ - account::{Account, AccountWithMetadata, AccountId}, compute_digest_for_path, encryption::Ciphertext, program::{validate_execution, ProgramOutput, DEFAULT_PROGRAM_ID}, Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput + Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, + PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + account::{Account, AccountId, AccountWithMetadata}, + compute_digest_for_path, + encryption::Ciphertext, + program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution}, }; fn main() { diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs new file mode 100644 index 0000000..e5680be --- /dev/null +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -0,0 +1,554 @@ +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data}, + program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, +}; + +// The token program has two 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: +// * 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]. + +const TOKEN_DEFINITION_TYPE: u8 = 0; +const TOKEN_DEFINITION_DATA_SIZE: usize = 23; + +const TOKEN_HOLDING_TYPE: u8 = 1; +const TOKEN_HOLDING_DATA_SIZE: usize = 49; + +struct TokenDefinition { + account_type: u8, + name: [u8; 6], + total_supply: u128, +} + +struct TokenHolding { + account_type: u8, + definition_id: AccountId, + balance: u128, +} + +impl TokenDefinition { + fn into_data(self) -> Vec { + let mut bytes = [0; TOKEN_DEFINITION_DATA_SIZE]; + bytes[0] = self.account_type; + bytes[1..7].copy_from_slice(&self.name); + bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes()); + bytes.into() + } +} + +impl TokenHolding { + fn new(definition_id: &AccountId) -> Self { + Self { + account_type: TOKEN_HOLDING_TYPE, + definition_id: definition_id.clone(), + balance: 0, + } + } + + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { + None + } else { + let account_type = data[0]; + let definition_id = AccountId::new(data[1..33].try_into().unwrap()); + let balance = u128::from_le_bytes(data[33..].try_into().unwrap()); + Some(Self { + definition_id, + balance, + account_type, + }) + } + } + + fn into_data(self) -> Data { + let mut bytes = [0; TOKEN_HOLDING_DATA_SIZE]; + bytes[0] = self.account_type; + bytes[1..33].copy_from_slice(&self.definition_id.to_bytes()); + bytes[33..].copy_from_slice(&self.balance.to_le_bytes()); + bytes.into() + } +} + +fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of input accounts"); + } + let sender = &pre_states[0]; + let recipient = &pre_states[1]; + + let mut sender_holding = + TokenHolding::parse(&sender.account.data).expect("Invalid sender data"); + let mut recipient_holding = if recipient.account == Account::default() { + TokenHolding::new(&sender_holding.definition_id) + } else { + TokenHolding::parse(&recipient.account.data).expect("Invalid recipient data") + }; + + if sender_holding.definition_id != recipient_holding.definition_id { + panic!("Sender and recipient definition id mismatch"); + } + + if sender_holding.balance < balance_to_move { + panic!("Insufficient balance"); + } + + if !sender.is_authorized { + panic!("Sender authorization is missing"); + } + + sender_holding.balance -= balance_to_move; + recipient_holding.balance = recipient_holding + .balance + .checked_add(balance_to_move) + .expect("Recipient balance overflow."); + + let sender_post = { + let mut this = sender.account.clone(); + this.data = sender_holding.into_data(); + this + }; + let recipient_post = { + let mut this = recipient.account.clone(); + this.data = recipient_holding.into_data(); + this + }; + + vec![sender_post, recipient_post] +} + +fn new_definition( + pre_states: &[AccountWithMetadata], + name: [u8; 6], + total_supply: u128, +) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of input accounts"); + } + let definition_target_account = &pre_states[0]; + let holding_target_account = &pre_states[1]; + + if definition_target_account.account != Account::default() { + panic!("Definition target account must have default values"); + } + + if holding_target_account.account != Account::default() { + panic!("Holding target account must have default values"); + } + + let token_definition = TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name, + total_supply, + }; + + let token_holding = TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: definition_target_account.account_id.clone(), + balance: total_supply, + }; + + let mut definition_target_account_post = definition_target_account.account.clone(); + definition_target_account_post.data = token_definition.into_data(); + + let mut holding_target_account_post = holding_target_account.account.clone(); + holding_target_account_post.data = token_holding.into_data(); + + vec![definition_target_account_post, holding_target_account_post] +} + +type Instruction = [u8; 23]; + +fn main() { + let ProgramInput { + pre_states, + instruction, + } = read_nssa_inputs::(); + + match instruction[0] { + 0 => { + // Parse instruction + let total_supply = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); + let name: [u8; 6] = instruction[17..].try_into().unwrap(); + assert_ne!(name, [0; 6]); + + // Execute + let post_states = new_definition(&pre_states, name, total_supply); + write_nssa_outputs(pre_states, post_states); + } + 1 => { + // Parse instruction + let balance_to_move = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); + let name: [u8; 6] = instruction[17..].try_into().unwrap(); + assert_eq!(name, [0; 6]); + + // Execute + let post_states = transfer(&pre_states, balance_to_move); + write_nssa_outputs(pre_states, post_states); + } + _ => panic!("Invalid instruction"), + }; +} + +#[cfg(test)] +mod tests { + use nssa_core::account::{Account, AccountId, AccountWithMetadata}; + + use crate::{new_definition, transfer, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE}; + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_1() { + let pre_states = vec![AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }]; + let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + } + + #[should_panic(expected = "Definition target account must have default values")] + #[test] + fn test_new_definition_non_default_first_account_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + } + + #[should_panic(expected = "Holding target account must have default values")] + #[test] + fn test_new_definition_non_default_second_account_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + } + + #[test] + fn test_new_definition_with_valid_inputs_succeeds() { + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: AccountId::new([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]), + }, + AccountWithMetadata { + account: Account { + ..Account::default() + }, + is_authorized: false, + account_id: AccountId::new([2; 32]), + }, + ]; + + let post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + let [definition_account, holding_account] = post_states.try_into().ok().unwrap(); + assert_eq!( + definition_account.data, + vec![ + 0, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 + ] + ); + assert_eq!( + holding_account.data, + vec![ + 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 + ] + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_transfer_with_invalid_number_of_accounts_1() { + let pre_states = vec![AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_transfer_with_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Invalid sender data")] + #[test] + fn test_transfer_invalid_instruction_type_should_fail() { + let invalid_type = TOKEN_HOLDING_TYPE ^ 1; + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // First byte should be `TOKEN_HOLDING_TYPE` for token holding accounts + data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Invalid sender data")] + #[test] + fn test_transfer_invalid_data_size_should_fail_1() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Invalid sender data")] + #[test] + fn test_transfer_invalid_data_size_should_fail_2() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` + data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Sender and recipient definition id mismatch")] + #[test] + fn test_transfer_with_different_definition_ids_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + data: vec![1; TOKEN_HOLDING_DATA_SIZE], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + data: vec![1] + .into_iter() + .chain(vec![2; TOKEN_HOLDING_DATA_SIZE - 1]) + .collect(), + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Insufficient balance")] + #[test] + fn test_transfer_with_insufficient_balance_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Account with balance 37 + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(37)) + .collect(), + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + data: vec![1; TOKEN_HOLDING_DATA_SIZE], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + // Attempt to transfer 38 tokens + let _post_states = transfer(&pre_states, 38); + } + + #[should_panic(expected = "Sender authorization is missing")] + #[test] + fn test_transfer_without_sender_authorization_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Account with balance 37 + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(37)) + .collect(), + ..Account::default() + }, + is_authorized: false, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + data: vec![1; TOKEN_HOLDING_DATA_SIZE], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 37); + } + + #[test] + fn test_transfer_with_valid_inputs_succeeds() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Account with balance 37 + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(37)) + .collect(), + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + // Account with balance 255 + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(255)) + .collect(), + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let post_states = transfer(&pre_states, 11); + let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); + assert_eq!( + sender_post.data, + vec![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + assert_eq!( + recipient_post.data, + vec![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + } +} diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index bdda8f6..31591ad 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -9,7 +9,7 @@ mod signature; mod state; pub use address::Address; -pub use nssa_core::account::Account; +pub use nssa_core::account::{Account, AccountId}; pub use privacy_preserving_transaction::{ PrivacyPreservingTransaction, circuit::execute_and_prove, }; diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 8b57303..5abc153 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -3,7 +3,8 @@ use nssa_core::{ program::{InstructionData, ProgramId, ProgramOutput}, }; use program_methods::{ - AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, + AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, + TOKEN_ID, }; use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor, serde::to_vec}; use serde::Serialize; @@ -75,6 +76,13 @@ impl Program { elf: AUTHENTICATED_TRANSFER_ELF, } } + + pub fn token() -> Self { + Self { + id: TOKEN_ID, + elf: TOKEN_ELF, + } + } } // TODO: Testnet only. Refactor to prevent compilation on mainnet. diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 3c7d2ec..65b440f 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -92,6 +92,7 @@ impl V01State { }; this.insert_program(Program::authenticated_transfer_program()); + this.insert_program(Program::token()); this } @@ -275,14 +276,14 @@ pub mod tests { let addr1 = Address::from(&PublicKey::new_from_private_key(&key1)); let addr2 = Address::from(&PublicKey::new_from_private_key(&key2)); let initial_data = [(addr1, 100u128), (addr2, 151u128)]; - let program = Program::authenticated_transfer_program(); + let authenticated_transfers_program = Program::authenticated_transfer_program(); let expected_public_state = { let mut this = HashMap::new(); this.insert( addr1, Account { balance: 100, - program_owner: program.id(), + program_owner: authenticated_transfers_program.id(), ..Account::default() }, ); @@ -290,7 +291,7 @@ pub mod tests { addr2, Account { balance: 151, - program_owner: program.id(), + program_owner: authenticated_transfers_program.id(), ..Account::default() }, ); @@ -298,7 +299,11 @@ pub mod tests { }; let expected_builtin_programs = { let mut this = HashMap::new(); - this.insert(program.id(), program); + this.insert( + authenticated_transfers_program.id(), + authenticated_transfers_program, + ); + this.insert(Program::token().id(), Program::token()); this }; @@ -683,13 +688,13 @@ pub mod tests { #[test] fn test_program_should_fail_if_modifies_data_of_non_owned_account() { let initial_data = []; - let mut state = - V01State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); - let address = Address::new([1; 32]); + let mut state = V01State::new_with_genesis_accounts(&initial_data, &[]) + .with_test_programs() + .with_non_default_accounts_but_default_program_owners(); + let address = Address::new([255; 32]); let program_id = Program::data_changer().id(); - // Consider the extreme case where the target account is the default account - assert_eq!(state.get_account_by_address(&address), Account::default()); + assert_ne!(state.get_account_by_address(&address), Account::default()); assert_ne!( state.get_account_by_address(&address).program_owner, program_id diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 26652d1..bbbbc79 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -104,6 +104,63 @@ impl WalletCore { Ok(self.sequencer_client.send_tx_public(tx).await?) } + pub async fn send_new_token_definition( + &self, + definition_address: Address, + supply_address: Address, + name: [u8; 6], + total_supply: u128, + ) -> Result { + let addresses = vec![definition_address, supply_address]; + let program_id = nssa::program::Program::token().id(); + // Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] + let mut instruction = [0; 23]; + instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); + instruction[17..].copy_from_slice(&name); + let message = + nssa::public_transaction::Message::try_new(program_id, addresses, vec![], instruction) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.sequencer_client.send_tx_public(tx).await?) + } + + pub async fn send_transfer_token_transaction( + &self, + sender_address: Address, + recipient_address: Address, + amount: u128, + ) -> Result { + let addresses = vec![sender_address, recipient_address]; + let program_id = nssa::program::Program::token().id(); + // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. + let mut instruction = [0; 23]; + instruction[0] = 0x01; + instruction[1..17].copy_from_slice(&amount.to_le_bytes()); + let Ok(nonces) = self.get_accounts_nonces(vec![sender_address]).await else { + return Err(ExecutionFailureKind::SequencerError); + }; + let message = + nssa::public_transaction::Message::try_new(program_id, addresses, nonces, instruction) + .unwrap(); + + let Some(signing_key) = self + .storage + .user_data + .get_pub_account_signing_key(&sender_address) + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.sequencer_client.send_tx_public(tx).await?) + } ///Get account balance pub async fn get_account_balance(&self, acc: Address) -> Result { Ok(self @@ -268,6 +325,26 @@ pub enum Command { #[arg(short, long)] addr: String, }, + //Create a new token using the token program + CreateNewToken { + #[arg(short, long)] + definition_addr: String, + #[arg(short, long)] + supply_addr: String, + #[arg(short, long)] + name: String, + #[arg(short, long)] + total_supply: u128, + }, + //Transfer tokens using the token program + TransferToken { + #[arg(short, long)] + sender_addr: String, + #[arg(short, long)] + recipient_addr: String, + #[arg(short, long)] + balance_to_move: u128, + }, // TODO: Testnet only. Refactor to prevent compilation on mainnet. // Claim piƱata prize ClaimPinata { @@ -678,6 +755,43 @@ pub async fn execute_subcommand(command: Command) -> Result { + let name = name.as_bytes(); + if name.len() > 6 { + // TODO: return error + panic!(); + } + let mut name_bytes = [0; 6]; + name_bytes[..name.len()].copy_from_slice(name); + wallet_core + .send_new_token_definition( + definition_addr.parse().unwrap(), + supply_addr.parse().unwrap(), + name_bytes, + total_supply, + ) + .await?; + SubcommandReturnValue::Empty + } + Command::TransferToken { + sender_addr, + recipient_addr, + balance_to_move, + } => { + wallet_core + .send_transfer_token_transaction( + sender_addr.parse().unwrap(), + recipient_addr.parse().unwrap(), + balance_to_move, + ) + .await?; + SubcommandReturnValue::Empty + } Command::ClaimPinata { pinata_addr, winner_addr,