diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index eb61930f..a7ddba52 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 ec016408..b0e5def5 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 948b55fc..f6d9672c 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/clock.bin b/artifacts/program_methods/clock.bin index 43441189..c24f463c 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index c34fecff..415c8ce3 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 4ec6b231..9a292cbe 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 7d99a1c0..3580ef74 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 c4074c2f..bf0f0571 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index 87f4b51d..7292d329 100644 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index af97ce05..30fdcaee 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 ff319aad..68edc95c 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 197dac46..5a71455c 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 32d593b6..42ca125b 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/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin index b5f3b309..3e84cd25 100644 Binary files a/artifacts/test_program_methods/clock_chain_caller.bin and b/artifacts/test_program_methods/clock_chain_caller.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 8b4a265d..705a1ec5 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 dcab8667..9f077174 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/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index cf772ab6..ec26c2ca 100644 Binary files a/artifacts/test_program_methods/flash_swap_callback.bin and b/artifacts/test_program_methods/flash_swap_callback.bin differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin index 3eb10f06..73b3bb32 100644 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 38cb3228..dba3f365 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/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin index 07516d82..4762d25d 100644 Binary files a/artifacts/test_program_methods/malicious_caller_program_id.bin and b/artifacts/test_program_methods/malicious_caller_program_id.bin differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin index bbac6fa9..653ece66 100644 Binary files a/artifacts/test_program_methods/malicious_self_program_id.bin and b/artifacts/test_program_methods/malicious_self_program_id.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 6aa244fa..a0144fce 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 563b8b8e..cbf3e467 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 8aca7923..e2b7fb47 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 f291e594..a99aa6ae 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 5cd93004..d7bffd5f 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/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin index 69218d03..b1dfd47f 100644 Binary files a/artifacts/test_program_methods/pda_claimer.bin and b/artifacts/test_program_methods/pda_claimer.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 31473b3b..eaad2613 100644 Binary files a/artifacts/test_program_methods/pinata_cooldown.bin and b/artifacts/test_program_methods/pinata_cooldown.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index 169cab36..c1caf235 100644 Binary files a/artifacts/test_program_methods/private_pda_delegator.bin and b/artifacts/test_program_methods/private_pda_delegator.bin differ diff --git a/artifacts/test_program_methods/private_pda_spender.bin b/artifacts/test_program_methods/private_pda_spender.bin index 461bfda2..70e4c5a0 100644 Binary files a/artifacts/test_program_methods/private_pda_spender.bin and b/artifacts/test_program_methods/private_pda_spender.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index f25e8673..2e22bfaa 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 0fe2907e..1f744230 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/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin index 03405269..90723ae0 100644 Binary files a/artifacts/test_program_methods/time_locked_transfer.bin and b/artifacts/test_program_methods/time_locked_transfer.bin differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin index a404e17c..05c17133 100644 Binary files a/artifacts/test_program_methods/two_pda_claimer.bin and b/artifacts/test_program_methods/two_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index dc8bf3fe..fd6423fc 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 b110d6be..0c86a460 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/src/block.rs b/common/src/block.rs index 92adbdb1..fbc4c9a6 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -85,9 +85,20 @@ impl HashableBlockData { signing_key: &nssa::PrivateKey, bedrock_parent_id: MantleMsgId, ) -> Block { + const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Block/\x00\x00\x00\x00\x00\x00\x00\x00"; + let data_bytes = borsh::to_vec(&self).unwrap(); - let signature = nssa::Signature::new(signing_key, &data_bytes); - let hash = OwnHasher::hash(&data_bytes); + let mut bytes = Vec::with_capacity( + PREFIX + .len() + .checked_add(data_bytes.len()) + .expect("length overflow"), + ); + bytes.extend_from_slice(PREFIX); + bytes.extend_from_slice(&data_bytes); + + let hash = OwnHasher::hash(&bytes); + let signature = nssa::Signature::new(signing_key, &hash.0); Block { header: BlockHeader { block_id: self.block_id, @@ -103,11 +114,6 @@ impl HashableBlockData { bedrock_parent_id, } } - - #[must_use] - pub fn block_hash(&self) -> BlockHash { - OwnHasher::hash(&borsh::to_vec(&self).unwrap()) - } } impl From for HashableBlockData { diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index 09ae96f5..c4a099b8 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -6,7 +6,7 @@ clippy::integer_division_remainder_used, reason = "Mock service uses intentional casts and format patterns for test data generation" )] -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc, time::Duration}; use indexer_service_protocol::{ Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment, @@ -19,15 +19,73 @@ use jsonrpsee::{ core::{SubscriptionResult, async_trait}, types::ErrorObjectOwned, }; +use tokio::sync::{RwLock, broadcast}; -/// A mock implementation of the `IndexerService` RPC for testing purposes. -pub struct MockIndexerService { +const MOCK_GENESIS_TIMESTAMP_MS: u64 = 1_704_067_200_000; +const MOCK_BLOCK_INTERVAL_MS: u64 = 30_000; + +struct MockState { blocks: Vec, accounts: HashMap, + account_ids: Vec, transactions: HashMap, } +/// A mock implementation of the `IndexerService` RPC for testing purposes. +pub struct MockIndexerService { + state: Arc>, + finalized_blocks_tx: broadcast::Sender, +} + impl MockIndexerService { + fn spawn_block_generation_task( + state: Arc>, + finalized_blocks_tx: broadcast::Sender, + ) { + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(30)).await; + + let new_block = { + let mut state = state.write().await; + + let next_block_id = state + .blocks + .last() + .map_or(1, |block| block.header.block_id.saturating_add(1)); + let prev_hash = state + .blocks + .last() + .map_or(HashType([0_u8; 32]), |block| block.header.hash); + let timestamp = state.blocks.last().map_or( + MOCK_GENESIS_TIMESTAMP_MS + MOCK_BLOCK_INTERVAL_MS, + |block| { + block + .header + .timestamp + .saturating_add(MOCK_BLOCK_INTERVAL_MS) + }, + ); + + let block = build_mock_block( + next_block_id, + prev_hash, + timestamp, + &state.account_ids, + BedrockStatus::Finalized, + ); + + index_block_transactions(&mut state.transactions, &block); + state.blocks.push(block.clone()); + + block + }; + + let _res = finalized_blocks_tx.send(new_block); + } + }); + } + #[must_use] pub fn new_with_mock_blocks() -> Self { let mut blocks = Vec::new(); @@ -59,119 +117,38 @@ impl MockIndexerService { let mut prev_hash = HashType([0_u8; 32]); for block_id in 1..=100 { - let block_hash = { - let mut hash = [0_u8; 32]; - hash[0] = block_id as u8; - hash[1] = 0xff; - HashType(hash) - }; - - // Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and - // ProgramDeployment) - let num_txs = 2 + (block_id % 3); - let mut block_transactions = Vec::new(); - - for tx_idx in 0..num_txs { - let tx_hash = { - let mut hash = [0_u8; 32]; - hash[0] = block_id as u8; - hash[1] = tx_idx as u8; - HashType(hash) - }; - - // Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment - let tx = match (block_id + tx_idx) % 5 { - // Public transactions (most common) - 0 | 1 => Transaction::Public(PublicTransaction { - hash: tx_hash, - message: PublicMessage { - program_id: ProgramId([1_u32; 8]), - account_ids: vec![ - account_ids[tx_idx as usize % account_ids.len()], - account_ids[(tx_idx as usize + 1) % account_ids.len()], - ], - nonces: vec![block_id as u128, (block_id + 1) as u128], - instruction_data: vec![1, 2, 3, 4], - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: None, - }, - }), - // PrivacyPreserving transactions - 2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction { - hash: tx_hash, - message: PrivacyPreservingMessage { - public_account_ids: vec![ - account_ids[tx_idx as usize % account_ids.len()], - ], - nonces: vec![block_id as u128], - public_post_states: vec![Account { - program_owner: ProgramId([1_u32; 8]), - balance: 500, - data: Data(vec![0xdd, 0xee]), - nonce: block_id as u128, - }], - encrypted_private_post_states: vec![EncryptedAccountData { - ciphertext: indexer_service_protocol::Ciphertext(vec![ - 0x01, 0x02, 0x03, 0x04, - ]), - epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), - view_tag: 42, - }], - new_commitments: vec![Commitment([block_id as u8; 32])], - new_nullifiers: vec![( - indexer_service_protocol::Nullifier([tx_idx as u8; 32]), - CommitmentSetDigest([0xff; 32]), - )], - block_validity_window: ValidityWindow((None, None)), - timestamp_validity_window: ValidityWindow((None, None)), - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: Some(indexer_service_protocol::Proof(vec![0; 32])), - }, - }), - // ProgramDeployment transactions (rare) - _ => Transaction::ProgramDeployment(ProgramDeploymentTransaction { - hash: tx_hash, - message: ProgramDeploymentMessage { - bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic number */ - }, - }), - }; - - transactions.insert(tx_hash, (tx.clone(), block_id)); - block_transactions.push(tx); - } - - let block = Block { - header: BlockHeader { - block_id, - prev_block_hash: prev_hash, - hash: block_hash, - timestamp: 1_704_067_200_000 + (block_id * 12_000), // ~12 seconds per block - signature: Signature([0_u8; 64]), - }, - body: BlockBody { - transactions: block_transactions, - }, - bedrock_status: match block_id { + let block = build_mock_block( + block_id, + prev_hash, + MOCK_GENESIS_TIMESTAMP_MS + (block_id * MOCK_BLOCK_INTERVAL_MS), + &account_ids, + match block_id { 0..=5 => BedrockStatus::Finalized, 6..=8 => BedrockStatus::Safe, _ => BedrockStatus::Pending, }, - bedrock_parent_id: MantleMsgId([0; 32]), - }; + ); - prev_hash = block_hash; + index_block_transactions(&mut transactions, &block); + + prev_hash = block.header.hash; blocks.push(block); } - Self { + let state = Arc::new(RwLock::new(MockState { blocks, accounts, + account_ids, transactions, + })); + + let (finalized_blocks_tx, _) = broadcast::channel(32); + + Self::spawn_block_generation_task(Arc::clone(&state), finalized_blocks_tx.clone()); + + Self { + state, + finalized_blocks_tx, } } } @@ -183,21 +160,45 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { subscription_sink: jsonrpsee::PendingSubscriptionSink, ) -> SubscriptionResult { let sink = subscription_sink.accept().await?; - for block in self - .blocks - .iter() - .filter(|b| b.bedrock_status == BedrockStatus::Finalized) - { + let initial_finalized_blocks: Vec = { + let state = self.state.read().await; + state + .blocks + .iter() + .filter(|b| b.bedrock_status == BedrockStatus::Finalized) + .cloned() + .collect() + }; + + for block in &initial_finalized_blocks { let json = serde_json::value::to_raw_value(block).unwrap(); sink.send(json).await?; } + + let mut receiver = self.finalized_blocks_tx.subscribe(); + loop { + match receiver.recv().await { + Ok(block) => { + let json = serde_json::value::to_raw_value(&block).unwrap(); + sink.send(json).await?; + } + Err(broadcast::error::RecvError::Lagged(_)) => {} + Err(broadcast::error::RecvError::Closed) => break, + } + } + Ok(()) } async fn get_last_finalized_block_id(&self) -> Result { - self.blocks - .last() - .map(|bl| bl.header.block_id) + self.state + .read() + .await + .blocks + .iter() + .rev() + .find(|block| block.bedrock_status == BedrockStatus::Finalized) + .map(|block| block.header.block_id) .ok_or_else(|| { ErrorObjectOwned::owned(-32001, "Last block not found".to_owned(), None::<()>) }) @@ -205,6 +206,9 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { async fn get_block_by_id(&self, block_id: BlockId) -> Result, ErrorObjectOwned> { Ok(self + .state + .read() + .await .blocks .iter() .find(|b| b.header.block_id == block_id) @@ -216,6 +220,9 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { block_hash: HashType, ) -> Result, ErrorObjectOwned> { Ok(self + .state + .read() + .await .blocks .iter() .find(|b| b.header.hash == block_hash) @@ -223,7 +230,10 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { } async fn get_account(&self, account_id: AccountId) -> Result { - self.accounts + self.state + .read() + .await + .accounts .get(&account_id) .cloned() .ok_or_else(|| ErrorObjectOwned::owned(-32001, "Account not found", None::<()>)) @@ -233,7 +243,13 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { &self, tx_hash: HashType, ) -> Result, ErrorObjectOwned> { - Ok(self.transactions.get(&tx_hash).map(|(tx, _)| tx.clone())) + Ok(self + .state + .read() + .await + .transactions + .get(&tx_hash) + .map(|(tx, _)| tx.clone())) } async fn get_blocks( @@ -241,15 +257,17 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { before: Option, limit: u64, ) -> Result, ErrorObjectOwned> { + let state = self.state.read().await; + let start_id = before.map_or_else( - || self.blocks.len(), + || state.blocks.len(), |id| usize::try_from(id.saturating_sub(1)).expect("u64 should fit in usize"), ); let result = (1..=start_id) .rev() .take(limit as usize) - .map_while(|block_id| self.blocks.get(block_id - 1).cloned()) + .map_while(|block_id| state.blocks.get(block_id - 1).cloned()) .collect(); Ok(result) @@ -261,20 +279,24 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { offset: u64, limit: u64, ) -> Result, ErrorObjectOwned> { - let mut account_txs: Vec<_> = self - .transactions - .values() - .filter(|(tx, _)| match tx { - Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id), - Transaction::PrivacyPreserving(priv_tx) => { - priv_tx.message.public_account_ids.contains(&account_id) - } - Transaction::ProgramDeployment(_) => false, - }) - .collect(); + let mut account_txs: Vec<(Transaction, BlockId)> = { + let state = self.state.read().await; + state + .transactions + .values() + .filter(|(tx, _)| match tx { + Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id), + Transaction::PrivacyPreserving(priv_tx) => { + priv_tx.message.public_account_ids.contains(&account_id) + } + Transaction::ProgramDeployment(_) => false, + }) + .cloned() + .collect() + }; // Sort by block ID descending (most recent first) - account_txs.sort_by_key(|b| std::cmp::Reverse(b.1)); + account_txs.sort_by_key(|(_, block_id)| std::cmp::Reverse(*block_id)); let start = offset as usize; if start >= account_txs.len() { @@ -293,3 +315,123 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { Ok(()) } } + +fn build_mock_block( + block_id: BlockId, + prev_hash: HashType, + timestamp: u64, + account_ids: &[AccountId], + bedrock_status: BedrockStatus, +) -> Block { + let block_hash = { + let mut hash = [0_u8; 32]; + hash[0] = block_id as u8; + hash[1] = 0xff; + HashType(hash) + }; + + // Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and ProgramDeployment) + let num_txs = 2 + (block_id % 3); + let mut block_transactions = Vec::new(); + + for tx_idx in 0..num_txs { + let tx_hash = { + let mut hash = [0_u8; 32]; + hash[0] = block_id as u8; + hash[1] = tx_idx as u8; + HashType(hash) + }; + + // Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment + let tx = match (block_id + tx_idx) % 5 { + // Public transactions (most common) + 0 | 1 => Transaction::Public(PublicTransaction { + hash: tx_hash, + message: PublicMessage { + program_id: ProgramId([1_u32; 8]), + account_ids: vec![ + account_ids[tx_idx as usize % account_ids.len()], + account_ids[(tx_idx as usize + 1) % account_ids.len()], + ], + nonces: vec![block_id as u128, (block_id + 1) as u128], + instruction_data: vec![1, 2, 3, 4], + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: None, + }, + }), + // PrivacyPreserving transactions + 2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction { + hash: tx_hash, + message: PrivacyPreservingMessage { + public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]], + nonces: vec![block_id as u128], + public_post_states: vec![Account { + program_owner: ProgramId([1_u32; 8]), + balance: 500, + data: Data(vec![0xdd, 0xee]), + nonce: block_id as u128, + }], + encrypted_private_post_states: vec![EncryptedAccountData { + ciphertext: indexer_service_protocol::Ciphertext(vec![ + 0x01, 0x02, 0x03, 0x04, + ]), + epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), + view_tag: 42, + }], + new_commitments: vec![Commitment([block_id as u8; 32])], + new_nullifiers: vec![( + indexer_service_protocol::Nullifier([tx_idx as u8; 32]), + CommitmentSetDigest([0xff; 32]), + )], + block_validity_window: ValidityWindow((None, None)), + timestamp_validity_window: ValidityWindow((None, None)), + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: Some(indexer_service_protocol::Proof(vec![0; 32])), + }, + }), + // ProgramDeployment transactions (rare) + _ => Transaction::ProgramDeployment(ProgramDeploymentTransaction { + hash: tx_hash, + message: ProgramDeploymentMessage { + bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic + * number */ + }, + }), + }; + + block_transactions.push(tx); + } + + Block { + header: BlockHeader { + block_id, + prev_block_hash: prev_hash, + hash: block_hash, + timestamp, + signature: Signature([0_u8; 64]), + }, + body: BlockBody { + transactions: block_transactions, + }, + bedrock_status, + bedrock_parent_id: MantleMsgId([0; 32]), + } +} + +fn index_block_transactions( + transactions: &mut HashMap, + block: &Block, +) { + for tx in &block.body.transactions { + let tx_hash = match tx { + Transaction::Public(public_tx) => public_tx.hash, + Transaction::PrivacyPreserving(private_tx) => private_tx.hash, + Transaction::ProgramDeployment(deployment_tx) => deployment_tx.hash, + }; + transactions.insert(tx_hash, (tx.clone(), block.header.block_id)); + } +} diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 41de30ed..df74daba 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -27,7 +27,7 @@ use nssa::{ public_transaction as putx, }; use nssa_core::{ - MembershipProof, NullifierPublicKey, + InputAccountIdentity, MembershipProof, NullifierPublicKey, account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; @@ -251,10 +251,19 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 2], - vec![(sender_npk, 0, sender_ss), (recipient_npk, 0, recipient_ss)], - vec![sender_nsk], - vec![Some(proof)], + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: sender_ss, + nsk: sender_nsk, + membership_proof: proof, + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_npk, + ssk: recipient_ss, + identifier: 0, + }, + ], &program.into(), ) .unwrap(); diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index c71003de..f52357ee 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -12,23 +12,92 @@ use crate::{ pub struct PrivacyPreservingCircuitInput { /// Outputs of the program execution. pub program_outputs: Vec, - /// Visibility mask for accounts. - /// - /// - `0` - public account - /// - `1` - private account with authentication - /// - `2` - private account without authentication - /// - `3` - private PDA account - pub visibility_mask: Vec, - /// Public keys and identifiers of private accounts. - pub private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>, - /// Nullifier secret keys for authorized private accounts. - pub private_account_nsks: Vec, - /// Membership proofs for private accounts. Can be [`None`] for uninitialized accounts. - pub private_account_membership_proofs: Vec>, + /// One entry per `pre_state`, in the same order as the program's `pre_states`. + /// Length must equal the number of `pre_states` derived from `program_outputs`. + /// The guest's `private_pda_npk_by_position` and `private_pda_bound_positions` + /// rely on this position alignment. + pub account_identities: Vec, /// Program ID. pub program_id: ProgramId, } +/// Per-account input to the privacy-preserving circuit. Each variant carries exactly the fields +/// the guest needs for that account's code path. +#[derive(Serialize, Deserialize, Clone)] +pub enum InputAccountIdentity { + /// Public account. The guest reads pre/post state from `program_outputs` and emits no + /// commitment, ciphertext, or nullifier. + Public, + /// Init of an authorized standalone private account: no membership proof. The `pre_state` + /// must be `Account::default()`. The `account_id` is derived as + /// `AccountId::from((&NullifierPublicKey::from(nsk), identifier))` and matched against + /// `pre_state.account_id`. + PrivateAuthorizedInit { + ssk: SharedSecretKey, + nsk: NullifierSecretKey, + identifier: Identifier, + }, + /// Update of an authorized standalone private account: existing on-chain commitment, with + /// membership proof. + PrivateAuthorizedUpdate { + ssk: SharedSecretKey, + nsk: NullifierSecretKey, + membership_proof: MembershipProof, + identifier: Identifier, + }, + /// Init of a standalone private account the caller does not own (e.g. a recipient who + /// doesn't yet exist on chain). No `nsk`, no membership proof. + PrivateUnauthorized { + npk: NullifierPublicKey, + ssk: SharedSecretKey, + identifier: Identifier, + }, + /// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream + /// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. Identifier is fixed by + /// convention to `PRIVATE_PDA_FIXED_IDENTIFIER` and not carried per-input. + PrivatePdaInit { + npk: NullifierPublicKey, + ssk: SharedSecretKey, + }, + /// Update of an existing private PDA, authorized, with membership proof. `npk` is derived + /// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a + /// previously-seen authorization in a chained call. Identifier is fixed. + PrivatePdaUpdate { + ssk: SharedSecretKey, + nsk: NullifierSecretKey, + membership_proof: MembershipProof, + }, +} + +impl InputAccountIdentity { + #[must_use] + pub const fn is_public(&self) -> bool { + matches!(self, Self::Public) + } + + #[must_use] + pub const fn is_private_pda(&self) -> bool { + matches!( + self, + Self::PrivatePdaInit { .. } | Self::PrivatePdaUpdate { .. } + ) + } + + /// For private PDA variants, return the nullifier public key. `Init` carries it directly; + /// `Update` derives it from `nsk`. For non-PDA variants returns `None`. + #[must_use] + pub fn npk_if_private_pda(&self) -> Option { + match self { + Self::PrivatePdaInit { npk, .. } => Some(*npk), + Self::PrivatePdaUpdate { nsk, .. } => Some(NullifierPublicKey::from(nsk)), + Self::Public + | Self::PrivateAuthorizedInit { .. } + | Self::PrivateAuthorizedUpdate { .. } + | Self::PrivateUnauthorized { .. } => None, + } + } +} + #[derive(Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct PrivacyPreservingCircuitOutput { diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index 478d475c..d660aed0 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -3,7 +3,9 @@ reason = "We prefer to group methods by functionality rather than by type for encoding" )] -pub use circuit_io::{PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput}; +pub use circuit_io::{ + InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, +}; pub use commitment::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, compute_digest_for_path, diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 5c09fad3..2526c700 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -2,8 +2,7 @@ use std::collections::{HashMap, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey, - PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, + InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, account::AccountWithMetadata, program::{ChainedCall, InstructionData, ProgramId, ProgramOutput}, }; @@ -63,14 +62,10 @@ impl From for ProgramWithDependencies { /// Generates a proof of the execution of a NSSA program inside the privacy preserving execution /// circuit. -/// TODO: too many parameters. pub fn execute_and_prove( pre_states: Vec, instruction_data: InstructionData, - visibility_mask: Vec, - private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>, - private_account_nsks: Vec, - private_account_membership_proofs: Vec>, + account_identities: Vec, program_with_dependencies: &ProgramWithDependencies, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { let ProgramWithDependencies { @@ -128,10 +123,7 @@ pub fn execute_and_prove( let circuit_input = PrivacyPreservingCircuitInput { program_outputs, - visibility_mask, - private_account_keys, - private_account_nsks, - private_account_membership_proofs, + account_identities, program_id: program_with_dependencies.program.id(), }; @@ -241,10 +233,14 @@ mod tests { let (output, proof) = execute_and_prove( vec![sender, recipient], Program::serialize_instruction(balance_to_move).unwrap(), - vec![0, 2], - vec![(recipient_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret, + identifier: 0, + }, + ], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -334,13 +330,21 @@ mod tests { let (output, proof) = execute_and_prove( vec![sender_pre, recipient], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 2], vec![ - (sender_keys.npk(), 0, shared_secret_1), - (recipient_keys.npk(), 0, shared_secret_2), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret_1, + nsk: sender_keys.nsk, + membership_proof: commitment_set + .get_proof_for(&commitment_sender) + .expect("sender's commitment must be in the set"), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret_2, + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![commitment_set.get_proof_for(&commitment_sender), None], &program.into(), ) .unwrap(); @@ -403,10 +407,11 @@ mod tests { let result = execute_and_prove( vec![pre], instruction, - vec![2], - vec![(account_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivateUnauthorized { + npk: account_keys.npk(), + ssk: shared_secret, + identifier: 0, + }], &program_with_deps, ); @@ -452,10 +457,13 @@ mod tests { let result = execute_and_prove( vec![pda_pre, sender_pre], instruction, - vec![3, 0], - vec![(npk, u128::MAX, shared_secret_pda)], - vec![], - vec![None], + vec![ + InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret_pda, + }, + InputAccountIdentity::Public, + ], &program_with_deps, ); @@ -498,10 +506,13 @@ mod tests { let result = execute_and_prove( vec![pda_pre, bob_pre], instruction, - vec![3, 0], - vec![(npk, u128::MAX, shared_secret_pda)], - vec![], - vec![None], + vec![ + InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret_pda, + }, + InputAccountIdentity::Public, + ], &program_with_deps, ); diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index ee46d0b3..697f66ac 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -9,6 +9,8 @@ use sha2::{Digest as _, Sha256}; use crate::{AccountId, error::NssaError}; +const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00"; + pub type ViewTag = u8; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] @@ -118,22 +120,34 @@ impl Message { timestamp_validity_window: output.timestamp_validity_window, }) } + + #[must_use] + pub fn hash(&self) -> [u8; 32] { + let msg = self.to_bytes(); + let mut bytes = Vec::with_capacity( + PREFIX + .len() + .checked_add(msg.len()) + .expect("length overflow"), + ); + bytes.extend_from_slice(PREFIX); + bytes.extend_from_slice(&msg); + + Sha256::digest(bytes).into() + } } #[cfg(test)] pub mod tests { use nssa_core::{ Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey, - account::Account, + account::{Account, AccountId, Nonce}, encryption::{EphemeralPublicKey, ViewingPublicKey}, program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; - use crate::{ - AccountId, - privacy_preserving_transaction::message::{EncryptedAccountData, Message}, - }; + use super::{EncryptedAccountData, Message, PREFIX}; #[must_use] pub fn message_for_tests() -> Message { @@ -176,6 +190,58 @@ pub mod tests { } } + #[test] + fn hash_privacy_pinned() { + let msg = Message { + public_account_ids: vec![AccountId::new([42_u8; 32])], + nonces: vec![Nonce(5)], + public_post_states: vec![], + encrypted_private_post_states: vec![], + new_commitments: vec![], + new_nullifiers: vec![], + block_validity_window: BlockValidityWindow::new_unbounded(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), + }; + + let public_account_ids_bytes: &[u8] = &[42_u8; 32]; + let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + // all remaining vec fields are empty: u32 len=0 + let empty_vec_bytes: &[u8] = &[0_u8; 4]; + // validity windows: unbounded = {from: None (0u8), to: None (0u8)} + let unbounded_window_bytes: &[u8] = &[0_u8; 2]; + + let expected_borsh_vec: Vec = [ + &[1_u8, 0, 0, 0], // public_account_ids + public_account_ids_bytes, + nonces_bytes, + empty_vec_bytes, // public_post_state + empty_vec_bytes, // encrypted_private_post_states + empty_vec_bytes, // new_commitments + empty_vec_bytes, // new_nullifiers + unbounded_window_bytes, // block_validity_window + unbounded_window_bytes, // timestamp_validity_window + ] + .concat(); + let expected_borsh: &[u8] = &expected_borsh_vec; + + assert_eq!( + borsh::to_vec(&msg).unwrap(), + expected_borsh, + "`privacy_preserving_transaction::hash()`: expected borsh order has changed" + ); + + let mut preimage = Vec::with_capacity(PREFIX.len() + expected_borsh.len()); + preimage.extend_from_slice(PREFIX); + preimage.extend_from_slice(expected_borsh); + let expected_hash: [u8; 32] = Sha256::digest(&preimage).into(); + + assert_eq!( + msg.hash(), + expected_hash, + "`privacy_preserving_transaction::hash()`: serialization has changed" + ); + } + #[test] fn encrypted_account_data_constructor() { let npk = NullifierPublicKey::from(&[1; 32]); diff --git a/nssa/src/privacy_preserving_transaction/witness_set.rs b/nssa/src/privacy_preserving_transaction/witness_set.rs index 373bbc9c..e17df90c 100644 --- a/nssa/src/privacy_preserving_transaction/witness_set.rs +++ b/nssa/src/privacy_preserving_transaction/witness_set.rs @@ -14,12 +14,12 @@ pub struct WitnessSet { impl WitnessSet { #[must_use] pub fn for_message(message: &Message, proof: Proof, private_keys: &[&PrivateKey]) -> Self { - let message_bytes = message.to_bytes(); + let message_hash = message.hash(); let signatures_and_public_keys = private_keys .iter() .map(|&key| { ( - Signature::new(key, &message_bytes), + Signature::new(key, &message_hash), PublicKey::new_from_private_key(key), ) }) @@ -32,9 +32,9 @@ impl WitnessSet { #[must_use] pub fn signatures_are_valid_for(&self, message: &Message) -> bool { - let message_bytes = message.to_bytes(); + let message_hash = message.hash(); for (signature, public_key) in self.signatures_and_public_keys() { - if !signature.is_valid_for(&message_bytes, public_key) { + if !signature.is_valid_for(&message_hash, public_key) { return false; } } diff --git a/nssa/src/public_transaction/message.rs b/nssa/src/public_transaction/message.rs index d4838b87..3ab7d74c 100644 --- a/nssa/src/public_transaction/message.rs +++ b/nssa/src/public_transaction/message.rs @@ -4,9 +4,12 @@ use nssa_core::{ program::{InstructionData, ProgramId}, }; use serde::Serialize; +use sha2::{Digest as _, Sha256}; use crate::{AccountId, error::NssaError, program::Program}; +const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Public/\x00\x00\x00\x00\x00\x00\x00"; + #[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Message { pub program_id: ProgramId, @@ -63,4 +66,74 @@ impl Message { instruction_data, } } + + #[must_use] + pub fn hash(&self) -> [u8; 32] { + let mut bytes = Vec::with_capacity( + PREFIX + .len() + .checked_add(self.to_bytes().len()) + .expect("length overflow"), + ); + bytes.extend_from_slice(PREFIX); + bytes.extend_from_slice(&self.to_bytes()); + + Sha256::digest(bytes).into() + } +} + +#[cfg(test)] +mod tests { + use nssa_core::account::{AccountId, Nonce}; + use sha2::{Digest as _, Sha256}; + + use super::{Message, PREFIX}; + + #[test] + fn hash_public_pinned() { + let msg = Message::new_preserialized( + [1_u32; 8], + vec![AccountId::new([42_u8; 32])], + vec![Nonce(5)], + vec![], + ); + + // program_id: [1_u32; 8], each word as LE u32 + let program_id_bytes: &[u8] = &[ + 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, + ]; + // account_ids: AccountId([42_u8; 32]) + let account_ids_bytes: &[u8] = &[42_u8; 32]; + // nonces: u32 len=1, then Nonce(5) as LE u128 + let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let instruction_data_bytes: &[u8] = &[0_u8; 4]; + + let expected_borsh_vec: Vec = [ + program_id_bytes, + &[1_u8, 0, 0, 0], // account_ids len=1 + account_ids_bytes, + nonces_bytes, + instruction_data_bytes, + ] + .concat(); + let expected_borsh: &[u8] = &expected_borsh_vec; + + assert_eq!( + borsh::to_vec(&msg).unwrap(), + expected_borsh, + "`public_transaction::hash()`: expected borsh order has changed" + ); + + let mut preimage = Vec::with_capacity(PREFIX.len() + expected_borsh.len()); + preimage.extend_from_slice(PREFIX); + preimage.extend_from_slice(expected_borsh); + let expected_hash: [u8; 32] = Sha256::digest(&preimage).into(); + + assert_eq!( + msg.hash(), + expected_hash, + "`public_transaction::hash()`: serialization has changed" + ); + } } diff --git a/nssa/src/public_transaction/witness_set.rs b/nssa/src/public_transaction/witness_set.rs index d6b32891..1605f488 100644 --- a/nssa/src/public_transaction/witness_set.rs +++ b/nssa/src/public_transaction/witness_set.rs @@ -10,12 +10,12 @@ pub struct WitnessSet { impl WitnessSet { #[must_use] pub fn for_message(message: &Message, private_keys: &[&PrivateKey]) -> Self { - let message_bytes = message.to_bytes(); + let message_hash = message.hash(); let signatures_and_public_keys = private_keys .iter() .map(|&key| { ( - Signature::new(key, &message_bytes), + Signature::new(key, &message_hash), PublicKey::new_from_private_key(key), ) }) @@ -27,9 +27,9 @@ impl WitnessSet { #[must_use] pub fn is_valid_for(&self, message: &Message) -> bool { - let message_bytes = message.to_bytes(); + let message_hash = message.hash(); for (signature, public_key) in self.signatures_and_public_keys() { - if !signature.is_valid_for(&message_bytes, public_key) { + if !signature.is_valid_for(&message_hash, public_key) { return false; } } @@ -75,7 +75,7 @@ mod tests { assert_eq!(witness_set.signatures_and_public_keys.len(), 2); - let message_bytes = message.to_bytes(); + let message_bytes = message.hash(); for ((signature, public_key), expected_public_key) in witness_set .signatures_and_public_keys .into_iter() diff --git a/nssa/src/signature/bip340_test_vectors.rs b/nssa/src/signature/bip340_test_vectors.rs index e316db5e..ac3eb044 100644 --- a/nssa/src/signature/bip340_test_vectors.rs +++ b/nssa/src/signature/bip340_test_vectors.rs @@ -4,7 +4,7 @@ pub struct TestVector { pub seckey: Option, pub pubkey: PublicKey, pub aux_rand: Option<[u8; 32]>, - pub message: Option>, + pub message: [u8; 32], pub signature: Signature, pub verification_result: bool, } @@ -15,18 +15,21 @@ pub struct TestVector { pub fn test_vectors() -> Vec { vec![ TestVector { - seckey: Some(PrivateKey::try_new(hex_to_bytes( - "0000000000000000000000000000000000000000000000000000000000000003", - )).unwrap()), + seckey: Some( + PrivateKey::try_new(hex_to_bytes( + "0000000000000000000000000000000000000000000000000000000000000003", + )) + .unwrap(), + ), pubkey: PublicKey::try_new(hex_to_bytes( "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", - )).unwrap(), + )) + .unwrap(), aux_rand: Some(hex_to_bytes::<32>( "0000000000000000000000000000000000000000000000000000000000000000", )), - message: Some( - hex::decode("0000000000000000000000000000000000000000000000000000000000000000") - .unwrap(), + message: hex_to_bytes::<32>( + "0000000000000000000000000000000000000000000000000000000000000000", ), signature: Signature { value: hex_to_bytes( @@ -36,18 +39,21 @@ pub fn test_vectors() -> Vec { verification_result: true, }, TestVector { - seckey: Some(PrivateKey::try_new(hex_to_bytes( - "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF", - )).unwrap()), + seckey: Some( + PrivateKey::try_new(hex_to_bytes( + "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF", + )) + .unwrap(), + ), pubkey: PublicKey::try_new(hex_to_bytes( "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - )).unwrap(), + )) + .unwrap(), aux_rand: Some(hex_to_bytes::<32>( "0000000000000000000000000000000000000000000000000000000000000001", )), - message: Some( - hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") - .unwrap(), + message: hex_to_bytes::<32>( + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", ), signature: Signature { value: hex_to_bytes( @@ -57,18 +63,21 @@ pub fn test_vectors() -> Vec { verification_result: true, }, TestVector { - seckey: Some(PrivateKey::try_new(hex_to_bytes( - "C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9", - )).unwrap()), + seckey: Some( + PrivateKey::try_new(hex_to_bytes( + "C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9", + )) + .unwrap(), + ), pubkey: PublicKey::try_new(hex_to_bytes( "DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", - )).unwrap(), + )) + .unwrap(), aux_rand: Some(hex_to_bytes::<32>( "C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906", )), - message: Some( - hex::decode("7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C") - .unwrap(), + message: hex_to_bytes::<32>( + "7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C", ), signature: Signature { value: hex_to_bytes( @@ -78,18 +87,21 @@ pub fn test_vectors() -> Vec { verification_result: true, }, TestVector { - seckey: Some(PrivateKey::try_new(hex_to_bytes( - "0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710", - )).unwrap()), + seckey: Some( + PrivateKey::try_new(hex_to_bytes( + "0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710", + )) + .unwrap(), + ), pubkey: PublicKey::try_new(hex_to_bytes( "25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517", - )).unwrap(), + )) + .unwrap(), aux_rand: Some(hex_to_bytes::<32>( "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", )), - message: Some( - hex::decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") - .unwrap(), + message: hex_to_bytes::<32>( + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", ), signature: Signature { value: hex_to_bytes( @@ -102,11 +114,11 @@ pub fn test_vectors() -> Vec { seckey: None, pubkey: PublicKey::try_new(hex_to_bytes( "D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9", - )).unwrap(), + )) + .unwrap(), aux_rand: None, - message: Some( - hex::decode("4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703") - .unwrap(), + message: hex_to_bytes::<32>( + "4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703", ), signature: Signature { value: hex_to_bytes( @@ -122,13 +134,15 @@ pub fn test_vectors() -> Vec { // "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34", // )).unwrap(), // aux_rand: None, - // message: Some( - // hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89").unwrap(), - // ), + // message: + // + // hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89"). + // unwrap(), ), // signature: Signature { // value: hex_to_bytes( - // "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", - // ), + // + // "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B" + // , ), // }, // verification_result: false, // }, @@ -136,11 +150,11 @@ pub fn test_vectors() -> Vec { seckey: None, pubkey: PublicKey::try_new(hex_to_bytes( "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - )).unwrap(), + )) + .unwrap(), aux_rand: None, - message: Some( - hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") - .unwrap(), + message: hex_to_bytes::<32>( + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", ), signature: Signature { value: hex_to_bytes( @@ -153,11 +167,11 @@ pub fn test_vectors() -> Vec { seckey: None, pubkey: PublicKey::try_new(hex_to_bytes( "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - )).unwrap(), + )) + .unwrap(), aux_rand: None, - message: Some( - hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") - .unwrap(), + message: hex_to_bytes::<32>( + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", ), signature: Signature { value: hex_to_bytes( @@ -170,11 +184,11 @@ pub fn test_vectors() -> Vec { seckey: None, pubkey: PublicKey::try_new(hex_to_bytes( "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - )).unwrap(), + )) + .unwrap(), aux_rand: None, - message: Some( - hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") - .unwrap(), + message: hex_to_bytes::<32>( + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", ), signature: Signature { value: hex_to_bytes( @@ -187,11 +201,11 @@ pub fn test_vectors() -> Vec { seckey: None, pubkey: PublicKey::try_new(hex_to_bytes( "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - )).unwrap(), + )) + .unwrap(), aux_rand: None, - message: Some( - hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") - .unwrap(), + message: hex_to_bytes::<32>( + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", ), signature: Signature { value: hex_to_bytes( @@ -204,11 +218,11 @@ pub fn test_vectors() -> Vec { seckey: None, pubkey: PublicKey::try_new(hex_to_bytes( "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - )).unwrap(), + )) + .unwrap(), aux_rand: None, - message: Some( - hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") - .unwrap(), + message: hex_to_bytes::<32>( + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", ), signature: Signature { value: hex_to_bytes( @@ -221,11 +235,11 @@ pub fn test_vectors() -> Vec { seckey: None, pubkey: PublicKey::try_new(hex_to_bytes( "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - )).unwrap(), + )) + .unwrap(), aux_rand: None, - message: Some( - hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") - .unwrap(), + message: hex_to_bytes::<32>( + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", ), signature: Signature { value: hex_to_bytes( @@ -238,11 +252,11 @@ pub fn test_vectors() -> Vec { seckey: None, pubkey: PublicKey::try_new(hex_to_bytes( "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - )).unwrap(), + )) + .unwrap(), aux_rand: None, - message: Some( - hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") - .unwrap(), + message: hex_to_bytes::<32>( + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", ), signature: Signature { value: hex_to_bytes( @@ -255,11 +269,11 @@ pub fn test_vectors() -> Vec { seckey: None, pubkey: PublicKey::try_new(hex_to_bytes( "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - )).unwrap(), + )) + .unwrap(), aux_rand: None, - message: Some( - hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") - .unwrap(), + message: hex_to_bytes::<32>( + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", ), signature: Signature { value: hex_to_bytes( @@ -275,90 +289,96 @@ pub fn test_vectors() -> Vec { // "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", // )).unwrap(), // aux_rand: None, - // message: Some( - // hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89").unwrap(), - // ), + // message: + // + // hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89"). + // unwrap(), ), // signature: Signature { // value: hex_to_bytes( - // "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", - // ), + // + // "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B" + // , ), // }, // verification_result: false, // }, - TestVector { - seckey: Some(PrivateKey::try_new(hex_to_bytes( - "0340034003400340034003400340034003400340034003400340034003400340", - )).unwrap()), - pubkey: PublicKey::try_new(hex_to_bytes( - "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", - )).unwrap(), - aux_rand: Some(hex_to_bytes::<32>( - "0000000000000000000000000000000000000000000000000000000000000000", - )), - message: None, - signature: Signature { - value: hex_to_bytes( - "71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63", - ), - }, - verification_result: true, - }, - TestVector { - seckey: Some(PrivateKey::try_new(hex_to_bytes( - "0340034003400340034003400340034003400340034003400340034003400340", - )).unwrap()), - pubkey: PublicKey::try_new(hex_to_bytes( - "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", - )).unwrap(), - aux_rand: Some(hex_to_bytes::<32>( - "0000000000000000000000000000000000000000000000000000000000000000", - )), - message: Some(hex::decode("11").unwrap()), - signature: Signature { - value: hex_to_bytes( - "08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF", - ), - }, - verification_result: true, - }, - TestVector { - seckey: Some(PrivateKey::try_new(hex_to_bytes( - "0340034003400340034003400340034003400340034003400340034003400340", - )).unwrap()), - pubkey: PublicKey::try_new(hex_to_bytes( - "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", - )).unwrap(), - aux_rand: Some(hex_to_bytes::<32>( - "0000000000000000000000000000000000000000000000000000000000000000", - )), - message: Some(hex::decode("0102030405060708090A0B0C0D0E0F1011").unwrap()), - signature: Signature { - value: hex_to_bytes( - "5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5", - ), - }, - verification_result: true, - }, - TestVector { - seckey: Some(PrivateKey::try_new(hex_to_bytes( - "0340034003400340034003400340034003400340034003400340034003400340", - )).unwrap()), - pubkey: PublicKey::try_new(hex_to_bytes( - "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", - )).unwrap(), - aux_rand: Some(hex_to_bytes::<32>( - "0000000000000000000000000000000000000000000000000000000000000000", - )), - message: Some( - hex::decode("99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999").unwrap(), - ), - signature: Signature { - value: hex_to_bytes( - "403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367", - ), - }, - verification_result: true, - }, + // Test with invalid message length (0); valid test for BIP-340 post 2022. + // TestVector { + // seckey: PrivateKey::try_new(hex_to_bytes( + // "0340034003400340034003400340034003400340034003400340034003400340", + // )).unwrap()), + // pubkey: PublicKey::try_new(hex_to_bytes( + // "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + // )).unwrap(), + // aux_rand: hex_to_bytes::<32>( + // "0000000000000000000000000000000000000000000000000000000000000000", + // )), + // message: None, + // signature: Signature { + // value: hex_to_bytes( + // "71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63", + // ), + // }, + // verification_result: true, + // }, + // Test with invalid message length (1); valid test for BIP-340 post 2022. + // TestVector { + // seckey: PrivateKey::try_new(hex_to_bytes( + // "0340034003400340034003400340034003400340034003400340034003400340", + // )).unwrap()), + // pubkey: PublicKey::try_new(hex_to_bytes( + // "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + // )).unwrap(), + // aux_rand: hex_to_bytes::<32>( + // "0000000000000000000000000000000000000000000000000000000000000000", + // )), + // message: hex::decode("11").unwrap()), + // signature: Signature { + // value: hex_to_bytes( + // "08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF", + // ), + // }, + // verification_result: true, + // }, + // Test with invalid message length (17); valid test for BIP-340 post 2022. + // TestVector { + // seckey: PrivateKey::try_new(hex_to_bytes( + // "0340034003400340034003400340034003400340034003400340034003400340", + // )).unwrap()), + // pubkey: PublicKey::try_new(hex_to_bytes( + // "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + // )).unwrap(), + // aux_rand: hex_to_bytes::<32>( + // "0000000000000000000000000000000000000000000000000000000000000000", + // )), + // message: hex::decode("0102030405060708090A0B0C0D0E0F1011").unwrap()), + // signature: Signature { + // value: hex_to_bytes( + // "5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5", + // ), + // }, + // erification_result: true, + // }, + // Test with invalid message length (100); valid test for BIP-340 post 2022. + // TestVector { + // seckey: PrivateKey::try_new(hex_to_bytes( + // "0340034003400340034003400340034003400340034003400340034003400340", + // )).unwrap()), + // pubkey: PublicKey::try_new(hex_to_bytes( + // "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + // )).unwrap(), + // aux_rand: hex_to_bytes::<32>( + // "0000000000000000000000000000000000000000000000000000000000000000", + // )), + // message: + // hex::decode("99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999").unwrap(), + // ), + // signature: Signature { + // value: hex_to_bytes( + // "403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367", + // ), + // }, + // verification_result: true, + // }, ] } diff --git a/nssa/src/signature/mod.rs b/nssa/src/signature/mod.rs index 3a594da6..a46b1ff5 100644 --- a/nssa/src/signature/mod.rs +++ b/nssa/src/signature/mod.rs @@ -36,8 +36,10 @@ impl FromStr for Signature { } impl Signature { + /// This function expects the incoming message to be prehashed to be pre-2022 BIP-340/Keycard + /// compatible. #[must_use] - pub fn new(key: &PrivateKey, message: &[u8]) -> Self { + pub fn new(key: &PrivateKey, message: &[u8; 32]) -> Self { let mut aux_random = [0_u8; 32]; OsRng.fill_bytes(&mut aux_random); Self::new_with_aux_random(key, message, aux_random) @@ -45,14 +47,14 @@ impl Signature { pub(crate) fn new_with_aux_random( key: &PrivateKey, - message: &[u8], + message: &[u8; 32], aux_random: [u8; 32], ) -> Self { let value = { let signing_key = k256::schnorr::SigningKey::from_bytes(key.value()) .expect("Expect valid signing key"); signing_key - .sign_raw(message, &aux_random) + .sign_prehash_with_aux_rand(message, &aux_random) .expect("Expect to produce a valid signature") .to_bytes() }; @@ -61,7 +63,7 @@ impl Signature { } #[must_use] - pub fn is_valid_for(&self, bytes: &[u8], public_key: &PublicKey) -> bool { + pub fn is_valid_for(&self, bytes: &[u8; 32], public_key: &PublicKey) -> bool { let Ok(pk) = k256::schnorr::VerifyingKey::from_bytes(public_key.value()) else { return false; }; @@ -97,9 +99,8 @@ mod tests { let Some(aux_random) = test_vector.aux_rand else { continue; }; - let Some(message) = test_vector.message else { - continue; - }; + let message = test_vector.message; + if !test_vector.verification_result { continue; } @@ -114,7 +115,7 @@ mod tests { #[test] fn signature_verification_from_bip340_test_vectors() { for (i, test_vector) in bip340_test_vectors::test_vectors().into_iter().enumerate() { - let message = test_vector.message.unwrap_or(vec![]); + let message = test_vector.message; let expected_result = test_vector.verification_result; let result = test_vector diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 6de2127b..9e4d8524 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -362,8 +362,8 @@ pub mod tests { use std::collections::HashMap; use nssa_core::{ - BlockId, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, - Timestamp, + BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, + NullifierSecretKey, SharedSecretKey, Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, program::{ @@ -1294,10 +1294,14 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![sender, recipient], Program::serialize_instruction(balance_to_move).unwrap(), - vec![0, 2], - vec![(recipient_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret, + identifier: 0, + }, + ], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -1343,13 +1347,21 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 2], vec![ - (sender_keys.npk(), 0, shared_secret_1), - (recipient_keys.npk(), 0, shared_secret_2), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret_1, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret_2, + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![state.get_proof_for_commitment(&sender_commitment), None], &program.into(), ) .unwrap(); @@ -1398,10 +1410,17 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 0], - vec![(sender_keys.npk(), 0, shared_secret)], - vec![sender_keys.nsk], - vec![state.get_proof_for_commitment(&sender_commitment)], + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], &program.into(), ) .unwrap(); @@ -1615,10 +1634,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(10_u128).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1641,10 +1657,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(10_u128).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1667,10 +1680,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(()).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1693,10 +1703,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(vec![0]).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1727,10 +1734,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(large_data).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1753,10 +1757,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(()).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1788,10 +1789,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(()).unwrap(), - vec![0, 0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public, InputAccountIdentity::Public], &program.into(), ); @@ -1814,10 +1812,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(()).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1849,10 +1844,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![0, 0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public, InputAccountIdentity::Public], &program.into(), ); @@ -1881,177 +1873,11 @@ pub mod tests { AccountId::new([1; 32]), ); - // Setting only one visibility mask for a circuit execution with two pre_state accounts. - let visibility_mask = [0]; + // Single account_identity entry for a circuit execution with two pre_state accounts. let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(10_u128).unwrap(), - visibility_mask.to_vec(), - vec![], - vec![], - vec![], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_nonces_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_keys_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); - - // Setting only one key for an execution with two private accounts. - let private_account_keys = [( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - )]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - private_account_keys.to_vec(), - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_commitment_proofs_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - // Setting no second commitment proof. - let private_account_membership_proofs = [Some((0, vec![]))]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - vec![sender_keys.nsk], - private_account_membership_proofs.to_vec(), - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_auth_keys_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - // Setting no auth key for an execution with one non default private accounts. - let private_account_nsks = []; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - private_account_nsks.to_vec(), - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -2075,33 +1901,26 @@ pub mod tests { let private_account_2 = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - let private_account_keys = [ - // First private account is the sender - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - // Second private account is the recipient - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ]; - - // Setting the recipient key to authorize the sender. - // This should be set to the sender private account in - // a normal circumstance. The recipient can't authorize this. - let private_account_nsks = [recipient_keys.nsk]; - let private_account_membership_proofs = [Some((0, vec![]))]; + // Setting the recipient nsk to authorize the sender. + // This should be set to the sender private account in a normal circumstance. + // `PrivateAuthorizedUpdate` derives npk from nsk and asserts equality with + // `pre_state.account_id`, so a mismatched nsk fails that check. let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - private_account_keys.to_vec(), - private_account_nsks.to_vec(), - private_account_membership_proofs.to_vec(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: recipient_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, + ], &program.into(), ); @@ -2135,21 +1954,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2183,21 +2000,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2231,21 +2046,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2279,21 +2092,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2325,21 +2136,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2368,14 +2177,16 @@ pub mod tests { let private_pda_account = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); - let visibility_mask = [0, 3]; let result = execute_and_prove( vec![public_account_1, private_pda_account], Program::serialize_instruction(10_u128).unwrap(), - visibility_mask.to_vec(), - vec![(npk, 0, shared_secret)], - vec![], - vec![None], + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + }, + ], &program.into(), ); @@ -2401,10 +2212,10 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![3], - vec![(npk, u128::MAX, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + }], &program.into(), ); @@ -2439,10 +2250,10 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![3], - vec![(npk_b, 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivatePdaInit { + npk: npk_b, + ssk: shared_secret, + }], &program.into(), ); @@ -2473,10 +2284,10 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction((seed, seed, callee_id)).unwrap(), - vec![3], - vec![(npk, u128::MAX, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + }], &program_with_deps, ); @@ -2510,10 +2321,10 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), - vec![3], - vec![(npk, 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + }], &program_with_deps, ); @@ -2546,10 +2357,16 @@ pub mod tests { let result = execute_and_prove( vec![pre_a, pre_b], Program::serialize_instruction(seed).unwrap(), - vec![3, 3], - vec![(keys_a.npk(), 0, shared_a), (keys_b.npk(), 0, shared_b)], - vec![], - vec![None, None], + vec![ + InputAccountIdentity::PrivatePdaInit { + npk: keys_a.npk(), + ssk: shared_a, + }, + InputAccountIdentity::PrivatePdaInit { + npk: keys_b.npk(), + ssk: shared_b, + }, + ], &program.into(), ); @@ -2590,145 +2407,10 @@ pub mod tests { let result = execute_and_prove( vec![owned_pre_state], Program::serialize_instruction(()).unwrap(), - vec![3], - vec![(npk, 0, shared_secret)], - vec![], - vec![None], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - #[test] - fn circuit_should_fail_with_too_many_nonces() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_with_too_many_private_account_keys() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - // Setting three private account keys for a circuit execution with only two private - // accounts. - let private_account_keys = [ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[57; 32], &sender_keys.vpk()), - ), - ]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - private_account_keys.to_vec(), - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_with_too_many_private_account_auth_keys() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - // Setting two private account keys for a circuit execution with only one non default - // private account (visibility mask equal to 1 means that auth keys are expected). - let visibility_mask = [1, 2]; - let private_account_nsks = [sender_keys.nsk, recipient_keys.nsk]; - let private_account_membership_proofs = [Some((0, vec![])), Some((1, vec![]))]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - visibility_mask.to_vec(), - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - private_account_nsks.to_vec(), - private_account_membership_proofs.to_vec(), + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + }], &program.into(), ); @@ -2805,20 +2487,24 @@ pub mod tests { (&sender_keys.npk(), 0), ); - let visibility_mask = [1, 1]; - let private_account_nsks = [sender_keys.nsk, sender_keys.nsk]; - let private_account_membership_proofs = [Some((1, vec![])), Some((1, vec![]))]; let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.vpk()); let result = execute_and_prove( vec![private_account_1.clone(), private_account_1], Program::serialize_instruction(100_u128).unwrap(), - visibility_mask.to_vec(), vec![ - (sender_keys.npk(), 0, shared_secret), - (sender_keys.npk(), 0, shared_secret), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + identifier: 0, + }, ], - private_account_nsks.to_vec(), - private_account_membership_proofs.to_vec(), &program.into(), ); @@ -3109,10 +2795,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(0_u128).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -3151,10 +2834,17 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(37_u128).unwrap(), - vec![1, 0], - vec![(sender_keys.npk(), 0, shared_secret)], - vec![sender_keys.nsk], - vec![state.get_proof_for_commitment(&sender_commitment)], + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], &program.into(), ) .unwrap(); @@ -3272,12 +2962,23 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![to_account, from_account], Program::serialize_instruction(instruction).unwrap(), - vec![1, 1], - vec![(from_keys.npk(), 0, to_ss), (to_keys.npk(), 0, from_ss)], - vec![from_keys.nsk, to_keys.nsk], vec![ - state.get_proof_for_commitment(&from_commitment), - state.get_proof_for_commitment(&to_commitment), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: to_ss, + nsk: from_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&from_commitment) + .expect("from's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: from_ss, + nsk: to_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&to_commitment) + .expect("to's commitment must be in state"), + identifier: 0, + }, ], &program_with_deps, ) @@ -3541,10 +3242,11 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![authorized_account], Program::serialize_instruction(balance).unwrap(), - vec![1], - vec![(private_keys.npk(), 0, shared_secret)], - vec![private_keys.nsk], - vec![None], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret, + nsk: private_keys.nsk, + identifier: 0, + }], &program.into(), ) .unwrap(); @@ -3589,10 +3291,11 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![unauthorized_account], Program::serialize_instruction(0_u128).unwrap(), - vec![2], - vec![(private_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivateUnauthorized { + npk: private_keys.npk(), + ssk: shared_secret, + identifier: 0, + }], &program.into(), ) .unwrap(); @@ -3641,10 +3344,11 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![authorized_account.clone()], Program::serialize_instruction(balance).unwrap(), - vec![1], - vec![(private_keys.npk(), 0, shared_secret)], - vec![private_keys.nsk], - vec![None], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret, + nsk: private_keys.nsk, + identifier: 0, + }], &claimer_program.into(), ) .unwrap(); @@ -3687,10 +3391,11 @@ pub mod tests { let res = execute_and_prove( vec![account_metadata], Program::serialize_instruction(()).unwrap(), - vec![1], - vec![(private_keys.npk(), 0, shared_secret2)], - vec![private_keys.nsk], - vec![None], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret2, + nsk: private_keys.nsk, + identifier: 0, + }], &noop_program.into(), ); @@ -3763,14 +3468,12 @@ pub mod tests { let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![1], - vec![( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), - )], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], + vec![InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }], &program.into(), ); @@ -3791,14 +3494,12 @@ pub mod tests { let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![1], - vec![( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), - )], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], + vec![InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }], &program.into(), ); @@ -3851,10 +3552,17 @@ pub mod tests { let result = execute_and_prove( vec![sender_account, recipient_account], Program::serialize_instruction(instruction).unwrap(), - vec![0, 1], - vec![(recipient_keys.npk(), 0, recipient)], - vec![recipient_keys.nsk], - vec![state.get_proof_for_commitment(&recipient_commitment)], + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: recipient, + nsk: recipient_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&recipient_commitment) + .expect("recipient's commitment must be in state"), + identifier: 0, + }, + ], &program_with_deps, ); @@ -4001,10 +3709,11 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![pre], Program::serialize_instruction(instruction).unwrap(), - vec![2], - vec![(account_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivateUnauthorized { + npk: account_keys.npk(), + ssk: shared_secret, + identifier: 0, + }], &validity_window_program.into(), ) .unwrap(); @@ -4070,10 +3779,11 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![pre], Program::serialize_instruction(instruction).unwrap(), - vec![2], - vec![(account_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivateUnauthorized { + npk: account_keys.npk(), + ssk: shared_secret, + identifier: 0, + }], &validity_window_program.into(), ) .unwrap(); diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 70979b7e..f658ea53 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -5,7 +5,7 @@ use std::{ use nssa_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier, - MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, + InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce}, compute_digest_for_path, @@ -17,7 +17,7 @@ use nssa_core::{ }; use risc0_zkvm::{guest::env, serde::to_vec}; -const PRIVATE_PDA_FIXED_IDENTIFIER: u128 = u128::MAX; +const PRIVATE_PDA_FIXED_IDENTIFIER: Identifier = u128::MAX; /// State of the involved accounts before and after program execution. struct ExecutionState { @@ -25,16 +25,16 @@ struct ExecutionState { post_states: HashMap, block_validity_window: BlockValidityWindow, timestamp_validity_window: TimestampValidityWindow, - /// Positions (in `pre_states`) of mask-3 accounts whose supplied npk has been bound to - /// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk)` + /// Positions (in `pre_states`) of private-PDA accounts whose supplied npk has been bound + /// to their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk)` /// check. /// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on /// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state` /// under the private derivation. Binding is an idempotent property, not an event: the same /// position can legitimately be bound through both paths in the same tx (e.g. a program /// claims a private PDA and then delegates it to a callee), and the set uses `contains`, - /// not `assert!(insert)`. After the main loop, every mask-3 position must appear in this - /// set; otherwise the npk is unbound and the circuit rejects. + /// not `assert!(insert)`. After the main loop, every private-PDA position must appear in + /// this set; otherwise the npk is unbound and the circuit rejects. private_pda_bound_positions: HashSet, /// Across the whole transaction, each `(program_id, seed)` pair may resolve to at most one /// `AccountId`. A seed under a program can derive a family of accounts, one public PDA and @@ -45,39 +45,29 @@ struct ExecutionState { /// `AccountId` entry or as an equality check against the existing one, making the rule: one /// `(program, seed)` → one account per tx. pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>, - /// Map from a mask-3 `pre_state`'s position in `visibility_mask` to the npk supplied for - /// that position in `private_account_keys`. Built once in `derive_from_outputs` by walking - /// `visibility_mask` in lock-step with `private_account_keys`, used later by the claim and - /// caller-seeds authorization paths. + /// Map from a private-PDA `pre_state`'s position in `account_identities` to the npk that + /// variant supplies for that position. Populated once in `derive_from_outputs` by walking + /// `account_identities` and consulting `npk_if_private_pda`. Used later by the claim and + /// caller-seeds authorization paths to verify + /// `AccountId::for_private_pda(program_id, seed, npk) == pre_state.account_id`. private_pda_npk_by_position: HashMap, } impl ExecutionState { /// Validate program outputs and derive the overall execution state. pub fn derive_from_outputs( - visibility_mask: &[u8], - private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)], + account_identities: &[InputAccountIdentity], program_id: ProgramId, program_outputs: Vec, ) -> Self { - // Build position → npk map for mask-3 pre_states. `private_account_keys` is consumed in - // pre_state order across all masks 1/2/3, so walk `visibility_mask` in lock-step. The - // downstream `compute_circuit_output` also consumes the same iterator and its trailing - // assertions catch an over-supply of keys; under-supply surfaces here. + // Build position → npk map for private-PDA pre_states, indexed by position in + // `account_identities`. The vec is documented as 1:1 with the program's pre_state order, + // so position here matches `pre_state_position` used downstream in + // `validate_and_sync_states`. let mut private_pda_npk_by_position: HashMap = HashMap::new(); - { - let mut keys_iter = private_account_keys.iter(); - for (pos, &mask) in visibility_mask.iter().enumerate() { - if matches!(mask, 1..=3) { - let (npk, _, _) = keys_iter.next().unwrap_or_else(|| { - panic!( - "private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})" - ) - }); - if mask == 3 { - private_pda_npk_by_position.insert(pos, *npk); - } - } + for (pos, account_identity) in account_identities.iter().enumerate() { + if let Some(npk) = account_identity.npk_if_private_pda() { + private_pda_npk_by_position.insert(pos, npk); } } @@ -194,7 +184,7 @@ impl ExecutionState { } execution_state.validate_and_sync_states( - visibility_mask, + account_identities, chained_call.program_id, caller_program_id, &chained_call.pda_seeds, @@ -211,12 +201,12 @@ impl ExecutionState { "Inner call without a chained call found", ); - // Every mask-3 pre_state must have had its npk bound to its account_id, either via a - // `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` matching - // the private derivation. An unbound mask-3 pre_state has no cryptographic link between - // the supplied npk and the account_id, and must be rejected. - for (pos, &mask) in visibility_mask.iter().enumerate() { - if mask == 3 { + // Every private-PDA pre_state must have had its npk bound to its account_id, either via + // a `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` + // matching the private derivation. An unbound private-PDA pre_state has no + // cryptographic link between the supplied npk and the account_id, and must be rejected. + for (pos, account_identity) in account_identities.iter().enumerate() { + if account_identity.is_private_pda() { assert!( execution_state.private_pda_bound_positions.contains(&pos), "private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" @@ -251,7 +241,7 @@ impl ExecutionState { /// Validate program pre and post states and populate the execution state. fn validate_and_sync_states( &mut self, - visibility_mask: &[u8], + account_identities: &[InputAccountIdentity], program_id: ProgramId, caller_program_id: Option, caller_pda_seeds: &[PdaSeed], @@ -329,9 +319,9 @@ impl ExecutionState { .position(|acc| acc.account_id == pre_account_id) .expect("Pre state must exist at this point"); - let mask = visibility_mask[pre_state_position]; - match mask { - 0 => match claim { + let account_identity = &account_identities[pre_state_position]; + if account_identity.is_public() { + match claim { Claim::Authorized => { // Note: no need to check authorized pdas because we have already // checked consistency of authorization above. @@ -353,40 +343,40 @@ impl ExecutionState { pre_account_id, ); } - }, - 3 => { - match claim { - Claim::Authorized => { - assert!( - pre_is_authorized, - "Cannot claim unauthorized private PDA {pre_account_id}" - ); - } - Claim::Pda(seed) => { - let npk = self + } + } else if account_identity.is_private_pda() { + match claim { + Claim::Authorized => { + assert!( + pre_is_authorized, + "Cannot claim unauthorized private PDA {pre_account_id}" + ); + } + Claim::Pda(seed) => { + let npk = self .private_pda_npk_by_position .get(&pre_state_position) - .expect("private PDA pre_state must have an npk in the position map"); - let pda = AccountId::for_private_pda(&program_id, &seed, npk); - assert_eq!( - pre_account_id, pda, - "Invalid private PDA claim for account {pre_account_id}" + .expect( + "private PDA pre_state must have an npk in the position map", ); - self.private_pda_bound_positions.insert(pre_state_position); - assert_family_binding( - &mut self.pda_family_binding, - program_id, - seed, - pre_account_id, - ); - } + let pda = AccountId::for_private_pda(&program_id, &seed, npk); + assert_eq!( + pre_account_id, pda, + "Invalid private PDA claim for account {pre_account_id}" + ); + self.private_pda_bound_positions.insert(pre_state_position); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); } } - _ => { - // Mask 1/2: standard private accounts don't enforce the claim semantics. - // Unauthorized private claiming is intentionally allowed since operating - // these accounts requires the npk/nsk keypair anyway. - } + } else { + // Standalone private accounts: don't enforce the claim semantics. + // Unauthorized private claiming is intentionally allowed since operating + // these accounts requires the npk/nsk keypair anyway. } post.account_mut().program_owner = program_id; @@ -488,10 +478,7 @@ fn resolve_authorization_and_record_bindings( fn compute_circuit_output( execution_state: ExecutionState, - visibility_mask: &[u8], - private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)], - private_account_nsks: &[NullifierSecretKey], - private_account_membership_proofs: &[Option], + account_identities: &[InputAccountIdentity], ) -> PrivacyPreservingCircuitOutput { let mut output = PrivacyPreservingCircuitOutput { public_pre_states: Vec::new(), @@ -505,290 +492,268 @@ fn compute_circuit_output( let states_iter = execution_state.into_states_iter(); assert_eq!( - visibility_mask.len(), + account_identities.len(), states_iter.len(), - "Invalid visibility mask length" + "Invalid account_identities length" ); - let mut private_keys_iter = private_account_keys.iter(); - let mut private_nsks_iter = private_account_nsks.iter(); - let mut private_membership_proofs_iter = private_account_membership_proofs.iter(); - let mut output_index = 0; - for (account_visibility_mask, (pre_state, post_state)) in - visibility_mask.iter().copied().zip(states_iter) - { - match account_visibility_mask { - 0 => { - // Public account + for (account_identity, (pre_state, post_state)) in account_identities.iter().zip(states_iter) { + match account_identity { + InputAccountIdentity::Public => { output.public_pre_states.push(pre_state); output.public_post_states.push(post_state); } - 1 | 2 => { - let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else { - panic!("Missing private account key"); - }; + InputAccountIdentity::PrivateAuthorizedInit { + ssk, + nsk, + identifier, + } => { assert_ne!( *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, "Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA." ); + let npk = NullifierPublicKey::from(nsk); + let account_id = AccountId::from((&npk, *identifier)); + assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); + assert!( + pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" + ); + assert_eq!( + pre_state.account, + Account::default(), + "Found new private account with non default values" + ); + + let new_nullifier = ( + Nullifier::for_account_initialization(&account_id), + DUMMY_COMMITMENT_HASH, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + *identifier, + ssk, + new_nullifier, + new_nonce, + ); + } + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk, + nsk, + membership_proof, + identifier, + } => { + assert_ne!( + *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, + "Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA." + ); + let npk = NullifierPublicKey::from(nsk); + let account_id = AccountId::from((&npk, *identifier)); + + assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); + assert!( + pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" + ); + + let new_nullifier = compute_update_nullifier_and_set_digest( + membership_proof, + &pre_state.account, + &account_id, + nsk, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + *identifier, + ssk, + new_nullifier, + new_nonce, + ); + } + InputAccountIdentity::PrivateUnauthorized { + npk, + ssk, + identifier, + } => { + assert_ne!( + *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, + "Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA." + ); let account_id = AccountId::from((npk, *identifier)); assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); - - let (new_nullifier, new_nonce) = if account_visibility_mask == 1 { - // Private account with authentication - - let Some(nsk) = private_nsks_iter.next() else { - panic!("Missing private account nullifier secret key"); - }; - - // Verify the nullifier public key - assert_eq!( - npk, - &NullifierPublicKey::from(nsk), - "Nullifier public key mismatch" - ); - - // Check pre_state authorization - assert!( - pre_state.is_authorized, - "Pre-state not authorized for authenticated private account" - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - - let new_nullifier = compute_nullifier_and_set_digest( - membership_proof_opt.as_ref(), - &pre_state.account, - &account_id, - nsk, - ); - - let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); - - (new_nullifier, new_nonce) - } else { - // Private account without authentication - - assert_eq!( - pre_state.account, - Account::default(), - "Found new private account with non default values", - ); - - assert!( - !pre_state.is_authorized, - "Found new private account marked as authorized." - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - - assert!( - membership_proof_opt.is_none(), - "Membership proof must be None for unauthorized accounts" - ); - - let nullifier = Nullifier::for_account_initialization(&account_id); - - let new_nonce = Nonce::private_account_nonce_init(&account_id); - - ((nullifier, DUMMY_COMMITMENT_HASH), new_nonce) - }; - output.new_nullifiers.push(new_nullifier); - - // Update post-state with new nonce - let mut post_with_updated_nonce = post_state; - post_with_updated_nonce.nonce = new_nonce; - - // Compute commitment - let commitment_post = Commitment::new(&account_id, &post_with_updated_nonce); - - // Encrypt and push post state - let encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_nonce, - *identifier, - shared_secret, - &commitment_post, - output_index, - ); - - output.new_commitments.push(commitment_post); - output.ciphertexts.push(encrypted_account); - output_index = output_index - .checked_add(1) - .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); - } - 3 => { - // Private PDA account. The supplied npk has already been bound to - // `pre_state.account_id` upstream in `validate_and_sync_states`, either via a - // `Claim::Pda(seed)` match or via a caller `pda_seeds` match, both of which - // assert `AccountId::for_private_pda(owner, seed, npk) == account_id`. The - // post-loop assertion in `derive_from_outputs` (see the - // `private_pda_bound_positions` check) guarantees that every mask-3 - // position has been through at least one such binding, so this - // branch can safely use the wallet npk without re-verifying. - let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else { - panic!("Missing private account key"); - }; - assert_eq!( - *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, - "Identifier for private PDAs must be {PRIVATE_PDA_FIXED_IDENTIFIER}." + pre_state.account, + Account::default(), + "Found new private account with non default values", + ); + assert!( + !pre_state.is_authorized, + "Found new private account marked as authorized." ); - let (new_nullifier, new_nonce) = if pre_state.is_authorized { - // Existing private PDA with authentication (like mask 1) - let Some(nsk) = private_nsks_iter.next() else { - panic!("Missing private account nullifier secret key"); - }; - assert_eq!( - npk, - &NullifierPublicKey::from(nsk), - "Nullifier public key mismatch" - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - - let new_nullifier = compute_nullifier_and_set_digest( - membership_proof_opt.as_ref(), - &pre_state.account, - &pre_state.account_id, - nsk, - ); - let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); - (new_nullifier, new_nonce) - } else { - // New private PDA (like mask 2). The default + unauthorized requirement - // here rules out use cases like a fully-private multisig, which would need - // a non-default, non-authorized private PDA input account. - // TODO(private-pdas-pr-2/3): relax this once the wallet can supply a - // `(seed, owner)` side input so the npk-to-account_id binding can be - // re-verified for an existing private PDA without a `Claim::Pda` or caller - // `pda_seeds` match. - assert_eq!( - pre_state.account, - Account::default(), - "New private PDA must be default" - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - assert!( - membership_proof_opt.is_none(), - "Membership proof must be None for new accounts" - ); - - let nullifier = Nullifier::for_account_initialization(&pre_state.account_id); - let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); - ((nullifier, DUMMY_COMMITMENT_HASH), new_nonce) - }; - output.new_nullifiers.push(new_nullifier); - - let mut post_with_updated_nonce = post_state; - post_with_updated_nonce.nonce = new_nonce; - - let commitment_post = - Commitment::new(&pre_state.account_id, &post_with_updated_nonce); - - let encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_nonce, - PRIVATE_PDA_FIXED_IDENTIFIER, - shared_secret, - &commitment_post, - output_index, + let new_nullifier = ( + Nullifier::for_account_initialization(&account_id), + DUMMY_COMMITMENT_HASH, ); + let new_nonce = Nonce::private_account_nonce_init(&account_id); - output.new_commitments.push(commitment_post); - output.ciphertexts.push(encrypted_account); - output_index = output_index - .checked_add(1) - .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + *identifier, + ssk, + new_nullifier, + new_nonce, + ); + } + InputAccountIdentity::PrivatePdaInit { npk: _, ssk } => { + // The npk-to-account_id binding is established upstream in + // `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` + // match. Here we only enforce the init pre-conditions. The supplied npk on + // the variant has been recorded into `private_pda_npk_by_position` and used + // for the binding check; we use `pre_state.account_id` directly for nullifier + // and commitment derivation. + assert!( + !pre_state.is_authorized, + "PrivatePdaInit requires unauthorized pre_state" + ); + assert_eq!( + pre_state.account, + Account::default(), + "New private PDA must be default" + ); + + let new_nullifier = ( + Nullifier::for_account_initialization(&pre_state.account_id), + DUMMY_COMMITMENT_HASH, + ); + let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); + + let account_id = pre_state.account_id; + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + PRIVATE_PDA_FIXED_IDENTIFIER, + ssk, + new_nullifier, + new_nonce, + ); + } + InputAccountIdentity::PrivatePdaUpdate { + ssk, + nsk, + membership_proof, + } => { + // The npk binding is established upstream. Authorization must already be set; + // an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an + // unbound PDA, which the upstream binding check would have rejected anyway, + // but we assert here to fail fast and document the precondition. + assert!( + pre_state.is_authorized, + "PrivatePdaUpdate requires authorized pre_state" + ); + + let new_nullifier = compute_update_nullifier_and_set_digest( + membership_proof, + &pre_state.account, + &pre_state.account_id, + nsk, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + + let account_id = pre_state.account_id; + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + PRIVATE_PDA_FIXED_IDENTIFIER, + ssk, + new_nullifier, + new_nonce, + ); } - _ => panic!("Invalid visibility mask value"), } } - assert!( - private_keys_iter.next().is_none(), - "Too many private account keys" - ); - - assert!( - private_nsks_iter.next().is_none(), - "Too many private account nullifier secret keys" - ); - - assert!( - private_membership_proofs_iter.next().is_none(), - "Too many private account membership proofs" - ); - output } -fn compute_nullifier_and_set_digest( - membership_proof_opt: Option<&MembershipProof>, +#[expect( + clippy::too_many_arguments, + reason = "All seven inputs are distinct concerns from the variant arms; bundling would be artificial" +)] +fn emit_private_output( + output: &mut PrivacyPreservingCircuitOutput, + output_index: &mut u32, + post_state: Account, + account_id: &AccountId, + identifier: Identifier, + shared_secret: &SharedSecretKey, + new_nullifier: (Nullifier, CommitmentSetDigest), + new_nonce: Nonce, +) { + output.new_nullifiers.push(new_nullifier); + + let mut post_with_updated_nonce = post_state; + post_with_updated_nonce.nonce = new_nonce; + + let commitment_post = Commitment::new(account_id, &post_with_updated_nonce); + let encrypted_account = EncryptionScheme::encrypt( + &post_with_updated_nonce, + identifier, + shared_secret, + &commitment_post, + *output_index, + ); + + output.new_commitments.push(commitment_post); + output.ciphertexts.push(encrypted_account); + *output_index = output_index + .checked_add(1) + .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); +} + +fn compute_update_nullifier_and_set_digest( + membership_proof: &MembershipProof, pre_account: &Account, account_id: &AccountId, nsk: &NullifierSecretKey, ) -> (Nullifier, CommitmentSetDigest) { - membership_proof_opt.as_ref().map_or_else( - || { - assert_eq!( - *pre_account, - Account::default(), - "Found new private account with non default values" - ); - - // Compute initialization nullifier - let nullifier = Nullifier::for_account_initialization(account_id); - (nullifier, DUMMY_COMMITMENT_HASH) - }, - |membership_proof| { - // Compute commitment set digest associated with provided auth path - let commitment_pre = Commitment::new(account_id, pre_account); - let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); - - // Compute update nullifier - let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); - (nullifier, set_digest) - }, - ) + let commitment_pre = Commitment::new(account_id, pre_account); + let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); + let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); + (nullifier, set_digest) } fn main() { let PrivacyPreservingCircuitInput { program_outputs, - visibility_mask, - private_account_keys, - private_account_nsks, - private_account_membership_proofs, + account_identities, program_id, } = env::read(); - let execution_state = ExecutionState::derive_from_outputs( - &visibility_mask, - &private_account_keys, - program_id, - program_outputs, - ); + let execution_state = + ExecutionState::derive_from_outputs(&account_identities, program_id, program_outputs); - let output = compute_circuit_output( - execution_state, - &visibility_mask, - &private_account_keys, - &private_account_nsks, - &private_account_membership_proofs, - ); + let output = compute_circuit_output(execution_state, &account_identities); env::commit(&output); } diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 22c09d85..5f118a2f 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -1076,7 +1076,7 @@ mod tests { program::Program, }; use nssa_core::{ - SharedSecretKey, + InputAccountIdentity, SharedSecretKey, account::AccountWithMetadata, encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}, }; @@ -1114,10 +1114,11 @@ mod tests { (&npk, 0), )], Program::serialize_instruction(0_u128).unwrap(), - vec![1], - vec![(npk, 0, shared_secret)], - vec![nsk], - vec![None], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret, + nsk, + identifier: 0, + }], &Program::authenticated_transfer_program().into(), ) .unwrap(); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index f0c69fea..c8244ef9 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -413,13 +413,7 @@ impl WalletCore { let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( pre_states, instruction_data, - acc_manager.visibility_mask().to_vec(), - private_account_keys - .iter() - .map(|keys| (keys.npk, keys.identifier, keys.ssk)) - .collect::>(), - acc_manager.private_account_auth(), - acc_manager.private_account_membership_proofs(), + acc_manager.account_identities(), &program.to_owned(), ) .unwrap(); diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 252cdfad..35419534 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -2,7 +2,8 @@ use anyhow::Result; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use nssa::{AccountId, PrivateKey}; use nssa_core::{ - Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, + SharedSecretKey, account::{AccountWithMetadata, Nonce}, encryption::{EphemeralPublicKey, ViewingPublicKey}, program::{PdaSeed, ProgramId}, @@ -54,7 +55,6 @@ impl PrivacyPreservingAccount { pub struct PrivateAccountKeys { pub npk: NullifierPublicKey, - pub identifier: Identifier, pub ssk: SharedSecretKey, pub vpk: ViewingPublicKey, pub epk: EphemeralPublicKey, @@ -70,7 +70,6 @@ enum State { pub struct AccountManager { states: Vec, - visibility_mask: Vec, } impl AccountManager { @@ -78,11 +77,10 @@ impl AccountManager { wallet: &WalletCore, accounts: Vec, ) -> Result { - let mut pre_states = Vec::with_capacity(accounts.len()); - let mut visibility_mask = Vec::with_capacity(accounts.len()); + let mut states = Vec::with_capacity(accounts.len()); for account in accounts { - let (state, mask) = match account { + let state = match account { PrivacyPreservingAccount::Public(account_id) => { let acc = wallet .get_account_public(account_id) @@ -92,13 +90,12 @@ impl AccountManager { let sk = wallet.get_account_public_signing_key(account_id).cloned(); let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); - (State::Public { account, sk }, 0) + State::Public { account, sk } } PrivacyPreservingAccount::PrivateOwned(account_id) => { let pre = private_acc_preparation(wallet, account_id).await?; - let mask = if pre.pre_state.is_authorized { 1 } else { 2 }; - (State::Private(pre), mask) + State::Private(pre) } PrivacyPreservingAccount::PrivateForeign { npk, @@ -107,6 +104,9 @@ impl AccountManager { } => { let acc = nssa_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, (&npk, identifier)); + let eph_holder = EphemeralKeyHolder::new(&npk); + let ssk = eph_holder.calculate_shared_secret_sender(&vpk); + let epk = eph_holder.generate_ephemeral_public_key(); let pre = AccountPreparedData { nsk: None, npk, @@ -114,9 +114,11 @@ impl AccountManager { vpk, pre_state: auth_acc, proof: None, + ssk, + epk, }; - (State::Private(pre), 2) + State::Private(pre) } PrivacyPreservingAccount::PrivatePda { nsk, @@ -128,21 +130,16 @@ impl AccountManager { let pre = private_pda_preparation(wallet, nsk, npk, vpk, &program_id, &seed).await?; - (State::Private(pre), 3) + State::Private(pre) } }; - pre_states.push(state); - visibility_mask.push(mask); + states.push(state); } - Ok(Self { - states: pre_states, - visibility_mask, - }) + Ok(Self { states }) } - #[must_use] pub fn pre_states(&self) -> Vec { self.states .iter() @@ -153,12 +150,6 @@ impl AccountManager { .collect() } - #[must_use] - pub fn visibility_mask(&self) -> &[u8] { - &self.visibility_mask - } - - #[must_use] pub fn public_account_nonces(&self) -> Vec { self.states .iter() @@ -169,50 +160,70 @@ impl AccountManager { .collect() } - #[must_use] pub fn private_account_keys(&self) -> Vec { self.states .iter() .filter_map(|state| match state { - State::Private(pre) => { - let eph_holder = EphemeralKeyHolder::new(&pre.npk); + State::Private(pre) => Some(PrivateAccountKeys { + npk: pre.npk, + ssk: pre.ssk, + vpk: pre.vpk.clone(), + epk: pre.epk.clone(), + }), + State::Public { .. } => None, + }) + .collect() + } - Some(PrivateAccountKeys { - npk: pre.npk, - identifier: pre.identifier, - ssk: eph_holder.calculate_shared_secret_sender(&pre.vpk), - vpk: pre.vpk.clone(), - epk: eph_holder.generate_ephemeral_public_key(), - }) + /// Build the per-account input vec for the privacy-preserving circuit. Each variant carries + /// exactly the fields the circuit's code path for that account needs, with the ephemeral + /// keys (`ssk`) drawn from the cached values that `private_account_keys` and the message + /// construction also use, so all three views agree on the same ephemeral key. + pub fn account_identities(&self) -> Vec { + self.states + .iter() + .map(|state| match state { + State::Public { .. } => InputAccountIdentity::Public, + State::Private(pre) if pre.identifier == u128::MAX => { + // Private PDA account + match (pre.nsk, pre.proof.clone()) { + (Some(nsk), Some(membership_proof)) => { + InputAccountIdentity::PrivatePdaUpdate { + ssk: pre.ssk, + nsk, + membership_proof, + } + } + _ => InputAccountIdentity::PrivatePdaInit { + npk: pre.npk, + ssk: pre.ssk, + }, + } } - State::Public { .. } => None, + State::Private(pre) => match (pre.nsk, pre.proof.clone()) { + (Some(nsk), Some(membership_proof)) => { + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: pre.ssk, + nsk, + membership_proof, + identifier: pre.identifier, + } + } + (Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit { + ssk: pre.ssk, + nsk, + identifier: pre.identifier, + }, + (None, _) => InputAccountIdentity::PrivateUnauthorized { + npk: pre.npk, + ssk: pre.ssk, + identifier: pre.identifier, + }, + }, }) .collect() } - #[must_use] - pub fn private_account_auth(&self) -> Vec { - self.states - .iter() - .filter_map(|state| match state { - State::Private(pre) => pre.nsk, - State::Public { .. } => None, - }) - .collect() - } - - #[must_use] - pub fn private_account_membership_proofs(&self) -> Vec> { - self.states - .iter() - .filter_map(|state| match state { - State::Private(pre) => Some(pre.proof.clone()), - State::Public { .. } => None, - }) - .collect() - } - - #[must_use] pub fn public_account_ids(&self) -> Vec { self.states .iter() @@ -223,7 +234,6 @@ impl AccountManager { .collect() } - #[must_use] pub fn public_account_auth(&self) -> Vec<&PrivateKey> { self.states .iter() @@ -242,6 +252,54 @@ struct AccountPreparedData { vpk: ViewingPublicKey, pre_state: AccountWithMetadata, proof: Option, + /// Cached shared-secret key derived once at `AccountManager::new`. Reused for both the + /// circuit input variant (`account_identities()`) and the message ephemeral-key tuples + /// (`private_account_keys()`), so all consumers see the same key. The corresponding + /// `EphemeralKeyHolder` uses `OsRng` and would produce a different value on a second call. + ssk: SharedSecretKey, + /// Cached ephemeral public key, paired with `ssk`. + epk: EphemeralPublicKey, +} + +async fn private_acc_preparation( + wallet: &WalletCore, + account_id: AccountId, +) -> Result { + let Some((from_keys, from_acc, from_identifier)) = + wallet.storage.user_data.get_private_account(account_id) + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let nsk = from_keys.private_key_holder.nullifier_secret_key; + + let from_npk = from_keys.nullifier_public_key; + let from_vpk = from_keys.viewing_public_key; + + // TODO: Remove this unwrap, error types must be compatible + let proof = wallet + .check_private_account_initialized(account_id) + .await + .unwrap(); + + // TODO: Technically we could allow unauthorized owned accounts, but currently we don't have + // support from that in the wallet. + let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, (&from_npk, from_identifier)); + + let eph_holder = EphemeralKeyHolder::new(&from_npk); + let ssk = eph_holder.calculate_shared_secret_sender(&from_vpk); + let epk = eph_holder.generate_ephemeral_public_key(); + + Ok(AccountPreparedData { + nsk: Some(nsk), + npk: from_npk, + identifier: from_identifier, + vpk: from_vpk, + pre_state: sender_pre, + proof, + ssk, + epk, + }) } async fn private_pda_preparation( @@ -281,6 +339,10 @@ async fn private_pda_preparation( None }; + let eph_holder = EphemeralKeyHolder::new(&npk); + let ssk = eph_holder.calculate_shared_secret_sender(&vpk); + let epk = eph_holder.generate_ephemeral_public_key(); + Ok(AccountPreparedData { nsk: exists.then_some(nsk), npk, @@ -288,58 +350,7 @@ async fn private_pda_preparation( vpk, pre_state, proof, + ssk, + epk, }) } - -async fn private_acc_preparation( - wallet: &WalletCore, - account_id: AccountId, -) -> Result { - let Some((from_keys, from_acc, from_identifier)) = - wallet.storage.user_data.get_private_account(account_id) - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let nsk = from_keys.private_key_holder.nullifier_secret_key; - - let from_npk = from_keys.nullifier_public_key; - let from_vpk = from_keys.viewing_public_key; - - // TODO: Remove this unwrap, error types must be compatible - let proof = wallet - .check_private_account_initialized(account_id) - .await - .unwrap(); - - // TODO: Technically we could allow unauthorized owned accounts, but currently we don't have - // support from that in the wallet. - let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, (&from_npk, from_identifier)); - - Ok(AccountPreparedData { - nsk: Some(nsk), - npk: from_npk, - identifier: from_identifier, - vpk: from_vpk, - pre_state: sender_pre, - proof, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn private_pda_is_private() { - let acc = PrivacyPreservingAccount::PrivatePda { - nsk: [0; 32], - npk: NullifierPublicKey([1; 32]), - vpk: ViewingPublicKey::from_scalar([2; 32]), - program_id: [3; 8], - seed: PdaSeed::new([4; 32]), - }; - assert!(acc.is_private()); - assert!(!acc.is_public()); - } -}