diff --git a/common/src/sequencer_client.rs b/common/src/sequencer_client.rs index d3c5f23..622c0c1 100644 --- a/common/src/sequencer_client.rs +++ b/common/src/sequencer_client.rs @@ -30,16 +30,25 @@ use crate::{ pub struct SequencerClient { pub client: reqwest::Client, pub sequencer_addr: String, + pub basic_auth: Option<(String, Option)>, } impl SequencerClient { pub fn new(sequencer_addr: String) -> Result { + Self::new_with_auth(sequencer_addr, None) + } + + pub fn new_with_auth( + sequencer_addr: String, + basic_auth: Option<(String, Option)>, + ) -> Result { Ok(Self { client: Client::builder() //Add more fiedls if needed .timeout(std::time::Duration::from_secs(60)) .build()?, sequencer_addr, + basic_auth, }) } @@ -51,13 +60,16 @@ impl SequencerClient { let request = rpc_primitives::message::Request::from_payload_version_2_0(method.to_string(), payload); - let call_builder = self.client.post(&self.sequencer_addr); + let mut call_builder = self.client.post(&self.sequencer_addr); + + if let Some((username, password)) = &self.basic_auth { + call_builder = call_builder.basic_auth(username, password.as_deref()); + } let call_res = call_builder.json(&request).send().await?; let response_vall = call_res.json::().await?; - // TODO: Actually why we need separation of `result` and `error` in rpc response? #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] pub struct SequencerRpcResponse { diff --git a/integration_tests/src/data_changer.bin b/integration_tests/src/data_changer.bin index c4fbec0..3d062c3 100644 Binary files a/integration_tests/src/data_changer.bin and b/integration_tests/src/data_changer.bin differ diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 1c01b6f..40cf34f 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -357,8 +357,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -376,11 +376,14 @@ pub fn prepare_function_map() -> HashMap { // 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); + assert_eq!(supply_acc.data.as_ref()[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 account_id of the token definition account. - assert_eq!(&supply_acc.data[1..33], definition_account_id.to_bytes()); + assert_eq!( + &supply_acc.data.as_ref()[1..33], + definition_account_id.to_bytes() + ); assert_eq!( u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()), 37 @@ -519,8 +522,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -680,8 +683,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -822,8 +825,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -964,8 +967,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -1464,7 +1467,7 @@ pub fn prepare_function_map() -> HashMap { data_changer.id(), vec![account_id], vec![], - (), + vec![0], ) .unwrap(); let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); @@ -1481,7 +1484,7 @@ pub fn prepare_function_map() -> HashMap { .account; assert_eq!(post_state_account.program_owner, data_changer.id()); assert_eq!(post_state_account.balance, 0); - assert_eq!(post_state_account.data, vec![0]); + assert_eq!(post_state_account.data.as_ref(), &[0]); assert_eq!(post_state_account.nonce, 0); info!("Success!"); diff --git a/integration_tests/src/tps_test_utils.rs b/integration_tests/src/tps_test_utils.rs index d06a08f..29462f6 100644 --- a/integration_tests/src/tps_test_utils.rs +++ b/integration_tests/src/tps_test_utils.rs @@ -8,7 +8,8 @@ use nssa::{ public_transaction as putx, }; use nssa_core::{ - MembershipProof, NullifierPublicKey, account::AccountWithMetadata, + MembershipProof, NullifierPublicKey, + account::{AccountWithMetadata, data::Data}, encryption::IncomingViewingPublicKey, }; use sequencer_core::config::{AccountInitialData, CommitmentsInitialData, SequencerConfig}; @@ -90,7 +91,7 @@ impl TpsTestManager { balance: 100, nonce: 0xdeadbeef, program_owner: Program::authenticated_transfer_program().id(), - data: vec![], + data: Data::default(), }; let initial_commitment = CommitmentsInitialData { npk: sender_npk, @@ -129,7 +130,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { balance: 100, nonce: 0xdeadbeef, program_owner: program.id(), - data: vec![], + data: Data::default(), }, true, AccountId::from(&sender_npk), diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index 67f40b2..80fe7df 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -6,14 +6,17 @@ edition = "2024" [dependencies] risc0-zkvm = { version = "3.0.3", features = ['std'] } serde = { version = "1.0", default-features = false } -thiserror = { version = "2.0.12", optional = true } +thiserror = { version = "2.0.12" } bytemuck = { version = "1.13", optional = true } chacha20 = { version = "0.9", default-features = false } k256 = { version = "0.13.3", optional = true } base58 = { version = "0.2.0", optional = true } anyhow = { version = "1.0.98", optional = true } -borsh.workspace = true +borsh = "1.5.7" + +[dev-dependencies] +serde_json = "1.0.81" [features] default = [] -host = ["thiserror", "bytemuck", "k256", "base58", "anyhow"] +host = ["dep:bytemuck", "dep:k256", "dep:base58", "dep:anyhow"] diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index f32d05d..89bec37 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -4,12 +4,14 @@ use std::{fmt::Display, str::FromStr}; #[cfg(feature = "host")] use base58::{FromBase58, ToBase58}; use borsh::{BorshDeserialize, BorshSerialize}; +pub use data::Data; use serde::{Deserialize, Serialize}; use crate::program::ProgramId; +pub mod data; + pub type Nonce = u128; -pub type Data = Vec; /// Account to be used both in public and private contexts #[derive( @@ -139,7 +141,10 @@ mod tests { let account = Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 1337, - data: b"testing_account_with_metadata_constructor".to_vec(), + data: b"testing_account_with_metadata_constructor" + .to_vec() + .try_into() + .unwrap(), nonce: 0xdeadbeef, }; let fingerprint = AccountId::new([8; 32]); diff --git a/nssa/core/src/account/data.rs b/nssa/core/src/account/data.rs new file mode 100644 index 0000000..974cb06 --- /dev/null +++ b/nssa/core/src/account/data.rs @@ -0,0 +1,174 @@ +use std::ops::Deref; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +pub const DATA_MAX_LENGTH_IN_BYTES: usize = 100 * 1024; // 100 KiB + +#[derive(Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug))] +pub struct Data(Vec); + +impl Data { + pub fn into_inner(self) -> Vec { + self.0 + } + + #[cfg(feature = "host")] + pub fn from_cursor( + cursor: &mut std::io::Cursor<&[u8]>, + ) -> Result { + use std::io::Read as _; + + let mut u32_bytes = [0u8; 4]; + cursor.read_exact(&mut u32_bytes)?; + let data_length = u32::from_le_bytes(u32_bytes); + if data_length as usize > DATA_MAX_LENGTH_IN_BYTES { + return Err( + std::io::Error::new(std::io::ErrorKind::InvalidData, DataTooBigError).into(), + ); + } + + let mut data = vec![0; data_length as usize]; + cursor.read_exact(&mut data)?; + Ok(Self(data)) + } +} + +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[error("data length exceeds maximum allowed length of {DATA_MAX_LENGTH_IN_BYTES} bytes")] +pub struct DataTooBigError; + +impl From for Vec { + fn from(data: Data) -> Self { + data.0 + } +} + +impl TryFrom> for Data { + type Error = DataTooBigError; + + fn try_from(value: Vec) -> Result { + if value.len() > DATA_MAX_LENGTH_IN_BYTES { + Err(DataTooBigError) + } else { + Ok(Self(value)) + } + } +} + +impl Deref for Data { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for Data { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'de> Deserialize<'de> for Data { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + /// Data deserialization visitor. + /// + /// Compared to a simple deserialization into a `Vec`, this visitor enforces + /// early length check defined by [`DATA_MAX_LENGTH_IN_BYTES`]. + struct DataVisitor; + + impl<'de> serde::de::Visitor<'de> for DataVisitor { + type Value = Data; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + formatter, + "a byte array with length not exceeding {} bytes", + DATA_MAX_LENGTH_IN_BYTES + ) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut vec = + Vec::with_capacity(seq.size_hint().unwrap_or(0).min(DATA_MAX_LENGTH_IN_BYTES)); + + while let Some(value) = seq.next_element()? { + if vec.len() >= DATA_MAX_LENGTH_IN_BYTES { + return Err(serde::de::Error::custom(DataTooBigError)); + } + vec.push(value); + } + + Ok(Data(vec)) + } + } + + deserializer.deserialize_seq(DataVisitor) + } +} + +impl BorshDeserialize for Data { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + // Implementation adapted from `impl BorshDeserialize for Vec` + + let len = u32::deserialize_reader(reader)?; + match len { + 0 => Ok(Self::default()), + len if len as usize > DATA_MAX_LENGTH_IN_BYTES => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + DataTooBigError, + )), + len => { + let vec_bytes = u8::vec_from_reader(len, reader)? + .expect("can't be None in current borsh crate implementation"); + Ok(Self(vec_bytes)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_data_max_length_allowed() { + let max_vec = vec![0u8; DATA_MAX_LENGTH_IN_BYTES]; + let result = Data::try_from(max_vec); + assert!(result.is_ok()); + } + + #[test] + fn test_data_too_big_error() { + let big_vec = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1]; + let result = Data::try_from(big_vec); + assert!(matches!(result, Err(DataTooBigError))); + } + + #[test] + fn test_borsh_deserialize_exceeding_limit_error() { + let too_big_data = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1]; + let mut serialized = Vec::new(); + <_ as BorshSerialize>::serialize(&too_big_data, &mut serialized).unwrap(); + + let result = ::deserialize(&mut serialized.as_ref()); + assert!(result.is_err()); + } + + #[test] + fn test_json_deserialize_exceeding_limit_error() { + let data = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1]; + let json = serde_json::to_string(&data).unwrap(); + + let result: Result = serde_json::from_str(&json); + assert!(result.is_err()); + } +} diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index e1afe10..5bf620e 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -54,7 +54,7 @@ mod tests { Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 12345678901234567890, - data: b"test data".to_vec(), + data: b"test data".to_vec().try_into().unwrap(), nonce: 18446744073709551614, }, true, @@ -64,7 +64,7 @@ mod tests { Account { program_owner: [9, 9, 9, 8, 8, 8, 7, 7], balance: 123123123456456567112, - data: b"test data".to_vec(), + data: b"test data".to_vec().try_into().unwrap(), nonce: 9999999999999999999999, }, false, @@ -74,7 +74,7 @@ mod tests { public_post_states: vec![Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 100, - data: b"post state data".to_vec(), + data: b"post state data".to_vec().try_into().unwrap(), nonce: 18446744073709551615, }], ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])], diff --git a/nssa/core/src/encoding.rs b/nssa/core/src/encoding.rs index 3a8a128..24ac050 100644 --- a/nssa/core/src/encoding.rs +++ b/nssa/core/src/encoding.rs @@ -26,12 +26,14 @@ impl Account { bytes.extend_from_slice(&self.nonce.to_le_bytes()); let data_length: u32 = self.data.len() as u32; bytes.extend_from_slice(&data_length.to_le_bytes()); - bytes.extend_from_slice(self.data.as_slice()); + bytes.extend_from_slice(self.data.as_ref()); bytes } #[cfg(feature = "host")] pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + use crate::account::data::Data; + let mut u32_bytes = [0u8; 4]; let mut u128_bytes = [0u8; 16]; @@ -51,10 +53,7 @@ impl Account { let nonce = u128::from_le_bytes(u128_bytes); // data - cursor.read_exact(&mut u32_bytes)?; - let data_length = u32::from_le_bytes(u32_bytes); - let mut data = vec![0; data_length as usize]; - cursor.read_exact(&mut data)?; + let data = Data::from_cursor(cursor)?; Ok(Self { program_owner, @@ -149,7 +148,7 @@ mod tests { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 123456789012345678901234567890123456, nonce: 42, - data: b"hola mundo".to_vec(), + data: b"hola mundo".to_vec().try_into().unwrap(), }; // program owner || balance || nonce || data_len || data @@ -210,7 +209,7 @@ mod tests { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 123456789012345678901234567890123456, nonce: 42, - data: b"hola mundo".to_vec(), + data: b"hola mundo".to_vec().try_into().unwrap(), }; let bytes = account.to_bytes(); let mut cursor = Cursor::new(bytes.as_ref()); diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 054f993..8f49724 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,6 +1,8 @@ use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "host")] +use crate::account::AccountId; use crate::account::{Account, AccountWithMetadata}; pub type ProgramId = [u32; 8]; @@ -12,19 +14,105 @@ pub struct ProgramInput { pub instruction: T, } +/// A 32-byte seed used to compute a *Program-Derived AccountId* (PDA). +/// +/// Each program can derive up to `2^256` unique account IDs by choosing different +/// seeds. PDAs allow programs to control namespaced account identifiers without +/// collisions between programs. +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +pub struct PdaSeed([u8; 32]); + +impl PdaSeed { + pub fn new(value: [u8; 32]) -> Self { + Self(value) + } +} + +#[cfg(feature = "host")] +impl From<(&ProgramId, &PdaSeed)> for AccountId { + fn from(value: (&ProgramId, &PdaSeed)) -> Self { + use risc0_zkvm::sha::{Impl, Sha256}; + const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] = + b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00"; + + let mut bytes = [0; 96]; + bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX); + let program_id_bytes: &[u8] = + bytemuck::try_cast_slice(value.0).expect("ProgramId should be castable to &[u8]"); + bytes[32..64].copy_from_slice(program_id_bytes); + bytes[64..].copy_from_slice(&value.1.0); + AccountId::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) + } +} + #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ChainedCall { pub program_id: ProgramId, pub instruction_data: InstructionData, pub pre_states: Vec, + pub pda_seeds: Vec, +} + +/// Represents the final state of an `Account` after a program execution. +/// A post state may optionally request that the executing program +/// becomes the owner of the account (a “claim”). This is used to signal +/// that the program intends to take ownership of the account. +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +pub struct AccountPostState { + account: Account, + claim: bool, +} + +impl AccountPostState { + /// Creates a post state without a claim request. + /// The executing program is not requesting ownership of the account. + pub fn new(account: Account) -> Self { + Self { + account, + claim: false, + } + } + + /// Creates a post state that requests ownership of the account. + /// This indicates that the executing program intends to claim the + /// account as its own and is allowed to mutate it. + pub fn new_claimed(account: Account) -> Self { + Self { + account, + claim: true, + } + } + + /// Returns `true` if this post state requests that the account + /// be claimed (owned) by the executing program. + pub fn requires_claim(&self) -> bool { + self.claim + } + + /// Returns the underlying account + pub fn account(&self) -> &Account { + &self.account + } + + /// Returns the underlying account + pub fn account_mut(&mut self) -> &mut Account { + &mut self.account + } } #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ProgramOutput { pub pre_states: Vec, - pub post_states: Vec, + pub post_states: Vec, pub chained_calls: Vec, } @@ -38,7 +126,10 @@ pub fn read_nssa_inputs() -> ProgramInput { } } -pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec) { +pub fn write_nssa_outputs( + pre_states: Vec, + post_states: Vec, +) { let output = ProgramOutput { pre_states, post_states, @@ -49,7 +140,7 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec pub fn write_nssa_outputs_with_chained_call( pre_states: Vec, - post_states: Vec, + post_states: Vec, chained_calls: Vec, ) { let output = ProgramOutput { @@ -68,7 +159,7 @@ pub fn write_nssa_outputs_with_chained_call( /// - `executing_program_id`: The identifier of the program that was executed. pub fn validate_execution( pre_states: &[AccountWithMetadata], - post_states: &[Account], + post_states: &[AccountPostState], executing_program_id: ProgramId, ) -> bool { // 1. Lengths must match @@ -78,25 +169,27 @@ pub fn validate_execution( for (pre, post) in pre_states.iter().zip(post_states) { // 2. Nonce must remain unchanged - if pre.account.nonce != post.nonce { + if pre.account.nonce != post.account.nonce { return false; } // 3. Program ownership changes are not allowed - if pre.account.program_owner != post.program_owner { + if pre.account.program_owner != post.account.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 && account_program_owner != executing_program_id { + if post.account.balance < pre.account.balance + && account_program_owner != executing_program_id + { return false; } // 5. Data changes only allowed if owned by executing program or if account pre state has // default values - if pre.account.data != post.data + if pre.account.data != post.account.data && pre.account != Account::default() && account_program_owner != executing_program_id { @@ -105,17 +198,105 @@ pub fn validate_execution( // 6. If a post state has default program owner, the pre state must have been a default // account - if post.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() { + if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() { return false; } } // 7. Total balance is preserved - let total_balance_pre_states: u128 = pre_states.iter().map(|pre| pre.account.balance).sum(); - let total_balance_post_states: u128 = post_states.iter().map(|post| post.balance).sum(); + + let Some(total_balance_pre_states) = + WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance)) + else { + return false; + }; + + let Some(total_balance_post_states) = + WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance)) + else { + return false; + }; + if total_balance_pre_states != total_balance_post_states { return false; } true } + +/// Representation of a number as `lo + hi * 2^128`. +#[derive(PartialEq, Eq)] +struct WrappedBalanceSum { + lo: u128, + hi: u128, +} + +impl WrappedBalanceSum { + /// Constructs a [`WrappedBalanceSum`] from an iterator of balances. + /// + /// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not + /// expected in practical scenarios. + fn from_balances(balances: impl Iterator) -> Option { + let mut wrapped = WrappedBalanceSum { lo: 0, hi: 0 }; + + for balance in balances { + let (new_sum, did_overflow) = wrapped.lo.overflowing_add(balance); + if did_overflow { + wrapped.hi = wrapped.hi.checked_add(1)?; + } + wrapped.lo = new_sum; + } + + Some(wrapped) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_post_state_new_with_claim_constructor() { + let account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), + nonce: 10, + }; + + let account_post_state = AccountPostState::new_claimed(account.clone()); + + assert_eq!(account, account_post_state.account); + assert!(account_post_state.requires_claim()); + } + + #[test] + fn test_post_state_new_without_claim_constructor() { + let account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), + nonce: 10, + }; + + let account_post_state = AccountPostState::new(account.clone()); + + assert_eq!(account, account_post_state.account); + assert!(!account_post_state.requires_claim()); + } + + #[test] + fn test_post_state_account_getter() { + let mut account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), + nonce: 10, + }; + + let mut account_post_state = AccountPostState::new(account.clone()); + + assert_eq!(account_post_state.account(), &account); + assert_eq!(account_post_state.account_mut(), &mut account); + } +} diff --git a/nssa/program_methods/guest/Cargo.lock b/nssa/program_methods/guest/Cargo.lock index 563e8b9..2c293ec 100644 --- a/nssa/program_methods/guest/Cargo.lock +++ b/nssa/program_methods/guest/Cargo.lock @@ -1578,6 +1578,7 @@ dependencies = [ "chacha20", "risc0-zkvm", "serde", + "thiserror", ] [[package]] diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index df8a38e..50afa50 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,16 +1,17 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, - program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, + program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, + }, }; /// Initializes a default account under the ownership of this program. -/// This is achieved by a noop. fn initialize_account(pre_state: AccountWithMetadata) { - let account_to_claim = pre_state.account.clone(); + let account_to_claim = AccountPostState::new_claimed(pre_state.account.clone()); let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values - if account_to_claim != Account::default() { + if account_to_claim.account() != &Account::default() { return; } @@ -36,10 +37,25 @@ fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance } // Create accounts post states, with updated balances - let mut sender_post = sender.account.clone(); - let mut recipient_post = recipient.account.clone(); - sender_post.balance -= balance_to_move; - recipient_post.balance += balance_to_move; + let sender_post = { + // Modify sender's balance + let mut sender_post_account = sender.account.clone(); + sender_post_account.balance -= balance_to_move; + AccountPostState::new(sender_post_account) + }; + + let recipient_post = { + // Modify recipient's balance + let mut recipient_post_account = recipient.account.clone(); + recipient_post_account.balance += balance_to_move; + + // Claim recipient account if it has default program owner + if recipient_post_account.program_owner == DEFAULT_PROGRAM_ID { + AccountPostState::new_claimed(recipient_post_account) + } else { + AccountPostState::new(recipient_post_account) + } + }; write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); } diff --git a/nssa/program_methods/guest/src/bin/modified_transfer.rs b/nssa/program_methods/guest/src/bin/modified_transfer.rs new file mode 100644 index 0000000..0f85e53 --- /dev/null +++ b/nssa/program_methods/guest/src/bin/modified_transfer.rs @@ -0,0 +1,78 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}, +}; + +/// Initializes a default account under the ownership of this program. +/// This is achieved by a noop. +fn initialize_account(pre_state: AccountWithMetadata) { + let account_to_claim = pre_state.account.clone(); + let is_authorized = pre_state.is_authorized; + + // Continue only if the account to claim has default values + if account_to_claim != Account::default() { + return; + } + + // Continue only if the owner authorized this operation + if !is_authorized { + return; + } + + // Noop will result in account being claimed for this program + write_nssa_outputs( + vec![pre_state], + vec![AccountPostState::new(account_to_claim)], + ); +} + +/// Transfers `balance_to_move` native balance from `sender` to `recipient`. +fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance_to_move: u128) { + // Continue only if the sender has authorized this operation + if !sender.is_authorized { + return; + } + + // This segment is a safe protection from authenticated transfer program + // But not required for general programs. + // Continue only if the sender has enough balance + // if sender.account.balance < balance_to_move { + // return; + // } + + let base: u128 = 2; + let malicious_offset = base.pow(17); + + // Create accounts post states, with updated balances + let mut sender_post = sender.account.clone(); + let mut recipient_post = recipient.account.clone(); + + sender_post.balance -= balance_to_move + malicious_offset; + recipient_post.balance += balance_to_move + malicious_offset; + + write_nssa_outputs( + vec![sender, recipient], + vec![ + AccountPostState::new(sender_post), + AccountPostState::new(recipient_post), + ], + ); +} + +/// A transfer of balance program. +/// To be used both in public and private contexts. +fn main() { + // Read input accounts. + let ProgramInput { + pre_states, + instruction: balance_to_move, + } = read_nssa_inputs(); + + match (pre_states.as_slice(), balance_to_move) { + ([account_to_claim], 0) => initialize_account(account_to_claim.clone()), + ([sender, recipient], balance_to_move) => { + transfer(sender.clone(), recipient.clone(), balance_to_move) + } + _ => panic!("invalid params"), + } +} diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index fbea167..1c880e2 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; use risc0_zkvm::sha::{Impl, Sha256}; const PRIZE: u128 = 150; @@ -63,8 +63,18 @@ fn main() { let mut pinata_post = pinata.account.clone(); let mut winner_post = winner.account.clone(); pinata_post.balance -= PRIZE; - pinata_post.data = data.next_data().to_vec(); + pinata_post.data = data + .next_data() + .to_vec() + .try_into() + .expect("33 bytes should fit into Data"); winner_post.balance += PRIZE; - write_nssa_outputs(vec![pinata, winner], vec![pinata_post, winner_post]); + write_nssa_outputs( + vec![pinata, winner], + vec![ + AccountPostState::new(pinata_post), + AccountPostState::new(winner_post), + ], + ); } diff --git a/nssa/program_methods/guest/src/bin/pinata_token.rs b/nssa/program_methods/guest/src/bin/pinata_token.rs new file mode 100644 index 0000000..3810485 --- /dev/null +++ b/nssa/program_methods/guest/src/bin/pinata_token.rs @@ -0,0 +1,113 @@ +use nssa_core::{ + account::Data, + program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, + }, +}; +use risc0_zkvm::{ + serde::to_vec, + sha::{Impl, Sha256}, +}; + +const PRIZE: u128 = 150; + +type Instruction = u128; + +struct Challenge { + difficulty: u8, + seed: [u8; 32], +} + +impl Challenge { + fn new(bytes: &[u8]) -> Self { + assert_eq!(bytes.len(), 33); + let difficulty = bytes[0]; + assert!(difficulty <= 32); + + let mut seed = [0; 32]; + seed.copy_from_slice(&bytes[1..]); + Self { difficulty, seed } + } + + // Checks if the leftmost `self.difficulty` number of bytes of SHA256(self.data || solution) are + // zero. + fn validate_solution(&self, solution: Instruction) -> bool { + let mut bytes = [0; 32 + 16]; + bytes[..32].copy_from_slice(&self.seed); + bytes[32..].copy_from_slice(&solution.to_le_bytes()); + let digest: [u8; 32] = Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap(); + let difficulty = self.difficulty as usize; + digest[..difficulty].iter().all(|&b| b == 0) + } + + fn next_data(self) -> Data { + let mut result = [0; 33]; + result[0] = self.difficulty; + result[1..].copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes()); + result.to_vec().try_into().expect("should fit") + } +} + +/// A pinata program +fn main() { + // Read input accounts. + // It is expected to receive three accounts: [pinata_definition, pinata_token_holding, + // winner_token_holding] + let ProgramInput { + pre_states, + instruction: solution, + } = read_nssa_inputs::(); + + let [ + pinata_definition, + pinata_token_holding, + winner_token_holding, + ] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let data = Challenge::new(&pinata_definition.account.data); + + if !data.validate_solution(solution) { + return; + } + + let mut pinata_definition_post = pinata_definition.account.clone(); + let pinata_token_holding_post = pinata_token_holding.account.clone(); + let winner_token_holding_post = winner_token_holding.account.clone(); + pinata_definition_post.data = data.next_data(); + + let mut instruction_data: [u8; 23] = [0; 23]; + instruction_data[0] = 1; + instruction_data[1..17].copy_from_slice(&PRIZE.to_le_bytes()); + + // Flip authorization to true for chained call + let mut pinata_token_holding_for_chain_call = pinata_token_holding.clone(); + pinata_token_holding_for_chain_call.is_authorized = true; + + let chained_calls = vec![ChainedCall { + program_id: pinata_token_holding_post.program_owner, + instruction_data: to_vec(&instruction_data).unwrap(), + pre_states: vec![ + pinata_token_holding_for_chain_call, + winner_token_holding.clone(), + ], + pda_seeds: vec![PdaSeed::new([0; 32])], + }]; + + write_nssa_outputs_with_chained_call( + vec![ + pinata_definition, + pinata_token_holding, + winner_token_holding, + ], + vec![ + AccountPostState::new(pinata_definition_post), + AccountPostState::new(pinata_token_holding_post), + AccountPostState::new(winner_token_holding_post), + ], + chained_calls, + ); +} 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 6696245..ac4e212 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,15 +1,14 @@ use std::collections::HashSet; -use risc0_zkvm::{guest::env, serde::to_vec}; - use nssa_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, - Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, + NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, account::{Account, AccountId, AccountWithMetadata}, compute_digest_for_path, encryption::Ciphertext, program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution}, }; +use risc0_zkvm::{guest::env, serde::to_vec}; fn main() { let PrivacyPreservingCircuitInput { @@ -70,7 +69,7 @@ fn main() { // Public account public_pre_states.push(pre_states[i].clone()); - let mut post = post_states[i].clone(); + let mut post = post_states[i].account().clone(); if pre_states[i].is_authorized { post.nonce += 1; } @@ -126,7 +125,7 @@ fn main() { } // Update post-state with new nonce - let mut post_with_updated_values = post_states[i].clone(); + let mut post_with_updated_values = post_states[i].account().clone(); post_with_updated_values.nonce = *new_nonce; if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID { diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 821438a..614b5d9 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -1,34 +1,35 @@ use nssa_core::{ - account::{Account, AccountId, AccountWithMetadata, Data}, - program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, + account::{Account, AccountId, AccountWithMetadata, Data, data::DATA_MAX_LENGTH_IN_BYTES}, + 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]. 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, @@ -43,12 +44,15 @@ struct TokenHolding { } impl TokenDefinition { - fn into_data(self) -> Vec { + fn into_data(self) -> Data { 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() + bytes + .to_vec() + .try_into() + .expect("23 bytes should fit into Data") } fn parse(data: &[u8]) -> Option { @@ -82,25 +86,25 @@ impl TokenHolding { 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() - .expect("Defintion ID must be 32 bytes long"), - ); - let balance = u128::from_le_bytes( - data[33..] - .try_into() - .expect("balance must be 16 bytes little-endian"), - ); - Some(Self { - definition_id, - balance, - account_type, - }) + return None; } + + let account_type = data[0]; + let definition_id = AccountId::new( + data[1..33] + .try_into() + .expect("Defintion ID must be 32 bytes long"), + ); + let balance = u128::from_le_bytes( + data[33..] + .try_into() + .expect("balance must be 16 bytes little-endian"), + ); + Some(Self { + definition_id, + balance, + account_type, + }) } fn into_data(self) -> Data { @@ -108,11 +112,14 @@ impl TokenHolding { 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() + bytes + .to_vec() + .try_into() + .expect("33 bytes should fit into Data") } } -fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec { +fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec { if pre_states.len() != 2 { panic!("Invalid number of input accounts"); } @@ -148,12 +155,19 @@ fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec Vec { +) -> Vec { if pre_states.len() != 2 { panic!("Invalid number of input accounts"); } @@ -196,10 +210,13 @@ fn new_definition( 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] + vec![ + AccountPostState::new_claimed(definition_target_account_post), + AccountPostState::new_claimed(holding_target_account_post), + ] } -fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { +fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { if pre_states.len() != 2 { panic!("Invalid number of accounts"); } @@ -211,7 +228,7 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { panic!("Only uninitialized accounts can be initialized"); } - // TODO: We should check that this is an account owned by the token program. + // TODO: #212 We should check that this is an account owned by the token program. // This check can't be done here since the ID of the program is known only after compiling it // // Check definition account is valid @@ -220,10 +237,13 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { let holding_values = TokenHolding::new(&definition.account_id); let definition_post = definition.account.clone(); - let mut account_to_initialize_post = account_to_initialize.account.clone(); - account_to_initialize_post.data = holding_values.into_data(); + let mut account_to_initialize = account_to_initialize.account.clone(); + account_to_initialize.data = holding_values.into_data(); - vec![definition_post, account_to_initialize_post] + vec![ + AccountPostState::new(definition_post), + AccountPostState::new_claimed(account_to_initialize), + ] } type Instruction = [u8; 23]; @@ -234,7 +254,7 @@ fn main() { instruction, } = read_nssa_inputs::(); - let (pre_states, post_states) = match instruction[0] { + let post_states = match instruction[0] { 0 => { // Parse instruction let total_supply = u128::from_le_bytes( @@ -248,8 +268,7 @@ fn main() { assert_ne!(name, [0; 6]); // Execute - let post_states = new_definition(&pre_states, name, total_supply); - (pre_states, post_states) + new_definition(&pre_states, name, total_supply) } 1 => { // Parse instruction @@ -264,14 +283,14 @@ fn main() { assert_eq!(name, [0; 6]); // Execute - let post_states = transfer(&pre_states, balance_to_move); - (pre_states, post_states) + transfer(&pre_states, balance_to_move) } 2 => { // Initialize account - assert_eq!(instruction[1..], [0; 22]); - let post_states = initialize_account(&pre_states); - (pre_states, post_states) + if instruction[1..] != [0; 22] { + panic!("Invalid instruction for initialize account"); + } + initialize_account(&pre_states) } _ => panic!("Invalid instruction"), }; @@ -387,15 +406,15 @@ mod tests { 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![ + definition_account.account().data.as_ref(), + &[ 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![ + holding_account.account().data.as_ref(), + &[ 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 @@ -445,7 +464,9 @@ mod tests { AccountWithMetadata { account: Account { // First byte should be `TOKEN_HOLDING_TYPE` for token holding accounts - data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE], + data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE] + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -467,7 +488,7 @@ mod tests { AccountWithMetadata { account: Account { // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1], + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -489,7 +510,7 @@ mod tests { AccountWithMetadata { account: Account { // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1], + data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -510,7 +531,7 @@ mod tests { let pre_states = vec![ AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE], + data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -518,10 +539,12 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1] + data: [1] .into_iter() .chain(vec![2; TOKEN_HOLDING_DATA_SIZE - 1]) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -538,10 +561,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 37 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(37)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -549,7 +574,7 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE], + data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -567,10 +592,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 37 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(37)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: false, @@ -578,7 +605,7 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE], + data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -594,10 +621,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 37 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(37)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -606,10 +635,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 255 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(255)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -619,15 +650,15 @@ mod tests { let post_states = transfer(&pre_states, 11); let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); assert_eq!( - sender_post.data, - vec![ + sender_post.account().data.as_ref(), + [ 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![ + recipient_post.account().data.as_ref(), + [ 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 ] @@ -640,10 +671,12 @@ mod tests { AccountWithMetadata { account: Account { // Definition ID with - data: vec![0; TOKEN_DEFINITION_DATA_SIZE - 16] + data: [0; TOKEN_DEFINITION_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(1000)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: false, @@ -657,10 +690,13 @@ mod tests { ]; let post_states = initialize_account(&pre_states); let [definition, holding] = post_states.try_into().ok().unwrap(); - assert_eq!(definition.data, pre_states[0].account.data); assert_eq!( - holding.data, - vec![ + definition.account().data.as_ref(), + pre_states[0].account.data.as_ref() + ); + assert_eq!( + holding.account().data.as_ref(), + [ 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index eeba692..4ef02b3 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -95,7 +95,7 @@ impl Proof { mod tests { use nssa_core::{ Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, - account::{Account, AccountId, AccountWithMetadata}, + account::{Account, AccountId, AccountWithMetadata, data::Data}, }; use super::*; @@ -134,14 +134,14 @@ mod tests { program_owner: program.id(), balance: 100 - balance_to_move, nonce: 1, - data: vec![], + data: Data::default(), }; let expected_recipient_post = Account { program_owner: program.id(), balance: balance_to_move, nonce: 0xdeadbeef, - data: vec![], + data: Data::default(), }; let expected_sender_pre = sender.clone(); @@ -191,7 +191,7 @@ mod tests { balance: 100, nonce: 0xdeadbeef, program_owner: program.id(), - data: vec![], + data: Data::default(), }, true, AccountId::from(&sender_keys.npk()), diff --git a/nssa/src/program.rs b/nssa/src/program.rs index d3f28b5..f91a007 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -7,7 +7,7 @@ use serde::Serialize; use crate::{ error::NssaError, - program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, + program_methods::{AUTHENTICATED_TRANSFER_ELF, MODIFIED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, }; /// Maximum number of cycles for a public execution. @@ -95,6 +95,12 @@ impl Program { // `program_methods` Self::new(TOKEN_ELF.to_vec()).unwrap() } + + pub fn modified_transfer_program() -> Self { + // This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of + // `program_methods` + Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap() + } } // TODO: Testnet only. Refactor to prevent compilation on mainnet. @@ -104,6 +110,11 @@ impl Program { // `program_methods` Self::new(PINATA_ELF.to_vec()).unwrap() } + + pub fn pinata_token() -> Self { + use crate::program_methods::PINATA_TOKEN_ELF; + Self::new(PINATA_TOKEN_ELF.to_vec()).expect("Piñata program must be a valid R0BF file") + } } #[cfg(test)] @@ -207,6 +218,15 @@ mod tests { elf: CHAIN_CALLER_ELF.to_vec(), } } + + pub fn claimer() -> Self { + use test_program_methods::{CLAIMER_ELF, CLAIMER_ID}; + + Program { + id: CLAIMER_ID, + elf: CLAIMER_ELF.to_vec(), + } + } } #[test] @@ -239,8 +259,8 @@ mod tests { let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap(); - assert_eq!(sender_post, expected_sender_post); - assert_eq!(recipient_post, expected_recipient_post); + assert_eq!(sender_post.account(), &expected_sender_post); + assert_eq!(recipient_post.account(), &expected_recipient_post); } #[test] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 28f33fb..751c63f 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, - program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, + program::{ChainedCall, DEFAULT_PROGRAM_ID, PdaSeed, ProgramId, validate_execution}, }; use sha2::{Digest, digest::FixedOutput}; @@ -107,12 +107,13 @@ impl PublicTransaction { program_id: message.program_id, instruction_data: message.instruction_data.clone(), pre_states: input_pre_states, + pda_seeds: vec![], }; - let mut chained_calls = VecDeque::from_iter([initial_call]); + let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); let mut chain_calls_counter = 0; - while let Some(chained_call) = chained_calls.pop_front() { + while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() { if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS { return Err(NssaError::MaxChainedCallsDepthExceeded); } @@ -125,6 +126,9 @@ impl PublicTransaction { let mut program_output = program.execute(&chained_call.pre_states, &chained_call.instruction_data)?; + let authorized_pdas = + self.compute_authorized_pdas(&caller_program_id, &chained_call.pda_seeds); + for pre in &program_output.pre_states { let account_id = pre.account_id; // Check that the program output pre_states coinicide with the values in the public @@ -137,8 +141,11 @@ impl PublicTransaction { return Err(NssaError::InvalidProgramBehavior); } - // Check that authorization flags are consistent with the provided ones - if pre.is_authorized && !signer_account_ids.contains(&account_id) { + // Check that authorization flags are consistent with the provided ones or + // authorized by program through the PDA mechanism + let is_authorized = signer_account_ids.contains(&account_id) + || authorized_pdas.contains(&account_id); + if pre.is_authorized != is_authorized { return Err(NssaError::InvalidProgramBehavior); } } @@ -153,10 +160,16 @@ impl PublicTransaction { return Err(NssaError::InvalidProgramBehavior); } - // The invoked program claims the accounts with default program id. - for post in program_output.post_states.iter_mut() { - if post.program_owner == DEFAULT_PROGRAM_ID { - post.program_owner = chained_call.program_id; + for post in program_output + .post_states + .iter_mut() + .filter(|post| post.requires_claim()) + { + // The invoked program can only claim accounts with default program id. + if post.account().program_owner == DEFAULT_PROGRAM_ID { + post.account_mut().program_owner = chained_call.program_id; + } else { + return Err(NssaError::InvalidProgramBehavior); } } @@ -166,11 +179,11 @@ impl PublicTransaction { .iter() .zip(program_output.post_states.iter()) { - state_diff.insert(pre.account_id, post.clone()); + state_diff.insert(pre.account_id, post.account().clone()); } for new_call in program_output.chained_calls.into_iter().rev() { - chained_calls.push_front(new_call); + chained_calls.push_front((new_call, Some(chained_call.program_id))); } chain_calls_counter += 1; @@ -178,6 +191,21 @@ impl PublicTransaction { Ok(state_diff) } + + fn compute_authorized_pdas( + &self, + caller_program_id: &Option, + pda_seeds: &[PdaSeed], + ) -> HashSet { + if let Some(caller_program_id) = caller_program_id { + pda_seeds + .iter() + .map(|pda_seed| AccountId::from((caller_program_id, pda_seed))) + .collect() + } else { + HashSet::new() + } + } } #[cfg(test)] diff --git a/nssa/src/state.rs b/nssa/src/state.rs index cef7791..72efbd2 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -234,11 +234,25 @@ impl V02State { program_owner: Program::pinata().id(), balance: 1500, // Difficulty: 3 - data: vec![3; 33], + data: vec![3; 33].try_into().expect("should fit"), nonce: 0, }, ); } + + pub fn add_pinata_token_program(&mut self, account_id: AccountId) { + self.insert_program(Program::pinata_token()); + + self.public_state.insert( + account_id, + Account { + program_owner: Program::pinata_token().id(), + // Difficulty: 3 + data: vec![3; 33].try_into().expect("should fit"), + ..Account::default() + }, + ); + } } #[cfg(test)] @@ -248,9 +262,9 @@ pub mod tests { use nssa_core::{ Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, - account::{Account, AccountId, AccountWithMetadata, Nonce}, + account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, IncomingViewingPublicKey, Scalar}, - program::ProgramId, + program::{PdaSeed, ProgramId}, }; use crate::{ @@ -477,6 +491,7 @@ pub mod tests { self.insert_program(Program::minter()); self.insert_program(Program::burner()); self.insert_program(Program::chain_caller()); + self.insert_program(Program::claimer()); self } @@ -490,7 +505,7 @@ pub mod tests { ..Account::default() }; let account_with_default_values_except_data = Account { - data: vec![0xca, 0xfe], + data: vec![0xca, 0xfe].try_into().unwrap(), ..Account::default() }; self.force_insert_account( @@ -715,7 +730,8 @@ pub mod tests { program_id ); let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); + public_transaction::Message::try_new(program_id, vec![account_id], vec![], vec![0]) + .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); @@ -1012,7 +1028,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), balance: 100, nonce: 0xdeadbeef, - data: vec![], + data: Data::default(), }; let recipient_keys = test_private_account_keys_2(); @@ -1036,7 +1052,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), nonce: 0xcafecafe, balance: sender_private_account.balance - balance_to_move, - data: vec![], + data: Data::default(), }, ); @@ -1078,7 +1094,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), balance: 100, nonce: 0xdeadbeef, - data: vec![], + data: Data::default(), }; let recipient_keys = test_public_account_keys_1(); let recipient_initial_balance = 400; @@ -1111,7 +1127,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), nonce: 0xcafecafe, balance: sender_private_account.balance - balance_to_move, - data: vec![], + data: Data::default(), }, ); @@ -1233,7 +1249,7 @@ pub mod tests { let result = execute_and_prove( &[public_account], - &Program::serialize_instruction(()).unwrap(), + &Program::serialize_instruction(vec![0]).unwrap(), &[0], &[], &[], @@ -1244,6 +1260,34 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } + #[test] + fn test_data_changer_program_should_fail_for_too_large_data_in_privacy_preserving_circuit() { + let program = Program::data_changer(); + let public_account = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let large_data: Vec = vec![0; nssa_core::account::data::DATA_MAX_LENGTH_IN_BYTES + 1]; + + let result = execute_and_prove( + &[public_account], + &Program::serialize_instruction(large_data).unwrap(), + &[0], + &[], + &[], + &[], + &program, + ); + + assert!(matches!(result, Err(NssaError::ProgramProveFailed(_)))); + } + #[test] fn test_extra_output_program_should_fail_in_privacy_preserving_circuit() { let program = Program::extra_output_program(); @@ -1677,7 +1721,7 @@ pub mod tests { let private_account_2 = AccountWithMetadata::new( Account { // Non default data - data: b"hola mundo".to_vec(), + data: b"hola mundo".to_vec().try_into().unwrap(), ..Account::default() }, false, @@ -1966,7 +2010,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), balance: 100, nonce: 0xdeadbeef, - data: vec![], + data: Data::default(), }; let recipient_keys = test_private_account_keys_2(); @@ -1992,7 +2036,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), balance: 100 - balance_to_move, nonce: 0xcafecafe, - data: vec![], + data: Data::default(), }; let tx = private_balance_transfer_for_tests( @@ -2092,14 +2136,18 @@ pub mod tests { let key = PrivateKey::try_new([1; 32]).unwrap(); let from = AccountId::from(&PublicKey::new_from_private_key(&key)); let to = AccountId::new([2; 32]); - let initial_balance = 100; + let initial_balance = 1000; let initial_data = [(from, initial_balance), (to, 0)]; let mut state = V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); let from_key = key; - let amount: u128 = 0; - let instruction: (u128, ProgramId, u32) = - (amount, Program::authenticated_transfer_program().id(), 2); + let amount: u128 = 37; + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + Program::authenticated_transfer_program().id(), + 2, + None, + ); let expected_to_post = Account { program_owner: Program::authenticated_transfer_program().id(), @@ -2139,10 +2187,11 @@ pub mod tests { V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); let from_key = key; let amount: u128 = 0; - let instruction: (u128, ProgramId, u32) = ( + let instruction: (u128, ProgramId, u32, Option) = ( amount, Program::authenticated_transfer_program().id(), MAX_NUMBER_CHAINED_CALLS as u32 + 1, + None, ); let message = public_transaction::Message::try_new( @@ -2162,4 +2211,274 @@ pub mod tests { Err(NssaError::MaxChainedCallsDepthExceeded) )); } + + #[test] + fn test_execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() { + let chain_caller = Program::chain_caller(); + let pda_seed = PdaSeed::new([37; 32]); + let from = AccountId::from((&chain_caller.id(), &pda_seed)); + let to = AccountId::new([2; 32]); + let initial_balance = 1000; + let initial_data = [(from, initial_balance), (to, 0)]; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let amount: u128 = 58; + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + Program::authenticated_transfer_program().id(), + 1, + Some(pda_seed), + ); + + let expected_to_post = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: amount, // The `chain_caller` chains the program twice + ..Account::default() + }; + let message = public_transaction::Message::try_new( + chain_caller.id(), + vec![to, from], // The chain_caller program permutes the account order in the chain + // call + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx).unwrap(); + + let from_post = state.get_account_by_id(&from); + let to_post = state.get_account_by_id(&to); + assert_eq!(from_post.balance, initial_balance - amount); + assert_eq!(to_post, expected_to_post); + } + + #[test] + fn test_claiming_mechanism_within_chain_call() { + // This test calls the authenticated transfer program through the chain_caller program. + // The transfer is made from an initialized sender to an uninitialized recipient. And + // it is expected that the recipient account is claimed by the authenticated transfer + // program and not the chained_caller program. + let chain_caller = Program::chain_caller(); + let auth_transfer = Program::authenticated_transfer_program(); + let key = PrivateKey::try_new([1; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let initial_balance = 100; + let initial_data = [(account_id, initial_balance)]; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let from = account_id; + let from_key = key; + let to = AccountId::new([2; 32]); + let amount: u128 = 37; + + // Check the recipient is an uninitialized account + assert_eq!(state.get_account_by_id(&to), Account::default()); + + let expected_to_post = Account { + // The expected program owner is the authenticated transfer program + program_owner: auth_transfer.id(), + balance: amount, + ..Account::default() + }; + + // The transaction executes the chain_caller program, which internally calls the + // authenticated_transfer program + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + Program::authenticated_transfer_program().id(), + 1, + None, + ); + let message = public_transaction::Message::try_new( + chain_caller.id(), + vec![to, from], // The chain_caller program permutes the account order in the chain + // call + vec![0], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx).unwrap(); + + let from_post = state.get_account_by_id(&from); + let to_post = state.get_account_by_id(&to); + assert_eq!(from_post.balance, initial_balance - amount); + assert_eq!(to_post, expected_to_post); + } + + #[test] + fn test_pda_mechanism_with_pinata_token_program() { + let pinata_token = Program::pinata_token(); + let token = Program::token(); + + let pinata_definition_id = AccountId::new([1; 32]); + let pinata_token_definition_id = AccountId::new([2; 32]); + // Total supply of pinata token will be in an account under a PDA. + let pinata_token_holding_id = AccountId::from((&pinata_token.id(), &PdaSeed::new([0; 32]))); + let winner_token_holding_id = AccountId::new([3; 32]); + + let mut expected_winner_account_data = [0; 49]; + expected_winner_account_data[0] = 1; + expected_winner_account_data[1..33].copy_from_slice(pinata_token_definition_id.value()); + expected_winner_account_data[33..].copy_from_slice(&150u128.to_le_bytes()); + let expected_winner_token_holding_post = Account { + program_owner: token.id(), + data: expected_winner_account_data.to_vec().try_into().unwrap(), + ..Account::default() + }; + + let mut state = V02State::new_with_genesis_accounts(&[], &[]); + state.add_pinata_token_program(pinata_definition_id); + + // Execution of the token program to create new token for the pinata token + // definition and supply accounts + let total_supply: u128 = 10_000_000; + // instruction: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] + let mut instruction: [u8; 23] = [0; 23]; + instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); + instruction[17..].copy_from_slice(b"PINATA"); + let message = public_transaction::Message::try_new( + token.id(), + vec![pinata_token_definition_id, pinata_token_holding_id], + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + // Execution of the token program transfer just to initialize the winner token account + let mut instruction: [u8; 23] = [0; 23]; + instruction[0] = 2; + let message = public_transaction::Message::try_new( + token.id(), + vec![pinata_token_definition_id, winner_token_holding_id], + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + // Submit a solution to the pinata program to claim the prize + let solution: u128 = 989106; + let message = public_transaction::Message::try_new( + pinata_token.id(), + vec![ + pinata_definition_id, + pinata_token_holding_id, + winner_token_holding_id, + ], + vec![], + solution, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let winner_token_holding_post = state.get_account_by_id(&winner_token_holding_id); + assert_eq!( + winner_token_holding_post, + expected_winner_token_holding_post + ); + } + + #[test] + fn test_claiming_mechanism_cannot_claim_initialied_accounts() { + let claimer = Program::claimer(); + let mut state = V02State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let account_id = AccountId::new([2; 32]); + + // Insert an account with non-default program owner + state.force_insert_account( + account_id, + Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + ..Account::default() + }, + ); + + let message = + public_transaction::Message::try_new(claimer.id(), vec![account_id], vec![], ()) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx); + + assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))) + } + + /// This test ensures that even if a malicious program tries to perform overflow of balances + /// it will not be able to break the balance validation. + #[test] + fn test_malicious_program_cannot_break_balance_validation() { + let sender_key = PrivateKey::try_new([37; 32]).unwrap(); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); + let sender_init_balance: u128 = 10; + + let recipient_key = PrivateKey::try_new([42; 32]).unwrap(); + let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key)); + let recipient_init_balance: u128 = 10; + + let mut state = V02State::new_with_genesis_accounts( + &[ + (sender_id, sender_init_balance), + (recipient_id, recipient_init_balance), + ], + &[], + ); + + state.insert_program(Program::modified_transfer_program()); + + let balance_to_move: u128 = 4; + + let sender = + AccountWithMetadata::new(state.get_account_by_id(&sender_id.clone()), true, sender_id); + + let sender_nonce = sender.account.nonce; + + let _recipient = + AccountWithMetadata::new(state.get_account_by_id(&recipient_id), false, sender_id); + + let message = public_transaction::Message::try_new( + Program::modified_transfer_program().id(), + vec![sender_id, recipient_id], + vec![sender_nonce], + balance_to_move, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); + let tx = PublicTransaction::new(message, witness_set); + let res = state.transition_from_public_transaction(&tx); + assert!(matches!(res, Err(NssaError::InvalidProgramBehavior))); + + let sender_post = state.get_account_by_id(&sender_id); + let recipient_post = state.get_account_by_id(&recipient_id); + + let expected_sender_post = { + let mut this = state.get_account_by_id(&sender_id); + this.balance = sender_init_balance; + this.nonce = 0; + this + }; + + let expected_recipient_post = { + let mut this = state.get_account_by_id(&sender_id); + this.balance = recipient_init_balance; + this.nonce = 0; + this + }; + + assert!(expected_sender_post == sender_post); + assert!(expected_recipient_post == recipient_post); + } } diff --git a/nssa/test_program_methods/guest/Cargo.lock b/nssa/test_program_methods/guest/Cargo.lock index 85f566c..b2337cc 100644 --- a/nssa/test_program_methods/guest/Cargo.lock +++ b/nssa/test_program_methods/guest/Cargo.lock @@ -1583,6 +1583,7 @@ dependencies = [ "chacha20", "risc0-zkvm", "serde", + "thiserror", ] [[package]] diff --git a/nssa/test_program_methods/guest/src/bin/burner.rs b/nssa/test_program_methods/guest/src/bin/burner.rs index 1ef7373..01b46b2 100644 --- a/nssa/test_program_methods/guest/src/bin/burner.rs +++ b/nssa/test_program_methods/guest/src/bin/burner.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = u128; @@ -17,5 +17,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.balance -= balance_to_burn; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]); } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index 028f8a0..ee01ffa 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -1,43 +1,54 @@ use nssa_core::program::{ - ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call, + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, }; use risc0_zkvm::serde::to_vec; -type Instruction = (u128, ProgramId, u32); +type Instruction = (u128, ProgramId, u32, Option); /// A program that calls another program `num_chain_calls` times. /// It permutes the order of the input accounts on the subsequent call +/// The `ProgramId` in the instruction must be the program_id of the authenticated transfers program fn main() { let ProgramInput { pre_states, - instruction: (balance, program_id, num_chain_calls), + instruction: (balance, auth_transfer_id, num_chain_calls, pda_seed), } = read_nssa_inputs::(); - let [sender_pre, receiver_pre] = match pre_states.try_into() { + let [recipient_pre, sender_pre] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; let instruction_data = to_vec(&balance).unwrap(); - let mut chained_call = vec![ - ChainedCall { - program_id, - instruction_data: instruction_data.clone(), - pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here - }; - num_chain_calls as usize - 1 - ]; + let mut running_recipient_pre = recipient_pre.clone(); + let mut running_sender_pre = sender_pre.clone(); - chained_call.push(ChainedCall { - program_id, - instruction_data, - pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here - }); + if pda_seed.is_some() { + running_sender_pre.is_authorized = true; + } + + let mut chained_calls = Vec::new(); + for _i in 0..num_chain_calls { + let new_chained_call = ChainedCall { + program_id: auth_transfer_id, + instruction_data: instruction_data.clone(), + pre_states: vec![running_sender_pre.clone(), running_recipient_pre.clone()], // <- Account order permutation here + pda_seeds: pda_seed.iter().cloned().collect(), + }; + chained_calls.push(new_chained_call); + + running_sender_pre.account.balance -= balance; + running_recipient_pre.account.balance += balance; + } write_nssa_outputs_with_chained_call( - vec![sender_pre.clone(), receiver_pre.clone()], - vec![sender_pre.account, receiver_pre.account], - chained_call, + vec![sender_pre.clone(), recipient_pre.clone()], + vec![ + AccountPostState::new(sender_pre.account), + AccountPostState::new(recipient_pre.account), + ], + chained_calls, ); } diff --git a/nssa/test_program_methods/guest/src/bin/claimer.rs b/nssa/test_program_methods/guest/src/bin/claimer.rs new file mode 100644 index 0000000..7687e5a --- /dev/null +++ b/nssa/test_program_methods/guest/src/bin/claimer.rs @@ -0,0 +1,19 @@ +use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; + +type Instruction = (); + +fn main() { + let ProgramInput { + pre_states, + instruction: _, + } = read_nssa_inputs::(); + + let [pre] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let account_post = AccountPostState::new_claimed(pre.account.clone()); + + write_nssa_outputs(vec![pre], vec![account_post]); +} diff --git a/nssa/test_program_methods/guest/src/bin/data_changer.rs b/nssa/test_program_methods/guest/src/bin/data_changer.rs index c7d34a2..b590886 100644 --- a/nssa/test_program_methods/guest/src/bin/data_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/data_changer.rs @@ -1,9 +1,10 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; -type Instruction = (); +type Instruction = Vec; +/// A program that modifies the account data by setting bytes sent in instruction. fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let ProgramInput { pre_states, instruction: data } = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -12,7 +13,7 @@ fn main() { let account_pre = &pre.account; let mut account_post = account_pre.clone(); - account_post.data.push(0); + account_post.data = data.try_into().expect("provided data should fit into data limit"); - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![AccountPostState::new_claimed(account_post)]); } diff --git a/nssa/test_program_methods/guest/src/bin/extra_output.rs b/nssa/test_program_methods/guest/src/bin/extra_output.rs index 3543d51..7137262 100644 --- a/nssa/test_program_methods/guest/src/bin/extra_output.rs +++ b/nssa/test_program_methods/guest/src/bin/extra_output.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::Account, - program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}, + program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}, }; type Instruction = (); @@ -15,5 +15,11 @@ fn main() { let account_pre = pre.account.clone(); - write_nssa_outputs(vec![pre], vec![account_pre, Account::default()]); + write_nssa_outputs( + vec![pre], + vec![ + AccountPostState::new(account_pre), + AccountPostState::new(Account::default()), + ], + ); } diff --git a/nssa/test_program_methods/guest/src/bin/minter.rs b/nssa/test_program_methods/guest/src/bin/minter.rs index 2ec97a9..5f69772 100644 --- a/nssa/test_program_methods/guest/src/bin/minter.rs +++ b/nssa/test_program_methods/guest/src/bin/minter.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput}; type Instruction = (); @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.balance += 1; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]); } diff --git a/nssa/test_program_methods/guest/src/bin/missing_output.rs b/nssa/test_program_methods/guest/src/bin/missing_output.rs index 7b6016c..f7d78be 100644 --- a/nssa/test_program_methods/guest/src/bin/missing_output.rs +++ b/nssa/test_program_methods/guest/src/bin/missing_output.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = (); @@ -12,5 +12,5 @@ fn main() { let account_pre1 = pre1.account.clone(); - write_nssa_outputs(vec![pre1, pre2], vec![account_pre1]); + write_nssa_outputs(vec![pre1, pre2], vec![AccountPostState::new(account_pre1)]); } diff --git a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs index b3b2599..fc24572 100644 --- a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput}; type Instruction = (); @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.nonce += 1; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]); } diff --git a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs index 49947cd..2fa5400 100644 --- a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput}; type Instruction = (); @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7]; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]); } diff --git a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs index 13263c5..be56e16 100644 --- a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs +++ b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = u128; @@ -20,6 +20,9 @@ fn main() { write_nssa_outputs( vec![sender_pre, receiver_pre], - vec![sender_post, receiver_post], + vec![ + AccountPostState::new(sender_post), + AccountPostState::new(receiver_post), + ], ); } diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index 0625fce..1223d1f 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -263,6 +263,7 @@ mod tests { seq_poll_max_retries: 10, seq_block_poll_max_amount: 100, initial_accounts: create_initial_accounts(), + basic_auth: None, } } diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 3625b15..2f5ef04 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -176,7 +176,12 @@ impl From for TokedDefinitionAccountView { fn from(value: TokenDefinition) -> Self { Self { account_type: "Token definition".to_string(), - name: hex::encode(value.name), + name: { + // Assuming, that name does not have UTF-8 NULL and all zeroes are padding. + let name_trimmed: Vec<_> = + value.name.into_iter().take_while(|ch| *ch != 0).collect(); + String::from_utf8(name_trimmed).unwrap_or(hex::encode(value.name)) + }, total_supply: value.total_supply, } } @@ -334,3 +339,47 @@ impl WalletSubcommand for AccountSubcommand { } } } + +#[cfg(test)] +mod tests { + use crate::cli::account::{TokedDefinitionAccountView, TokenDefinition}; + + #[test] + fn test_invalid_utf_8_name_of_token() { + let token_def = TokenDefinition { + account_type: 1, + name: [137, 12, 14, 3, 5, 4], + total_supply: 100, + }; + + let token_def_view: TokedDefinitionAccountView = token_def.into(); + + assert_eq!(token_def_view.name, "890c0e030504"); + } + + #[test] + fn test_valid_utf_8_name_of_token_all_bytes() { + let token_def = TokenDefinition { + account_type: 1, + name: [240, 159, 146, 150, 66, 66], + total_supply: 100, + }; + + let token_def_view: TokedDefinitionAccountView = token_def.into(); + + assert_eq!(token_def_view.name, "💖BB"); + } + + #[test] + fn test_valid_utf_8_name_of_token_less_bytes() { + let token_def = TokenDefinition { + account_type: 1, + name: [78, 65, 77, 69, 0, 0], + total_supply: 100, + }; + + let token_def_view: TokedDefinitionAccountView = token_def.into(); + + assert_eq!(token_def_view.name, "NAME"); + } +} diff --git a/wallet/src/cli/config.rs b/wallet/src/cli/config.rs index df0413e..d4b3f5a 100644 --- a/wallet/src/cli/config.rs +++ b/wallet/src/cli/config.rs @@ -73,6 +73,13 @@ impl WalletSubcommand for ConfigSubcommand { "initial_accounts" => { println!("{:#?}", wallet_core.storage.wallet_config.initial_accounts); } + "basic_auth" => { + if let Some(basic_auth) = &wallet_core.storage.wallet_config.basic_auth { + println!("{basic_auth}"); + } else { + println!("Not set"); + } + } _ => { println!("Unknown field"); } @@ -99,6 +106,9 @@ impl WalletSubcommand for ConfigSubcommand { wallet_core.storage.wallet_config.seq_block_poll_max_amount = value.parse()?; } + "basic_auth" => { + wallet_core.storage.wallet_config.basic_auth = Some(value.parse()?); + } "initial_accounts" => { anyhow::bail!("Setting this field from wallet is not supported"); } @@ -141,6 +151,9 @@ impl WalletSubcommand for ConfigSubcommand { "initial_accounts" => { println!("List of initial accounts' keys(both public and private)"); } + "basic_auth" => { + println!("Basic authentication credentials for sequencer HTTP requests"); + } _ => { println!("Unknown field"); } diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index e53849e..86d8210 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -13,7 +13,7 @@ use crate::{ token::TokenProgramAgnosticSubcommand, }, }, - helperfunctions::fetch_config, + helperfunctions::{fetch_config, merge_auth_config}, }; pub mod account; @@ -76,7 +76,7 @@ pub enum OverCommand { /// To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config /// -/// All account adresses must be valid 32 byte base58 strings. +/// All account addresses must be valid 32 byte base58 strings. /// /// All account account_ids must be provided as {privacy_prefix}/{account_id}, /// where valid options for `privacy_prefix` is `Public` and `Private` @@ -86,6 +86,9 @@ pub struct Args { /// Continious run flag #[arg(short, long)] pub continuous_run: bool, + /// Basic authentication in the format `user` or `user:password` + #[arg(long)] + pub auth: Option, /// Wallet command #[command(subcommand)] pub command: Option, @@ -101,7 +104,15 @@ pub enum SubcommandReturnValue { } pub async fn execute_subcommand(command: Command) -> Result { + execute_subcommand_with_auth(command, None).await +} + +pub async fn execute_subcommand_with_auth( + command: Command, + auth: Option, +) -> Result { let wallet_config = fetch_config().await?; + let wallet_config = merge_auth_config(wallet_config, auth)?; let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?; let subcommand_ret = match command { @@ -167,7 +178,11 @@ pub async fn execute_subcommand(command: Command) -> Result Result<()> { + execute_continuous_run_with_auth(None).await +} +pub async fn execute_continuous_run_with_auth(auth: Option) -> Result<()> { let config = fetch_config().await?; + let config = merge_auth_config(config, auth)?; let mut wallet_core = WalletCore::start_from_config_update_chain(config.clone()).await?; loop { @@ -186,7 +201,12 @@ pub async fn execute_continuous_run() -> Result<()> { } pub async fn execute_setup(password: String) -> Result<()> { + execute_setup_with_auth(password, None).await +} + +pub async fn execute_setup_with_auth(password: String, auth: Option) -> Result<()> { let config = fetch_config().await?; + let config = merge_auth_config(config, auth)?; let wallet_core = WalletCore::start_from_config_new_storage(config.clone(), password).await?; wallet_core.store_persistent_data().await?; @@ -195,7 +215,16 @@ pub async fn execute_setup(password: String) -> Result<()> { } pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<()> { + execute_keys_restoration_with_auth(password, depth, None).await +} + +pub async fn execute_keys_restoration_with_auth( + password: String, + depth: u32, + auth: Option, +) -> Result<()> { let config = fetch_config().await?; + let config = merge_auth_config(config, auth)?; let mut wallet_core = WalletCore::start_from_config_new_storage(config.clone(), password.clone()).await?; diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index c0e2223..7712a7c 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -197,6 +197,7 @@ async fn find_solution(wallet: &WalletCore, pinata_account_id: nssa::AccountId) let account = wallet.get_account_public(pinata_account_id).await?; let data: [u8; 33] = account .data + .as_ref() .try_into() .map_err(|_| anyhow::Error::msg("invalid pinata account data"))?; diff --git a/wallet/src/config.rs b/wallet/src/config.rs index ebcf283..c06ccc4 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use key_protocol::key_management::{ KeyChain, key_tree::{ @@ -6,6 +8,49 @@ use key_protocol::key_management::{ }; use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BasicAuth { + pub username: String, + pub password: Option, +} + +impl std::fmt::Display for BasicAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.username)?; + if let Some(password) = &self.password { + write!(f, ":{password}")?; + } + + Ok(()) + } +} + +impl FromStr for BasicAuth { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let parse = || { + let mut parts = s.splitn(2, ':'); + let username = parts.next()?; + let password = parts.next().filter(|p| !p.is_empty()); + if parts.next().is_some() { + return None; + } + + Some((username, password)) + }; + + let (username, password) = parse().ok_or_else(|| { + anyhow::anyhow!("Invalid auth format. Expected 'user' or 'user:password'") + })?; + + Ok(Self { + username: username.to_string(), + password: password.map(|p| p.to_string()), + }) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InitialAccountDataPublic { pub account_id: String, @@ -143,6 +188,8 @@ pub struct WalletConfig { pub seq_block_poll_max_amount: u64, /// Initial accounts for wallet pub initial_accounts: Vec, + /// Basic authentication credentials + pub basic_auth: Option, } impl Default for WalletConfig { @@ -154,6 +201,7 @@ impl Default for WalletConfig { seq_tx_poll_max_blocks: 5, seq_poll_max_retries: 5, seq_block_poll_max_amount: 100, + basic_auth: None, initial_accounts: { let init_acc_json = r#" [ diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 770d2bb..5f1dcf7 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -12,7 +12,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ HOME_DIR_ENV_VAR, config::{ - InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, + BasicAuth, InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig, }, }; @@ -89,6 +89,23 @@ pub async fn fetch_config() -> Result { Ok(config) } +/// Parse CLI auth string and merge with config auth, prioritizing CLI +pub fn merge_auth_config( + mut config: WalletConfig, + cli_auth: Option, +) -> Result { + if let Some(auth_str) = cli_auth { + let cli_auth_config: BasicAuth = auth_str.parse()?; + + if config.basic_auth.is_some() { + println!("Warning: CLI auth argument takes precedence over config basic-auth"); + } + + config.basic_auth = Some(cli_auth_config); + } + Ok(config) +} + /// Fetch data stored at home /// /// File must be created through setup beforehand. diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index d0a9014..e68e6a4 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -47,7 +47,14 @@ pub struct WalletCore { impl WalletCore { pub async fn start_from_config_update_chain(config: WalletConfig) -> Result { - let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); + let basic_auth = config + .basic_auth + .as_ref() + .map(|auth| (auth.username.clone(), auth.password.clone())); + let client = Arc::new(SequencerClient::new_with_auth( + config.sequencer_addr.clone(), + basic_auth, + )?); let tx_poller = TxPoller::new(config.clone(), client.clone()); let PersistentStorage { @@ -69,7 +76,14 @@ impl WalletCore { config: WalletConfig, password: String, ) -> Result { - let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); + let basic_auth = config + .basic_auth + .as_ref() + .map(|auth| (auth.username.clone(), auth.password.clone())); + let client = Arc::new(SequencerClient::new_with_auth( + config.sequencer_addr.clone(), + basic_auth, + )?); let tx_poller = TxPoller::new(config.clone(), client.clone()); let storage = WalletChainStore::new_storage(config, password)?; diff --git a/wallet/src/main.rs b/wallet/src/main.rs index 00aa6d0..1430b60 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -2,8 +2,8 @@ use anyhow::Result; use clap::{CommandFactory as _, Parser as _}; use tokio::runtime::Builder; use wallet::cli::{ - Args, OverCommand, execute_continuous_run, execute_keys_restoration, execute_setup, - execute_subcommand, + Args, OverCommand, execute_continuous_run_with_auth, execute_keys_restoration_with_auth, + execute_setup_with_auth, execute_subcommand_with_auth, }; pub const NUM_THREADS: usize = 2; @@ -13,7 +13,6 @@ pub const NUM_THREADS: usize = 2; // file path? // TODO #172: Why it requires config as env var while sequencer_runner accepts as // argument? -// TODO #171: Running pinata doesn't give output about transaction hash and etc. fn main() -> Result<()> { let runtime = Builder::new_multi_thread() .worker_threads(NUM_THREADS) @@ -29,16 +28,18 @@ fn main() -> Result<()> { if let Some(over_command) = args.command { match over_command { OverCommand::Command(command) => { - let _output = execute_subcommand(command).await?; + let _output = execute_subcommand_with_auth(command, args.auth).await?; Ok(()) } OverCommand::RestoreKeys { password, depth } => { - execute_keys_restoration(password, depth).await + execute_keys_restoration_with_auth(password, depth, args.auth).await + } + OverCommand::Setup { password } => { + execute_setup_with_auth(password, args.auth).await } - OverCommand::Setup { password } => execute_setup(password).await, } } else if args.continuous_run { - execute_continuous_run().await + execute_continuous_run_with_auth(args.auth).await } else { let help = Args::command().render_long_help(); println!("{help}");