diff --git a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin index c0645c49..452347f7 100644 Binary files a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin and b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin differ diff --git a/artifacts/lez/programs/amm.bin b/artifacts/lez/programs/amm.bin index a23b5517..e522e482 100644 Binary files a/artifacts/lez/programs/amm.bin and b/artifacts/lez/programs/amm.bin differ diff --git a/artifacts/lez/programs/associated_token_account.bin b/artifacts/lez/programs/associated_token_account.bin index 99076825..8db70d97 100644 Binary files a/artifacts/lez/programs/associated_token_account.bin and b/artifacts/lez/programs/associated_token_account.bin differ diff --git a/artifacts/lez/programs/authenticated_transfer.bin b/artifacts/lez/programs/authenticated_transfer.bin index ee8929dd..1260845a 100644 Binary files a/artifacts/lez/programs/authenticated_transfer.bin and b/artifacts/lez/programs/authenticated_transfer.bin differ diff --git a/artifacts/lez/programs/bridge.bin b/artifacts/lez/programs/bridge.bin index b887af29..efb3bb5d 100644 Binary files a/artifacts/lez/programs/bridge.bin and b/artifacts/lez/programs/bridge.bin differ diff --git a/artifacts/lez/programs/bridge_lock.bin b/artifacts/lez/programs/bridge_lock.bin index 87da00db..ba19bbdd 100644 Binary files a/artifacts/lez/programs/bridge_lock.bin and b/artifacts/lez/programs/bridge_lock.bin differ diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index e94c08bf..c60f054a 100644 Binary files a/artifacts/lez/programs/clock.bin and b/artifacts/lez/programs/clock.bin differ diff --git a/artifacts/lez/programs/cross_zone_inbox.bin b/artifacts/lez/programs/cross_zone_inbox.bin index 2cbd007d..10cf3b52 100644 Binary files a/artifacts/lez/programs/cross_zone_inbox.bin and b/artifacts/lez/programs/cross_zone_inbox.bin differ diff --git a/artifacts/lez/programs/cross_zone_outbox.bin b/artifacts/lez/programs/cross_zone_outbox.bin index 19dfce4b..92631b24 100644 Binary files a/artifacts/lez/programs/cross_zone_outbox.bin and b/artifacts/lez/programs/cross_zone_outbox.bin differ diff --git a/artifacts/lez/programs/faucet.bin b/artifacts/lez/programs/faucet.bin index f0c1d99e..f13abd9c 100644 Binary files a/artifacts/lez/programs/faucet.bin and b/artifacts/lez/programs/faucet.bin differ diff --git a/artifacts/lez/programs/pinata.bin b/artifacts/lez/programs/pinata.bin index df02776e..1d9ccc30 100644 Binary files a/artifacts/lez/programs/pinata.bin and b/artifacts/lez/programs/pinata.bin differ diff --git a/artifacts/lez/programs/pinata_token.bin b/artifacts/lez/programs/pinata_token.bin index e787a117..86db2921 100644 Binary files a/artifacts/lez/programs/pinata_token.bin and b/artifacts/lez/programs/pinata_token.bin differ diff --git a/artifacts/lez/programs/ping_receiver.bin b/artifacts/lez/programs/ping_receiver.bin index 7454d1ff..e19b89f4 100644 Binary files a/artifacts/lez/programs/ping_receiver.bin and b/artifacts/lez/programs/ping_receiver.bin differ diff --git a/artifacts/lez/programs/ping_sender.bin b/artifacts/lez/programs/ping_sender.bin index 09240097..df9acc32 100644 Binary files a/artifacts/lez/programs/ping_sender.bin and b/artifacts/lez/programs/ping_sender.bin differ diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index 109cd9c4..dec3c8cd 100644 Binary files a/artifacts/lez/programs/token.bin and b/artifacts/lez/programs/token.bin differ diff --git a/artifacts/lez/programs/vault.bin b/artifacts/lez/programs/vault.bin index 45b89776..2523b2ce 100644 Binary files a/artifacts/lez/programs/vault.bin and b/artifacts/lez/programs/vault.bin differ diff --git a/artifacts/lez/programs/wrapped_token.bin b/artifacts/lez/programs/wrapped_token.bin index 777d20fc..ee5a84b7 100644 Binary files a/artifacts/lez/programs/wrapped_token.bin and b/artifacts/lez/programs/wrapped_token.bin differ diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index a472c260..5feb0283 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -81,9 +81,9 @@ async fn private_transfer_to_foreign_account() -> Result<()> { .context("Failed to get private account commitment for sender")?; let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await; - assert!(tx.message.new_commitments.contains(&new_commitment1)); + assert!(tx.message.commitments().contains(&new_commitment1)); - for commitment in tx.message.new_commitments { + for commitment in tx.message.commitments() { assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); } @@ -169,9 +169,9 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { .wallet() .get_private_account_commitment(from) .context("Failed to get private account commitment for sender")?; - assert!(tx.message.new_commitments.contains(&sender_commitment)); + assert!(tx.message.commitments().contains(&sender_commitment)); - for commitment in tx.message.new_commitments { + for commitment in tx.message.commitments() { assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); } @@ -245,7 +245,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> { let acc_1_balance = account_balance(&ctx, from).await?; - for commitment in tx.message.new_commitments { + for commitment in tx.message.commitments() { assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); } @@ -301,7 +301,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Verify commitments are in state - for commitment in tx.message.new_commitments { + for commitment in tx.message.commitments() { assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); } @@ -657,8 +657,9 @@ async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> { let output = prove_init_with_commitment_root(&ctx, expected_digest).await?; - assert_eq!(output.new_nullifiers.len(), 1); - let (nullifier, digest) = &output.new_nullifiers[0]; + assert_eq!(output.private_actions.len(), 1); + let action = &output.private_actions[0]; + let (nullifier, digest) = (&action.nullifier, &action.root); assert_eq!( *nullifier, Nullifier::for_account_initialization(&recipient_account_id) @@ -678,14 +679,14 @@ async fn init_nullifier_digest_is_bound_to_commitment_root() -> Result<()> { let output_with_root = prove_init_with_commitment_root(&ctx, expected_digest).await?; let output_without_root = prove_init_with_commitment_root(&ctx, DUMMY_COMMITMENT_HASH).await?; - assert_eq!(output_with_root.new_nullifiers[0].1, expected_digest); + assert_eq!(output_with_root.private_actions[0].root, expected_digest); assert_eq!( - output_without_root.new_nullifiers[0].1, + output_without_root.private_actions[0].root, DUMMY_COMMITMENT_HASH ); assert_ne!( - output_with_root.new_nullifiers[0].1, - output_without_root.new_nullifiers[0].1, + output_with_root.private_actions[0].root, + output_without_root.private_actions[0].root, ); Ok(()) diff --git a/integration_tests/tests/bridge.rs b/integration_tests/tests/bridge.rs index 9c241ebb..b42d7788 100644 --- a/integration_tests/tests/bridge.rs +++ b/integration_tests/tests/bridge.rs @@ -201,12 +201,10 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { .context("Failed to execute/prove bridge deposit")?; // Create privacy-preserving transaction from circuit output - let message = privacy_preserving_transaction::Message::try_from_circuit_output( - vec![bridge_account_id, recipient_vault_id], + let message = privacy_preserving_transaction::Message::from_circuit_output( vec![bridge_pre.account.nonce, vault_pre.account.nonce], output, - ) - .context("Failed to build privacy-preserving bridge deposit message")?; + ); let witness_set = privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); let attack_tx = LeeTransaction::PrivacyPreserving(lee::PrivacyPreservingTransaction::new( diff --git a/integration_tests/tests/keys.rs b/integration_tests/tests/keys.rs index fb958ec0..af4bbda8 100644 --- a/integration_tests/tests/keys.rs +++ b/integration_tests/tests/keys.rs @@ -71,9 +71,9 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { .wallet() .get_private_account_commitment(from) .context("Failed to get private account commitment for sender")?; - assert!(tx.message.new_commitments.contains(&new_commitment1)); + assert!(tx.message.commitments().contains(&new_commitment1)); - for commitment in tx.message.new_commitments { + for commitment in tx.message.commitments() { assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); } diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index 0c67049f..f4e5b297 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -83,9 +83,7 @@ async fn fund_private_pda( ) .map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?; - let message = - Message::try_from_circuit_output(vec![sender], vec![sender_account.nonce], output) - .map_err(|e| anyhow::anyhow!("message build failed: {e}"))?; + let message = Message::from_circuit_output(vec![sender_account.nonce], output); let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]); let tx = PrivacyPreservingTransaction::new(message, witness_set); diff --git a/integration_tests/tests/private_transaction_padding.rs b/integration_tests/tests/private_transaction_padding.rs index d9fceefe..925d058d 100644 --- a/integration_tests/tests/private_transaction_padding.rs +++ b/integration_tests/tests/private_transaction_padding.rs @@ -26,9 +26,7 @@ async fn private_transaction_pads_notes_to_max() -> Result<()> { let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await; - assert_eq!(tx.message.new_commitments.len(), 7); - assert_eq!(tx.message.new_nullifiers.len(), 7); - assert_eq!(tx.message.encrypted_private_post_states.len(), 7); + assert_eq!(tx.message.private_actions.len(), 7); Ok(()) } diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 7141303e..6f936510 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -310,7 +310,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { &program.into(), ) .unwrap(); - let message = pptx::message::Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + let message = pptx::message::Message::from_circuit_output(vec![], output); let witness_set = pptx::witness_set::WitnessSet::for_message(&message, proof, &[]); pptx::PrivacyPreservingTransaction::new(message, witness_set) } diff --git a/lee/privacy_preserving_circuit/src/output.rs b/lee/privacy_preserving_circuit/src/output.rs index ab00f8c3..e1c7a146 100644 --- a/lee/privacy_preserving_circuit/src/output.rs +++ b/lee/privacy_preserving_circuit/src/output.rs @@ -1,7 +1,8 @@ use lee_core::{ Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme, EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, - NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, + NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, PrivateAction, + PublicAction, SharedSecretKey, account::{Account, AccountId, Nonce}, compute_digest_for_path, encryption::{ViewTag, ViewingPublicKey}, @@ -17,11 +18,8 @@ pub fn compute_circuit_output( let (block_validity_window, timestamp_validity_window, pda_seed_by_position, states_iter) = execution_state.into_parts(); let mut output = PrivacyPreservingCircuitOutput { - public_pre_states: Vec::new(), - public_post_states: Vec::new(), - encrypted_private_post_states: Vec::new(), - new_commitments: Vec::new(), - new_nullifiers: Vec::new(), + public_actions: Vec::new(), + private_actions: Vec::new(), block_validity_window, timestamp_validity_window, }; @@ -37,8 +35,10 @@ pub fn compute_circuit_output( { match account_identity { InputAccountIdentity::Public => { - output.public_pre_states.push(pre_state); - output.public_post_states.push(post_state); + output.public_actions.push(PublicAction { + pre: pre_state, + post: post_state, + }); } InputAccountIdentity::PrivateAuthorizedInit { vpk, @@ -267,16 +267,20 @@ pub fn compute_circuit_output( } fn obfuscate_output_ordering(output: &mut PrivacyPreservingCircuitOutput) { - output - .new_commitments - .sort_unstable_by_key(Commitment::to_byte_array); - - let mut notes: Vec<_> = core::mem::take(&mut output.new_nullifiers) - .into_iter() - .zip(core::mem::take(&mut output.encrypted_private_post_states)) + let mut commitments: Vec<_> = output + .private_actions + .iter() + .map(|action| action.commitment.clone()) .collect(); - notes.sort_unstable_by_key(|((nullifier, _), _)| nullifier.to_byte_array()); - (output.new_nullifiers, output.encrypted_private_post_states) = notes.into_iter().unzip(); + commitments.sort_unstable_by_key(Commitment::to_byte_array); + + output + .private_actions + .sort_unstable_by_key(|action| action.nullifier.to_byte_array()); + + for (action, commitment) in output.private_actions.iter_mut().zip(commitments) { + action.commitment = commitment; + } } fn emit_dummy_output(output: &mut PrivacyPreservingCircuitOutput, dummy: DummyInput) { @@ -284,17 +288,18 @@ fn emit_dummy_output(output: &mut PrivacyPreservingCircuitOutput, dummy: DummyIn // The prover is responsible for their randomness. let nullifier = Nullifier::for_dummy(&dummy.nullifier_seed); let commitment = Commitment::for_dummy(&nullifier, &dummy.commitment_seed); - output - .new_nullifiers - .push((nullifier, dummy.commitment_root)); - output.new_commitments.push(commitment); // Note: the encrypted post states are pushed as fed into the circuit. // That means that the prover is responsible for managing the randomness // so as to not reveal the padding. // // In particular, it is recommended to generate the ML KEM ciphertext // explicitly as these are not uniformly random. - output.encrypted_private_post_states.push(dummy.note); + output.private_actions.push(PrivateAction { + nullifier, + root: dummy.commitment_root, + commitment, + encrypted_post_state: dummy.note, + }); } #[expect( @@ -327,15 +332,16 @@ fn emit_private_output( &new_nullifier.0, ); - output.new_nullifiers.push(new_nullifier); - output.new_commitments.push(commitment_post); - output - .encrypted_private_post_states - .push(EncryptedAccountData { + output.private_actions.push(PrivateAction { + nullifier: new_nullifier.0, + root: new_nullifier.1, + commitment: commitment_post, + encrypted_post_state: EncryptedAccountData { ciphertext: encrypted_account, epk, view_tag, - }); + }, + }); } fn compute_update_nullifier_and_set_digest( @@ -358,7 +364,7 @@ mod tests { use super::*; - fn note(tag: u8) -> (Nullifier, Commitment, EncryptedAccountData) { + fn note(tag: u8) -> PrivateAction { let nullifier = Nullifier::for_dummy(&[tag; 32]); let commitment = Commitment::for_dummy(&nullifier, &[tag; 32]); let ciphertext = EncryptionScheme::encrypt( @@ -367,37 +373,36 @@ mod tests { &SharedSecretKey([0; 32]), &nullifier, ); - let encrypted = EncryptedAccountData { - ciphertext, - epk: EphemeralPublicKey(vec![tag]), - view_tag: 0, - }; - (nullifier, commitment, encrypted) + PrivateAction { + nullifier, + root: DUMMY_COMMITMENT_HASH, + commitment, + encrypted_post_state: EncryptedAccountData { + ciphertext, + epk: EphemeralPublicKey(vec![tag]), + view_tag: 0, + }, + } } #[test] fn obfuscate_byte_sorts_commitments_and_nullifiers() { let mut output = PrivacyPreservingCircuitOutput::default(); for tag in 0..3 { - let (nullifier, commitment, encrypted) = note(tag); - output - .new_nullifiers - .push((nullifier, DUMMY_COMMITMENT_HASH)); - output.new_commitments.push(commitment); - output.encrypted_private_post_states.push(encrypted); + output.private_actions.push(note(tag)); } obfuscate_output_ordering(&mut output); assert!( output - .new_commitments - .is_sorted_by_key(Commitment::to_byte_array) + .private_actions + .is_sorted_by_key(|action| action.nullifier.to_byte_array()) ); assert!( output - .new_nullifiers - .is_sorted_by_key(|(nullifier, _)| nullifier.to_byte_array()) + .private_actions + .is_sorted_by_key(|action| action.commitment.to_byte_array()) ); } @@ -405,27 +410,26 @@ mod tests { fn obfuscate_keeps_each_nullifier_with_its_ciphertext() { let mut output = PrivacyPreservingCircuitOutput::default(); for tag in 0..3 { - let (nullifier, _, encrypted) = note(tag); - output - .new_nullifiers - .push((nullifier, DUMMY_COMMITMENT_HASH)); - output.encrypted_private_post_states.push(encrypted); + output.private_actions.push(note(tag)); } let paired: HashMap<[u8; 32], EphemeralPublicKey> = output - .new_nullifiers + .private_actions .iter() - .zip(&output.encrypted_private_post_states) - .map(|((nullifier, _), note)| (nullifier.to_byte_array(), note.epk.clone())) + .map(|action| { + ( + action.nullifier.to_byte_array(), + action.encrypted_post_state.epk.clone(), + ) + }) .collect(); obfuscate_output_ordering(&mut output); - for ((nullifier, _), note) in output - .new_nullifiers - .iter() - .zip(&output.encrypted_private_post_states) - { - assert_eq!(paired[&nullifier.to_byte_array()], note.epk); + for action in &output.private_actions { + assert_eq!( + paired[&action.nullifier.to_byte_array()], + action.encrypted_post_state.epk + ); } } } diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index b5e0986f..81cb37bc 100644 --- a/lee/state_machine/core/src/circuit_io.rs +++ b/lee/state_machine/core/src/circuit_io.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use crate::{ @@ -147,18 +148,53 @@ impl InputAccountIdentity { } } +#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[cfg_attr( + any(feature = "host", test), + derive(Debug, Clone, Default, PartialEq, Eq) +)] +pub struct PrivateAction { + pub nullifier: Nullifier, + pub root: CommitmentSetDigest, + pub commitment: Commitment, + pub encrypted_post_state: EncryptedAccountData, +} + +#[derive(Serialize, Deserialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +pub struct PublicAction { + pub pre: AccountWithMetadata, + pub post: Account, +} + #[derive(Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq, Default))] pub struct PrivacyPreservingCircuitOutput { - pub public_pre_states: Vec, - pub public_post_states: Vec, - pub encrypted_private_post_states: Vec, - pub new_commitments: Vec, - pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub public_actions: Vec, + pub private_actions: Vec, pub block_validity_window: BlockValidityWindow, pub timestamp_validity_window: TimestampValidityWindow, } +#[cfg(any(feature = "host", test))] +impl PrivacyPreservingCircuitOutput { + #[must_use] + pub fn commitments(&self) -> Vec { + self.private_actions + .iter() + .map(|action| action.commitment.clone()) + .collect() + } + + #[must_use] + pub fn nullifiers(&self) -> Vec<(Nullifier, CommitmentSetDigest)> { + self.private_actions + .iter() + .map(|action| (action.nullifier, action.root)) + .collect() + } +} + #[cfg(feature = "host")] impl PrivacyPreservingCircuitOutput { /// Serializes the circuit output to a byte vector. @@ -183,50 +219,57 @@ mod tests { #[test] fn privacy_preserving_circuit_output_to_bytes_is_compatible_with_from_slice() { let output = PrivacyPreservingCircuitOutput { - public_pre_states: vec![ - AccountWithMetadata::new( - Account { + public_actions: vec![ + PublicAction { + pre: AccountWithMetadata::new( + Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 12_345_678_901_234_567_890, + data: b"test data".to_vec().try_into().unwrap(), + nonce: Nonce(0xFFFF_FFFF_FFFF_FFFE), + }, + true, + AccountId::new([0; 32]), + ), + post: Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], - balance: 12_345_678_901_234_567_890, - data: b"test data".to_vec().try_into().unwrap(), - nonce: Nonce(0xFFFF_FFFF_FFFF_FFFE), + balance: 100, + data: b"post state data".to_vec().try_into().unwrap(), + nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF), }, - true, - AccountId::new([0; 32]), - ), - AccountWithMetadata::new( - Account { - program_owner: [9, 9, 9, 8, 8, 8, 7, 7], - balance: 123_123_123_456_456_567_112, - data: b"test data".to_vec().try_into().unwrap(), - nonce: Nonce(9_999_999_999_999_999_999_999), + }, + PublicAction { + pre: AccountWithMetadata::new( + Account { + program_owner: [9, 9, 9, 8, 8, 8, 7, 7], + balance: 123_123_123_456_456_567_112, + data: b"test data".to_vec().try_into().unwrap(), + nonce: Nonce(9_999_999_999_999_999_999_999), + }, + false, + AccountId::new([1; 32]), + ), + post: Account { + program_owner: [2, 3, 4, 5, 6, 7, 8, 9], + balance: 200, + data: b"post state data 2".to_vec().try_into().unwrap(), + nonce: Nonce(0xFFFF_FFFF_FFFF_FFFD), }, - false, - AccountId::new([1; 32]), - ), + }, ], - public_post_states: vec![Account { - program_owner: [1, 2, 3, 4, 5, 6, 7, 8], - balance: 100, - data: b"post state data".to_vec().try_into().unwrap(), - nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF), - }], - encrypted_private_post_states: vec![EncryptedAccountData { - ciphertext: Ciphertext(vec![255, 255, 1, 1, 2, 2]), - epk: EphemeralPublicKey(vec![9, 9, 9]), - view_tag: 42, - }], - new_commitments: vec![Commitment::new( - &AccountId::new([1; 32]), - &Account::default(), - )], - new_nullifiers: vec![( - Nullifier::for_account_update( + private_actions: vec![PrivateAction { + nullifier: Nullifier::for_account_update( &Commitment::new(&AccountId::new([2; 32]), &Account::default()), &[1; 32], ), - [0xab; 32], - )], + root: [0xab; 32], + commitment: Commitment::new(&AccountId::new([1; 32]), &Account::default()), + encrypted_post_state: EncryptedAccountData { + ciphertext: Ciphertext(vec![255, 255, 1, 1, 2, 2]), + epk: EphemeralPublicKey(vec![9, 9, 9]), + view_tag: 42, + }, + }], block_validity_window: (1..).into(), timestamp_validity_window: TimestampValidityWindow::new_unbounded(), }; diff --git a/lee/state_machine/core/src/commitment.rs b/lee/state_machine/core/src/commitment.rs index 46b475e4..2a4aec32 100644 --- a/lee/state_machine/core/src/commitment.rs +++ b/lee/state_machine/core/src/commitment.rs @@ -32,10 +32,10 @@ pub const DUMMY_COMMITMENT_HASH: [u8; 32] = [ 129, 241, 118, 39, 41, 253, 141, 171, 184, 71, 8, 41, ]; -#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr( any(feature = "host", test), - derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord) + derive(Default, PartialEq, Eq, Hash, PartialOrd, Ord) )] pub struct Commitment(pub(super) [u8; 32]); diff --git a/lee/state_machine/core/src/encryption/mod.rs b/lee/state_machine/core/src/encryption/mod.rs index 13ad7f5e..1400a30f 100644 --- a/lee/state_machine/core/src/encryption/mod.rs +++ b/lee/state_machine/core/src/encryption/mod.rs @@ -45,13 +45,15 @@ pub struct SharedSecretKey(pub [u8; 32]); /// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the /// former ECDH ephemeral public key. Always `ML_KEM_768_CIPHERTEXT_LEN` (1088) bytes. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive( + Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, +)] pub struct EphemeralPublicKey(pub Vec); pub struct EncryptionScheme; #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -#[cfg_attr(any(feature = "host", test), derive(Clone, PartialEq, Eq))] +#[cfg_attr(any(feature = "host", test), derive(Clone, Default, PartialEq, Eq))] pub struct Ciphertext(pub(crate) Vec); #[cfg(any(feature = "host", test))] @@ -71,7 +73,10 @@ pub type ViewTag = u8; /// Encrypted private-account note for one output. #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))] +#[cfg_attr( + any(feature = "host", test), + derive(Debug, Clone, Default, PartialEq, Eq) +)] pub struct EncryptedAccountData { pub ciphertext: Ciphertext, pub epk: EphemeralPublicKey, diff --git a/lee/state_machine/core/src/lib.rs b/lee/state_machine/core/src/lib.rs index 62dbd3cc..df247532 100644 --- a/lee/state_machine/core/src/lib.rs +++ b/lee/state_machine/core/src/lib.rs @@ -4,7 +4,8 @@ )] pub use circuit_io::{ - DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput, + PrivacyPreservingCircuitOutput, PrivateAction, PublicAction, }; pub use commitment::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, diff --git a/lee/state_machine/core/src/nullifier.rs b/lee/state_machine/core/src/nullifier.rs index 59e7b5c2..755a2adc 100644 --- a/lee/state_machine/core/src/nullifier.rs +++ b/lee/state_machine/core/src/nullifier.rs @@ -72,7 +72,7 @@ pub type NullifierSecretKey = [u8; 32]; #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr( any(feature = "host", test), - derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash) + derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash) )] pub struct Nullifier(pub(super) [u8; 32]); diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs index d5663b6d..bc7e7f89 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs @@ -24,9 +24,9 @@ fn decrypt_kind( idx: usize, ) -> PrivateAccountKind { let (kind, _) = EncryptionScheme::decrypt( - &output.encrypted_private_post_states[idx].ciphertext, + &output.private_actions[idx].encrypted_post_state.ciphertext, ssk, - &output.new_nullifiers[idx].0, + &output.private_actions[idx].nullifier, ) .unwrap(); kind @@ -102,18 +102,16 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() assert!(proof.is_valid_for(&output)); - let [sender_pre] = output.public_pre_states.try_into().unwrap(); - let [sender_post] = output.public_post_states.try_into().unwrap(); + let [action] = output.public_actions.try_into().unwrap(); + let (sender_pre, sender_post) = (action.pre, action.post); assert_eq!(sender_pre, expected_sender_pre); assert_eq!(sender_post, expected_sender_post); - assert_eq!(output.new_commitments.len(), 1); - assert_eq!(output.new_nullifiers.len(), 1); - assert_eq!(output.encrypted_private_post_states.len(), 1); + assert_eq!(output.private_actions.len(), 1); let (_identifier, recipient_post) = EncryptionScheme::decrypt( - &output.encrypted_private_post_states[0].ciphertext, + &output.private_actions[0].encrypted_post_state.ciphertext, &shared_secret, - &output.new_nullifiers[0].0, + &output.private_actions[0].nullifier, ) .unwrap(); assert_eq!(recipient_post, expected_recipient_post); @@ -216,43 +214,46 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { .unwrap(); assert!(proof.is_valid_for(&output)); - assert!(output.public_pre_states.is_empty()); - assert!(output.public_post_states.is_empty()); + assert!(output.public_actions.is_empty()); let sender_nullifier = expected_new_nullifiers[0].0; let recipient_nullifier = expected_new_nullifiers[1].0; - let mut expected_new_commitments = expected_new_commitments; - expected_new_commitments.sort_unstable_by_key(Commitment::to_byte_array); - assert_eq!(output.new_commitments, expected_new_commitments); + let mut sorted_commitments = expected_new_commitments; + sorted_commitments.sort_unstable_by_key(Commitment::to_byte_array); + assert_eq!(output.commitments(), sorted_commitments); - let mut expected_new_nullifiers = expected_new_nullifiers; - expected_new_nullifiers.sort_unstable_by_key(|(nullifier, _)| nullifier.to_byte_array()); - assert_eq!(output.new_nullifiers, expected_new_nullifiers); + let mut sorted_nullifiers = expected_new_nullifiers; + sorted_nullifiers.sort_unstable_by_key(|(nullifier, _)| nullifier.to_byte_array()); + assert_eq!(output.nullifiers(), sorted_nullifiers); - assert_eq!(output.encrypted_private_post_states.len(), 2); + assert_eq!(output.private_actions.len(), 2); let sender_slot = output - .new_nullifiers + .private_actions .iter() - .position(|(nullifier, _)| *nullifier == sender_nullifier) + .position(|action| action.nullifier == sender_nullifier) .unwrap(); let (_identifier, sender_post) = EncryptionScheme::decrypt( - &output.encrypted_private_post_states[sender_slot].ciphertext, + &output.private_actions[sender_slot] + .encrypted_post_state + .ciphertext, &shared_secret_1, - &output.new_nullifiers[sender_slot].0, + &output.private_actions[sender_slot].nullifier, ) .unwrap(); assert_eq!(sender_post, expected_private_account_1); let recipient_slot = output - .new_nullifiers + .private_actions .iter() - .position(|(nullifier, _)| *nullifier == recipient_nullifier) + .position(|action| action.nullifier == recipient_nullifier) .unwrap(); let (_identifier, recipient_post) = EncryptionScheme::decrypt( - &output.encrypted_private_post_states[recipient_slot].ciphertext, + &output.private_actions[recipient_slot] + .encrypted_post_state + .ciphertext, &shared_secret_2, - &output.new_nullifiers[recipient_slot].0, + &output.private_actions[recipient_slot].nullifier, ) .unwrap(); assert_eq!(recipient_post, expected_private_account_2); @@ -281,9 +282,9 @@ fn init_note_view_tag_is_derived_from_account_keys() { .unwrap(); assert!(proof.is_valid_for(&output)); - assert_eq!(output.encrypted_private_post_states.len(), 1); + assert_eq!(output.private_actions.len(), 1); assert_eq!( - output.encrypted_private_post_states[0].view_tag, + output.private_actions[0].encrypted_post_state.view_tag, EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()), ); } @@ -324,8 +325,11 @@ fn update_note_view_tag_is_the_supplied_value() { .unwrap(); assert!(proof.is_valid_for(&output)); - assert_eq!(output.encrypted_private_post_states.len(), 1); - assert_eq!(output.encrypted_private_post_states[0].view_tag, fed_tag); + assert_eq!(output.private_actions.len(), 1); + assert_eq!( + output.private_actions[0].encrypted_post_state.view_tag, + fed_tag + ); } #[test] @@ -448,7 +452,7 @@ fn private_pda_init() { ); let (output, _proof) = result.expect("PDA init should succeed"); - assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.private_actions.len(), 1); } /// PDA withdraw: chains to `simple_balance_transfer` to move balance from PDA to recipient. @@ -502,7 +506,7 @@ fn private_pda_withdraw() { ); let (output, _proof) = result.expect("PDA withdraw should succeed"); - assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.private_actions.len(), 1); } /// Shared regular private account: receives funds via `authenticated_transfer` directly, @@ -554,7 +558,7 @@ fn shared_account_receives_via_simple_transfer() { let (output, _proof) = result.expect("shared account receive should succeed"); // Sender is public (no commitment), recipient is private (1 commitment) - assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.private_actions.len(), 1); } /// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts diff --git a/lee/state_machine/src/privacy_preserving_transaction/message.rs b/lee/state_machine/src/privacy_preserving_transaction/message.rs index 3b6704ff..22c29a5d 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/message.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/message.rs @@ -1,24 +1,27 @@ use borsh::{BorshDeserialize, BorshSerialize}; use lee_core::{ - Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, + Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, PrivateAction, account::{Account, Nonce}, program::{BlockValidityWindow, TimestampValidityWindow}, }; pub use lee_core::{EncryptedAccountData, ViewTag}; use sha2::{Digest as _, Sha256}; -use crate::{AccountId, error::LeeError}; +use crate::AccountId; const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00"; +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct PublicActionWithID { + pub account_id: AccountId, + pub post_state: Account, +} + #[derive(Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Message { - pub public_account_ids: Vec, + pub public_actions: Vec, pub nonces: Vec, - pub public_post_states: Vec, - pub encrypted_private_post_states: Vec, - pub new_commitments: Vec, - pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub private_actions: Vec, pub block_validity_window: BlockValidityWindow, pub timestamp_validity_window: TimestampValidityWindow, } @@ -31,21 +34,22 @@ impl std::fmt::Debug for Message { write!(f, "{}", hex::encode(self.0)) } } - let nullifiers: Vec<_> = self - .new_nullifiers + let private_actions: Vec<_> = self + .private_actions .iter() - .map(|(n, d)| (n, HexDigest(d))) + .map(|a| { + ( + &a.nullifier, + HexDigest(&a.root), + &a.commitment, + &a.encrypted_post_state, + ) + }) .collect(); f.debug_struct("Message") - .field("public_account_ids", &self.public_account_ids) + .field("public_actions", &self.public_actions) .field("nonces", &self.nonces) - .field("public_post_states", &self.public_post_states) - .field( - "encrypted_private_post_states", - &self.encrypted_private_post_states, - ) - .field("new_commitments", &self.new_commitments) - .field("new_nullifiers", &nullifiers) + .field("private_actions", &private_actions) .field("block_validity_window", &self.block_validity_window) .field("timestamp_validity_window", &self.timestamp_validity_window) .finish() @@ -53,21 +57,47 @@ impl std::fmt::Debug for Message { } impl Message { - pub fn try_from_circuit_output( - public_account_ids: Vec, - nonces: Vec, - output: PrivacyPreservingCircuitOutput, - ) -> Result { - Ok(Self { - public_account_ids, + #[must_use] + pub fn from_circuit_output(nonces: Vec, output: PrivacyPreservingCircuitOutput) -> Self { + let public_actions = output + .public_actions + .into_iter() + .map(|action| PublicActionWithID { + account_id: action.pre.account_id, + post_state: action.post, + }) + .collect(); + Self { + public_actions, nonces, - public_post_states: output.public_post_states, - encrypted_private_post_states: output.encrypted_private_post_states, - new_commitments: output.new_commitments, - new_nullifiers: output.new_nullifiers, + private_actions: output.private_actions, block_validity_window: output.block_validity_window, timestamp_validity_window: output.timestamp_validity_window, - }) + } + } + + #[must_use] + pub fn commitments(&self) -> Vec { + self.private_actions + .iter() + .map(|action| action.commitment.clone()) + .collect() + } + + #[must_use] + pub fn nullifiers(&self) -> Vec<(Nullifier, CommitmentSetDigest)> { + self.private_actions + .iter() + .map(|action| (action.nullifier, action.root)) + .collect() + } + + #[must_use] + pub fn public_account_ids(&self) -> Vec { + self.public_actions + .iter() + .map(|action| action.account_id) + .collect() } #[must_use] @@ -84,34 +114,20 @@ impl Message { Sha256::digest(bytes).into() } - - /// Ensure that the commitments, nullifiers, and ciphertexts agree. - pub fn validate_note_lengths(&self) -> Result { - let count = self.new_nullifiers.len(); - if self.new_commitments.len() != count || self.encrypted_private_post_states.len() != count - { - return Err(LeeError::InvalidInput(format!( - "Note vectors disagree in length with {count} nullifiers, {} commitments, and {} ciphertexts", - self.new_commitments.len(), - self.encrypted_private_post_states.len(), - ))); - } - Ok(count) - } } #[cfg(test)] pub mod tests { use lee_core::{ - Commitment, EncryptionScheme, EphemeralSecretKey, Nullifier, NullifierPublicKey, - PrivateAccountKind, SharedSecretKey, + Commitment, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey, Nullifier, + NullifierPublicKey, PrivateAccountKind, PrivateAction, SharedSecretKey, account::{Account, AccountId, Nonce}, - encryption::ViewingPublicKey, + encryption::{Ciphertext, ViewingPublicKey}, program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; - use super::{EncryptedAccountData, Message, PREFIX}; + use super::{EncryptedAccountData, Message, PREFIX, PublicActionWithID}; #[must_use] pub fn message_for_tests() -> Message { @@ -125,78 +141,57 @@ pub mod tests { let npk2 = NullifierPublicKey::from(&nsk2); let vpk = ViewingPublicKey::from_seed(&[7; 32], &[8; 32]); - let public_account_ids = vec![AccountId::new([1; 32])]; - let nonces = vec![1_u128.into(), 2_u128.into(), 3_u128.into()]; - let public_post_states = vec![Account::default()]; - - let encrypted_private_post_states = Vec::new(); - let account_id2 = lee_core::account::AccountId::for_regular_private_account(&npk2, &vpk, 0); - let new_commitments = vec![Commitment::new(&account_id2, &account2)]; + let commitment = Commitment::new(&account_id2, &account2); let account_id1 = lee_core::account::AccountId::for_regular_private_account(&npk1, &vpk, 0); let old_commitment = Commitment::new(&account_id1, &account1); - let new_nullifiers = vec![( - Nullifier::for_account_update(&old_commitment, &nsk1), - [0; 32], - )]; + let nullifier = Nullifier::for_account_update(&old_commitment, &nsk1); Message { - public_account_ids, + public_actions: vec![PublicActionWithID { + account_id: AccountId::new([1; 32]), + post_state: Account::default(), + }], nonces, - public_post_states, - encrypted_private_post_states, - new_commitments, - new_nullifiers, + private_actions: vec![PrivateAction { + nullifier, + root: [0; 32], + commitment, + encrypted_post_state: EncryptedAccountData { + ciphertext: Ciphertext::from_inner(vec![]), + epk: EphemeralPublicKey(vec![]), + view_tag: 0, + }, + }], block_validity_window: BlockValidityWindow::new_unbounded(), timestamp_validity_window: TimestampValidityWindow::new_unbounded(), } } - #[test] - fn validate_note_lengths_accepts_matching_and_rejects_mismatched() { - assert_eq!(Message::default().validate_note_lengths().unwrap(), 0); - - let mismatched = Message { - new_commitments: vec![Commitment::new( - &AccountId::new([0; 32]), - &Account::default(), - )], - ..Default::default() - }; - assert!(mismatched.validate_note_lengths().is_err()); - } - #[test] fn hash_privacy_pinned() { let msg = Message { - public_account_ids: vec![AccountId::new([42_u8; 32])], + public_actions: vec![], nonces: vec![Nonce(5)], - public_post_states: vec![], - encrypted_private_post_states: vec![], - new_commitments: vec![], - new_nullifiers: vec![], + private_actions: vec![], block_validity_window: BlockValidityWindow::new_unbounded(), timestamp_validity_window: TimestampValidityWindow::new_unbounded(), }; - let public_account_ids_bytes: &[u8] = &[42_u8; 32]; + // empty vec fields: u32 len=0 + let public_actions_bytes: &[u8] = &[0, 0, 0, 0]; 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]; + let private_actions_bytes: &[u8] = &[0, 0, 0, 0]; // validity windows: unbounded = {from: None (0_u8), to: None (0_u8)} - let unbounded_window_bytes: &[u8] = &[0_u8; 2]; + let unbounded_window_bytes: &[u8] = &[0, 0]; let expected_borsh_vec: Vec = [ - &[1_u8, 0, 0, 0], // public_account_ids - public_account_ids_bytes, + public_actions_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 + private_actions_bytes, unbounded_window_bytes, // block_validity_window unbounded_window_bytes, // timestamp_validity_window ] diff --git a/lee/state_machine/src/privacy_preserving_transaction/transaction.rs b/lee/state_machine/src/privacy_preserving_transaction/transaction.rs index 055d65c1..1b0a6567 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/transaction.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/transaction.rs @@ -53,7 +53,12 @@ impl PrivacyPreservingTransaction { .signer_account_ids() .into_iter() .collect::>(); - acc_set.extend(&self.message.public_account_ids); + acc_set.extend( + self.message + .public_actions + .iter() + .map(|action| action.account_id), + ); acc_set.into_iter().collect() } diff --git a/lee/state_machine/src/state/tests/circuit.rs b/lee/state_machine/src/state/tests/circuit.rs index 0b234b30..b8b32dbe 100644 --- a/lee/state_machine/src/state/tests/circuit.rs +++ b/lee/state_machine/src/state/tests/circuit.rs @@ -402,11 +402,8 @@ fn private_pda_claim_succeeds() { ); let (output, _proof) = result.expect("private PDA claim should succeed"); - assert_eq!(output.new_nullifiers.len(), 1); - assert_eq!(output.new_commitments.len(), 1); - assert_eq!(output.encrypted_private_post_states.len(), 1); - assert!(output.public_pre_states.is_empty()); - assert!(output.public_post_states.is_empty()); + assert_eq!(output.private_actions.len(), 1); + assert!(output.public_actions.is_empty()); } /// An npk is supplied that does not match the `pre_state`'s `account_id` under @@ -482,8 +479,7 @@ fn caller_pda_seeds_authorize_private_pda_for_callee() { let (output, _proof) = result.expect("caller-seeds authorization of private PDA should succeed"); - assert_eq!(output.new_commitments.len(), 1); - assert_eq!(output.new_nullifiers.len(), 1); + assert_eq!(output.private_actions.len(), 1); } /// The delegator chains with a different seed than the one it claimed with. In the callee @@ -756,7 +752,7 @@ fn private_authorized_uninitialized_account() { .unwrap(); // Create message from circuit output - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + let message = Message::from_circuit_output(vec![], output); let witness_set = WitnessSet::for_message(&message, proof, &[]); @@ -801,7 +797,7 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() { ) .unwrap(); - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + let message = Message::from_circuit_output(vec![], output); let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -851,7 +847,7 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() { ) .unwrap(); - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + let message = Message::from_circuit_output(vec![], output); let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -961,8 +957,7 @@ fn two_private_pda_family_members_receive_and_spend() { &simple_transfer.clone().into(), ) .unwrap(); - let message = - Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output).unwrap(); + let message = Message::from_circuit_output(vec![funder_nonce], output); let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); state .transition_from_privacy_preserving_transaction( @@ -997,8 +992,7 @@ fn two_private_pda_family_members_receive_and_spend() { &simple_transfer.into(), ) .unwrap(); - let message = - Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output).unwrap(); + let message = Message::from_circuit_output(vec![funder_nonce], output); let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); state .transition_from_privacy_preserving_transaction( @@ -1041,8 +1035,7 @@ fn two_private_pda_family_members_receive_and_spend() { &spend_with_deps, ) .unwrap(); - let message = - Message::try_from_circuit_output(vec![recipient_id], vec![Nonce(0)], output).unwrap(); + let message = Message::from_circuit_output(vec![Nonce(0)], output); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); state .transition_from_privacy_preserving_transaction( @@ -1079,7 +1072,7 @@ fn two_private_pda_family_members_receive_and_spend() { &spend_with_deps, ) .unwrap(); - let message = Message::try_from_circuit_output(vec![recipient_id], vec![], output).unwrap(); + let message = Message::from_circuit_output(vec![], output); let witness_set = WitnessSet::for_message(&message, proof, &[]); state .transition_from_privacy_preserving_transaction( @@ -1130,9 +1123,7 @@ fn two_private_pda_family_members_receive_and_spend() { &crate::test_methods::simple_balance_transfer().into(), ) .unwrap(); - let message = - Message::try_from_circuit_output(vec![recipient_id], vec![recipient_nonce], output) - .unwrap(); + let message = Message::from_circuit_output(vec![recipient_nonce], output); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); state .transition_from_privacy_preserving_transaction( diff --git a/lee/state_machine/src/state/tests/claiming.rs b/lee/state_machine/src/state/tests/claiming.rs index acd96ef6..ee6494ae 100644 --- a/lee/state_machine/src/state/tests/claiming.rs +++ b/lee/state_machine/src/state/tests/claiming.rs @@ -341,9 +341,7 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() { ) .unwrap(); - let message = - Message::try_from_circuit_output(vec![recipient_account_id], vec![Nonce(0)], output) - .unwrap(); + let message = Message::from_circuit_output(vec![Nonce(0)], output); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]); let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -466,7 +464,7 @@ fn private_chained_call(number_of_calls: u32) { ) .unwrap(); - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + let message = Message::from_circuit_output(vec![], output); let witness_set = WitnessSet::for_message(&message, proof, &[]); let transaction = PrivacyPreservingTransaction::new(message, witness_set); diff --git a/lee/state_machine/src/state/tests/mod.rs b/lee/state_machine/src/state/tests/mod.rs index 96e08dc2..b5f3e0ac 100644 --- a/lee/state_machine/src/state/tests/mod.rs +++ b/lee/state_machine/src/state/tests/mod.rs @@ -290,12 +290,7 @@ fn shielded_balance_transfer_for_tests( ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![sender_keys.account_id()], - vec![sender_nonce], - output, - ) - .unwrap(); + let message = Message::from_circuit_output(vec![sender_nonce], output); let witness_set = WitnessSet::for_message(&message, proof, &[&sender_keys.signing_key]); PrivacyPreservingTransaction::new(message, witness_set) @@ -349,7 +344,7 @@ fn private_balance_transfer_for_tests( ) .unwrap(); - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + let message = Message::from_circuit_output(vec![], output); let witness_set = WitnessSet::for_message(&message, proof, &[]); @@ -398,8 +393,7 @@ fn deshielded_balance_transfer_for_tests( ) .unwrap(); - let message = - Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap(); + let message = Message::from_circuit_output(vec![], output); let witness_set = WitnessSet::for_message(&message, proof, &[]); diff --git a/lee/state_machine/src/state/tests/privacy_preserving.rs b/lee/state_machine/src/state/tests/privacy_preserving.rs index 34a61c19..07bd4b16 100644 --- a/lee/state_machine/src/state/tests/privacy_preserving.rs +++ b/lee/state_machine/src/state/tests/privacy_preserving.rs @@ -26,7 +26,7 @@ fn transition_from_privacy_preserving_transaction_shielded() { this }; - let [expected_new_commitment] = tx.message().new_commitments.clone().try_into().unwrap(); + let [expected_new_commitment] = tx.message().commitments().try_into().unwrap(); assert!(!state.private_state.0.contains(&expected_new_commitment)); state @@ -128,7 +128,7 @@ fn privacy_tampered_epk_is_rejected() { ); // Flip a byte of the first note's epk - tx.message.encrypted_private_post_states[0].epk.0[0] ^= 0xFF; + tx.message.private_actions[0].encrypted_post_state.epk.0[0] ^= 0xFF; assert!( matches!( @@ -154,7 +154,7 @@ fn privacy_tampered_view_tag_is_rejected() { ); // Flip the first note's view_tag - tx.message.encrypted_private_post_states[0].view_tag ^= 0xFF; + tx.message.private_actions[0].encrypted_post_state.view_tag ^= 0xFF; assert!( matches!( diff --git a/lee/state_machine/src/state/tests/validity_window.rs b/lee/state_machine/src/state/tests/validity_window.rs index 12e7c229..28431106 100644 --- a/lee/state_machine/src/state/tests/validity_window.rs +++ b/lee/state_machine/src/state/tests/validity_window.rs @@ -149,7 +149,7 @@ fn validity_window_works_in_privacy_preserving_transactions( ) .unwrap(); - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + let message = Message::from_circuit_output(vec![], output); let witness_set = WitnessSet::for_message(&message, proof, &[]); PrivacyPreservingTransaction::new(message, witness_set) @@ -214,7 +214,7 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions( ) .unwrap(); - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + let message = Message::from_circuit_output(vec![], output); let witness_set = WitnessSet::for_message(&message, proof, &[]); PrivacyPreservingTransaction::new(message, witness_set) diff --git a/lee/state_machine/src/validated_state_diff/mod.rs b/lee/state_machine/src/validated_state_diff/mod.rs index 8fae4fee..4d28bebe 100644 --- a/lee/state_machine/src/validated_state_diff/mod.rs +++ b/lee/state_machine/src/validated_state_diff/mod.rs @@ -4,7 +4,7 @@ use std::{ }; use lee_core::{ - BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp, + BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, PublicAction, Timestamp, account::{Account, AccountId, AccountWithMetadata}, program::{ ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, compute_public_authorized_pdas, @@ -310,10 +310,13 @@ impl ValidatedStateDiff { ) -> Result { let message = &tx.message; let witness_set = &tx.witness_set; + let commitments = message.commitments(); + let nullifiers = message.nullifiers(); + let public_account_ids = message.public_account_ids(); // 1. Commitments or nullifiers are non empty ensure!( - !message.new_commitments.is_empty() || !message.new_nullifiers.is_empty(), + !message.private_actions.is_empty(), LeeError::InvalidInput( "Empty commitments and empty nullifiers found in message".into(), ) @@ -321,25 +324,19 @@ impl ValidatedStateDiff { // 2. Check there are no duplicate account_ids in the public_account_ids list. ensure!( - n_unique(&message.public_account_ids) == message.public_account_ids.len(), + n_unique(&public_account_ids) == public_account_ids.len(), LeeError::InvalidInput("Duplicate account_ids found in message".into()) ); // Check there are no duplicate nullifiers in the new_nullifiers list ensure!( - n_unique( - &message - .new_nullifiers - .iter() - .map(|(n, _)| n) - .collect::>() - ) == message.new_nullifiers.len(), + n_unique(&nullifiers.iter().map(|(n, _)| n).collect::>()) == nullifiers.len(), LeeError::InvalidInput("Duplicate nullifiers found in message".into()) ); // Check there are no duplicate commitments in the new_commitments list ensure!( - n_unique(&message.new_commitments) == message.new_commitments.len(), + n_unique(&commitments) == commitments.len(), LeeError::InvalidInput("Duplicate commitments found in message".into()) ); @@ -376,8 +373,7 @@ impl ValidatedStateDiff { ); // Build pre_states for proof verification - let public_pre_states: Vec<_> = message - .public_account_ids + let public_pre_states: Vec<_> = public_account_ids .iter() .map(|account_id| { AccountWithMetadata::new( @@ -396,28 +392,22 @@ impl ValidatedStateDiff { )?; // 5. Commitment freshness - state.check_commitments_are_new(&message.new_commitments)?; + state.check_commitments_are_new(&commitments)?; // 6. Nullifier uniqueness - state.check_nullifiers_are_valid(&message.new_nullifiers)?; + state.check_nullifiers_are_valid(&nullifiers)?; let public_diff = message - .public_account_ids + .public_actions .iter() - .copied() - .zip(message.public_post_states.clone()) - .collect(); - let new_nullifiers = message - .new_nullifiers - .iter() - .copied() - .map(|(nullifier, _)| nullifier) + .map(|action| (action.account_id, action.post_state.clone())) .collect(); + let new_nullifiers = nullifiers.iter().map(|(nullifier, _)| *nullifier).collect(); Ok(Self(StateDiff { signer_account_ids, public_diff, - new_commitments: message.new_commitments.clone(), + new_commitments: commitments, new_nullifiers, program: None, })) @@ -498,11 +488,16 @@ fn check_privacy_preserving_circuit_proof_is_valid( message: &Message, ) -> Result<(), LeeError> { let output = PrivacyPreservingCircuitOutput { - public_pre_states: public_pre_states.to_vec(), - public_post_states: message.public_post_states.clone(), - encrypted_private_post_states: message.encrypted_private_post_states.clone(), - new_commitments: message.new_commitments.clone(), - new_nullifiers: message.new_nullifiers.clone(), + public_actions: public_pre_states + .iter() + .cloned() + .zip(&message.public_actions) + .map(|(pre, action)| PublicAction { + pre, + post: action.post_state.clone(), + }) + .collect(), + private_actions: message.private_actions.clone(), block_validity_window: message.block_validity_window, timestamp_validity_window: message.timestamp_validity_window, }; diff --git a/lee/state_machine/src/validated_state_diff/tests.rs b/lee/state_machine/src/validated_state_diff/tests.rs index e8bb3f14..19b00939 100644 --- a/lee/state_machine/src/validated_state_diff/tests.rs +++ b/lee/state_machine/src/validated_state_diff/tests.rs @@ -188,12 +188,10 @@ fn privacy_malicious_programs_cannot_drain_public_victim() { // public_account_ids lists the Public entries from account_identities, in order. // The single ciphertext belongs to attacker's private account update. - let message = Message::try_from_circuit_output( - vec![victim_id, recipient_id], + let message = Message::from_circuit_output( vec![], // no public signers, no nonces circuit_output, - ) - .unwrap(); + ); let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -350,12 +348,10 @@ fn privacy_malicious_programs_cannot_drain_private_victim() { // public_account_ids lists the Public entries from account_identities, in order. // The single ciphertext belongs to attacker's private account update. - let message = Message::try_from_circuit_output( - vec![victim_id, recipient_id], + let message = Message::from_circuit_output( vec![], // no public signers, no nonces circuit_output, - ) - .unwrap(); + ); let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -480,8 +476,9 @@ fn malicious_programs_cannot_drain_victim_without_signature() { #[test] fn privacy_garbage_proof_is_rejected() { use lee_core::{ - Commitment, + Commitment, EncryptedAccountData, Nullifier, PrivateAction, account::Account, + encryption::{Ciphertext, EphemeralPublicKey}, program::{BlockValidityWindow, TimestampValidityWindow}, }; @@ -503,12 +500,18 @@ fn privacy_garbage_proof_is_rejected() { )); let commitment = Commitment::new(&account_id, &Account::default()); let message = Message { - public_account_ids: vec![], + public_actions: vec![], nonces: vec![], - public_post_states: vec![], - encrypted_private_post_states: vec![], - new_commitments: vec![commitment], - new_nullifiers: vec![], + private_actions: vec![PrivateAction { + nullifier: Nullifier::for_account_initialization(&account_id), + root: [0; 32], + commitment, + encrypted_post_state: EncryptedAccountData { + ciphertext: Ciphertext::from_inner(vec![]), + epk: EphemeralPublicKey(vec![]), + view_tag: 0, + }, + }], block_validity_window: BlockValidityWindow::new_unbounded(), timestamp_validity_window: TimestampValidityWindow::new_unbounded(), }; diff --git a/lez/explorer_service/src/components/transaction_details.rs b/lez/explorer_service/src/components/transaction_details.rs index c82f7d80..01e5bc48 100644 --- a/lez/explorer_service/src/components/transaction_details.rs +++ b/lez/explorer_service/src/components/transaction_details.rs @@ -68,15 +68,18 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into witness_set, } = tx; let PrivacyPreservingMessage { - public_account_ids, + public_actions, nonces, - public_post_states: _, - encrypted_private_post_states, - new_commitments, - new_nullifiers, + private_actions, block_validity_window, timestamp_validity_window, } = message; + let private_action_count = private_actions.len(); + let public_account_ids: Vec<_> = public_actions + .into_iter() + .map(|action| action.account_id) + .collect(); + let public_account_count = public_account_ids.len(); let WitnessSet { signatures_and_public_keys: _, proof, @@ -90,22 +93,12 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into
"Public Accounts:" - {public_account_ids.len().to_string()} + {public_account_count.to_string()}
- "New Commitments:" - {new_commitments.len().to_string()} -
-
- "Nullifiers:" - {new_nullifiers.len().to_string()} -
-
- "Encrypted States:" - - {encrypted_private_post_states.len().to_string()} - + "Private Actions:" + {private_action_count.to_string()}
"Proof Size:" diff --git a/lez/explorer_service/src/components/transaction_preview.rs b/lez/explorer_service/src/components/transaction_preview.rs index 094ca4ff..20c26235 100644 --- a/lez/explorer_service/src/components/transaction_preview.rs +++ b/lez/explorer_service/src/components/transaction_preview.rs @@ -40,8 +40,8 @@ pub fn TransactionPreview(transaction: Transaction) -> impl IntoView { } = tx; format!( "{} public accounts, {} commitments", - message.public_account_ids.len(), - message.new_commitments.len() + message.public_actions.len(), + message.private_actions.len() ) } Transaction::ProgramDeployment(tx) => { diff --git a/lez/indexer/ffi/indexer_ffi.h b/lez/indexer/ffi/indexer_ffi.h index 26857af7..16e5a17c 100644 --- a/lez/indexer/ffi/indexer_ffi.h +++ b/lez/indexer/ffi/indexer_ffi.h @@ -225,13 +225,18 @@ typedef struct FfiAccount { struct FfiU128 nonce; } FfiAccount; -typedef struct FfiVec_FfiAccount { - struct FfiAccount *entries; +typedef struct FfiPublicAction { + FfiAccountId account_id; + struct FfiAccount post_state; +} FfiPublicAction; + +typedef struct FfiVec_FfiPublicAction { + struct FfiPublicAction *entries; uintptr_t len; uintptr_t capacity; -} FfiVec_FfiAccount; +} FfiVec_FfiPublicAction; -typedef struct FfiVec_FfiAccount FfiAccountList; +typedef struct FfiVec_FfiPublicAction FfiPublicActionList; typedef struct FfiVec_u8 { uint8_t *entries; @@ -247,42 +252,25 @@ typedef struct FfiEncryptedAccountData { uint8_t view_tag; } FfiEncryptedAccountData; -typedef struct FfiVec_FfiEncryptedAccountData { - struct FfiEncryptedAccountData *entries; - uintptr_t len; - uintptr_t capacity; -} FfiVec_FfiEncryptedAccountData; - -typedef struct FfiVec_FfiEncryptedAccountData FfiEncryptedAccountDataList; - -typedef struct FfiVec_FfiBytes32 { - struct FfiBytes32 *entries; - uintptr_t len; - uintptr_t capacity; -} FfiVec_FfiBytes32; - -typedef struct FfiVec_FfiBytes32 FfiVecBytes32; - -typedef struct FfiNullifierCommitmentSet { +typedef struct FfiPrivateAction { struct FfiBytes32 nullifier; - struct FfiBytes32 commitment_set_digest; -} FfiNullifierCommitmentSet; + struct FfiBytes32 root; + struct FfiBytes32 commitment; + struct FfiEncryptedAccountData encrypted_post_state; +} FfiPrivateAction; -typedef struct FfiVec_FfiNullifierCommitmentSet { - struct FfiNullifierCommitmentSet *entries; +typedef struct FfiVec_FfiPrivateAction { + struct FfiPrivateAction *entries; uintptr_t len; uintptr_t capacity; -} FfiVec_FfiNullifierCommitmentSet; +} FfiVec_FfiPrivateAction; -typedef struct FfiVec_FfiNullifierCommitmentSet FfiNullifierCommitmentSetList; +typedef struct FfiVec_FfiPrivateAction FfiPrivateActionList; typedef struct FfiPrivacyPreservingMessage { - FfiAccountIdList public_account_ids; + FfiPublicActionList public_actions; FfiNonceList nonces; - FfiAccountList public_post_states; - FfiEncryptedAccountDataList encrypted_private_post_states; - FfiVecBytes32 new_commitments; - FfiNullifierCommitmentSetList new_nullifiers; + FfiPrivateActionList private_actions; uint64_t block_validity_window[2]; uint64_t timestamp_validity_window[2]; } FfiPrivacyPreservingMessage; diff --git a/lez/indexer/ffi/src/api/types/transaction.rs b/lez/indexer/ffi/src/api/types/transaction.rs index d5cb9035..83f50ede 100644 --- a/lez/indexer/ffi/src/api/types/transaction.rs +++ b/lez/indexer/ffi/src/api/types/transaction.rs @@ -1,17 +1,19 @@ use indexer_service_protocol::{ AccountId, Ciphertext, Commitment, CommitmentSetDigest, EncryptedAccountData, EphemeralPublicKey, HashType, Nullifier, PrivacyPreservingMessage, - PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, - ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, Transaction, - ValidityWindow, WitnessSet, + PrivacyPreservingTransaction, PrivateAction, ProgramDeploymentMessage, + ProgramDeploymentTransaction, ProgramId, Proof, PublicActionWithID, PublicKey, PublicMessage, + PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet, }; use crate::api::types::{ - FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature, FfiVec, + FfiAccountId, FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature, + FfiVec, + account::FfiAccount, vectors::{ - FfiAccountIdList, FfiAccountList, FfiEncryptedAccountDataList, FfiInstructionDataList, - FfiNonceList, FfiNullifierCommitmentSetList, FfiProgramDeploymentMessage, FfiProof, - FfiSignaturePubKeyList, FfiVecBytes32, FfiVecU8, + FfiAccountIdList, FfiInstructionDataList, FfiNonceList, FfiPrivateActionList, + FfiProgramDeploymentMessage, FfiProof, FfiPublicActionList, FfiSignaturePubKeyList, + FfiVecU8, }, }; @@ -156,12 +158,15 @@ impl From> for PrivacyPreservingTransaction { Self { hash: HashType(value.hash.data), message: PrivacyPreservingMessage { - public_account_ids: { - let std_vec: Vec<_> = value.message.public_account_ids.into(); + public_actions: { + let std_vec: Vec<_> = value.message.public_actions.into(); std_vec .into_iter() - .map(|ffi_val| AccountId { - value: ffi_val.data, + .map(|ffi_val| PublicActionWithID { + account_id: AccountId { + value: ffi_val.account_id.data, + }, + post_state: ffi_val.post_state.into(), }) .collect() }, @@ -169,37 +174,21 @@ impl From> for PrivacyPreservingTransaction { let std_vec: Vec<_> = value.message.nonces.into(); std_vec.into_iter().map(Into::into).collect() }, - public_post_states: { - let std_vec: Vec<_> = value.message.public_post_states.into(); - std_vec.into_iter().map(Into::into).collect() - }, - encrypted_private_post_states: { - let std_vec: Vec<_> = value.message.encrypted_private_post_states.into(); + private_actions: { + let std_vec: Vec<_> = value.message.private_actions.into(); std_vec .into_iter() - .map(|ffi_val| EncryptedAccountData { - ciphertext: Ciphertext(ffi_val.ciphertext.into()), - epk: EphemeralPublicKey(ffi_val.epk.into()), - view_tag: ffi_val.view_tag, - }) - .collect() - }, - new_commitments: { - let std_vec: Vec<_> = value.message.new_commitments.into(); - std_vec - .into_iter() - .map(|ffi_val| Commitment(ffi_val.data)) - .collect() - }, - new_nullifiers: { - let std_vec: Vec<_> = value.message.new_nullifiers.into(); - std_vec - .into_iter() - .map(|ffi_val| { - ( - Nullifier(ffi_val.nullifier.data), - CommitmentSetDigest(ffi_val.commitment_set_digest.data), - ) + .map(|ffi_val| PrivateAction { + nullifier: Nullifier(ffi_val.nullifier.data), + root: CommitmentSetDigest(ffi_val.root.data), + commitment: Commitment(ffi_val.commitment.data), + encrypted_post_state: EncryptedAccountData { + ciphertext: Ciphertext( + ffi_val.encrypted_post_state.ciphertext.into(), + ), + epk: EphemeralPublicKey(ffi_val.encrypted_post_state.epk.into()), + view_tag: ffi_val.encrypted_post_state.view_tag, + }, }) .collect() }, @@ -229,14 +218,53 @@ impl From> for PrivacyPreservingTransaction { } } +#[repr(C)] +pub struct FfiPublicAction { + pub account_id: FfiAccountId, + pub post_state: FfiAccount, +} + +impl From for FfiPublicAction { + fn from(value: PublicActionWithID) -> Self { + let post_state: lee::Account = value + .post_state + .try_into() + .expect("Source is in blocks, must fit"); + Self { + account_id: value.account_id.into(), + post_state: post_state.into(), + } + } +} + +#[repr(C)] +pub struct FfiPrivateAction { + pub nullifier: FfiBytes32, + pub root: FfiBytes32, + pub commitment: FfiBytes32, + pub encrypted_post_state: FfiEncryptedAccountData, +} + +impl From for FfiPrivateAction { + fn from(value: PrivateAction) -> Self { + Self { + nullifier: FfiBytes32 { + data: value.nullifier.0, + }, + root: FfiBytes32 { data: value.root.0 }, + commitment: FfiBytes32 { + data: value.commitment.0, + }, + encrypted_post_state: value.encrypted_post_state.into(), + } + } +} + #[repr(C)] pub struct FfiPrivacyPreservingMessage { - pub public_account_ids: FfiAccountIdList, + pub public_actions: FfiPublicActionList, pub nonces: FfiNonceList, - pub public_post_states: FfiAccountList, - pub encrypted_private_post_states: FfiEncryptedAccountDataList, - pub new_commitments: FfiVecBytes32, - pub new_nullifiers: FfiNullifierCommitmentSetList, + pub private_actions: FfiPrivateActionList, pub block_validity_window: [u64; 2], pub timestamp_validity_window: [u64; 2], } @@ -244,18 +272,15 @@ pub struct FfiPrivacyPreservingMessage { impl From for FfiPrivacyPreservingMessage { fn from(value: PrivacyPreservingMessage) -> Self { let PrivacyPreservingMessage { - public_account_ids, + public_actions, nonces, - public_post_states, - encrypted_private_post_states, - new_commitments, - new_nullifiers, + private_actions, block_validity_window, timestamp_validity_window, } = value; Self { - public_account_ids: public_account_ids + public_actions: public_actions .into_iter() .map(Into::into) .collect::>() @@ -265,25 +290,7 @@ impl From for FfiPrivacyPreservingMessage { .map(Into::into) .collect::>() .into(), - public_post_states: public_post_states - .into_iter() - .map(|acc_ind| -> lee::Account { - acc_ind.try_into().expect("Source is in blocks, must fit") - }) - .map(Into::into) - .collect::>() - .into(), - encrypted_private_post_states: encrypted_private_post_states - .into_iter() - .map(Into::into) - .collect::>() - .into(), - new_commitments: new_commitments - .into_iter() - .map(|comm| FfiBytes32 { data: comm.0 }) - .collect::>() - .into(), - new_nullifiers: new_nullifiers + private_actions: private_actions .into_iter() .map(Into::into) .collect::>() @@ -294,21 +301,6 @@ impl From for FfiPrivacyPreservingMessage { } } -#[repr(C)] -pub struct FfiNullifierCommitmentSet { - pub nullifier: FfiBytes32, - pub commitment_set_digest: FfiBytes32, -} - -impl From<(Nullifier, CommitmentSetDigest)> for FfiNullifierCommitmentSet { - fn from(value: (Nullifier, CommitmentSetDigest)) -> Self { - Self { - nullifier: FfiBytes32 { data: value.0.0 }, - commitment_set_digest: FfiBytes32 { data: value.1.0 }, - } - } -} - #[repr(C)] pub struct FfiEncryptedAccountData { pub ciphertext: FfiVecU8, diff --git a/lez/indexer/ffi/src/api/types/vectors.rs b/lez/indexer/ffi/src/api/types/vectors.rs index 46f08737..4cccb949 100644 --- a/lez/indexer/ffi/src/api/types/vectors.rs +++ b/lez/indexer/ffi/src/api/types/vectors.rs @@ -1,19 +1,12 @@ use crate::api::types::{ - FfiAccountId, FfiBytes32, FfiNonce, FfiVec, - account::FfiAccount, - transaction::{ - FfiEncryptedAccountData, FfiNullifierCommitmentSet, FfiSignaturePubKeyEntry, FfiTransaction, - }, + FfiAccountId, FfiNonce, FfiVec, + transaction::{FfiPrivateAction, FfiPublicAction, FfiSignaturePubKeyEntry, FfiTransaction}, }; pub type FfiVecU8 = FfiVec; -pub type FfiAccountList = FfiVec; - pub type FfiAccountIdList = FfiVec; -pub type FfiVecBytes32 = FfiVec; - pub type FfiBlockBody = FfiVec; pub type FfiNonceList = FfiVec; @@ -26,6 +19,6 @@ pub type FfiProof = FfiVecU8; pub type FfiProgramDeploymentMessage = FfiVecU8; -pub type FfiEncryptedAccountDataList = FfiVec; +pub type FfiPublicActionList = FfiVec; -pub type FfiNullifierCommitmentSetList = FfiVec; +pub type FfiPrivateActionList = FfiVec; diff --git a/lez/indexer/service/protocol/src/convert.rs b/lez/indexer/service/protocol/src/convert.rs index 55c4dc6c..79ea0fe7 100644 --- a/lez/indexer/service/protocol/src/convert.rs +++ b/lez/indexer/service/protocol/src/convert.rs @@ -6,9 +6,9 @@ use crate::{ Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockIngestError, Ciphertext, Commitment, CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, IndexerStatus, IndexerSyncState, Nullifier, PrivacyPreservingMessage, - PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, - ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, StallReason, - Transaction, ValidityWindow, WitnessSet, + PrivacyPreservingTransaction, PrivateAction, ProgramDeploymentMessage, + ProgramDeploymentTransaction, ProgramId, Proof, PublicActionWithID, PublicKey, PublicMessage, + PublicTransaction, Signature, StallReason, Transaction, ValidityWindow, WitnessSet, }; // ============================================================================ @@ -279,71 +279,97 @@ impl From for lee::public_transaction::Message { } } +impl From for PublicActionWithID { + fn from(value: lee::privacy_preserving_transaction::message::PublicActionWithID) -> Self { + Self { + account_id: value.account_id.into(), + post_state: value.post_state.into(), + } + } +} + +impl From for PrivateAction { + fn from(value: lee_core::PrivateAction) -> Self { + Self { + nullifier: value.nullifier.into(), + root: value.root.into(), + commitment: value.commitment.into(), + encrypted_post_state: value.encrypted_post_state.into(), + } + } +} + impl From for PrivacyPreservingMessage { fn from(value: lee::privacy_preserving_transaction::message::Message) -> Self { let lee::privacy_preserving_transaction::message::Message { - public_account_ids, + public_actions, nonces, - public_post_states, - encrypted_private_post_states, - new_commitments, - new_nullifiers, + private_actions, block_validity_window, timestamp_validity_window, } = value; Self { - public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), + public_actions: public_actions.into_iter().map(Into::into).collect(), nonces: nonces.iter().map(|x| x.0).collect(), - public_post_states: public_post_states.into_iter().map(Into::into).collect(), - encrypted_private_post_states: encrypted_private_post_states - .into_iter() - .map(Into::into) - .collect(), - new_commitments: new_commitments.into_iter().map(Into::into).collect(), - new_nullifiers: new_nullifiers - .into_iter() - .map(|(n, d)| (n.into(), d.into())) - .collect(), + private_actions: private_actions.into_iter().map(Into::into).collect(), block_validity_window: block_validity_window.into(), timestamp_validity_window: timestamp_validity_window.into(), } } } +impl TryFrom + for lee::privacy_preserving_transaction::message::PublicActionWithID +{ + type Error = lee::error::LeeError; + + fn try_from(value: PublicActionWithID) -> Result { + Ok(Self { + account_id: value.account_id.into(), + post_state: value + .post_state + .try_into() + .map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?, + }) + } +} + +impl From for lee_core::PrivateAction { + fn from(value: PrivateAction) -> Self { + Self { + nullifier: value.nullifier.into(), + root: value.root.into(), + commitment: value.commitment.into(), + encrypted_post_state: value.encrypted_post_state.into(), + } + } +} + impl TryFrom for lee::privacy_preserving_transaction::message::Message { type Error = lee::error::LeeError; fn try_from(value: PrivacyPreservingMessage) -> Result { let PrivacyPreservingMessage { - public_account_ids, + public_actions, nonces, - public_post_states, - encrypted_private_post_states, - new_commitments, - new_nullifiers, + private_actions, block_validity_window, timestamp_validity_window, } = value; + + let public_actions = public_actions + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?; + let private_actions = private_actions.into_iter().map(Into::into).collect(); + Ok(Self { - public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), + public_actions, nonces: nonces .iter() .map(|x| lee_core::account::Nonce(*x)) .collect(), - public_post_states: public_post_states - .into_iter() - .map(TryInto::try_into) - .collect::, _>>() - .map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?, - encrypted_private_post_states: encrypted_private_post_states - .into_iter() - .map(Into::into) - .collect(), - new_commitments: new_commitments.into_iter().map(Into::into).collect(), - new_nullifiers: new_nullifiers - .into_iter() - .map(|(n, d)| (n.into(), d.into())) - .collect(), + private_actions, block_validity_window: block_validity_window .try_into() .map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?, diff --git a/lez/indexer/service/protocol/src/lib.rs b/lez/indexer/service/protocol/src/lib.rs index e17d539b..b19cd8dd 100644 --- a/lez/indexer/service/protocol/src/lib.rs +++ b/lez/indexer/service/protocol/src/lib.rs @@ -226,14 +226,25 @@ pub struct PublicMessage { pub type InstructionData = Vec; +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +pub struct PublicActionWithID { + pub account_id: AccountId, + pub post_state: Account, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +pub struct PrivateAction { + pub nullifier: Nullifier, + pub root: CommitmentSetDigest, + pub commitment: Commitment, + pub encrypted_post_state: EncryptedAccountData, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct PrivacyPreservingMessage { - pub public_account_ids: Vec, + pub public_actions: Vec, pub nonces: Vec, - pub public_post_states: Vec, - pub encrypted_private_post_states: Vec, - pub new_commitments: Vec, - pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub private_actions: Vec, pub block_validity_window: ValidityWindow, pub timestamp_validity_window: ValidityWindow, } diff --git a/lez/indexer/service/src/mock_service.rs b/lez/indexer/service/src/mock_service.rs index 70af6239..c9a1912a 100644 --- a/lez/indexer/service/src/mock_service.rs +++ b/lez/indexer/service/src/mock_service.rs @@ -11,9 +11,9 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; use indexer_service_protocol::{ Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment, CommitmentSetDigest, Data, EncryptedAccountData, HashType, IndexerStatus, IndexerSyncState, - PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, - ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature, - Transaction, ValidityWindow, WitnessSet, + PrivacyPreservingMessage, PrivacyPreservingTransaction, PrivateAction, + ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, PublicActionWithID, + PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet, }; use jsonrpsee::{ core::{SubscriptionResult, async_trait}, @@ -300,9 +300,11 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { .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::PrivacyPreserving(priv_tx) => priv_tx + .message + .public_actions + .iter() + .any(|action| action.account_id == account_id), Transaction::ProgramDeployment(_) => false, }) .cloned() @@ -381,24 +383,26 @@ fn mock_privacy_preserving_tx( Transaction::PrivacyPreserving(PrivacyPreservingTransaction { hash: tx_hash, message: PrivacyPreservingMessage { - public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]], + public_actions: vec![PublicActionWithID { + account_id: account_ids[tx_idx as usize % account_ids.len()], + post_state: Account { + program_owner: ProgramId([1_u32; 8]), + balance: 500, + data: Data(vec![0xdd, 0xee]), + nonce: block_id as u128, + }, + }], 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, + private_actions: vec![PrivateAction { + nullifier: indexer_service_protocol::Nullifier([tx_idx as u8; 32]), + root: CommitmentSetDigest([0xff; 32]), + commitment: Commitment([block_id as u8; 32]), + encrypted_post_state: EncryptedAccountData { + ciphertext: indexer_service_protocol::Ciphertext(vec![0x01, 0x02, 0x03, 0x04]), + epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), + view_tag: 42, + }, }], - 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)), }, diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs index 8b0ba669..e584e8f8 100644 --- a/lez/sequencer/core/src/tests.rs +++ b/lez/sequencer/core/src/tests.rs @@ -875,8 +875,7 @@ fn private_bridge_withdraw_invocation_is_dropped() { ) .expect("Execution should succeed"); - let message = Message::try_from_circuit_output(vec![bridge_account_id], vec![], output) - .expect("Message construction should succeed"); + let message = Message::from_circuit_output(vec![], output); let witness_set = lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); let tx = diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 86f456d6..3ae5e9ed 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -383,7 +383,6 @@ impl WalletCore { let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else { continue; }; - pp_tx.message.validate_note_lengths()?; // Sync updates while watching only the init nullifier. self.storage .key_chain_mut() @@ -550,7 +549,7 @@ impl WalletCore { tx: &lee::privacy_preserving_transaction::PrivacyPreservingTransaction, acc_decode_mask: &[AccDecodeData], ) -> Result<()> { - let note_count = tx.message.validate_note_lengths()?; + let note_count = tx.message.private_actions.len(); anyhow::ensure!( note_count >= acc_decode_mask.len(), "Decode mask has {} entries but the transaction has {note_count} notes", @@ -655,12 +654,10 @@ impl WalletCore { &program.to_owned(), )?; - let message = - lee::privacy_preserving_transaction::message::Message::try_from_circuit_output( - acc_manager.public_account_ids(), - acc_manager.public_account_nonces(), - output, - )?; + let message = lee::privacy_preserving_transaction::message::Message::from_circuit_output( + acc_manager.public_account_nonces(), + output, + ); let message_hash = message.hash(); let signatures_public_keys = acc_manager @@ -785,7 +782,6 @@ impl WalletCore { let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else { continue; }; - pp_tx.message.validate_note_lengths()?; // Eagerly decrypt note updates using expected nullifiers. let handled = self .storage @@ -824,18 +820,19 @@ impl WalletCore { &key_chain.viewing_public_key, ); message - .encrypted_private_post_states + .private_actions .iter() .enumerate() - .filter(move |(ciph_id, encrypted_data)| { + .filter(move |(ciph_id, action)| { // If we have not decrypted the update using the nullifiers, // the note may be an initialized one, for which we should // scan. - !handled.contains(ciph_id) && encrypted_data.view_tag == view_tag + !handled.contains(ciph_id) + && action.encrypted_post_state.view_tag == view_tag }) - .filter_map(move |(ciph_id, encrypted_data)| { - let shared_secret = - key_chain.calculate_shared_secret_receiver(&encrypted_data.epk)?; + .filter_map(move |(ciph_id, action)| { + let shared_secret = key_chain + .calculate_shared_secret_receiver(&action.encrypted_post_state.epk)?; decrypt_note_at(message, ciph_id, &shared_secret).map(|(kind, res_acc)| { let npk = &key_chain.nullifier_public_key; @@ -892,16 +889,14 @@ impl WalletCore { for (account_id, npk, vpk, vsk, nsk) in shared_keys { let view_tag = EncryptedAccountData::compute_view_tag(&npk, &vpk); - for (ciph_id, encrypted_data) in - message.encrypted_private_post_states.iter().enumerate() - { + for (ciph_id, action) in message.private_actions.iter().enumerate() { // If already decrypted or the tag does not match, skip. - if handled.contains(&ciph_id) || encrypted_data.view_tag != view_tag { + if handled.contains(&ciph_id) || action.encrypted_post_state.view_tag != view_tag { continue; } let Some(shared_secret) = - SharedSecretKey::decapsulate(&encrypted_data.epk, &vsk.d, &vsk.z) + SharedSecretKey::decapsulate(&action.encrypted_post_state.epk, &vsk.d, &vsk.z) else { continue; }; @@ -938,9 +933,9 @@ fn decrypt_note_at( secret: &SharedSecretKey, ) -> Option<(lee_core::PrivateAccountKind, Account)> { lee_core::EncryptionScheme::decrypt( - &message.encrypted_private_post_states[i].ciphertext, + &message.private_actions[i].encrypted_post_state.ciphertext, secret, - &message.new_nullifiers[i].0, + &message.private_actions[i].nullifier, ) } diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 54493386..dc88cf2a 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -390,9 +390,9 @@ impl UserKeyChain { index: &mut NullifierIndex, ) -> HashSet { let mut handled = HashSet::new(); - for (i, (old_nullifier, _)) in message.new_nullifiers.iter().enumerate() { + for (i, action) in message.private_actions.iter().enumerate() { // Get the nullifier information if awaiting the nullifier. - let Some(account_id) = index.account_for(old_nullifier) else { + let Some(account_id) = index.account_for(&action.nullifier) else { continue; }; // Try decrypting the commitment connected to the nullifier and get the next @@ -400,7 +400,7 @@ impl UserKeyChain { if let Some(new_nullifier) = self.apply_nullifier_update(account_id, message, i) { // Update the index to await for the new state of the account, i.e. // the new nullifier. - index.update(old_nullifier, new_nullifier, account_id); + index.update(&action.nullifier, new_nullifier, account_id); // Record that this nullifier's position can be skipped for scanning. handled.insert(i); } @@ -416,7 +416,7 @@ impl UserKeyChain { message: &Message, i: usize, ) -> Option { - let encrypted = &message.encrypted_private_post_states[i]; + let encrypted = &message.private_actions[i].encrypted_post_state; let (nsk, secret, is_shared) = if let Some(entry) = self.shared_private_account(account_id) { @@ -460,10 +460,9 @@ impl UserKeyChain { &acc.key_chain.private_key_holder.nullifier_secret_key, ) }); - message - .new_nullifiers - .iter() - .position(|(nullifier, _)| *nullifier == init || Some(nullifier) == update.as_ref()) + message.private_actions.iter().position(|action| { + action.nullifier == init || Some(&action.nullifier) == update.as_ref() + }) } pub fn add_imported_public_account(&mut self, private_key: lee::PrivateKey) { @@ -876,7 +875,7 @@ impl Default for UserKeyChain { #[cfg(test)] mod tests { - use lee_core::{EncryptionScheme, encryption::EncryptedAccountData}; + use lee_core::{EncryptionScheme, PrivateAction, encryption::EncryptedAccountData}; use super::*; @@ -921,9 +920,12 @@ mod tests { ); let message = Message { - encrypted_private_post_states: vec![note], - new_commitments: vec![new_commitment], - new_nullifiers: vec![(old_nullifier, [0; 32])], + private_actions: vec![PrivateAction { + nullifier: old_nullifier, + commitment: new_commitment, + encrypted_post_state: note, + ..Default::default() + }], ..Default::default() }; @@ -985,9 +987,12 @@ mod tests { ); let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk); let message = Message { - encrypted_private_post_states: vec![note], - new_commitments: vec![new_commitment], - new_nullifiers: vec![(old_nullifier, [0; 32])], + private_actions: vec![PrivateAction { + nullifier: old_nullifier, + commitment: new_commitment, + encrypted_post_state: note, + ..Default::default() + }], ..Default::default() }; @@ -1047,9 +1052,12 @@ mod tests { ); let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk); Message { - encrypted_private_post_states: vec![note], - new_commitments: vec![commitment], - new_nullifiers: vec![(spent, [0; 32])], + private_actions: vec![PrivateAction { + nullifier: spent, + commitment, + encrypted_post_state: note, + ..Default::default() + }], ..Default::default() } }; @@ -1110,7 +1118,10 @@ mod tests { &[9; 32], ); let message = Message { - new_nullifiers: vec![(unindexed, [0; 32])], + private_actions: vec![PrivateAction { + nullifier: unindexed, + ..Default::default() + }], ..Default::default() }; diff --git a/test_fixtures/fixtures/prebuilt_sequencer_db.dump b/test_fixtures/fixtures/prebuilt_sequencer_db.dump index 771acc7a..259500a9 100644 Binary files a/test_fixtures/fixtures/prebuilt_sequencer_db.dump and b/test_fixtures/fixtures/prebuilt_sequencer_db.dump differ