diff --git a/Cargo.lock b/Cargo.lock index c3b8c5f8..cf582227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1019,19 +1019,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bitcoin-io" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" - [[package]] name = "bitcoin_hashes" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ - "bitcoin-io", "hex-conservative", ] @@ -1522,6 +1515,7 @@ dependencies = [ "log", "logos-blockchain-common-http-client", "nssa", + "nssa_core", "serde", "serde_with", "sha2", @@ -3977,7 +3971,6 @@ dependencies = [ "nssa", "nssa_core", "rand 0.8.5", - "secp256k1", "serde", "sha2", "thiserror 2.0.18", @@ -5269,13 +5262,13 @@ dependencies = [ "env_logger", "hex", "hex-literal 1.1.0", + "k256", "log", "nssa_core", "rand 0.8.5", "risc0-binfmt", "risc0-build", "risc0-zkvm", - "secp256k1", "serde", "serde_with", "sha2", @@ -7086,26 +7079,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" -dependencies = [ - "bitcoin_hashes", - "rand 0.9.2", - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" -dependencies = [ - "cc", -] - [[package]] name = "security-framework" version = "3.7.0" diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 3c5dc07d..b6867713 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index 2229f944..bcd528f1 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 24ca8f1e..142397f7 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 1ca1da1c..280a834f 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index 8662e7e5..4cd06c56 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index 3356ff8d..55880e41 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 1f5f0f67..b9c82387 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index f859956b..a740bdb8 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index f26e9642..112ca113 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index b5b18ca0..a130510b 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index a981ebe0..41a5cb3b 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 8eb4c87c..3dddebe1 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 5b3b299f..1d682ec3 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index d2a25ebe..c68496ab 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index c2c51f9b..ffd29461 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index 77794cc7..a2bbecd8 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index c417948e..b44b1233 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index fde6e1bd..e006fc75 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index b106e773..da811f60 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index e97267a2..3963873e 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 6df96057..08db47f0 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index 1a01c568..ceb5ae74 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index e6f06294..a7661f03 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/common/Cargo.toml b/common/Cargo.toml index a3884b70..0ae0b220 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] nssa.workspace = true +nssa_core.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/common/src/block.rs b/common/src/block.rs index 01ba586e..6decc390 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -1,4 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use nssa_core::{BlockId, Timestamp}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256, digest::FixedOutput as _}; @@ -6,8 +7,6 @@ use crate::{HashType, transaction::NSSATransaction}; pub type MantleMsgId = [u8; 32]; pub type BlockHash = HashType; -pub type BlockId = u64; -pub type TimeStamp = u64; #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] pub struct BlockMeta { @@ -35,7 +34,7 @@ pub struct BlockHeader { pub block_id: BlockId, pub prev_block_hash: BlockHash, pub hash: BlockHash, - pub timestamp: TimeStamp, + pub timestamp: Timestamp, pub signature: nssa::Signature, } @@ -75,7 +74,7 @@ impl<'de> Deserialize<'de> for Block { pub struct HashableBlockData { pub block_id: BlockId, pub prev_block_hash: BlockHash, - pub timestamp: TimeStamp, + pub timestamp: Timestamp, pub transactions: Vec, } diff --git a/common/src/transaction.rs b/common/src/transaction.rs index 1862dcc8..ea0b9819 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -1,9 +1,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; use log::warn; use nssa::{AccountId, V03State}; +use nssa_core::{BlockId, Timestamp}; use serde::{Deserialize, Serialize}; -use crate::{HashType, block::BlockId}; +use crate::HashType; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub enum NSSATransaction { @@ -69,11 +70,12 @@ impl NSSATransaction { self, state: &mut V03State, block_id: BlockId, + timestamp: Timestamp, ) -> Result { match &self { - Self::Public(tx) => state.transition_from_public_transaction(tx, block_id), + Self::Public(tx) => state.transition_from_public_transaction(tx, block_id, timestamp), Self::PrivacyPreserving(tx) => { - state.transition_from_privacy_preserving_transaction(tx, block_id) + state.transition_from_privacy_preserving_transaction(tx, block_id, timestamp) } Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx), } diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world.rs b/examples/program_deployment/methods/guest/src/bin/hello_world.rs index 381b71c3..810e83f3 100644 --- a/examples/program_deployment/methods/guest/src/bin/hello_world.rs +++ b/examples/program_deployment/methods/guest/src/bin/hello_world.rs @@ -1,6 +1,4 @@ -use nssa_core::program::{ - AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs, -}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; // Hello-world example program. // @@ -45,13 +43,7 @@ fn main() { // Wrap the post state account values inside a `AccountPostState` instance. // This is used to forward the account claiming request if any - let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { - // This produces a claim request - AccountPostState::new_claimed(post_account) - } else { - // This doesn't produce a claim request - AccountPostState::new(post_account) - }; + let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized); // The output is a proposed state difference. It will only succeed if the pre states coincide // with the previous values of the accounts, and the transition to the post states conforms diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs index d90c072b..62908870 100644 --- a/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs @@ -1,6 +1,4 @@ -use nssa_core::program::{ - AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs, -}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; // Hello-world with authorization example program. // @@ -52,13 +50,7 @@ fn main() { // Wrap the post state account values inside a `AccountPostState` instance. // This is used to forward the account claiming request if any - let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { - // This produces a claim request - AccountPostState::new_claimed(post_account) - } else { - // This doesn't produce a claim request - AccountPostState::new(post_account) - }; + let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized); // The output is a proposed state difference. It will only succeed if the pre states coincide // with the previous values of the accounts, and the transition to the post states conforms diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs index 0b2885a8..7e29b5de 100644 --- a/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs @@ -1,8 +1,6 @@ use nssa_core::{ - account::{Account, AccountWithMetadata, Data}, - program::{ - AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs, - }, + account::{AccountWithMetadata, Data}, + program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}, }; // Hello-world with write + move_data example program. @@ -26,16 +24,6 @@ const MOVE_DATA_FUNCTION_ID: u8 = 1; type Instruction = (u8, Vec); -fn build_post_state(post_account: Account) -> AccountPostState { - if post_account.program_owner == DEFAULT_PROGRAM_ID { - // This produces a claim request - AccountPostState::new_claimed(post_account) - } else { - // This doesn't produce a claim request - AccountPostState::new(post_account) - } -} - fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState { // Construct the post state account values let post_account = { @@ -48,7 +36,7 @@ fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState { this }; - build_post_state(post_account) + AccountPostState::new_claimed_if_default(post_account, Claim::Authorized) } fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec { @@ -58,7 +46,7 @@ fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec< let from_post = { let mut this = from_pre.account; this.data = Data::default(); - build_post_state(this) + AccountPostState::new_claimed_if_default(this, Claim::Authorized) }; let to_post = { @@ -68,7 +56,7 @@ fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec< this.data = bytes .try_into() .expect("Data should fit within the allowed limits"); - build_post_state(this) + AccountPostState::new_claimed_if_default(this, Claim::Authorized) }; vec![from_post, to_post] diff --git a/explorer_service/src/pages/transaction_page.rs b/explorer_service/src/pages/transaction_page.rs index ed3d8aac..0a3fc8e2 100644 --- a/explorer_service/src/pages/transaction_page.rs +++ b/explorer_service/src/pages/transaction_page.rs @@ -177,13 +177,13 @@ pub fn TransactionPage() -> impl IntoView { encrypted_private_post_states, new_commitments, new_nullifiers, - validity_window + block_validity_window, + timestamp_validity_window, } = message; let WitnessSet { signatures_and_public_keys: _, proof, } = witness_set; - let proof_len = proof.map_or(0, |p| p.0.len()); view! {
@@ -214,8 +214,12 @@ pub fn TransactionPage() -> impl IntoView { {format!("{proof_len} bytes")}
- "Validity Window:" - {validity_window.to_string()} + "Block Validity Window:" + {block_validity_window.to_string()} +
+
+ "Timestamp Validity Window:" + {timestamp_validity_window.to_string()}
diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index e4534f76..7faf5376 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -3,10 +3,11 @@ use std::{path::Path, sync::Arc}; use anyhow::Result; use bedrock_client::HeaderId; use common::{ - block::{BedrockStatus, Block, BlockId}, + block::{BedrockStatus, Block}, transaction::NSSATransaction, }; use nssa::{Account, AccountId, V03State}; +use nssa_core::BlockId; use storage::indexer::RocksDBIO; use tokio::sync::RwLock; @@ -125,7 +126,11 @@ impl IndexerStore { transaction .clone() .transaction_stateless_check()? - .execute_check_on_state(&mut state_guard, block.header.block_id)?; + .execute_check_on_state( + &mut state_guard, + block.header.block_id, + block.header.timestamp, + )?; } } diff --git a/indexer/service/protocol/src/convert.rs b/indexer/service/protocol/src/convert.rs index 2777b512..eb79fa34 100644 --- a/indexer/service/protocol/src/convert.rs +++ b/indexer/service/protocol/src/convert.rs @@ -287,7 +287,8 @@ impl From for PrivacyPre encrypted_private_post_states, new_commitments, new_nullifiers, - validity_window, + block_validity_window, + timestamp_validity_window, } = value; Self { public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), @@ -302,7 +303,8 @@ impl From for PrivacyPre .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), - validity_window: validity_window.into(), + block_validity_window: block_validity_window.into(), + timestamp_validity_window: timestamp_validity_window.into(), } } } @@ -318,7 +320,8 @@ impl TryFrom for nssa::privacy_preserving_transaction: encrypted_private_post_states, new_commitments, new_nullifiers, - validity_window, + block_validity_window, + timestamp_validity_window, } = value; Ok(Self { public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), @@ -340,7 +343,10 @@ impl TryFrom for nssa::privacy_preserving_transaction: .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), - validity_window: validity_window + block_validity_window: block_validity_window + .try_into() + .map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?, + timestamp_validity_window: timestamp_validity_window .try_into() .map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?, }) @@ -692,13 +698,13 @@ impl From for common::HashType { // ValidityWindow conversions // ============================================================================ -impl From for ValidityWindow { - fn from(value: nssa_core::program::ValidityWindow) -> Self { +impl From> for ValidityWindow { + fn from(value: nssa_core::program::ValidityWindow) -> Self { Self((value.start(), value.end())) } } -impl TryFrom for nssa_core::program::ValidityWindow { +impl TryFrom for nssa_core::program::ValidityWindow { type Error = nssa_core::program::InvalidWindow; fn try_from(value: ValidityWindow) -> Result { diff --git a/indexer/service/protocol/src/lib.rs b/indexer/service/protocol/src/lib.rs index 0d8a7e14..59e936bf 100644 --- a/indexer/service/protocol/src/lib.rs +++ b/indexer/service/protocol/src/lib.rs @@ -235,7 +235,8 @@ pub struct PrivacyPreservingMessage { pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, - pub validity_window: ValidityWindow, + pub block_validity_window: ValidityWindow, + pub timestamp_validity_window: ValidityWindow, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index c5891b41..09ae96f5 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -124,7 +124,8 @@ impl MockIndexerService { indexer_service_protocol::Nullifier([tx_idx as u8; 32]), CommitmentSetDigest([0xff; 32]), )], - validity_window: ValidityWindow((None, None)), + block_validity_window: ValidityWindow((None, None)), + timestamp_validity_window: ValidityWindow((None, None)), }, witness_set: WitnessSet { signatures_and_public_keys: vec![], diff --git a/integration_tests/tests/program_deployment.rs b/integration_tests/tests/program_deployment.rs index bb46ba87..64f5a655 100644 --- a/integration_tests/tests/program_deployment.rs +++ b/integration_tests/tests/program_deployment.rs @@ -11,10 +11,13 @@ use integration_tests::{ NSSA_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, }; use log::info; -use nssa::{AccountId, program::Program}; +use nssa::program::Program; use sequencer_service_rpc::RpcClient as _; use tokio::test; -use wallet::cli::Command; +use wallet::cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, +}; #[test] async fn deploy_and_execute_program() -> Result<()> { @@ -40,14 +43,31 @@ async fn deploy_and_execute_program() -> Result<()> { // logic) let bytecode = std::fs::read(binary_filepath)?; let data_changer = Program::new(bytecode)?; - let account_id: AccountId = "11".repeat(16).parse()?; + + let SubcommandReturnValue::RegisterAccount { account_id } = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await? + else { + panic!("Expected RegisterAccount return value"); + }; + + let nonces = ctx.wallet().get_accounts_nonces(vec![account_id]).await?; + let private_key = ctx + .wallet() + .get_account_public_signing_key(account_id) + .unwrap(); let message = nssa::public_transaction::Message::try_new( data_changer.id(), vec![account_id], - vec![], + nonces, vec![0], )?; - let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[private_key]); let transaction = nssa::PublicTransaction::new(message, witness_set); let _response = ctx .sequencer_client() @@ -64,7 +84,7 @@ async fn deploy_and_execute_program() -> Result<()> { assert_eq!(post_state_account.program_owner, data_changer.id()); assert_eq!(post_state_account.balance, 0); assert_eq!(post_state_account.data.as_ref(), &[0]); - assert_eq!(post_state_account.nonce.0, 0); + assert_eq!(post_state_account.nonce.0, 1); info!("Successfully deployed and executed program"); diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index dad4c79e..6e6b190c 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -924,7 +924,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> { let home = tempfile::tempdir()?; let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into(); - let to = FfiBytes32::from_bytes([37; 32]); + let to: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into(); let amount: [u8; 16] = 100_u128.to_le_bytes(); let mut transfer_result = FfiTransferResult::default(); @@ -967,7 +967,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> { }; assert_eq!(from_balance, 9900); - assert_eq!(to_balance, 100); + assert_eq!(to_balance, 10100); unsafe { wallet_ffi_free_transfer_result(&raw mut transfer_result); diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index 5ce7e97c..022f3ccd 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -8,8 +8,6 @@ license = { workspace = true } workspace = true [dependencies] -secp256k1 = "0.31.1" - nssa.workspace = true nssa_core.workspace = true common.workspace = true diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs index 0b20a310..42130b1f 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -137,11 +137,12 @@ impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) { #[cfg(test)] mod tests { - use nssa_core::NullifierSecretKey; + use nssa_core::{NullifierPublicKey, NullifierSecretKey}; use super::*; use crate::key_management::{self, secret_holders::ViewingSecretKey}; + #[expect(clippy::redundant_type_annotations, reason = "TODO: clippy requires")] #[test] fn master_key_generation() { let seed: [u8; 64] = [ @@ -153,7 +154,7 @@ mod tests { let keys = ChildKeysPrivate::root(seed); - let expected_ssk = key_management::secret_holders::SecretSpendingKey([ + let expected_ssk: SecretSpendingKey = key_management::secret_holders::SecretSpendingKey([ 246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139, 222, 42, 195, 105, 33, 115, 97, 186, 0, 97, 14, 218, 191, ]); @@ -168,7 +169,7 @@ mod tests { 34, 234, 19, 222, 2, 22, 12, 163, 252, 88, 11, 0, 163, ]; - let expected_npk = nssa_core::NullifierPublicKey([ + let expected_npk: NullifierPublicKey = nssa_core::NullifierPublicKey([ 7, 123, 125, 191, 233, 183, 201, 4, 20, 214, 155, 210, 45, 234, 27, 240, 194, 111, 97, 247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2, ]); diff --git a/key_protocol/src/key_management/key_tree/keys_public.rs b/key_protocol/src/key_management/key_tree/keys_public.rs index d7c4f7cd..44d34936 100644 --- a/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -1,4 +1,4 @@ -use secp256k1::Scalar; +use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _}; use serde::{Deserialize, Serialize}; use crate::key_management::key_tree::traits::KeyNode; @@ -14,7 +14,6 @@ pub struct ChildKeysPublic { } impl ChildKeysPublic { - #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] fn compute_hash_value(&self, cci: u32) -> [u8; 64] { let mut hash_input = vec![]; @@ -22,16 +21,17 @@ impl ChildKeysPublic { // Non-harden. // BIP-032 compatibility requires 1-byte header from the public_key; // Not stored in `self.cpk.value()`. - let sk = secp256k1::SecretKey::from_byte_array(*self.cssk.value()) + let sk = k256::SecretKey::from_bytes(self.csk.value().into()) .expect("32 bytes, within curve order"); - let pk = secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &sk); - hash_input.extend_from_slice(&secp256k1::PublicKey::serialize(&pk)); + let pk = sk.public_key(); + hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes()); } else { // Harden. hash_input.extend_from_slice(&[0_u8]); hash_input.extend_from_slice(self.cssk.value()); } + #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] hash_input.extend_from_slice(&cci.to_be_bytes()); hmac_sha512::HMAC::mac(hash_input, self.ccc) @@ -42,8 +42,14 @@ impl KeyNode for ChildKeysPublic { fn root(seed: [u8; 64]) -> Self { let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub"); - let cssk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); + let cssk = nssa::PrivateKey::try_new( + *hash_value + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"), + ) + .expect("Expect a valid Private Key"); let csk = nssa::PrivateKey::tweak(cssk.value()).unwrap(); + let ccc = *hash_value.last_chunk::<32>().unwrap(); let cpk = nssa::PublicKey::new_from_private_key(&csk); @@ -65,7 +71,7 @@ impl KeyNode for ChildKeysPublic { .expect("hash_value is 64 bytes, must be safe to get first 32"), ) .unwrap(); - +/* let cssk = nssa::PrivateKey::try_new({ cssk.add_tweak(&Scalar::from_be_bytes(*self.cssk.value()).unwrap()) .expect("Expect a valid Scalar") @@ -79,6 +85,21 @@ impl KeyNode for ChildKeysPublic { secp256k1::constants::CURVE_ORDER >= *csk.value(), "Secret key cannot exceed curve order" ); + */ + let csk = nssa::PrivateKey::try_new({ + let hash_value = hash_value + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"); + + let value_1 = + k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar"); + let value_2 = k256::Scalar::from_repr((*self.csk.value()).into()) + .expect("Expect a valid k256 scalar"); + + let sum = value_1.add(&value_2); + sum.to_bytes().into() + }) + .expect("Expect a valid private key"); let ccc = *hash_value .last_chunk::<32>() diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index ceebaa2e..07f5fe53 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -19,7 +19,7 @@ sha2.workspace = true rand.workspace = true borsh.workspace = true hex.workspace = true -secp256k1 = "0.31.1" +k256.workspace = true risc0-binfmt = "3.0.2" log.workspace = true diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index f9cd9239..998f6d71 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -5,7 +5,7 @@ use crate::{ NullifierSecretKey, SharedSecretKey, account::{Account, AccountWithMetadata}, encryption::Ciphertext, - program::{ProgramId, ProgramOutput, ValidityWindow}, + program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow}, }; #[derive(Serialize, Deserialize)] @@ -36,7 +36,8 @@ pub struct PrivacyPreservingCircuitOutput { pub ciphertexts: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, - pub validity_window: ValidityWindow, + pub block_validity_window: BlockValidityWindow, + pub timestamp_validity_window: TimestampValidityWindow, } #[cfg(feature = "host")] @@ -102,7 +103,8 @@ mod tests { ), [0xab; 32], )], - validity_window: (Some(1), None).try_into().unwrap(), + block_validity_window: (1..).into(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), }; let bytes = output.to_bytes(); let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap(); diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index 8014c7ca..a4fcdee1 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -21,3 +21,7 @@ pub mod program; #[cfg(feature = "host")] pub mod error; + +pub type BlockId = u64; +/// Unix timestamp in milliseconds. +pub type Timestamp = u64; diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index b9fa5de2..673e09b3 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -5,7 +5,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; -use crate::account::{Account, AccountId, AccountWithMetadata}; +use crate::{ + BlockId, Timestamp, + account::{Account, AccountId, AccountWithMetadata}, +}; pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8]; pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; @@ -22,7 +25,7 @@ pub struct ProgramInput { /// Each program can derive up to `2^256` unique account IDs by choosing different /// seeds. PDAs allow programs to control namespaced account identifiers without /// collisions between programs. -#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] pub struct PdaSeed([u8; 32]); impl PdaSeed { @@ -91,11 +94,26 @@ impl ChainedCall { /// A post state may optionally request that the executing program /// becomes the owner of the account (a “claim”). This is used to signal /// that the program intends to take ownership of the account. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(PartialEq, Eq))] pub struct AccountPostState { account: Account, - claim: bool, + claim: Option, +} + +/// A claim request for an account, indicating that the executing program intends to take ownership +/// of the account. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Claim { + /// The program requests ownership of the account which was authorized by the signer. + /// + /// Note that it's possible to successfully execute program outputting [`AccountPostState`] with + /// `is_authorized == false` and `claim == Some(Claim::Authorized)`. + /// This will give no error if program had authorization in pre state and may be useful + /// if program decides to give up authorization for a chained call. + Authorized, + /// The program requests ownership of the account through a PDA. + Pda(PdaSeed), } impl AccountPostState { @@ -105,7 +123,7 @@ impl AccountPostState { pub const fn new(account: Account) -> Self { Self { account, - claim: false, + claim: None, } } @@ -113,25 +131,27 @@ impl AccountPostState { /// This indicates that the executing program intends to claim the /// account as its own and is allowed to mutate it. #[must_use] - pub const fn new_claimed(account: Account) -> Self { + pub const fn new_claimed(account: Account, claim: Claim) -> Self { Self { account, - claim: true, + claim: Some(claim), } } /// Creates a post state that requests ownership of the account /// if the account's program owner is the default program ID. #[must_use] - pub fn new_claimed_if_default(account: Account) -> Self { - let claim = account.program_owner == DEFAULT_PROGRAM_ID; - Self { account, claim } + pub fn new_claimed_if_default(account: Account, claim: Claim) -> Self { + let is_default_owner = account.program_owner == DEFAULT_PROGRAM_ID; + Self { + account, + claim: is_default_owner.then_some(claim), + } } - /// Returns `true` if this post state requests that the account - /// be claimed (owned) by the executing program. + /// Returns whether this post state requires a claim. #[must_use] - pub const fn requires_claim(&self) -> bool { + pub const fn required_claim(&self) -> Option { self.claim } @@ -142,6 +162,7 @@ impl AccountPostState { } /// Returns the underlying account. + #[must_use] pub const fn account_mut(&mut self) -> &mut Account { &mut self.account } @@ -153,20 +174,21 @@ impl AccountPostState { } } -pub type BlockId = u64; +pub type BlockValidityWindow = ValidityWindow; +pub type TimestampValidityWindow = ValidityWindow; #[derive(Clone, Copy, Serialize, Deserialize)] #[cfg_attr( any(feature = "host", test), derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize) )] -pub struct ValidityWindow { - from: Option, - to: Option, +pub struct ValidityWindow { + from: Option, + to: Option, } -impl ValidityWindow { - /// Creates a window with no bounds, valid for every block ID. +impl ValidityWindow { + /// Creates a window with no bounds. #[must_use] pub const fn new_unbounded() -> Self { Self { @@ -174,42 +196,42 @@ impl ValidityWindow { to: None, } } +} - /// Returns `true` if `id` falls within the half-open range `[from, to)`. - /// A `None` bound on either side is treated as unbounded in that direction. +impl ValidityWindow { + /// Valid for values in the range [from, to), where `from` is included and `to` is excluded. #[must_use] - pub fn is_valid_for_block_id(&self, id: BlockId) -> bool { - self.from.is_none_or(|start| id >= start) && self.to.is_none_or(|end| id < end) + pub fn is_valid_for(&self, value: T) -> bool { + self.from.is_none_or(|start| value >= start) && self.to.is_none_or(|end| value < end) } /// Returns `Err(InvalidWindow)` if both bounds are set and `from >= to`. - const fn check_window(&self) -> Result<(), InvalidWindow> { - if let (Some(from_id), Some(until_id)) = (self.from, self.to) - && from_id >= until_id + fn check_window(&self) -> Result<(), InvalidWindow> { + if let (Some(from), Some(to)) = (self.from, self.to) + && from >= to { - Err(InvalidWindow) - } else { - Ok(()) + return Err(InvalidWindow); } + Ok(()) } - /// Inclusive lower bound. `None` means the window starts at the beginning of the chain. + /// Inclusive lower bound. `None` means no lower bound. #[must_use] - pub const fn start(&self) -> Option { + pub const fn start(&self) -> Option { self.from } - /// Exclusive upper bound. `None` means the window has no expiry. + /// Exclusive upper bound. `None` means no upper bound. #[must_use] - pub const fn end(&self) -> Option { + pub const fn end(&self) -> Option { self.to } } -impl TryFrom<(Option, Option)> for ValidityWindow { +impl TryFrom<(Option, Option)> for ValidityWindow { type Error = InvalidWindow; - fn try_from(value: (Option, Option)) -> Result { + fn try_from(value: (Option, Option)) -> Result { let this = Self { from: value.0, to: value.1, @@ -219,16 +241,16 @@ impl TryFrom<(Option, Option)> for ValidityWindow { } } -impl TryFrom> for ValidityWindow { +impl TryFrom> for ValidityWindow { type Error = InvalidWindow; - fn try_from(value: std::ops::Range) -> Result { + fn try_from(value: std::ops::Range) -> Result { (Some(value.start), Some(value.end)).try_into() } } -impl From> for ValidityWindow { - fn from(value: std::ops::RangeFrom) -> Self { +impl From> for ValidityWindow { + fn from(value: std::ops::RangeFrom) -> Self { Self { from: Some(value.start), to: None, @@ -236,8 +258,8 @@ impl From> for ValidityWindow { } } -impl From> for ValidityWindow { - fn from(value: std::ops::RangeTo) -> Self { +impl From> for ValidityWindow { + fn from(value: std::ops::RangeTo) -> Self { Self { from: None, to: Some(value.end), @@ -245,7 +267,7 @@ impl From> for ValidityWindow { } } -impl From for ValidityWindow { +impl From for ValidityWindow { fn from(_: std::ops::RangeFull) -> Self { Self::new_unbounded() } @@ -267,8 +289,10 @@ pub struct ProgramOutput { pub post_states: Vec, /// The list of chained calls to other programs. pub chained_calls: Vec, - /// The window where the program output is valid. - pub validity_window: ValidityWindow, + /// The block ID window where the program output is valid. + pub block_validity_window: BlockValidityWindow, + /// The timestamp window where the program output is valid. + pub timestamp_validity_window: TimestampValidityWindow, } impl ProgramOutput { @@ -282,7 +306,8 @@ impl ProgramOutput { pre_states, post_states, chained_calls: Vec::new(), - validity_window: ValidityWindow::new_unbounded(), + block_validity_window: ValidityWindow::new_unbounded(), + timestamp_validity_window: ValidityWindow::new_unbounded(), } } @@ -295,19 +320,52 @@ impl ProgramOutput { self } - /// Sets the validity window from an infallible range conversion (`1..`, `..5`, `..`). - pub fn with_validity_window>(mut self, window: W) -> Self { - self.validity_window = window.into(); + /// Sets the block ID validity window from an infallible range conversion (`1..`, `..5`, `..`). + pub fn with_block_validity_window>(mut self, window: W) -> Self { + self.block_validity_window = window.into(); self } - /// Sets the validity window from a fallible range conversion (`1..5`). + /// Sets the block ID validity window from a fallible range conversion (`1..5`). /// Returns `Err` if the range is empty. - pub fn try_with_validity_window>( + pub fn try_with_block_validity_window< + W: TryInto, + >( mut self, window: W, ) -> Result { - self.validity_window = window.try_into()?; + self.block_validity_window = window.try_into()?; + Ok(self) + } + + /// Sets the timestamp validity window from an infallible range conversion. + pub fn with_timestamp_validity_window>( + mut self, + window: W, + ) -> Self { + self.timestamp_validity_window = window.into(); + self + } + + /// Sets the timestamp validity window from a fallible range conversion. + /// Returns `Err` if the range is empty. + pub fn try_with_timestamp_validity_window< + W: TryInto, + >( + mut self, + window: W, + ) -> Result { + self.timestamp_validity_window = window.try_into()?; + Ok(self) + } + + pub fn valid_from_timestamp(mut self, ts: Option) -> Result { + self.timestamp_validity_window = (ts, self.timestamp_validity_window.end()).try_into()?; + Ok(self) + } + + pub fn valid_until_timestamp(mut self, ts: Option) -> Result { + self.timestamp_validity_window = (self.timestamp_validity_window.start(), ts).try_into()?; Ok(self) } } @@ -464,128 +522,131 @@ mod tests { use super::*; #[test] - fn validity_window_unbounded_accepts_any_block() { - let w = ValidityWindow::new_unbounded(); - assert!(w.is_valid_for_block_id(0)); - assert!(w.is_valid_for_block_id(u64::MAX)); + fn validity_window_unbounded_accepts_any_value() { + let w: ValidityWindow = ValidityWindow::new_unbounded(); + assert!(w.is_valid_for(0)); + assert!(w.is_valid_for(u64::MAX)); } #[test] fn validity_window_bounded_range_includes_from_excludes_to() { - let w: ValidityWindow = (Some(5), Some(10)).try_into().unwrap(); - assert!(!w.is_valid_for_block_id(4)); - assert!(w.is_valid_for_block_id(5)); - assert!(w.is_valid_for_block_id(9)); - assert!(!w.is_valid_for_block_id(10)); + let w: ValidityWindow = (Some(5), Some(10)).try_into().unwrap(); + assert!(!w.is_valid_for(4)); + assert!(w.is_valid_for(5)); + assert!(w.is_valid_for(9)); + assert!(!w.is_valid_for(10)); } #[test] fn validity_window_only_from_bound() { - let w: ValidityWindow = (Some(5), None).try_into().unwrap(); - assert!(!w.is_valid_for_block_id(4)); - assert!(w.is_valid_for_block_id(5)); - assert!(w.is_valid_for_block_id(u64::MAX)); + let w: ValidityWindow = (Some(5), None).try_into().unwrap(); + assert!(!w.is_valid_for(4)); + assert!(w.is_valid_for(5)); + assert!(w.is_valid_for(u64::MAX)); } #[test] fn validity_window_only_to_bound() { - let w: ValidityWindow = (None, Some(5)).try_into().unwrap(); - assert!(w.is_valid_for_block_id(0)); - assert!(w.is_valid_for_block_id(4)); - assert!(!w.is_valid_for_block_id(5)); + let w: ValidityWindow = (None, Some(5)).try_into().unwrap(); + assert!(w.is_valid_for(0)); + assert!(w.is_valid_for(4)); + assert!(!w.is_valid_for(5)); } #[test] fn validity_window_adjacent_bounds_are_invalid() { // [5, 5) is an empty range — from == to - assert!(ValidityWindow::try_from((Some(5), Some(5))).is_err()); + assert!(ValidityWindow::::try_from((Some(5), Some(5))).is_err()); } #[test] fn validity_window_inverted_bounds_are_invalid() { - assert!(ValidityWindow::try_from((Some(10), Some(5))).is_err()); + assert!(ValidityWindow::::try_from((Some(10), Some(5))).is_err()); } #[test] fn validity_window_getters_match_construction() { - let w: ValidityWindow = (Some(3), Some(7)).try_into().unwrap(); + let w: ValidityWindow = (Some(3), Some(7)).try_into().unwrap(); assert_eq!(w.start(), Some(3)); assert_eq!(w.end(), Some(7)); } #[test] fn validity_window_getters_for_unbounded() { - let w = ValidityWindow::new_unbounded(); + let w: ValidityWindow = ValidityWindow::new_unbounded(); assert_eq!(w.start(), None); assert_eq!(w.end(), None); } #[test] fn validity_window_from_range() { - let w = ValidityWindow::try_from(5_u64..10).unwrap(); + let w: ValidityWindow = ValidityWindow::try_from(5_u64..10).unwrap(); assert_eq!(w.start(), Some(5)); assert_eq!(w.end(), Some(10)); } #[test] fn validity_window_from_range_empty_is_invalid() { - assert!(ValidityWindow::try_from(5_u64..5).is_err()); + assert!(ValidityWindow::::try_from(5_u64..5).is_err()); } #[test] fn validity_window_from_range_inverted_is_invalid() { let from = 10_u64; let to = 5_u64; - assert!(ValidityWindow::try_from(from..to).is_err()); + assert!(ValidityWindow::::try_from(from..to).is_err()); } #[test] fn validity_window_from_range_from() { - let w: ValidityWindow = (5_u64..).into(); + let w: ValidityWindow = (5_u64..).into(); assert_eq!(w.start(), Some(5)); assert_eq!(w.end(), None); } #[test] fn validity_window_from_range_to() { - let w: ValidityWindow = (..10_u64).into(); + let w: ValidityWindow = (..10_u64).into(); assert_eq!(w.start(), None); assert_eq!(w.end(), Some(10)); } #[test] fn validity_window_from_range_full() { - let w: ValidityWindow = (..).into(); + let w: ValidityWindow = (..).into(); assert_eq!(w.start(), None); assert_eq!(w.end(), None); } #[test] - fn program_output_try_with_validity_window_range() { + fn program_output_try_with_block_validity_window_range() { let output = ProgramOutput::new(vec![], vec![], vec![]) - .try_with_validity_window(10_u64..100) + .try_with_block_validity_window(10_u64..100) .unwrap(); - assert_eq!(output.validity_window.start(), Some(10)); - assert_eq!(output.validity_window.end(), Some(100)); + assert_eq!(output.block_validity_window.start(), Some(10)); + assert_eq!(output.block_validity_window.end(), Some(100)); } #[test] - fn program_output_with_validity_window_range_from() { - let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(10_u64..); - assert_eq!(output.validity_window.start(), Some(10)); - assert_eq!(output.validity_window.end(), None); + fn program_output_with_block_validity_window_range_from() { + let output = + ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(10_u64..); + assert_eq!(output.block_validity_window.start(), Some(10)); + assert_eq!(output.block_validity_window.end(), None); } #[test] - fn program_output_with_validity_window_range_to() { - let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(..100_u64); - assert_eq!(output.validity_window.start(), None); - assert_eq!(output.validity_window.end(), Some(100)); + fn program_output_with_block_validity_window_range_to() { + let output = + ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(..100_u64); + assert_eq!(output.block_validity_window.start(), None); + assert_eq!(output.block_validity_window.end(), Some(100)); } #[test] - fn program_output_try_with_validity_window_empty_range_fails() { - let result = ProgramOutput::new(vec![], vec![], vec![]).try_with_validity_window(5_u64..5); + fn program_output_try_with_block_validity_window_empty_range_fails() { + let result = + ProgramOutput::new(vec![], vec![], vec![]).try_with_block_validity_window(5_u64..5); assert!(result.is_err()); } @@ -598,10 +659,10 @@ mod tests { nonce: 10_u128.into(), }; - let account_post_state = AccountPostState::new_claimed(account.clone()); + let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized); assert_eq!(account, account_post_state.account); - assert!(account_post_state.requires_claim()); + assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized)); } #[test] @@ -616,7 +677,7 @@ mod tests { let account_post_state = AccountPostState::new(account.clone()); assert_eq!(account, account_post_state.account); - assert!(!account_post_state.requires_claim()); + assert!(account_post_state.required_claim().is_none()); } #[test] diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 15d4f044..61966515 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -29,7 +29,10 @@ pub enum NssaError { Io(#[from] io::Error), #[error("Invalid Public Key")] - InvalidPublicKey(#[source] secp256k1::Error), + InvalidPublicKey(#[source] k256::schnorr::Error), + + #[error("Invalid hex for public key")] + InvalidHexPublicKey(hex::FromHexError), #[error("Risc0 error: {0}")] ProgramWriteInputFailed(String), diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 755b54f3..85f4a202 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -3,7 +3,7 @@ use nssa_core::{ Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput, account::{Account, Nonce}, encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey}, - program::ValidityWindow, + program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; @@ -53,7 +53,8 @@ pub struct Message { pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, - pub validity_window: ValidityWindow, + pub block_validity_window: BlockValidityWindow, + pub timestamp_validity_window: TimestampValidityWindow, } impl std::fmt::Debug for Message { @@ -79,7 +80,8 @@ impl std::fmt::Debug for Message { ) .field("new_commitments", &self.new_commitments) .field("new_nullifiers", &nullifiers) - .field("validity_window", &self.validity_window) + .field("block_validity_window", &self.block_validity_window) + .field("timestamp_validity_window", &self.timestamp_validity_window) .finish() } } @@ -112,7 +114,8 @@ impl Message { encrypted_private_post_states, new_commitments: output.new_commitments, new_nullifiers: output.new_nullifiers, - validity_window: output.validity_window, + block_validity_window: output.block_validity_window, + timestamp_validity_window: output.timestamp_validity_window, }) } } @@ -123,6 +126,7 @@ pub mod tests { Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey, account::Account, encryption::{EphemeralPublicKey, ViewingPublicKey}, + program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; @@ -165,7 +169,8 @@ pub mod tests { encrypted_private_post_states, new_commitments, new_nullifiers, - validity_window: (None, None).try_into().unwrap(), + block_validity_window: BlockValidityWindow::new_unbounded(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), } } diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index b1c30109..977bb0d0 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -5,17 +5,14 @@ use std::{ use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, + BlockId, PrivacyPreservingCircuitOutput, Timestamp, account::{Account, AccountWithMetadata}, - program::{BlockId, ValidityWindow}, }; use sha2::{Digest as _, digest::FixedOutput as _}; use super::{message::Message, witness_set::WitnessSet}; use crate::{ - AccountId, V03State, - error::NssaError, - privacy_preserving_transaction::{circuit::Proof, message::EncryptedAccountData}, + AccountId, V03State, error::NssaError, privacy_preserving_transaction::circuit::Proof, }; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] @@ -37,6 +34,7 @@ impl PrivacyPreservingTransaction { &self, state: &V03State, block_id: BlockId, + timestamp: Timestamp, ) -> Result, NssaError> { let message = &self.message; let witness_set = &self.witness_set; @@ -94,7 +92,9 @@ impl PrivacyPreservingTransaction { } // Verify validity window - if !message.validity_window.is_valid_for_block_id(block_id) { + if !message.block_validity_window.is_valid_for(block_id) + || !message.timestamp_validity_window.is_valid_for(timestamp) + { return Err(NssaError::OutOfValidityWindow); } @@ -115,11 +115,7 @@ impl PrivacyPreservingTransaction { check_privacy_preserving_circuit_proof_is_valid( &witness_set.proof, &public_pre_states, - &message.public_post_states, - &message.encrypted_private_post_states, - &message.new_commitments, - &message.new_nullifiers, - &message.validity_window, + message, )?; // 5. Commitment freshness @@ -177,23 +173,21 @@ impl PrivacyPreservingTransaction { fn check_privacy_preserving_circuit_proof_is_valid( proof: &Proof, public_pre_states: &[AccountWithMetadata], - public_post_states: &[Account], - encrypted_private_post_states: &[EncryptedAccountData], - new_commitments: &[Commitment], - new_nullifiers: &[(Nullifier, CommitmentSetDigest)], - validity_window: &ValidityWindow, + message: &Message, ) -> Result<(), NssaError> { let output = PrivacyPreservingCircuitOutput { public_pre_states: public_pre_states.to_vec(), - public_post_states: public_post_states.to_vec(), - ciphertexts: encrypted_private_post_states + public_post_states: message.public_post_states.clone(), + ciphertexts: message + .encrypted_private_post_states .iter() .cloned() .map(|value| value.ciphertext) .collect(), - new_commitments: new_commitments.to_vec(), - new_nullifiers: new_nullifiers.to_vec(), - validity_window: validity_window.to_owned(), + new_commitments: message.new_commitments.clone(), + new_nullifiers: message.new_nullifiers.clone(), + block_validity_window: message.block_validity_window, + timestamp_validity_window: message.timestamp_validity_window, }; proof .is_valid_for(&output) diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 8aaf039e..6a27c0a4 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -3,8 +3,9 @@ use std::collections::{HashMap, HashSet, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use log::debug; use nssa_core::{ + BlockId, Timestamp, account::{Account, AccountId, AccountWithMetadata}, - program::{BlockId, ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, + program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution}, }; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -71,6 +72,7 @@ impl PublicTransaction { &self, state: &V03State, block_id: BlockId, + timestamp: Timestamp, ) -> Result, NssaError> { let message = self.message(); let witness_set = self.witness_set(); @@ -157,6 +159,10 @@ impl PublicTransaction { &chained_call.pda_seeds, ); + let is_authorized = |account_id: &AccountId| { + signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id) + }; + for pre in &program_output.pre_states { let account_id = pre.account_id; // Check that the program output pre_states coincide with the values in the public @@ -172,10 +178,8 @@ impl PublicTransaction { // Check that authorization flags are consistent with the provided ones or // authorized by program through the PDA mechanism - let is_authorized = signer_account_ids.contains(&account_id) - || authorized_pdas.contains(&account_id); ensure!( - pre.is_authorized == is_authorized, + pre.is_authorized == is_authorized(&account_id), NssaError::InvalidProgramBehavior ); } @@ -193,23 +197,42 @@ impl PublicTransaction { // Verify validity window ensure!( - program_output - .validity_window - .is_valid_for_block_id(block_id), + program_output.block_validity_window.is_valid_for(block_id) + && program_output + .timestamp_validity_window + .is_valid_for(timestamp), NssaError::OutOfValidityWindow ); - for post in program_output - .post_states - .iter_mut() - .filter(|post| post.requires_claim()) - { + for (i, post) in program_output.post_states.iter_mut().enumerate() { + let Some(claim) = post.required_claim() else { + continue; + }; // The invoked program can only claim accounts with default program id. - if post.account().program_owner == DEFAULT_PROGRAM_ID { - post.account_mut().program_owner = chained_call.program_id; - } else { - return Err(NssaError::InvalidProgramBehavior); + ensure!( + post.account().program_owner == DEFAULT_PROGRAM_ID, + NssaError::InvalidProgramBehavior + ); + + let account_id = program_output.pre_states[i].account_id; + + match claim { + Claim::Authorized => { + // The program can only claim accounts that were authorized by the signer. + ensure!( + is_authorized(&account_id), + NssaError::InvalidProgramBehavior + ); + } + Claim::Pda(seed) => { + // The program can only claim accounts that correspond to the PDAs it is + // authorized to claim. + let pda = AccountId::from((&chained_call.program_id, &seed)); + ensure!(account_id == pda, NssaError::InvalidProgramBehavior); + } } + + post.account_mut().program_owner = chained_call.program_id; } // Update the state diff @@ -368,7 +391,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -388,7 +411,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -409,7 +432,7 @@ pub mod tests { let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -429,7 +452,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -445,7 +468,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } } diff --git a/nssa/src/signature/mod.rs b/nssa/src/signature/mod.rs index 9dfd47b3..3a594da6 100644 --- a/nssa/src/signature/mod.rs +++ b/nssa/src/signature/mod.rs @@ -49,21 +49,28 @@ impl Signature { aux_random: [u8; 32], ) -> Self { let value = { - let secp = secp256k1::Secp256k1::new(); - let secret_key = secp256k1::SecretKey::from_byte_array(*key.value()).unwrap(); - let keypair = secp256k1::Keypair::from_secret_key(&secp, &secret_key); - let signature = secp.sign_schnorr_with_aux_rand(message, &keypair, &aux_random); - signature.to_byte_array() + let signing_key = k256::schnorr::SigningKey::from_bytes(key.value()) + .expect("Expect valid signing key"); + signing_key + .sign_raw(message, &aux_random) + .expect("Expect to produce a valid signature") + .to_bytes() }; + Self { value } } #[must_use] pub fn is_valid_for(&self, bytes: &[u8], public_key: &PublicKey) -> bool { - let pk = secp256k1::XOnlyPublicKey::from_byte_array(*public_key.value()).unwrap(); - let secp = secp256k1::Secp256k1::new(); - let sig = secp256k1::schnorr::Signature::from_byte_array(self.value); - secp.verify_schnorr(&sig, bytes, &pk).is_ok() + let Ok(pk) = k256::schnorr::VerifyingKey::from_bytes(public_key.value()) else { + return false; + }; + + let Ok(sig) = k256::schnorr::Signature::try_from(self.value.as_slice()) else { + return false; + }; + + pk.verify_raw(bytes, &sig).is_ok() } } diff --git a/nssa/src/signature/private_key.rs b/nssa/src/signature/private_key.rs index fc4a8aa2..be68110a 100644 --- a/nssa/src/signature/private_key.rs +++ b/nssa/src/signature/private_key.rs @@ -46,7 +46,7 @@ impl PrivateKey { } fn is_valid_key(value: [u8; 32]) -> bool { - secp256k1::SecretKey::from_byte_array(value).is_ok() + k256::SecretKey::from_bytes(&value.into()).is_ok() } pub fn try_new(value: [u8; 32]) -> Result { diff --git a/nssa/src/signature/public_key.rs b/nssa/src/signature/public_key.rs index ee0f5dbc..ebec6b62 100644 --- a/nssa/src/signature/public_key.rs +++ b/nssa/src/signature/public_key.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use k256::elliptic_curve::sec1::ToEncodedPoint as _; use nssa_core::account::AccountId; use serde_with::{DeserializeFromStr, SerializeDisplay}; use sha2::{Digest as _, Sha256}; @@ -27,8 +28,7 @@ impl FromStr for PublicKey { fn from_str(s: &str) -> Result { let mut bytes = [0_u8; 32]; - hex::decode_to_slice(s, &mut bytes) - .map_err(|_err| NssaError::InvalidPublicKey(secp256k1::Error::InvalidPublicKey))?; + hex::decode_to_slice(s, &mut bytes).map_err(NssaError::InvalidHexPublicKey)?; Self::try_new(bytes) } } @@ -46,19 +46,24 @@ impl PublicKey { #[must_use] pub fn new_from_private_key(key: &PrivateKey) -> Self { let value = { - let secret_key = secp256k1::SecretKey::from_byte_array(*key.value()).unwrap(); - let public_key = - secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &secret_key); - let (x_only, _) = public_key.x_only_public_key(); - x_only.serialize() + let secret_key = k256::SecretKey::from_bytes(&(*key.value()).into()) + .expect("Expect a valid private key"); + + let encoded = secret_key.public_key().to_encoded_point(false); + let x_only = encoded + .x() + .expect("Expect k256 point to have a x-coordinate"); + + *x_only.first_chunk().expect("x_only is exactly 32 bytes") }; Self(value) } pub fn try_new(value: [u8; 32]) -> Result { - // Check point is valid - let _ = secp256k1::XOnlyPublicKey::from_byte_array(value) - .map_err(NssaError::InvalidPublicKey)?; + // Check point is a valid x-only public key + let _ = + k256::schnorr::VerifyingKey::from_bytes(&value).map_err(NssaError::InvalidPublicKey)?; + Ok(Self(value)) } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 9b4b4980..ec37884e 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2,9 +2,10 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, + BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, + Timestamp, account::{Account, AccountId, Nonce}, - program::{BlockId, ProgramId}, + program::ProgramId, }; use crate::{ @@ -159,8 +160,9 @@ impl V03State { &mut self, tx: &PublicTransaction, block_id: BlockId, + timestamp: Timestamp, ) -> Result<(), NssaError> { - let state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?; + let state_diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; #[expect( clippy::iter_over_hash_type, @@ -184,9 +186,11 @@ impl V03State { &mut self, tx: &PrivacyPreservingTransaction, block_id: BlockId, + timestamp: Timestamp, ) -> Result<(), NssaError> { // 1. Verify the transaction satisfies acceptance criteria - let public_state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?; + let public_state_diff = + tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; let message = tx.message(); @@ -338,10 +342,11 @@ pub mod tests { use std::collections::HashMap; use nssa_core::{ - Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + BlockId, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, - program::{BlockId, PdaSeed, ProgramId, ValidityWindow}, + program::{BlockValidityWindow, PdaSeed, ProgramId, TimestampValidityWindow}, }; use crate::{ @@ -456,16 +461,19 @@ pub mod tests { fn transfer_transaction( from: AccountId, from_key: &PrivateKey, - nonce: u128, + from_nonce: u128, to: AccountId, + to_key: &PrivateKey, + to_nonce: u128, balance: u128, ) -> PublicTransaction { let account_ids = vec![from, to]; - let nonces = vec![Nonce(nonce)]; + let nonces = vec![Nonce(from_nonce), Nonce(to_nonce)]; let program_id = Program::authenticated_transfer_program().id(); let message = public_transaction::Message::try_new(program_id, account_ids, nonces, balance).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key]); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[from_key, to_key]); PublicTransaction::new(message, witness_set) } @@ -567,17 +575,18 @@ pub mod tests { let initial_data = [(account_id, 100)]; let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); let from = account_id; - let to = AccountId::new([2; 32]); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); assert_eq!(state.get_account_by_id(to), Account::default()); let balance_to_move = 5; - let tx = transfer_transaction(from, &key, 0, to, balance_to_move); - state.transition_from_public_transaction(&tx, 1).unwrap(); + let tx = transfer_transaction(from, &key, 0, to, &to_key, 0, balance_to_move); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); assert_eq!(state.get_account_by_id(from).balance, 95); assert_eq!(state.get_account_by_id(to).balance, 5); assert_eq!(state.get_account_by_id(from).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(to).nonce, Nonce(0)); + assert_eq!(state.get_account_by_id(to).nonce, Nonce(1)); } #[test] @@ -588,12 +597,13 @@ pub mod tests { let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); let from = account_id; let from_key = key; - let to = AccountId::new([2; 32]); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); let balance_to_move = 101; assert!(state.get_account_by_id(from).balance < balance_to_move); - let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move); - let result = state.transition_from_public_transaction(&tx, 1); + let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); assert_eq!(state.get_account_by_id(from).balance, 100); @@ -613,16 +623,17 @@ pub mod tests { let from = account_id2; let from_key = key2; let to = account_id1; + let to_key = key1; assert_ne!(state.get_account_by_id(to), Account::default()); let balance_to_move = 8; - let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move); - state.transition_from_public_transaction(&tx, 1).unwrap(); + let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); assert_eq!(state.get_account_by_id(from).balance, 192); assert_eq!(state.get_account_by_id(to).balance, 108); assert_eq!(state.get_account_by_id(from).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(to).nonce, Nonce(0)); + assert_eq!(state.get_account_by_id(to).nonce, Nonce(1)); } #[test] @@ -633,21 +644,38 @@ pub mod tests { let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); let initial_data = [(account_id1, 100)]; let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); - let account_id3 = AccountId::new([3; 32]); + let key3 = PrivateKey::try_new([3; 32]).unwrap(); + let account_id3 = AccountId::from(&PublicKey::new_from_private_key(&key3)); let balance_to_move = 5; - let tx = transfer_transaction(account_id1, &key1, 0, account_id2, balance_to_move); - state.transition_from_public_transaction(&tx, 1).unwrap(); + let tx = transfer_transaction( + account_id1, + &key1, + 0, + account_id2, + &key2, + 0, + balance_to_move, + ); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let balance_to_move = 3; - let tx = transfer_transaction(account_id2, &key2, 0, account_id3, balance_to_move); - state.transition_from_public_transaction(&tx, 1).unwrap(); + let tx = transfer_transaction( + account_id2, + &key2, + 1, + account_id3, + &key3, + 0, + balance_to_move, + ); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); assert_eq!(state.get_account_by_id(account_id1).balance, 95); assert_eq!(state.get_account_by_id(account_id2).balance, 2); assert_eq!(state.get_account_by_id(account_id3).balance, 3); assert_eq!(state.get_account_by_id(account_id1).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(0)); + assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(2)); + assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(1)); } #[test] @@ -662,7 +690,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -679,7 +707,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -696,7 +724,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -720,7 +748,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -744,7 +772,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -768,7 +796,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -792,7 +820,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -820,7 +848,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -845,7 +873,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -863,7 +891,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -892,7 +920,7 @@ pub mod tests { .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -1085,7 +1113,7 @@ pub mod tests { assert!(!state.private_state.0.contains(&expected_new_commitment)); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let sender_post = state.get_account_by_id(sender_keys.account_id()); @@ -1155,7 +1183,7 @@ pub mod tests { assert!(!state.private_state.1.contains(&expected_new_nullifier)); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); assert_eq!(state.public_state, previous_public_state); @@ -1219,7 +1247,7 @@ pub mod tests { assert!(!state.private_state.1.contains(&expected_new_nullifier)); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let recipient_post = state.get_account_by_id(recipient_keys.account_id()); @@ -2147,7 +2175,7 @@ pub mod tests { ); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let sender_private_account = Account { @@ -2165,7 +2193,7 @@ pub mod tests { &state, ); - let result = state.transition_from_privacy_preserving_transaction(&tx, 1); + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); let NssaError::InvalidInput(error_message) = result.err().unwrap() else { @@ -2212,15 +2240,14 @@ pub mod tests { #[test] fn claiming_mechanism() { let program = Program::authenticated_transfer_program(); - let key = PrivateKey::try_new([1; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let from_key = PrivateKey::try_new([1; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); let initial_balance = 100; - let initial_data = [(account_id, initial_balance)]; + let initial_data = [(from, initial_balance)]; let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); - let from = account_id; - let from_key = key; - let to = AccountId::new([2; 32]); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); let amount: u128 = 37; // Check the recipient is an uninitialized account @@ -2229,26 +2256,80 @@ pub mod tests { let expected_recipient_post = Account { program_owner: program.id(), balance: amount, + nonce: Nonce(1), ..Account::default() }; let message = public_transaction::Message::try_new( program.id(), vec![from, to], - vec![Nonce(0)], + vec![Nonce(0), Nonce(0)], amount, ) .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let recipient_post = state.get_account_by_id(to); assert_eq!(recipient_post, expected_recipient_post); } + #[test] + fn unauthorized_public_account_claiming_fails() { + let program = Program::authenticated_transfer_program(); + let account_key = PrivateKey::try_new([9; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); + let mut state = V03State::new_with_genesis_accounts(&[], &[]); + + assert_eq!(state.get_account_by_id(account_id), Account::default()); + + let message = + public_transaction::Message::try_new(program.id(), vec![account_id], vec![], 0_u128) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); + assert_eq!(state.get_account_by_id(account_id), Account::default()); + } + + #[test] + fn authorized_public_account_claiming_succeeds() { + let program = Program::authenticated_transfer_program(); + let account_key = PrivateKey::try_new([10; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); + let mut state = V03State::new_with_genesis_accounts(&[], &[]); + + assert_eq!(state.get_account_by_id(account_id), Account::default()); + + let message = public_transaction::Message::try_new( + program.id(), + vec![account_id], + vec![Nonce(0)], + 0_u128, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&account_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + assert_eq!( + state.get_account_by_id(account_id), + Account { + program_owner: program.id(), + nonce: Nonce(1), + ..Account::default() + } + ); + } + #[test] fn public_chained_call() { let program = Program::chain_caller(); @@ -2285,7 +2366,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let from_post = state.get_account_by_id(from); let to_post = state.get_account_by_id(to); @@ -2325,7 +2406,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!( result, Err(NssaError::MaxChainedCallsDepthExceeded) @@ -2366,7 +2447,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let from_post = state.get_account_by_id(from); let to_post = state.get_account_by_id(to); @@ -2382,15 +2463,14 @@ pub mod tests { // program and not the chained_caller program. let chain_caller = Program::chain_caller(); let auth_transfer = Program::authenticated_transfer_program(); - let key = PrivateKey::try_new([1; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let from_key = PrivateKey::try_new([1; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); let initial_balance = 100; - let initial_data = [(account_id, initial_balance)]; + let initial_data = [(from, initial_balance)]; let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); - let from = account_id; - let from_key = key; - let to = AccountId::new([2; 32]); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); let amount: u128 = 37; // Check the recipient is an uninitialized account @@ -2400,6 +2480,7 @@ pub mod tests { // The expected program owner is the authenticated transfer program program_owner: auth_transfer.id(), balance: amount, + nonce: Nonce(1), ..Account::default() }; @@ -2415,14 +2496,15 @@ pub mod tests { chain_caller.id(), vec![to, from], // The chain_caller program permutes the account order in the chain // call - vec![Nonce(0)], + vec![Nonce(0), Nonce(0)], instruction, ) .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let from_post = state.get_account_by_id(from); let to_post = state.get_account_by_id(to); @@ -2430,6 +2512,88 @@ pub mod tests { assert_eq!(to_post, expected_to_post); } + #[test] + fn unauthorized_public_account_claiming_fails_when_executed_privately() { + let program = Program::authenticated_transfer_program(); + let account_id = AccountId::new([11; 32]); + let public_account = AccountWithMetadata::new(Account::default(), false, account_id); + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(0_u128).unwrap(), + vec![0], + vec![], + vec![], + vec![], + &program.into(), + ); + + assert!(matches!(result, Err(NssaError::ProgramProveFailed(_)))); + } + + #[test] + fn authorized_public_account_claiming_succeeds_when_executed_privately() { + let program = Program::authenticated_transfer_program(); + let program_id = program.id(); + let sender_keys = test_private_account_keys_1(); + let sender_private_account = Account { + program_owner: program_id, + balance: 100, + ..Account::default() + }; + let sender_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account); + let mut state = + V03State::new_with_genesis_accounts(&[], std::slice::from_ref(&sender_commitment)); + let sender_pre = AccountWithMetadata::new(sender_private_account, true, &sender_keys.npk()); + let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap(); + let recipient_account_id = + AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key)); + let recipient_pre = + AccountWithMetadata::new(Account::default(), true, recipient_account_id); + let esk = [5; 32]; + let shared_secret = SharedSecretKey::new(&esk, &sender_keys.vpk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let (output, proof) = execute_and_prove( + vec![sender_pre, recipient_pre], + Program::serialize_instruction(37_u128).unwrap(), + vec![1, 0], + vec![(sender_keys.npk(), shared_secret)], + vec![sender_keys.nsk], + vec![state.get_proof_for_commitment(&sender_commitment)], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![recipient_account_id], + vec![Nonce(0)], + vec![(sender_keys.npk(), sender_keys.vpk(), epk)], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk); + assert!(state.private_state.1.contains(&nullifier)); + + assert_eq!( + state.get_account_by_id(recipient_account_id), + Account { + program_owner: program_id, + balance: 37, + nonce: Nonce(1), + ..Account::default() + } + ); + } + #[test_case::test_case(1; "single call")] #[test_case::test_case(2; "two calls")] fn private_chained_call(number_of_calls: u32) { @@ -2531,7 +2695,7 @@ pub mod tests { let transaction = PrivacyPreservingTransaction::new(message, witness_set); state - .transition_from_privacy_preserving_transaction(&transaction, 1) + .transition_from_privacy_preserving_transaction(&transaction, 1, 0) .unwrap(); // Assert @@ -2571,36 +2735,47 @@ pub mod tests { let mut state = V03State::new_with_genesis_accounts(&[], &[]); state.add_pinata_token_program(pinata_definition_id); - // Execution of the token program to create new token for the pinata token - // definition and supply accounts + // Set up the token accounts directly (bypassing public transactions which + // would require signers for Claim::Authorized). The focus of this test is + // the PDA mechanism in the pinata program's chained call, not token creation. let total_supply: u128 = 10_000_000; - let instruction = token_core::Instruction::NewFungibleDefinition { + let token_definition = token_core::TokenDefinition::Fungible { name: String::from("PINATA"), total_supply, + metadata_id: None, }; - let message = public_transaction::Message::try_new( - token.id(), - vec![pinata_token_definition_id, pinata_token_holding_id], - vec![], - instruction, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); - - // Execution of winner's token holding account initialization - let instruction = token_core::Instruction::InitializeAccount; - let message = public_transaction::Message::try_new( - token.id(), - vec![pinata_token_definition_id, winner_token_holding_id], - vec![], - instruction, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + let token_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: total_supply, + }; + let winner_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: 0, + }; + state.force_insert_account( + pinata_token_definition_id, + Account { + program_owner: token.id(), + data: Data::from(&token_definition), + ..Account::default() + }, + ); + state.force_insert_account( + pinata_token_holding_id, + Account { + program_owner: token.id(), + data: Data::from(&token_holding), + ..Account::default() + }, + ); + state.force_insert_account( + winner_token_holding_id, + Account { + program_owner: token.id(), + data: Data::from(&winner_holding), + ..Account::default() + }, + ); // Submit a solution to the pinata program to claim the prize let solution: u128 = 989_106; @@ -2617,7 +2792,7 @@ pub mod tests { .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let winner_token_holding_post = state.get_account_by_id(winner_token_holding_id); assert_eq!( @@ -2647,7 +2822,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -2693,7 +2868,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); let tx = PublicTransaction::new(message, witness_set); - let res = state.transition_from_public_transaction(&tx, 1); + let res = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(res, Err(NssaError::InvalidProgramBehavior))); let sender_post = state.get_account_by_id(sender_id); @@ -2762,13 +2937,60 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); - let result = state.transition_from_privacy_preserving_transaction(&tx, 1); + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); assert!(result.is_ok()); let nullifier = Nullifier::for_account_initialization(&private_keys.npk()); assert!(state.private_state.1.contains(&nullifier)); } + #[test] + fn private_unauthorized_uninitialized_account_can_still_be_claimed() { + let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + + let private_keys = test_private_account_keys_1(); + // This is intentional: claim authorization was introduced to protect public accounts, + // especially PDAs. Private PDAs are not useful in practice because there is no way to + // operate them without the corresponding private keys, so unauthorized private claiming + // remains allowed. + let unauthorized_account = + AccountWithMetadata::new(Account::default(), false, &private_keys.npk()); + + let program = Program::claimer(); + let esk = [5; 32]; + let shared_secret = SharedSecretKey::new(&esk, &private_keys.vpk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let (output, proof) = execute_and_prove( + vec![unauthorized_account], + Program::serialize_instruction(0_u128).unwrap(), + vec![2], + vec![(private_keys.npk(), shared_secret)], + vec![], + vec![None], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![(private_keys.npk(), private_keys.vpk(), epk)], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + let nullifier = Nullifier::for_account_initialization(&private_keys.npk()); + assert!(state.private_state.1.contains(&nullifier)); + } + #[test] fn private_account_claimed_then_used_without_init_flag_should_fail() { let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); @@ -2815,7 +3037,7 @@ pub mod tests { // Claim should succeed assert!( state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .is_ok() ); @@ -2864,7 +3086,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); // Should succeed - no changes made, no claim needed assert!(result.is_ok()); @@ -2889,7 +3111,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); // Should fail - cannot modify data without claiming the account assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); @@ -3018,7 +3240,7 @@ pub mod tests { validity_window: (Option, Option), block_id: BlockId, ) { - let validity_window: ValidityWindow = validity_window.try_into().unwrap(); + let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap(); let validity_window_program = Program::validity_window(); let account_keys = test_public_account_keys_1(); let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); @@ -3027,21 +3249,76 @@ pub mod tests { let account_ids = vec![pre.account_id]; let nonces = vec![]; let program_id = validity_window_program.id(); - let message = public_transaction::Message::try_new( - program_id, - account_ids, - nonces, - validity_window, - ) - .unwrap(); + let instruction = ( + block_validity_window, + TimestampValidityWindow::new_unbounded(), + ); + let message = + public_transaction::Message::try_new(program_id, account_ids, nonces, instruction) + .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); PublicTransaction::new(message, witness_set) }; - let result = state.transition_from_public_transaction(&tx, block_id); - let is_inside_validity_window = match (validity_window.start(), validity_window.end()) { - (Some(s), Some(e)) => s <= block_id && block_id < e, - (Some(s), None) => s <= block_id, - (None, Some(e)) => block_id < e, + let result = state.transition_from_public_transaction(&tx, block_id, 0); + let is_inside_validity_window = + match (block_validity_window.start(), block_validity_window.end()) { + (Some(s), Some(e)) => s <= block_id && block_id < e, + (Some(s), None) => s <= block_id, + (None, Some(e)) => block_id < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(NssaError::OutOfValidityWindow))); + } + } + + #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] + #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] + #[test_case::test_case((Some(1), Some(3)), 0; "below range")] + #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] + #[test_case::test_case((Some(1), Some(3)), 4; "above range")] + #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] + #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] + #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] + #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] + #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] + #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] + #[test_case::test_case((None, None), 0; "no bounds - always valid")] + #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] + fn timestamp_validity_window_works_in_public_transactions( + validity_window: (Option, Option), + timestamp: Timestamp, + ) { + let timestamp_validity_window: TimestampValidityWindow = + validity_window.try_into().unwrap(); + let validity_window_program = Program::validity_window(); + let account_keys = test_public_account_keys_1(); + let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); + let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let tx = { + let account_ids = vec![pre.account_id]; + let nonces = vec![]; + let program_id = validity_window_program.id(); + let instruction = ( + BlockValidityWindow::new_unbounded(), + timestamp_validity_window, + ); + let message = + public_transaction::Message::try_new(program_id, account_ids, nonces, instruction) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) + }; + let result = state.transition_from_public_transaction(&tx, 1, timestamp); + let is_inside_validity_window = match ( + timestamp_validity_window.start(), + timestamp_validity_window.end(), + ) { + (Some(s), Some(e)) => s <= timestamp && timestamp < e, + (Some(s), None) => s <= timestamp, + (None, Some(e)) => timestamp < e, (None, None) => true, }; if is_inside_validity_window { @@ -3068,7 +3345,7 @@ pub mod tests { validity_window: (Option, Option), block_id: BlockId, ) { - let validity_window: ValidityWindow = validity_window.try_into().unwrap(); + let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap(); let validity_window_program = Program::validity_window(); let account_keys = test_private_account_keys_1(); let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk()); @@ -3078,9 +3355,13 @@ pub mod tests { let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); + let instruction = ( + block_validity_window, + TimestampValidityWindow::new_unbounded(), + ); let (output, proof) = circuit::execute_and_prove( vec![pre], - Program::serialize_instruction(validity_window).unwrap(), + Program::serialize_instruction(instruction).unwrap(), vec![2], vec![(account_keys.npk(), shared_secret)], vec![], @@ -3100,11 +3381,83 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, proof, &[]); PrivacyPreservingTransaction::new(message, witness_set) }; - let result = state.transition_from_privacy_preserving_transaction(&tx, block_id); - let is_inside_validity_window = match (validity_window.start(), validity_window.end()) { - (Some(s), Some(e)) => s <= block_id && block_id < e, - (Some(s), None) => s <= block_id, - (None, Some(e)) => block_id < e, + let result = state.transition_from_privacy_preserving_transaction(&tx, block_id, 0); + let is_inside_validity_window = + match (block_validity_window.start(), block_validity_window.end()) { + (Some(s), Some(e)) => s <= block_id && block_id < e, + (Some(s), None) => s <= block_id, + (None, Some(e)) => block_id < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(NssaError::OutOfValidityWindow))); + } + } + + #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] + #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] + #[test_case::test_case((Some(1), Some(3)), 0; "below range")] + #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] + #[test_case::test_case((Some(1), Some(3)), 4; "above range")] + #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] + #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] + #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] + #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] + #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] + #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] + #[test_case::test_case((None, None), 0; "no bounds - always valid")] + #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] + fn timestamp_validity_window_works_in_privacy_preserving_transactions( + validity_window: (Option, Option), + timestamp: Timestamp, + ) { + let timestamp_validity_window: TimestampValidityWindow = + validity_window.try_into().unwrap(); + let validity_window_program = Program::validity_window(); + let account_keys = test_private_account_keys_1(); + let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk()); + let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let tx = { + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let instruction = ( + BlockValidityWindow::new_unbounded(), + timestamp_validity_window, + ); + let (output, proof) = circuit::execute_and_prove( + vec![pre], + Program::serialize_instruction(instruction).unwrap(), + vec![2], + vec![(account_keys.npk(), shared_secret)], + vec![], + vec![None], + &validity_window_program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![(account_keys.npk(), account_keys.vpk(), epk)], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + PrivacyPreservingTransaction::new(message, witness_set) + }; + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, timestamp); + let is_inside_validity_window = match ( + timestamp_validity_window.start(), + timestamp_validity_window.end(), + ) { + (Some(s), Some(e)) => s <= timestamp && timestamp < e, + (Some(s), None) => s <= timestamp, + (None, Some(e)) => timestamp < e, (None, None) => true, }; if is_inside_validity_window { diff --git a/program_methods/guest/src/bin/authenticated_transfer.rs b/program_methods/guest/src/bin/authenticated_transfer.rs index 20f4dd68..2fb0ea8b 100644 --- a/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,13 +1,13 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, program::{ - AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs, + AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs, }, }; /// Initializes a default account under the ownership of this program. fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState { - let account_to_claim = AccountPostState::new_claimed(pre_state.account); + let account_to_claim = AccountPostState::new_claimed(pre_state.account, Claim::Authorized); let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values @@ -52,7 +52,7 @@ fn transfer( // Claim recipient account if it has default program owner if recipient_post_account.program_owner == DEFAULT_PROGRAM_ID { - AccountPostState::new_claimed(recipient_post_account) + AccountPostState::new_claimed(recipient_post_account, Claim::Authorized) } else { AccountPostState::new(recipient_post_account) } diff --git a/program_methods/guest/src/bin/pinata.rs b/program_methods/guest/src/bin/pinata.rs index cfe0a7e4..2f85f069 100644 --- a/program_methods/guest/src/bin/pinata.rs +++ b/program_methods/guest/src/bin/pinata.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; use risc0_zkvm::sha::{Impl, Sha256 as _}; const PRIZE: u128 = 150; @@ -82,7 +82,7 @@ fn main() { instruction_words, vec![pinata, winner], vec![ - AccountPostState::new_claimed_if_default(pinata_post), + AccountPostState::new_claimed_if_default(pinata_post, Claim::Authorized), AccountPostState::new(winner_post), ], ) diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index c1119dd8..e53334f9 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -10,8 +10,9 @@ use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Nonce}, compute_digest_for_path, program::{ - AccountPostState, ChainedCall, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramId, - ProgramOutput, ValidityWindow, validate_execution, + AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID, + MAX_NUMBER_CHAINED_CALLS, ProgramId, ProgramOutput, TimestampValidityWindow, + validate_execution, }, }; use risc0_zkvm::{guest::env, serde::to_vec}; @@ -20,29 +21,51 @@ use risc0_zkvm::{guest::env, serde::to_vec}; struct ExecutionState { pre_states: Vec, post_states: HashMap, - validity_window: ValidityWindow, + block_validity_window: BlockValidityWindow, + timestamp_validity_window: TimestampValidityWindow, } impl ExecutionState { /// Validate program outputs and derive the overall execution state. - pub fn derive_from_outputs(program_id: ProgramId, program_outputs: Vec) -> Self { - let valid_from_id = program_outputs + pub fn derive_from_outputs( + visibility_mask: &[u8], + program_id: ProgramId, + program_outputs: Vec, + ) -> Self { + let block_valid_from = program_outputs .iter() - .filter_map(|output| output.validity_window.start()) + .filter_map(|output| output.block_validity_window.start()) .max(); - let valid_until_id = program_outputs + let block_valid_until = program_outputs .iter() - .filter_map(|output| output.validity_window.end()) + .filter_map(|output| output.block_validity_window.end()) + .min(); + let ts_valid_from = program_outputs + .iter() + .filter_map(|output| output.timestamp_validity_window.start()) + .max(); + let ts_valid_until = program_outputs + .iter() + .filter_map(|output| output.timestamp_validity_window.end()) .min(); - let validity_window = (valid_from_id, valid_until_id).try_into().expect( - "There should be non empty intersection in the program output validity windows", - ); + let block_validity_window: BlockValidityWindow = (block_valid_from, block_valid_until) + .try_into() + .expect( + "There should be non empty intersection in the program output block validity windows", + ); + let timestamp_validity_window: TimestampValidityWindow = + (ts_valid_from, ts_valid_until) + .try_into() + .expect( + "There should be non empty intersection in the program output timestamp validity windows", + ); let mut execution_state = Self { pre_states: Vec::new(), post_states: HashMap::new(), - validity_window, + block_validity_window, + timestamp_validity_window, }; let Some(first_output) = program_outputs.first() else { @@ -102,6 +125,7 @@ impl ExecutionState { &chained_call.pda_seeds, ); execution_state.validate_and_sync_states( + visibility_mask, chained_call.program_id, &authorized_pdas, program_output.pre_states, @@ -134,7 +158,7 @@ impl ExecutionState { { assert_ne!( post.program_owner, DEFAULT_PROGRAM_ID, - "Account {account_id:?} was modified but not claimed" + "Account {account_id} was modified but not claimed" ); } @@ -144,6 +168,7 @@ impl ExecutionState { /// Validate program pre and post states and populate the execution state. fn validate_and_sync_states( &mut self, + visibility_mask: &[u8], program_id: ProgramId, authorized_pdas: &HashSet, pre_states: Vec, @@ -151,14 +176,25 @@ impl ExecutionState { ) { for (pre, mut post) in pre_states.into_iter().zip(post_states) { let pre_account_id = pre.account_id; + let pre_is_authorized = pre.is_authorized; let post_states_entry = self.post_states.entry(pre.account_id); match &post_states_entry { Entry::Occupied(occupied) => { + #[expect( + clippy::shadow_unrelated, + reason = "Shadowing is intentional to use all fields" + )] + let AccountWithMetadata { + account: pre_account, + account_id: pre_account_id, + is_authorized: pre_is_authorized, + } = pre; + // Ensure that new pre state is the same as known post state assert_eq!( occupied.get(), - &pre.account, - "Inconsistent pre state for account {pre_account_id:?}", + &pre_account, + "Inconsistent pre state for account {pre_account_id}", ); let previous_is_authorized = self @@ -167,7 +203,7 @@ impl ExecutionState { .find(|acc| acc.account_id == pre_account_id) .map_or_else( || panic!( - "Pre state must exist in execution state for account {pre_account_id:?}", + "Pre state must exist in execution state for account {pre_account_id}", ), |acc| acc.is_authorized ); @@ -176,22 +212,57 @@ impl ExecutionState { previous_is_authorized || authorized_pdas.contains(&pre_account_id); assert_eq!( - pre.is_authorized, is_authorized, - "Inconsistent authorization for account {pre_account_id:?}", + pre_is_authorized, is_authorized, + "Inconsistent authorization for account {pre_account_id}", ); } Entry::Vacant(_) => { + // Pre state for the initial call self.pre_states.push(pre); } } - if post.requires_claim() { + if let Some(claim) = post.required_claim() { // The invoked program can only claim accounts with default program id. - if post.account().program_owner == DEFAULT_PROGRAM_ID { - post.account_mut().program_owner = program_id; + assert_eq!( + post.account().program_owner, + DEFAULT_PROGRAM_ID, + "Cannot claim an initialized account {pre_account_id}" + ); + + let pre_state_position = self + .pre_states + .iter() + .position(|acc| acc.account_id == pre_account_id) + .expect("Pre state must exist at this point"); + + let is_public_account = visibility_mask[pre_state_position] == 0; + if is_public_account { + match claim { + Claim::Authorized => { + // Note: no need to check authorized pdas because we have already + // checked consistency of authorization above. + assert!( + pre_is_authorized, + "Cannot claim unauthorized account {pre_account_id}" + ); + } + Claim::Pda(seed) => { + let pda = AccountId::from((&program_id, &seed)); + assert_eq!( + pre_account_id, pda, + "Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}" + ); + } + } } else { - panic!("Cannot claim an initialized account {pre_account_id:?}"); + // We don't care about the exact claim mechanism for private accounts. + // This is because the main reason to have it is to protect against PDA griefing + // attacks in public execution, while private PDA doesn't make much sense + // anyway. } + + post.account_mut().program_owner = program_id; } post_states_entry.insert_entry(post.into_account()); @@ -225,7 +296,8 @@ fn compute_circuit_output( ciphertexts: Vec::new(), new_commitments: Vec::new(), new_nullifiers: Vec::new(), - validity_window: execution_state.validity_window, + block_validity_window: execution_state.block_validity_window, + timestamp_validity_window: execution_state.timestamp_validity_window, }; let states_iter = execution_state.into_states_iter(); @@ -408,7 +480,8 @@ fn main() { program_id, } = env::read(); - let execution_state = ExecutionState::derive_from_outputs(program_id, program_outputs); + let execution_state = + ExecutionState::derive_from_outputs(&visibility_mask, program_id, program_outputs); let output = compute_circuit_output( execution_state, diff --git a/programs/amm/src/new_definition.rs b/programs/amm/src/new_definition.rs index 366eb747..88263b87 100644 --- a/programs/amm/src/new_definition.rs +++ b/programs/amm/src/new_definition.rs @@ -2,11 +2,11 @@ use std::num::NonZeroU128; use amm_core::{ PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, - compute_pool_pda, compute_vault_pda, + compute_pool_pda, compute_pool_pda_seed, compute_vault_pda, compute_vault_pda_seed, }; use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::{AccountPostState, ChainedCall, ProgramId}, + program::{AccountPostState, ChainedCall, Claim, ProgramId}, }; #[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] @@ -108,36 +108,52 @@ pub fn new_definition( }; pool_post.data = Data::from(&pool_post_definition); - let pool_post = AccountPostState::new_claimed_if_default(pool_post); + let pool_pda_seed = compute_pool_pda_seed(definition_token_a_id, definition_token_b_id); + let pool_post = AccountPostState::new_claimed_if_default(pool_post, Claim::Pda(pool_pda_seed)); let token_program_id = user_holding_a.account.program_owner; // Chain call for Token A (user_holding_a -> Vault_A) + let vault_a_seed = compute_vault_pda_seed(pool.account_id, definition_token_a_id); + let vault_a_authorized = AccountWithMetadata { + is_authorized: true, + ..vault_a.clone() + }; let call_token_a = ChainedCall::new( token_program_id, - vec![user_holding_a.clone(), vault_a.clone()], + vec![user_holding_a.clone(), vault_a_authorized], &token_core::Instruction::Transfer { amount_to_transfer: token_a_amount.into(), }, - ); + ) + .with_pda_seeds(vec![vault_a_seed]); + // Chain call for Token B (user_holding_b -> Vault_B) + let vault_b_seed = compute_vault_pda_seed(pool.account_id, definition_token_b_id); + let vault_b_authorized = AccountWithMetadata { + is_authorized: true, + ..vault_b.clone() + }; let call_token_b = ChainedCall::new( token_program_id, - vec![user_holding_b.clone(), vault_b.clone()], + vec![user_holding_b.clone(), vault_b_authorized], &token_core::Instruction::Transfer { amount_to_transfer: token_b_amount.into(), }, - ); - - let mut pool_lp_auth = pool_definition_lp.clone(); - pool_lp_auth.is_authorized = true; + ) + .with_pda_seeds(vec![vault_b_seed]); + let pool_lp_pda_seed = compute_liquidity_token_pda_seed(pool.account_id); + let pool_lp_authorized = AccountWithMetadata { + is_authorized: true, + ..pool_definition_lp.clone() + }; let call_token_lp = ChainedCall::new( token_program_id, - vec![pool_lp_auth, user_holding_lp.clone()], + vec![pool_lp_authorized, user_holding_lp.clone()], &instruction, ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); + .with_pda_seeds(vec![pool_lp_pda_seed]); let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index 86fdb4ff..14638f9d 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -1,4 +1,4 @@ -use std::num::NonZero; +use std::{num::NonZero, vec}; use amm_core::{ PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, @@ -1756,7 +1756,7 @@ impl AccountsForExeTests { definition_id: IdForExeTests::token_lp_definition_id(), balance: BalanceForExeTests::lp_supply_init(), }), - nonce: 0_u128.into(), + nonce: 1_u128.into(), } } @@ -1801,7 +1801,7 @@ impl AccountsForExeTests { definition_id: IdForExeTests::token_lp_definition_id(), balance: 0, }), - nonce: 0_u128.into(), + nonce: 1.into(), } } } @@ -2733,7 +2733,7 @@ fn simple_amm_remove() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -2799,7 +2799,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() { IdForExeTests::user_token_b_id(), IdForExeTests::user_token_lp_id(), ], - vec![0_u128.into(), 0_u128.into()], + vec![0_u128.into(), 0_u128.into(), 0_u128.into()], instruction, ) .unwrap(); @@ -2809,11 +2809,12 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() { &[ &PrivateKeysForTests::user_token_a_key(), &PrivateKeysForTests::user_token_b_key(), + &PrivateKeysForTests::user_token_lp_key(), ], ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -2897,7 +2898,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_init_user_lp() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -2955,7 +2956,7 @@ fn simple_amm_new_definition_uninitialized_pool() { IdForExeTests::user_token_b_id(), IdForExeTests::user_token_lp_id(), ], - vec![0_u128.into(), 0_u128.into()], + vec![0_u128.into(), 0_u128.into(), 0_u128.into()], instruction, ) .unwrap(); @@ -2965,11 +2966,12 @@ fn simple_amm_new_definition_uninitialized_pool() { &[ &PrivateKeysForTests::user_token_a_key(), &PrivateKeysForTests::user_token_b_key(), + &PrivateKeysForTests::user_token_lp_key(), ], ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -3031,7 +3033,7 @@ fn simple_amm_add() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -3088,7 +3090,7 @@ fn simple_amm_swap_1() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -3138,7 +3140,7 @@ fn simple_amm_swap_2() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); diff --git a/programs/associated_token_account/src/create.rs b/programs/associated_token_account/src/create.rs index 86109952..d44f5d1c 100644 --- a/programs/associated_token_account/src/create.rs +++ b/programs/associated_token_account/src/create.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, - program::{AccountPostState, ChainedCall, ProgramId}, + program::{AccountPostState, ChainedCall, Claim, ProgramId}, }; pub fn create_associated_token_account( @@ -11,7 +11,7 @@ pub fn create_associated_token_account( ) -> (Vec, Vec) { // No authorization check needed: create is idempotent, so anyone can call it safely. let token_program_id = token_definition.account.program_owner; - ata_core::verify_ata_and_get_seed( + let ata_seed = ata_core::verify_ata_and_get_seed( &ata_account, &owner, token_definition.account_id, @@ -22,7 +22,7 @@ pub fn create_associated_token_account( if ata_account.account != Account::default() { return ( vec![ - AccountPostState::new_claimed_if_default(owner.account.clone()), + AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized), AccountPostState::new(token_definition.account.clone()), AccountPostState::new(ata_account.account.clone()), ], @@ -31,14 +31,20 @@ pub fn create_associated_token_account( } let post_states = vec![ - AccountPostState::new_claimed_if_default(owner.account.clone()), + AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized), AccountPostState::new(token_definition.account.clone()), AccountPostState::new(ata_account.account.clone()), ]; + let ata_account_auth = AccountWithMetadata { + is_authorized: true, + ..ata_account.clone() + }; let chained_call = ChainedCall::new( token_program_id, - vec![token_definition.clone(), ata_account.clone()], + vec![token_definition.clone(), ata_account_auth], &token_core::Instruction::InitializeAccount, - ); + ) + .with_pda_seeds(vec![ata_seed]); + (post_states, vec![chained_call]) } diff --git a/programs/token/core/src/lib.rs b/programs/token/core/src/lib.rs index 1edbc895..79f49303 100644 --- a/programs/token/core/src/lib.rs +++ b/programs/token/core/src/lib.rs @@ -10,23 +10,23 @@ pub enum Instruction { /// Transfer tokens from sender to recipient. /// /// Required accounts: - /// - Sender's Token Holding account (authorized), - /// - Recipient's Token Holding account. + /// - Sender's Token Holding account (initialized, authorized), + /// - Recipient's Token Holding account (initialized or authorized and uninitialized). Transfer { amount_to_transfer: u128 }, /// Create a new fungible token definition without metadata. /// /// Required accounts: - /// - Token Definition account (uninitialized), - /// - Token Holding account (uninitialized). + /// - Token Definition account (uninitialized, authorized), + /// - Token Holding account (uninitialized, authorized). NewFungibleDefinition { name: String, total_supply: u128 }, /// Create a new fungible or non-fungible token definition with metadata. /// /// Required accounts: - /// - Token Definition account (uninitialized), - /// - Token Holding account (uninitialized), - /// - Token Metadata account (uninitialized). + /// - Token Definition account (uninitialized, authorized), + /// - Token Holding account (uninitialized, authorized), + /// - Token Metadata account (uninitialized, authorized). NewDefinitionWithMetadata { new_definition: NewTokenDefinition, /// Boxed to avoid large enum variant size. @@ -36,29 +36,29 @@ pub enum Instruction { /// Initialize a token holding account for a given token definition. /// /// Required accounts: - /// - Token Definition account (initialized), - /// - Token Holding account (uninitialized), + /// - Token Definition account (initialized, any authorization), + /// - Token Holding account (uninitialized, authorized), InitializeAccount, /// Burn tokens from the holder's account. /// /// Required accounts: - /// - Token Definition account (initialized), - /// - Token Holding account (authorized). + /// - Token Definition account (initialized, any authorization), + /// - Token Holding account (initialized, authorized). Burn { amount_to_burn: u128 }, /// Mint new tokens to the holder's account. /// /// Required accounts: - /// - Token Definition account (authorized), - /// - Token Holding account (uninitialized or initialized). + /// - Token Definition account (initialized, authorized), + /// - Token Holding account (uninitialized or authorized and initialized). Mint { amount_to_mint: u128 }, /// Print a new NFT from the master copy. /// /// Required accounts: /// - NFT Master Token Holding account (authorized), - /// - NFT Printed Copy Token Holding account (uninitialized). + /// - NFT Printed Copy Token Holding account (uninitialized, authorized). PrintNft, } diff --git a/programs/token/src/initialize.rs b/programs/token/src/initialize.rs index dc0b612a..fabb8fd9 100644 --- a/programs/token/src/initialize.rs +++ b/programs/token/src/initialize.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::{TokenDefinition, TokenHolding}; @@ -30,6 +30,6 @@ pub fn initialize_account( vec![ AccountPostState::new(definition_post), - AccountPostState::new_claimed(account_to_initialize), + AccountPostState::new_claimed(account_to_initialize, Claim::Authorized), ] } diff --git a/programs/token/src/mint.rs b/programs/token/src/mint.rs index 8b157340..5a15d81f 100644 --- a/programs/token/src/mint.rs +++ b/programs/token/src/mint.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::{TokenDefinition, TokenHolding}; @@ -67,6 +67,6 @@ pub fn mint( vec![ AccountPostState::new(definition_post), - AccountPostState::new_claimed_if_default(holding_post), + AccountPostState::new_claimed_if_default(holding_post, Claim::Authorized), ] } diff --git a/programs/token/src/new_definition.rs b/programs/token/src/new_definition.rs index 8da55dc1..ba510feb 100644 --- a/programs/token/src/new_definition.rs +++ b/programs/token/src/new_definition.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::{ NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata, @@ -42,8 +42,8 @@ pub fn new_fungible_definition( holding_target_account_post.data = Data::from(&token_holding); vec![ - AccountPostState::new_claimed(definition_target_account_post), - AccountPostState::new_claimed(holding_target_account_post), + AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized), + AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized), ] } @@ -119,8 +119,8 @@ pub fn new_definition_with_metadata( metadata_target_account_post.data = Data::from(&token_metadata); vec![ - AccountPostState::new_claimed(definition_target_account_post), - AccountPostState::new_claimed(holding_target_account_post), - AccountPostState::new_claimed(metadata_target_account_post), + AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized), + AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized), + AccountPostState::new_claimed(metadata_target_account_post, Claim::Authorized), ] } diff --git a/programs/token/src/print_nft.rs b/programs/token/src/print_nft.rs index c7177a43..6bc9612d 100644 --- a/programs/token/src/print_nft.rs +++ b/programs/token/src/print_nft.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::TokenHolding; @@ -50,6 +50,6 @@ pub fn print_nft( vec![ AccountPostState::new(master_account_post), - AccountPostState::new_claimed(printed_account_post), + AccountPostState::new_claimed(printed_account_post, Claim::Authorized), ] } diff --git a/programs/token/src/tests.rs b/programs/token/src/tests.rs index 640d6d76..4c28d769 100644 --- a/programs/token/src/tests.rs +++ b/programs/token/src/tests.rs @@ -5,7 +5,10 @@ reason = "We don't care about it in tests" )] -use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data}; +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data}, + program::Claim, +}; use token_core::{ MetadataStandard, NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, }; @@ -851,7 +854,7 @@ fn mint_uninit_holding_success() { *holding_post.account(), AccountForTests::init_mint().account ); - assert!(holding_post.requires_claim()); + assert_eq!(holding_post.required_claim(), Some(Claim::Authorized)); } #[test] diff --git a/programs/token/src/transfer.rs b/programs/token/src/transfer.rs index 392f630e..2ffd2339 100644 --- a/programs/token/src/transfer.rs +++ b/programs/token/src/transfer.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::AccountPostState, + program::{AccountPostState, Claim}, }; use token_core::TokenHolding; @@ -106,6 +106,6 @@ pub fn transfer( vec![ AccountPostState::new(sender_post), - AccountPostState::new_claimed_if_default(recipient_post), + AccountPostState::new_claimed_if_default(recipient_post, Claim::Authorized), ] } diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 545c63fa..16667051 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -16,6 +16,7 @@ use mempool::{MemPool, MemPoolHandle}; #[cfg(feature = "mock")] pub use mock::SequencerCoreWithMockClients; use nssa::V03State; +use nssa_core::{BlockId, Timestamp}; pub use storage::error::DbError; use testnet_initial_state::initial_state; @@ -165,14 +166,16 @@ impl SequencerCore Result { match &tx { NSSATransaction::Public(tx) => self .state - .transition_from_public_transaction(tx, self.next_block_id()), + .transition_from_public_transaction(tx, block_id, timestamp), NSSATransaction::PrivacyPreserving(tx) => self .state - .transition_from_privacy_preserving_transaction(tx, self.next_block_id()), + .transition_from_privacy_preserving_transaction(tx, block_id, timestamp), NSSATransaction::ProgramDeployment(tx) => self .state .transition_from_program_deployment_transaction(tx), @@ -218,7 +221,7 @@ impl SequencerCore SequencerCore SequencerCore { valid_transactions.push(valid_tx); @@ -272,7 +276,7 @@ impl SequencerCore>, bool); @@ -28,7 +28,7 @@ fn main() { // Claim or not based on the boolean flag let post_state = if should_claim { - AccountPostState::new_claimed(account_post) + AccountPostState::new_claimed(account_post, Claim::Authorized) } else { AccountPostState::new(account_post) }; diff --git a/test_program_methods/guest/src/bin/claimer.rs b/test_program_methods/guest/src/bin/claimer.rs index 57e7e4e5..e6239381 100644 --- a/test_program_methods/guest/src/bin/claimer.rs +++ b/test_program_methods/guest/src/bin/claimer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = (); @@ -15,7 +15,7 @@ fn main() { return; }; - let account_post = AccountPostState::new_claimed(pre.account.clone()); + let account_post = AccountPostState::new_claimed(pre.account.clone(), Claim::Authorized); ProgramOutput::new(instruction_words, vec![pre], vec![account_post]).write(); } diff --git a/test_program_methods/guest/src/bin/data_changer.rs b/test_program_methods/guest/src/bin/data_changer.rs index 55f4e2a0..730a7180 100644 --- a/test_program_methods/guest/src/bin/data_changer.rs +++ b/test_program_methods/guest/src/bin/data_changer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; type Instruction = Vec; @@ -25,7 +25,10 @@ fn main() { ProgramOutput::new( instruction_words, vec![pre], - vec![AccountPostState::new_claimed(account_post)], + vec![AccountPostState::new_claimed( + account_post, + Claim::Authorized, + )], ) .write(); } diff --git a/test_program_methods/guest/src/bin/validity_window.rs b/test_program_methods/guest/src/bin/validity_window.rs index 00e8e5e8..a0ff9f36 100644 --- a/test_program_methods/guest/src/bin/validity_window.rs +++ b/test_program_methods/guest/src/bin/validity_window.rs @@ -1,14 +1,15 @@ use nssa_core::program::{ - AccountPostState, ProgramInput, ProgramOutput, ValidityWindow, read_nssa_inputs, + AccountPostState, BlockValidityWindow, ProgramInput, ProgramOutput, TimestampValidityWindow, + read_nssa_inputs, }; -type Instruction = ValidityWindow; +type Instruction = (BlockValidityWindow, TimestampValidityWindow); fn main() { let ( ProgramInput { pre_states, - instruction: validity_window, + instruction: (block_validity_window, timestamp_validity_window), }, instruction_words, ) = read_nssa_inputs::(); @@ -24,6 +25,7 @@ fn main() { vec![pre], vec![AccountPostState::new(post)], ) - .with_validity_window(validity_window) + .with_block_validity_window(block_validity_window) + .with_timestamp_validity_window(timestamp_validity_window) .write(); } diff --git a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs index cbd110dd..39f8ad69 100644 --- a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs +++ b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs @@ -1,21 +1,23 @@ use nssa_core::program::{ - AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, ValidityWindow, - read_nssa_inputs, + AccountPostState, BlockValidityWindow, ChainedCall, ProgramId, ProgramInput, ProgramOutput, + TimestampValidityWindow, read_nssa_inputs, }; use risc0_zkvm::serde::to_vec; -/// A program that sets a validity window on its output and chains to another program with a -/// potentially different validity window. +/// A program that sets a block validity window on its output and chains to another program with a +/// potentially different block validity window. /// /// Instruction: (`window`, `chained_program_id`, `chained_window`) /// The initial output uses `window` and chains to `chained_program_id` with `chained_window`. -type Instruction = (ValidityWindow, ProgramId, ValidityWindow); +/// The chained program (`validity_window`) expects `(BlockValidityWindow, TimestampValidityWindow)` +/// so an unbounded timestamp window is appended automatically. +type Instruction = (BlockValidityWindow, ProgramId, BlockValidityWindow); fn main() { let ( ProgramInput { pre_states, - instruction: (validity_window, chained_program_id, chained_validity_window), + instruction: (block_validity_window, chained_program_id, chained_block_validity_window), }, instruction_words, ) = read_nssa_inputs::(); @@ -23,7 +25,11 @@ fn main() { let [pre] = <[_; 1]>::try_from(pre_states.clone()).expect("Expected exactly one pre state"); let post = pre.account.clone(); - let chained_instruction = to_vec(&chained_validity_window).unwrap(); + let chained_instruction = to_vec(&( + chained_block_validity_window, + TimestampValidityWindow::new_unbounded(), + )) + .unwrap(); let chained_call = ChainedCall { program_id: chained_program_id, instruction_data: chained_instruction, @@ -36,7 +42,7 @@ fn main() { vec![pre], vec![AccountPostState::new(post)], ) - .with_validity_window(validity_window) + .with_block_validity_window(block_validity_window) .with_chained_calls(vec![chained_call]) .write(); } diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs index f1b94621..d68de7a5 100644 --- a/wallet/src/program_facades/amm.rs +++ b/wallet/src/program_facades/amm.rs @@ -58,18 +58,21 @@ impl Amm<'_> { user_holding_lp, ]; - let nonces = self + let mut nonces = self .0 .get_accounts_nonces(vec![user_holding_a, user_holding_b]) .await .map_err(ExecutionFailureKind::SequencerError)?; + let mut private_keys = Vec::new(); + let signing_key_a = self .0 .storage .user_data .get_pub_account_signing_key(user_holding_a) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + private_keys.push(signing_key_a); let signing_key_b = self .0 @@ -77,6 +80,26 @@ impl Amm<'_> { .user_data .get_pub_account_signing_key(user_holding_b) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + private_keys.push(signing_key_b); + + if let Some(signing_key_lp) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(user_holding_lp) + { + private_keys.push(signing_key_lp); + let lp_nonces = self + .0 + .get_accounts_nonces(vec![user_holding_lp]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.extend(lp_nonces); + } else { + println!( + "Liquidity pool tokens receiver's account ({user_holding_lp}) private key not found in wallet. Proceeding with only liquidity provider's keys." + ); + } let message = nssa::public_transaction::Message::try_new( program.id(), @@ -86,10 +109,8 @@ impl Amm<'_> { ) .unwrap(); - let witness_set = nssa::public_transaction::WitnessSet::for_message( - &message, - &[signing_key_a, signing_key_b], - ); + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &private_keys); let tx = nssa::PublicTransaction::new(message, witness_set); diff --git a/wallet/src/program_facades/native_token_transfer/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs index f91171db..2d936d3f 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -23,24 +23,40 @@ impl NativeTokenTransfer<'_> { .map_err(ExecutionFailureKind::SequencerError)?; if balance >= balance_to_move { - let nonces = self + let account_ids = vec![from, to]; + let program_id = Program::authenticated_transfer_program().id(); + + let mut nonces = self .0 .get_accounts_nonces(vec![from]) .await .map_err(ExecutionFailureKind::SequencerError)?; - let account_ids = vec![from, to]; - let program_id = Program::authenticated_transfer_program().id(); - let message = - Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap(); - - let signing_key = self.0.storage.user_data.get_pub_account_signing_key(from); - - let Some(signing_key) = signing_key else { + let mut private_keys = Vec::new(); + let from_signing_key = self.0.storage.user_data.get_pub_account_signing_key(from); + let Some(from_signing_key) = from_signing_key else { return Err(ExecutionFailureKind::KeyNotFoundError); }; + private_keys.push(from_signing_key); - let witness_set = WitnessSet::for_message(&message, &[signing_key]); + let to_signing_key = self.0.storage.user_data.get_pub_account_signing_key(to); + if let Some(to_signing_key) = to_signing_key { + private_keys.push(to_signing_key); + let to_nonces = self + .0 + .get_accounts_nonces(vec![to]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.extend(to_nonces); + } else { + println!( + "Receiver's account ({to}) private key not found in wallet. Proceeding with only sender's key." + ); + } + + let message = + Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap(); + let witness_set = WitnessSet::for_message(&message, &private_keys); let tx = PublicTransaction::new(message, witness_set); diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 3aa6891f..1f941c8c 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -19,15 +19,36 @@ impl Token<'_> { let account_ids = vec![definition_account_id, supply_account_id]; let program_id = nssa::program::Program::token().id(); let instruction = Instruction::NewFungibleDefinition { name, total_supply }; + let nonces = self + .0 + .get_accounts_nonces(account_ids.clone()) + .await + .map_err(ExecutionFailureKind::SequencerError)?; let message = nssa::public_transaction::Message::try_new( program_id, account_ids, - vec![], + nonces, instruction, ) .unwrap(); - let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + let def_private_key = self + .0 + .storage + .user_data + .get_pub_account_signing_key(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let supply_private_key = self + .0 + .storage + .user_data + .get_pub_account_signing_key(supply_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let witness_set = nssa::public_transaction::WitnessSet::for_message( + &message, + &[def_private_key, supply_private_key], + ); let tx = nssa::PublicTransaction::new(message, witness_set); @@ -138,11 +159,40 @@ impl Token<'_> { let instruction = Instruction::Transfer { amount_to_transfer: amount, }; - let nonces = self + let mut nonces = self .0 .get_accounts_nonces(vec![sender_account_id]) .await .map_err(ExecutionFailureKind::SequencerError)?; + + let mut private_keys = Vec::new(); + let sender_sk = self + .0 + .storage + .user_data + .get_pub_account_signing_key(sender_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + private_keys.push(sender_sk); + + if let Some(recipient_sk) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(recipient_account_id) + { + private_keys.push(recipient_sk); + let recipient_nonces = self + .0 + .get_accounts_nonces(vec![recipient_account_id]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.extend(recipient_nonces); + } else { + println!( + "Receiver's account ({recipient_account_id}) private key not found in wallet. Proceeding with only sender's key." + ); + } + let message = nssa::public_transaction::Message::try_new( program_id, account_ids, @@ -150,17 +200,8 @@ impl Token<'_> { instruction, ) .unwrap(); - - let Some(signing_key) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(sender_account_id) - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; let witness_set = - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + nssa::public_transaction::WitnessSet::for_message(&message, &private_keys); let tx = nssa::PublicTransaction::new(message, witness_set); @@ -477,11 +518,40 @@ impl Token<'_> { amount_to_mint: amount, }; - let nonces = self + let mut nonces = self .0 .get_accounts_nonces(vec![definition_account_id]) .await .map_err(ExecutionFailureKind::SequencerError)?; + + let mut private_keys = Vec::new(); + let definition_sk = self + .0 + .storage + .user_data + .get_pub_account_signing_key(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + private_keys.push(definition_sk); + + if let Some(holder_sk) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(holder_account_id) + { + private_keys.push(holder_sk); + let recipient_nonces = self + .0 + .get_accounts_nonces(vec![holder_account_id]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.extend(recipient_nonces); + } else { + println!( + "Holder's account ({holder_account_id}) private key not found in wallet. Proceeding with only definition's key." + ); + } + let message = nssa::public_transaction::Message::try_new( Program::token().id(), account_ids, @@ -489,17 +559,8 @@ impl Token<'_> { instruction, ) .unwrap(); - - let Some(signing_key) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(definition_account_id) - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; let witness_set = - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + nssa::public_transaction::WitnessSet::for_message(&message, &private_keys); let tx = nssa::PublicTransaction::new(message, witness_set);