refactor: bundle private/public i/o into action struccts

This commit is contained in:
Artem Gureev 2026-07-08 12:33:52 +00:00
parent 131ed6d025
commit f16c3bd1e2
44 changed files with 482 additions and 412 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -81,9 +81,9 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
.context("Failed to get private account commitment for sender")?; .context("Failed to get private account commitment for sender")?;
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await; 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); assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
} }
@ -210,9 +210,9 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
.wallet() .wallet()
.get_private_account_commitment(from) .get_private_account_commitment(from)
.context("Failed to get private account commitment for sender")?; .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); assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
} }
@ -286,7 +286,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
let acc_1_balance = account_balance(&ctx, from).await?; 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); assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
} }
@ -342,7 +342,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; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Verify commitments are in state // 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); assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
} }
@ -698,8 +698,9 @@ async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> {
let output = prove_init_with_commitment_root(&ctx, expected_digest).await?; let output = prove_init_with_commitment_root(&ctx, expected_digest).await?;
assert_eq!(output.new_nullifiers.len(), 1); assert_eq!(output.private_actions.len(), 1);
let (nullifier, digest) = &output.new_nullifiers[0]; let action = &output.private_actions[0];
let (nullifier, digest) = (&action.nullifier, &action.root);
assert_eq!( assert_eq!(
*nullifier, *nullifier,
Nullifier::for_account_initialization(&recipient_account_id) Nullifier::for_account_initialization(&recipient_account_id)
@ -719,14 +720,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_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?; 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!( assert_eq!(
output_without_root.new_nullifiers[0].1, output_without_root.private_actions[0].root,
DUMMY_COMMITMENT_HASH DUMMY_COMMITMENT_HASH
); );
assert_ne!( assert_ne!(
output_with_root.new_nullifiers[0].1, output_with_root.private_actions[0].root,
output_without_root.new_nullifiers[0].1, output_without_root.private_actions[0].root,
); );
Ok(()) Ok(())

View File

@ -201,12 +201,10 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
.context("Failed to execute/prove bridge deposit")?; .context("Failed to execute/prove bridge deposit")?;
// Create privacy-preserving transaction from circuit output // Create privacy-preserving transaction from circuit output
let message = privacy_preserving_transaction::Message::try_from_circuit_output( let message = privacy_preserving_transaction::Message::from_circuit_output(
vec![bridge_account_id, recipient_vault_id],
vec![bridge_pre.account.nonce, vault_pre.account.nonce], vec![bridge_pre.account.nonce, vault_pre.account.nonce],
output, output,
) );
.context("Failed to build privacy-preserving bridge deposit message")?;
let witness_set = privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); let witness_set = privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]);
let attack_tx = LeeTransaction::PrivacyPreserving(lee::PrivacyPreservingTransaction::new( let attack_tx = LeeTransaction::PrivacyPreserving(lee::PrivacyPreservingTransaction::new(

View File

@ -71,9 +71,9 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
.wallet() .wallet()
.get_private_account_commitment(from) .get_private_account_commitment(from)
.context("Failed to get private account commitment for sender")?; .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); assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
} }

View File

@ -83,9 +83,7 @@ async fn fund_private_pda(
) )
.map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?; .map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?;
let message = let message = Message::from_circuit_output(vec![sender_account.nonce], output);
Message::try_from_circuit_output(vec![sender], vec![sender_account.nonce], output)
.map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]); let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]);
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);

View File

@ -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; let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
assert_eq!(tx.message.new_commitments.len(), 7); assert_eq!(tx.message.private_actions.len(), 7);
assert_eq!(tx.message.new_nullifiers.len(), 7);
assert_eq!(tx.message.encrypted_private_post_states.len(), 7);
Ok(()) Ok(())
} }

View File

@ -310,7 +310,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
&program.into(), &program.into(),
) )
.unwrap(); .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, &[]); let witness_set = pptx::witness_set::WitnessSet::for_message(&message, proof, &[]);
pptx::PrivacyPreservingTransaction::new(message, witness_set) pptx::PrivacyPreservingTransaction::new(message, witness_set)
} }

View File

@ -1,7 +1,8 @@
use lee_core::{ use lee_core::{
Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme, Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme,
EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, PrivateAction,
PublicAction, SharedSecretKey,
account::{Account, AccountId, Nonce}, account::{Account, AccountId, Nonce},
compute_digest_for_path, compute_digest_for_path,
encryption::{ViewTag, ViewingPublicKey}, 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) = let (block_validity_window, timestamp_validity_window, pda_seed_by_position, states_iter) =
execution_state.into_parts(); execution_state.into_parts();
let mut output = PrivacyPreservingCircuitOutput { let mut output = PrivacyPreservingCircuitOutput {
public_pre_states: Vec::new(), public_actions: Vec::new(),
public_post_states: Vec::new(), private_actions: Vec::new(),
encrypted_private_post_states: Vec::new(),
new_commitments: Vec::new(),
new_nullifiers: Vec::new(),
block_validity_window, block_validity_window,
timestamp_validity_window, timestamp_validity_window,
}; };
@ -37,8 +35,10 @@ pub fn compute_circuit_output(
{ {
match account_identity { match account_identity {
InputAccountIdentity::Public => { InputAccountIdentity::Public => {
output.public_pre_states.push(pre_state); output.public_actions.push(PublicAction {
output.public_post_states.push(post_state); pre: pre_state,
post: post_state,
});
} }
InputAccountIdentity::PrivateAuthorizedInit { InputAccountIdentity::PrivateAuthorizedInit {
vpk, vpk,
@ -267,16 +267,20 @@ pub fn compute_circuit_output(
} }
fn obfuscate_output_ordering(output: &mut PrivacyPreservingCircuitOutput) { fn obfuscate_output_ordering(output: &mut PrivacyPreservingCircuitOutput) {
output let mut commitments: Vec<_> = output
.new_commitments .private_actions
.sort_unstable_by_key(Commitment::to_byte_array); .iter()
.map(|action| action.commitment)
let mut notes: Vec<_> = core::mem::take(&mut output.new_nullifiers)
.into_iter()
.zip(core::mem::take(&mut output.encrypted_private_post_states))
.collect(); .collect();
notes.sort_unstable_by_key(|((nullifier, _), _)| nullifier.to_byte_array()); commitments.sort_unstable_by_key(Commitment::to_byte_array);
(output.new_nullifiers, output.encrypted_private_post_states) = notes.into_iter().unzip();
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) { 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. // The prover is responsible for their randomness.
let nullifier = Nullifier::for_dummy(&dummy.nullifier_seed); let nullifier = Nullifier::for_dummy(&dummy.nullifier_seed);
let commitment = Commitment::for_dummy(&nullifier, &dummy.commitment_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. // Note: the encrypted post states are pushed as fed into the circuit.
// That means that the prover is responsible for managing the randomness // That means that the prover is responsible for managing the randomness
// so as to not reveal the padding. // so as to not reveal the padding.
// //
// In particular, it is recommended to generate the ML KEM ciphertext // In particular, it is recommended to generate the ML KEM ciphertext
// explicitly as these are not uniformly random. // 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( #[expect(
@ -327,15 +332,16 @@ fn emit_private_output(
&new_nullifier.0, &new_nullifier.0,
); );
output.new_nullifiers.push(new_nullifier); output.private_actions.push(PrivateAction {
output.new_commitments.push(commitment_post); nullifier: new_nullifier.0,
output root: new_nullifier.1,
.encrypted_private_post_states commitment: commitment_post,
.push(EncryptedAccountData { encrypted_post_state: EncryptedAccountData {
ciphertext: encrypted_account, ciphertext: encrypted_account,
epk, epk,
view_tag, view_tag,
}); },
});
} }
fn compute_update_nullifier_and_set_digest( fn compute_update_nullifier_and_set_digest(
@ -358,7 +364,7 @@ mod tests {
use super::*; use super::*;
fn note(tag: u8) -> (Nullifier, Commitment, EncryptedAccountData) { fn note(tag: u8) -> PrivateAction {
let nullifier = Nullifier::for_dummy(&[tag; 32]); let nullifier = Nullifier::for_dummy(&[tag; 32]);
let commitment = Commitment::for_dummy(&nullifier, &[tag; 32]); let commitment = Commitment::for_dummy(&nullifier, &[tag; 32]);
let ciphertext = EncryptionScheme::encrypt( let ciphertext = EncryptionScheme::encrypt(
@ -367,37 +373,36 @@ mod tests {
&SharedSecretKey([0; 32]), &SharedSecretKey([0; 32]),
&nullifier, &nullifier,
); );
let encrypted = EncryptedAccountData { PrivateAction {
ciphertext, nullifier,
epk: EphemeralPublicKey(vec![tag]), root: DUMMY_COMMITMENT_HASH,
view_tag: 0, commitment,
}; encrypted_post_state: EncryptedAccountData {
(nullifier, commitment, encrypted) ciphertext,
epk: EphemeralPublicKey(vec![tag]),
view_tag: 0,
},
}
} }
#[test] #[test]
fn obfuscate_byte_sorts_commitments_and_nullifiers() { fn obfuscate_byte_sorts_commitments_and_nullifiers() {
let mut output = PrivacyPreservingCircuitOutput::default(); let mut output = PrivacyPreservingCircuitOutput::default();
for tag in 0..3 { for tag in 0..3 {
let (nullifier, commitment, encrypted) = note(tag); output.private_actions.push(note(tag));
output
.new_nullifiers
.push((nullifier, DUMMY_COMMITMENT_HASH));
output.new_commitments.push(commitment);
output.encrypted_private_post_states.push(encrypted);
} }
obfuscate_output_ordering(&mut output); obfuscate_output_ordering(&mut output);
assert!( assert!(
output output
.new_commitments .private_actions
.is_sorted_by_key(Commitment::to_byte_array) .is_sorted_by_key(|action| action.nullifier.to_byte_array())
); );
assert!( assert!(
output output
.new_nullifiers .private_actions
.is_sorted_by_key(|(nullifier, _)| nullifier.to_byte_array()) .is_sorted_by_key(|action| action.commitment.to_byte_array())
); );
} }
@ -405,27 +410,26 @@ mod tests {
fn obfuscate_keeps_each_nullifier_with_its_ciphertext() { fn obfuscate_keeps_each_nullifier_with_its_ciphertext() {
let mut output = PrivacyPreservingCircuitOutput::default(); let mut output = PrivacyPreservingCircuitOutput::default();
for tag in 0..3 { for tag in 0..3 {
let (nullifier, _, encrypted) = note(tag); output.private_actions.push(note(tag));
output
.new_nullifiers
.push((nullifier, DUMMY_COMMITMENT_HASH));
output.encrypted_private_post_states.push(encrypted);
} }
let paired: HashMap<[u8; 32], EphemeralPublicKey> = output let paired: HashMap<[u8; 32], EphemeralPublicKey> = output
.new_nullifiers .private_actions
.iter() .iter()
.zip(&output.encrypted_private_post_states) .map(|action| {
.map(|((nullifier, _), note)| (nullifier.to_byte_array(), note.epk.clone())) (
action.nullifier.to_byte_array(),
action.encrypted_post_state.epk.clone(),
)
})
.collect(); .collect();
obfuscate_output_ordering(&mut output); obfuscate_output_ordering(&mut output);
for ((nullifier, _), note) in output for action in &output.private_actions {
.new_nullifiers assert_eq!(
.iter() paired[&action.nullifier.to_byte_array()],
.zip(&output.encrypted_private_post_states) action.encrypted_post_state.epk
{ );
assert_eq!(paired[&nullifier.to_byte_array()], note.epk);
} }
} }
} }

View File

@ -1,3 +1,4 @@
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ 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)] #[derive(Serialize, Deserialize)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq, Default))] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq, Default))]
pub struct PrivacyPreservingCircuitOutput { pub struct PrivacyPreservingCircuitOutput {
pub public_pre_states: Vec<AccountWithMetadata>, pub public_actions: Vec<PublicAction>,
pub public_post_states: Vec<Account>, pub private_actions: Vec<PrivateAction>,
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub block_validity_window: BlockValidityWindow, pub block_validity_window: BlockValidityWindow,
pub timestamp_validity_window: TimestampValidityWindow, pub timestamp_validity_window: TimestampValidityWindow,
} }
#[cfg(any(feature = "host", test))]
impl PrivacyPreservingCircuitOutput {
#[must_use]
pub fn commitments(&self) -> Vec<Commitment> {
self.private_actions
.iter()
.map(|action| action.commitment)
.collect()
}
#[must_use]
pub fn nullifiers(&self) -> Vec<(Nullifier, CommitmentSetDigest)> {
self.private_actions
.iter()
.map(|action| (action.nullifier, action.root))
.collect()
}
}
#[cfg(feature = "host")] #[cfg(feature = "host")]
impl PrivacyPreservingCircuitOutput { impl PrivacyPreservingCircuitOutput {
/// Serializes the circuit output to a byte vector. /// Serializes the circuit output to a byte vector.
@ -183,50 +219,57 @@ mod tests {
#[test] #[test]
fn privacy_preserving_circuit_output_to_bytes_is_compatible_with_from_slice() { fn privacy_preserving_circuit_output_to_bytes_is_compatible_with_from_slice() {
let output = PrivacyPreservingCircuitOutput { let output = PrivacyPreservingCircuitOutput {
public_pre_states: vec![ public_actions: vec![
AccountWithMetadata::new( PublicAction {
Account { 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], program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 12_345_678_901_234_567_890, balance: 100,
data: b"test data".to_vec().try_into().unwrap(), data: b"post state data".to_vec().try_into().unwrap(),
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFE), nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF),
}, },
true, },
AccountId::new([0; 32]), PublicAction {
), pre: AccountWithMetadata::new(
AccountWithMetadata::new( Account {
Account { program_owner: [9, 9, 9, 8, 8, 8, 7, 7],
program_owner: [9, 9, 9, 8, 8, 8, 7, 7], balance: 123_123_123_456_456_567_112,
balance: 123_123_123_456_456_567_112, data: b"test data".to_vec().try_into().unwrap(),
data: b"test data".to_vec().try_into().unwrap(), nonce: Nonce(9_999_999_999_999_999_999_999),
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 { private_actions: vec![PrivateAction {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8], nullifier: Nullifier::for_account_update(
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(
&Commitment::new(&AccountId::new([2; 32]), &Account::default()), &Commitment::new(&AccountId::new([2; 32]), &Account::default()),
&[1; 32], &[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(), block_validity_window: (1..).into(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(), timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
}; };

View File

@ -32,10 +32,10 @@ pub const DUMMY_COMMITMENT_HASH: [u8; 32] = [
129, 241, 118, 39, 41, 253, 141, 171, 184, 71, 8, 41, 129, 241, 118, 39, 41, 253, 141, 171, 184, 71, 8, 41,
]; ];
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[derive(Copy, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
#[cfg_attr( #[cfg_attr(
any(feature = "host", test), any(feature = "host", test),
derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord) derive(Default, PartialEq, Eq, Hash, PartialOrd, Ord)
)] )]
pub struct Commitment(pub(super) [u8; 32]); pub struct Commitment(pub(super) [u8; 32]);

View File

@ -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 /// 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. /// 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<u8>); pub struct EphemeralPublicKey(pub Vec<u8>);
pub struct EncryptionScheme; pub struct EncryptionScheme;
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[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<u8>); pub struct Ciphertext(pub(crate) Vec<u8>);
#[cfg(any(feature = "host", test))] #[cfg(any(feature = "host", test))]
@ -71,7 +73,10 @@ pub type ViewTag = u8;
/// Encrypted private-account note for one output. /// Encrypted private-account note for one output.
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[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 struct EncryptedAccountData {
pub ciphertext: Ciphertext, pub ciphertext: Ciphertext,
pub epk: EphemeralPublicKey, pub epk: EphemeralPublicKey,

View File

@ -4,7 +4,8 @@
)] )]
pub use circuit_io::{ pub use circuit_io::{
DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput,
PrivacyPreservingCircuitOutput, PrivateAction, PublicAction,
}; };
pub use commitment::{ pub use commitment::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof,

View File

@ -72,7 +72,7 @@ pub type NullifierSecretKey = [u8; 32];
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
#[cfg_attr( #[cfg_attr(
any(feature = "host", test), 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]); pub struct Nullifier(pub(super) [u8; 32]);

View File

@ -24,9 +24,9 @@ fn decrypt_kind(
idx: usize, idx: usize,
) -> PrivateAccountKind { ) -> PrivateAccountKind {
let (kind, _) = EncryptionScheme::decrypt( let (kind, _) = EncryptionScheme::decrypt(
&output.encrypted_private_post_states[idx].ciphertext, &output.private_actions[idx].encrypted_post_state.ciphertext,
ssk, ssk,
&output.new_nullifiers[idx].0, &output.private_actions[idx].nullifier,
) )
.unwrap(); .unwrap();
kind kind
@ -102,18 +102,16 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
assert!(proof.is_valid_for(&output)); assert!(proof.is_valid_for(&output));
let [sender_pre] = output.public_pre_states.try_into().unwrap(); let [action] = output.public_actions.try_into().unwrap();
let [sender_post] = output.public_post_states.try_into().unwrap(); let (sender_pre, sender_post) = (action.pre, action.post);
assert_eq!(sender_pre, expected_sender_pre); assert_eq!(sender_pre, expected_sender_pre);
assert_eq!(sender_post, expected_sender_post); assert_eq!(sender_post, expected_sender_post);
assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.private_actions.len(), 1);
assert_eq!(output.new_nullifiers.len(), 1);
assert_eq!(output.encrypted_private_post_states.len(), 1);
let (_identifier, recipient_post) = EncryptionScheme::decrypt( let (_identifier, recipient_post) = EncryptionScheme::decrypt(
&output.encrypted_private_post_states[0].ciphertext, &output.private_actions[0].encrypted_post_state.ciphertext,
&shared_secret, &shared_secret,
&output.new_nullifiers[0].0, &output.private_actions[0].nullifier,
) )
.unwrap(); .unwrap();
assert_eq!(recipient_post, expected_recipient_post); assert_eq!(recipient_post, expected_recipient_post);
@ -216,43 +214,46 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
.unwrap(); .unwrap();
assert!(proof.is_valid_for(&output)); assert!(proof.is_valid_for(&output));
assert!(output.public_pre_states.is_empty()); assert!(output.public_actions.is_empty());
assert!(output.public_post_states.is_empty());
let sender_nullifier = expected_new_nullifiers[0].0; let sender_nullifier = expected_new_nullifiers[0].0;
let recipient_nullifier = expected_new_nullifiers[1].0; let recipient_nullifier = expected_new_nullifiers[1].0;
let mut expected_new_commitments = expected_new_commitments; let mut sorted_commitments = expected_new_commitments;
expected_new_commitments.sort_unstable_by_key(Commitment::to_byte_array); sorted_commitments.sort_unstable_by_key(Commitment::to_byte_array);
assert_eq!(output.new_commitments, expected_new_commitments); assert_eq!(output.commitments(), sorted_commitments);
let mut expected_new_nullifiers = expected_new_nullifiers; let mut sorted_nullifiers = expected_new_nullifiers;
expected_new_nullifiers.sort_unstable_by_key(|(nullifier, _)| nullifier.to_byte_array()); sorted_nullifiers.sort_unstable_by_key(|(nullifier, _)| nullifier.to_byte_array());
assert_eq!(output.new_nullifiers, expected_new_nullifiers); 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 let sender_slot = output
.new_nullifiers .private_actions
.iter() .iter()
.position(|(nullifier, _)| *nullifier == sender_nullifier) .position(|action| action.nullifier == sender_nullifier)
.unwrap(); .unwrap();
let (_identifier, sender_post) = EncryptionScheme::decrypt( 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, &shared_secret_1,
&output.new_nullifiers[sender_slot].0, &output.private_actions[sender_slot].nullifier,
) )
.unwrap(); .unwrap();
assert_eq!(sender_post, expected_private_account_1); assert_eq!(sender_post, expected_private_account_1);
let recipient_slot = output let recipient_slot = output
.new_nullifiers .private_actions
.iter() .iter()
.position(|(nullifier, _)| *nullifier == recipient_nullifier) .position(|action| action.nullifier == recipient_nullifier)
.unwrap(); .unwrap();
let (_identifier, recipient_post) = EncryptionScheme::decrypt( 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, &shared_secret_2,
&output.new_nullifiers[recipient_slot].0, &output.private_actions[recipient_slot].nullifier,
) )
.unwrap(); .unwrap();
assert_eq!(recipient_post, expected_private_account_2); assert_eq!(recipient_post, expected_private_account_2);
@ -281,9 +282,9 @@ fn init_note_view_tag_is_derived_from_account_keys() {
.unwrap(); .unwrap();
assert!(proof.is_valid_for(&output)); assert!(proof.is_valid_for(&output));
assert_eq!(output.encrypted_private_post_states.len(), 1); assert_eq!(output.private_actions.len(), 1);
assert_eq!( 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()), EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
); );
} }
@ -324,8 +325,11 @@ fn update_note_view_tag_is_the_supplied_value() {
.unwrap(); .unwrap();
assert!(proof.is_valid_for(&output)); 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, fed_tag); assert_eq!(
output.private_actions[0].encrypted_post_state.view_tag,
fed_tag
);
} }
#[test] #[test]
@ -448,7 +452,7 @@ fn private_pda_init() {
); );
let (output, _proof) = result.expect("PDA init should succeed"); 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. /// 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"); 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, /// 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"); let (output, _proof) = result.expect("shared account receive should succeed");
// Sender is public (no commitment), recipient is private (1 commitment) // 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 /// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts

View File

@ -1,24 +1,27 @@
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use lee_core::{ use lee_core::{
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, PrivateAction,
account::{Account, Nonce}, account::{Account, Nonce},
program::{BlockValidityWindow, TimestampValidityWindow}, program::{BlockValidityWindow, TimestampValidityWindow},
}; };
pub use lee_core::{EncryptedAccountData, ViewTag}; pub use lee_core::{EncryptedAccountData, ViewTag};
use sha2::{Digest as _, Sha256}; 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"; 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)] #[derive(Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct Message { pub struct Message {
pub public_account_ids: Vec<AccountId>, pub public_actions: Vec<PublicActionWithID>,
pub nonces: Vec<Nonce>, pub nonces: Vec<Nonce>,
pub public_post_states: Vec<Account>, pub private_actions: Vec<PrivateAction>,
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub block_validity_window: BlockValidityWindow, pub block_validity_window: BlockValidityWindow,
pub timestamp_validity_window: TimestampValidityWindow, pub timestamp_validity_window: TimestampValidityWindow,
} }
@ -31,21 +34,22 @@ impl std::fmt::Debug for Message {
write!(f, "{}", hex::encode(self.0)) write!(f, "{}", hex::encode(self.0))
} }
} }
let nullifiers: Vec<_> = self let private_actions: Vec<_> = self
.new_nullifiers .private_actions
.iter() .iter()
.map(|(n, d)| (n, HexDigest(d))) .map(|a| {
(
&a.nullifier,
HexDigest(&a.root),
&a.commitment,
&a.encrypted_post_state,
)
})
.collect(); .collect();
f.debug_struct("Message") f.debug_struct("Message")
.field("public_account_ids", &self.public_account_ids) .field("public_actions", &self.public_actions)
.field("nonces", &self.nonces) .field("nonces", &self.nonces)
.field("public_post_states", &self.public_post_states) .field("private_actions", &private_actions)
.field(
"encrypted_private_post_states",
&self.encrypted_private_post_states,
)
.field("new_commitments", &self.new_commitments)
.field("new_nullifiers", &nullifiers)
.field("block_validity_window", &self.block_validity_window) .field("block_validity_window", &self.block_validity_window)
.field("timestamp_validity_window", &self.timestamp_validity_window) .field("timestamp_validity_window", &self.timestamp_validity_window)
.finish() .finish()
@ -53,21 +57,55 @@ impl std::fmt::Debug for Message {
} }
impl Message { impl Message {
pub fn try_from_circuit_output( #[must_use]
public_account_ids: Vec<AccountId>, pub fn from_circuit_output(nonces: Vec<Nonce>, output: PrivacyPreservingCircuitOutput) -> Self {
nonces: Vec<Nonce>, let public_actions = output
output: PrivacyPreservingCircuitOutput, .public_actions
) -> Result<Self, LeeError> { .into_iter()
Ok(Self { .map(|action| PublicActionWithID {
public_account_ids, account_id: action.pre.account_id,
post_state: action.post,
})
.collect();
Self {
public_actions,
nonces, nonces,
public_post_states: output.public_post_states, private_actions: output.private_actions,
encrypted_private_post_states: output.encrypted_private_post_states,
new_commitments: output.new_commitments,
new_nullifiers: output.new_nullifiers,
block_validity_window: output.block_validity_window, block_validity_window: output.block_validity_window,
timestamp_validity_window: output.timestamp_validity_window, timestamp_validity_window: output.timestamp_validity_window,
}) }
}
#[must_use]
pub fn commitments(&self) -> Vec<Commitment> {
self.private_actions
.iter()
.map(|action| action.commitment)
.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<AccountId> {
self.public_actions
.iter()
.map(|action| action.account_id)
.collect()
}
#[must_use]
pub fn public_post_states(&self) -> Vec<Account> {
self.public_actions
.iter()
.map(|action| action.post_state.clone())
.collect()
} }
#[must_use] #[must_use]
@ -84,34 +122,20 @@ impl Message {
Sha256::digest(bytes).into() Sha256::digest(bytes).into()
} }
/// Ensure that the commitments, nullifiers, and ciphertexts agree.
pub fn validate_note_lengths(&self) -> Result<usize, LeeError> {
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)] #[cfg(test)]
pub mod tests { pub mod tests {
use lee_core::{ use lee_core::{
Commitment, EncryptionScheme, EphemeralSecretKey, Nullifier, NullifierPublicKey, Commitment, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey, Nullifier,
PrivateAccountKind, SharedSecretKey, NullifierPublicKey, PrivateAccountKind, PrivateAction, SharedSecretKey,
account::{Account, AccountId, Nonce}, account::{Account, AccountId, Nonce},
encryption::ViewingPublicKey, encryption::{Ciphertext, ViewingPublicKey},
program::{BlockValidityWindow, TimestampValidityWindow}, program::{BlockValidityWindow, TimestampValidityWindow},
}; };
use sha2::{Digest as _, Sha256}; use sha2::{Digest as _, Sha256};
use super::{EncryptedAccountData, Message, PREFIX}; use super::{EncryptedAccountData, Message, PREFIX, PublicActionWithID};
#[must_use] #[must_use]
pub fn message_for_tests() -> Message { pub fn message_for_tests() -> Message {
@ -125,78 +149,57 @@ pub mod tests {
let npk2 = NullifierPublicKey::from(&nsk2); let npk2 = NullifierPublicKey::from(&nsk2);
let vpk = ViewingPublicKey::from_seed(&[7; 32], &[8; 32]); 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 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 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 account_id1 = lee_core::account::AccountId::for_regular_private_account(&npk1, &vpk, 0);
let old_commitment = Commitment::new(&account_id1, &account1); let old_commitment = Commitment::new(&account_id1, &account1);
let new_nullifiers = vec![( let nullifier = Nullifier::for_account_update(&old_commitment, &nsk1);
Nullifier::for_account_update(&old_commitment, &nsk1),
[0; 32],
)];
Message { Message {
public_account_ids, public_actions: vec![PublicActionWithID {
account_id: AccountId::new([1; 32]),
post_state: Account::default(),
}],
nonces, nonces,
public_post_states, private_actions: vec![PrivateAction {
encrypted_private_post_states, nullifier,
new_commitments, root: [0; 32],
new_nullifiers, commitment,
encrypted_post_state: EncryptedAccountData {
ciphertext: Ciphertext::from_inner(vec![]),
epk: EphemeralPublicKey(vec![]),
view_tag: 0,
},
}],
block_validity_window: BlockValidityWindow::new_unbounded(), block_validity_window: BlockValidityWindow::new_unbounded(),
timestamp_validity_window: TimestampValidityWindow::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] #[test]
fn hash_privacy_pinned() { fn hash_privacy_pinned() {
let msg = Message { let msg = Message {
public_account_ids: vec![AccountId::new([42_u8; 32])], public_actions: vec![],
nonces: vec![Nonce(5)], nonces: vec![Nonce(5)],
public_post_states: vec![], private_actions: vec![],
encrypted_private_post_states: vec![],
new_commitments: vec![],
new_nullifiers: vec![],
block_validity_window: BlockValidityWindow::new_unbounded(), block_validity_window: BlockValidityWindow::new_unbounded(),
timestamp_validity_window: TimestampValidityWindow::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]; 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 private_actions_bytes: &[u8] = &[0, 0, 0, 0];
let empty_vec_bytes: &[u8] = &[0_u8; 4];
// validity windows: unbounded = {from: None (0_u8), to: None (0_u8)} // 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<u8> = [ let expected_borsh_vec: Vec<u8> = [
&[1_u8, 0, 0, 0], // public_account_ids public_actions_bytes,
public_account_ids_bytes,
nonces_bytes, nonces_bytes,
empty_vec_bytes, // public_post_state private_actions_bytes,
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, // block_validity_window
unbounded_window_bytes, // timestamp_validity_window unbounded_window_bytes, // timestamp_validity_window
] ]

View File

@ -53,7 +53,12 @@ impl PrivacyPreservingTransaction {
.signer_account_ids() .signer_account_ids()
.into_iter() .into_iter()
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
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() acc_set.into_iter().collect()
} }

View File

@ -402,11 +402,8 @@ fn private_pda_claim_succeeds() {
); );
let (output, _proof) = result.expect("private PDA claim should succeed"); let (output, _proof) = result.expect("private PDA claim should succeed");
assert_eq!(output.new_nullifiers.len(), 1); assert_eq!(output.private_actions.len(), 1);
assert_eq!(output.new_commitments.len(), 1); assert!(output.public_actions.is_empty());
assert_eq!(output.encrypted_private_post_states.len(), 1);
assert!(output.public_pre_states.is_empty());
assert!(output.public_post_states.is_empty());
} }
/// An npk is supplied that does not match the `pre_state`'s `account_id` under /// 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) = let (output, _proof) =
result.expect("caller-seeds authorization of private PDA should succeed"); result.expect("caller-seeds authorization of private PDA should succeed");
assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.private_actions.len(), 1);
assert_eq!(output.new_nullifiers.len(), 1);
} }
/// The delegator chains with a different seed than the one it claimed with. In the callee /// 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(); .unwrap();
// Create message from circuit output // 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, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
@ -801,7 +797,7 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() {
) )
.unwrap(); .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 witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);
@ -851,7 +847,7 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() {
) )
.unwrap(); .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 witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);
@ -961,8 +957,7 @@ fn two_private_pda_family_members_receive_and_spend() {
&simple_transfer.clone().into(), &simple_transfer.clone().into(),
) )
.unwrap(); .unwrap();
let message = let message = Message::from_circuit_output(vec![funder_nonce], output);
Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output).unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
state state
.transition_from_privacy_preserving_transaction( .transition_from_privacy_preserving_transaction(
@ -997,8 +992,7 @@ fn two_private_pda_family_members_receive_and_spend() {
&simple_transfer.into(), &simple_transfer.into(),
) )
.unwrap(); .unwrap();
let message = let message = Message::from_circuit_output(vec![funder_nonce], output);
Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output).unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
state state
.transition_from_privacy_preserving_transaction( .transition_from_privacy_preserving_transaction(
@ -1041,8 +1035,7 @@ fn two_private_pda_family_members_receive_and_spend() {
&spend_with_deps, &spend_with_deps,
) )
.unwrap(); .unwrap();
let message = let message = Message::from_circuit_output(vec![Nonce(0)], output);
Message::try_from_circuit_output(vec![recipient_id], vec![Nonce(0)], output).unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
state state
.transition_from_privacy_preserving_transaction( .transition_from_privacy_preserving_transaction(
@ -1079,7 +1072,7 @@ fn two_private_pda_family_members_receive_and_spend() {
&spend_with_deps, &spend_with_deps,
) )
.unwrap(); .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, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
state state
.transition_from_privacy_preserving_transaction( .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(), &crate::test_methods::simple_balance_transfer().into(),
) )
.unwrap(); .unwrap();
let message = let message = Message::from_circuit_output(vec![recipient_nonce], output);
Message::try_from_circuit_output(vec![recipient_id], vec![recipient_nonce], output)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
state state
.transition_from_privacy_preserving_transaction( .transition_from_privacy_preserving_transaction(

View File

@ -341,9 +341,7 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() {
) )
.unwrap(); .unwrap();
let message = let message = Message::from_circuit_output(vec![Nonce(0)], output);
Message::try_from_circuit_output(vec![recipient_account_id], vec![Nonce(0)], output)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]);
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);
@ -466,7 +464,7 @@ fn private_chained_call(number_of_calls: u32) {
) )
.unwrap(); .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 witness_set = WitnessSet::for_message(&message, proof, &[]);
let transaction = PrivacyPreservingTransaction::new(message, witness_set); let transaction = PrivacyPreservingTransaction::new(message, witness_set);

View File

@ -291,12 +291,7 @@ fn shielded_balance_transfer_for_tests(
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message = Message::from_circuit_output(vec![sender_nonce], output);
vec![sender_keys.account_id()],
vec![sender_nonce],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&sender_keys.signing_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&sender_keys.signing_key]);
PrivacyPreservingTransaction::new(message, witness_set) PrivacyPreservingTransaction::new(message, witness_set)
@ -350,7 +345,7 @@ fn private_balance_transfer_for_tests(
) )
.unwrap(); .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 witness_set = WitnessSet::for_message(&message, proof, &[]);
@ -399,8 +394,7 @@ fn deshielded_balance_transfer_for_tests(
) )
.unwrap(); .unwrap();
let message = let message = Message::from_circuit_output(vec![], output);
Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);

View File

@ -26,7 +26,7 @@ fn transition_from_privacy_preserving_transaction_shielded() {
this 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)); assert!(!state.private_state.0.contains(&expected_new_commitment));
state state
@ -128,7 +128,7 @@ fn privacy_tampered_epk_is_rejected() {
); );
// Flip a byte of the first note's epk // 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!( assert!(
matches!( matches!(
@ -154,7 +154,7 @@ fn privacy_tampered_view_tag_is_rejected() {
); );
// Flip the first note's view_tag // 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!( assert!(
matches!( matches!(

View File

@ -149,7 +149,7 @@ fn validity_window_works_in_privacy_preserving_transactions(
) )
.unwrap(); .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 witness_set = WitnessSet::for_message(&message, proof, &[]);
PrivacyPreservingTransaction::new(message, witness_set) PrivacyPreservingTransaction::new(message, witness_set)
@ -214,7 +214,7 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions(
) )
.unwrap(); .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 witness_set = WitnessSet::for_message(&message, proof, &[]);
PrivacyPreservingTransaction::new(message, witness_set) PrivacyPreservingTransaction::new(message, witness_set)

View File

@ -4,7 +4,7 @@ use std::{
}; };
use lee_core::{ use lee_core::{
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp, BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, PublicAction, Timestamp,
account::{Account, AccountId, AccountWithMetadata}, account::{Account, AccountId, AccountWithMetadata},
program::{ program::{
ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, compute_public_authorized_pdas, ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, compute_public_authorized_pdas,
@ -321,10 +321,13 @@ impl ValidatedStateDiff {
) -> Result<Self, LeeError> { ) -> Result<Self, LeeError> {
let message = &tx.message; let message = &tx.message;
let witness_set = &tx.witness_set; 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 // 1. Commitments or nullifiers are non empty
ensure!( ensure!(
!message.new_commitments.is_empty() || !message.new_nullifiers.is_empty(), !message.private_actions.is_empty(),
LeeError::InvalidInput( LeeError::InvalidInput(
"Empty commitments and empty nullifiers found in message".into(), "Empty commitments and empty nullifiers found in message".into(),
) )
@ -332,25 +335,19 @@ impl ValidatedStateDiff {
// 2. Check there are no duplicate account_ids in the public_account_ids list. // 2. Check there are no duplicate account_ids in the public_account_ids list.
ensure!( 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()) LeeError::InvalidInput("Duplicate account_ids found in message".into())
); );
// Check there are no duplicate nullifiers in the new_nullifiers list // Check there are no duplicate nullifiers in the new_nullifiers list
ensure!( ensure!(
n_unique( n_unique(&nullifiers.iter().map(|(n, _)| n).collect::<Vec<_>>()) == nullifiers.len(),
&message
.new_nullifiers
.iter()
.map(|(n, _)| n)
.collect::<Vec<_>>()
) == message.new_nullifiers.len(),
LeeError::InvalidInput("Duplicate nullifiers found in message".into()) LeeError::InvalidInput("Duplicate nullifiers found in message".into())
); );
// Check there are no duplicate commitments in the new_commitments list // Check there are no duplicate commitments in the new_commitments list
ensure!( ensure!(
n_unique(&message.new_commitments) == message.new_commitments.len(), n_unique(&commitments) == commitments.len(),
LeeError::InvalidInput("Duplicate commitments found in message".into()) LeeError::InvalidInput("Duplicate commitments found in message".into())
); );
@ -387,8 +384,7 @@ impl ValidatedStateDiff {
); );
// Build pre_states for proof verification // Build pre_states for proof verification
let public_pre_states: Vec<_> = message let public_pre_states: Vec<_> = public_account_ids
.public_account_ids
.iter() .iter()
.map(|account_id| { .map(|account_id| {
AccountWithMetadata::new( AccountWithMetadata::new(
@ -407,28 +403,22 @@ impl ValidatedStateDiff {
)?; )?;
// 5. Commitment freshness // 5. Commitment freshness
state.check_commitments_are_new(&message.new_commitments)?; state.check_commitments_are_new(&commitments)?;
// 6. Nullifier uniqueness // 6. Nullifier uniqueness
state.check_nullifiers_are_valid(&message.new_nullifiers)?; state.check_nullifiers_are_valid(&nullifiers)?;
let public_diff = message let public_diff = public_account_ids
.public_account_ids
.iter() .iter()
.copied() .copied()
.zip(message.public_post_states.clone()) .zip(message.public_post_states())
.collect();
let new_nullifiers = message
.new_nullifiers
.iter()
.copied()
.map(|(nullifier, _)| nullifier)
.collect(); .collect();
let new_nullifiers = nullifiers.iter().map(|(nullifier, _)| *nullifier).collect();
Ok(Self(StateDiff { Ok(Self(StateDiff {
signer_account_ids, signer_account_ids,
public_diff, public_diff,
new_commitments: message.new_commitments.clone(), new_commitments: commitments,
new_nullifiers, new_nullifiers,
program: None, program: None,
})) }))
@ -509,11 +499,13 @@ fn check_privacy_preserving_circuit_proof_is_valid(
message: &Message, message: &Message,
) -> Result<(), LeeError> { ) -> Result<(), LeeError> {
let output = PrivacyPreservingCircuitOutput { let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(), public_actions: public_pre_states
public_post_states: message.public_post_states.clone(), .iter()
encrypted_private_post_states: message.encrypted_private_post_states.clone(), .cloned()
new_commitments: message.new_commitments.clone(), .zip(message.public_post_states())
new_nullifiers: message.new_nullifiers.clone(), .map(|(pre, post)| PublicAction { pre, post })
.collect(),
private_actions: message.private_actions.clone(),
block_validity_window: message.block_validity_window, block_validity_window: message.block_validity_window,
timestamp_validity_window: message.timestamp_validity_window, timestamp_validity_window: message.timestamp_validity_window,
}; };

View File

@ -188,12 +188,10 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
// public_account_ids lists the Public entries from account_identities, in order. // public_account_ids lists the Public entries from account_identities, in order.
// The single ciphertext belongs to attacker's private account update. // The single ciphertext belongs to attacker's private account update.
let message = Message::try_from_circuit_output( let message = Message::from_circuit_output(
vec![victim_id, recipient_id],
vec![], // no public signers, no nonces vec![], // no public signers, no nonces
circuit_output, circuit_output,
) );
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
let tx = PrivacyPreservingTransaction::new(message, witness_set); 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. // public_account_ids lists the Public entries from account_identities, in order.
// The single ciphertext belongs to attacker's private account update. // The single ciphertext belongs to attacker's private account update.
let message = Message::try_from_circuit_output( let message = Message::from_circuit_output(
vec![victim_id, recipient_id],
vec![], // no public signers, no nonces vec![], // no public signers, no nonces
circuit_output, circuit_output,
) );
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);
@ -480,8 +476,9 @@ fn malicious_programs_cannot_drain_victim_without_signature() {
#[test] #[test]
fn privacy_garbage_proof_is_rejected() { fn privacy_garbage_proof_is_rejected() {
use lee_core::{ use lee_core::{
Commitment, Commitment, EncryptedAccountData, Nullifier, PrivateAction,
account::Account, account::Account,
encryption::{Ciphertext, EphemeralPublicKey},
program::{BlockValidityWindow, TimestampValidityWindow}, program::{BlockValidityWindow, TimestampValidityWindow},
}; };
@ -503,12 +500,18 @@ fn privacy_garbage_proof_is_rejected() {
)); ));
let commitment = Commitment::new(&account_id, &Account::default()); let commitment = Commitment::new(&account_id, &Account::default());
let message = Message { let message = Message {
public_account_ids: vec![], public_actions: vec![],
nonces: vec![], nonces: vec![],
public_post_states: vec![], private_actions: vec![PrivateAction {
encrypted_private_post_states: vec![], nullifier: Nullifier::for_account_initialization(&account_id),
new_commitments: vec![commitment], root: [0; 32],
new_nullifiers: vec![], commitment,
encrypted_post_state: EncryptedAccountData {
ciphertext: Ciphertext::from_inner(vec![]),
epk: EphemeralPublicKey(vec![]),
view_tag: 0,
},
}],
block_validity_window: BlockValidityWindow::new_unbounded(), block_validity_window: BlockValidityWindow::new_unbounded(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(), timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
}; };

View File

@ -282,27 +282,30 @@ impl From<PublicMessage> for lee::public_transaction::Message {
impl From<lee::privacy_preserving_transaction::message::Message> for PrivacyPreservingMessage { impl From<lee::privacy_preserving_transaction::message::Message> for PrivacyPreservingMessage {
fn from(value: lee::privacy_preserving_transaction::message::Message) -> Self { fn from(value: lee::privacy_preserving_transaction::message::Message) -> Self {
let lee::privacy_preserving_transaction::message::Message { let lee::privacy_preserving_transaction::message::Message {
public_account_ids, public_actions,
nonces, nonces,
public_post_states, private_actions,
encrypted_private_post_states,
new_commitments,
new_nullifiers,
block_validity_window, block_validity_window,
timestamp_validity_window, timestamp_validity_window,
} = value; } = value;
Self { Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), public_account_ids: public_actions.iter().map(|a| a.account_id.into()).collect(),
nonces: nonces.iter().map(|x| x.0).collect(), nonces: nonces.iter().map(|x| x.0).collect(),
public_post_states: public_post_states.into_iter().map(Into::into).collect(), public_post_states: public_actions
encrypted_private_post_states: encrypted_private_post_states
.into_iter() .into_iter()
.map(Into::into) .map(|a| a.post_state.into())
.collect(), .collect(),
new_commitments: new_commitments.into_iter().map(Into::into).collect(), encrypted_private_post_states: private_actions
new_nullifiers: new_nullifiers .iter()
.map(|a| a.encrypted_post_state.clone().into())
.collect(),
new_commitments: private_actions
.iter()
.map(|a| a.commitment.into())
.collect(),
new_nullifiers: private_actions
.into_iter() .into_iter()
.map(|(n, d)| (n.into(), d.into())) .map(|a| (a.nullifier.into(), a.root.into()))
.collect(), .collect(),
block_validity_window: block_validity_window.into(), block_validity_window: block_validity_window.into(),
timestamp_validity_window: timestamp_validity_window.into(), timestamp_validity_window: timestamp_validity_window.into(),
@ -324,26 +327,50 @@ impl TryFrom<PrivacyPreservingMessage> for lee::privacy_preserving_transaction::
block_validity_window, block_validity_window,
timestamp_validity_window, timestamp_validity_window,
} = value; } = value;
let public_post_states = public_post_states
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<lee_core::account::Account>, _>>()
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?;
let public_actions = public_account_ids
.into_iter()
.map(Into::into)
.zip(public_post_states)
.map(|(account_id, post_state)| {
lee::privacy_preserving_transaction::message::PublicActionWithID {
account_id,
post_state,
}
})
.collect();
let private_actions = new_commitments
.into_iter()
.map(Into::into)
.zip(
new_nullifiers
.into_iter()
.map(|(n, d)| (n.into(), d.into())),
)
.zip(encrypted_private_post_states.into_iter().map(Into::into))
.map(|((commitment, (nullifier, root)), encrypted_post_state)| {
lee_core::PrivateAction {
nullifier,
root,
commitment,
encrypted_post_state,
}
})
.collect();
Ok(Self { Ok(Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), public_actions,
nonces: nonces nonces: nonces
.iter() .iter()
.map(|x| lee_core::account::Nonce(*x)) .map(|x| lee_core::account::Nonce(*x))
.collect(), .collect(),
public_post_states: public_post_states private_actions,
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()
.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(),
block_validity_window: block_validity_window block_validity_window: block_validity_window
.try_into() .try_into()
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?, .map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?,

View File

@ -905,8 +905,7 @@ fn private_bridge_withdraw_invocation_is_dropped() {
) )
.expect("Execution should succeed"); .expect("Execution should succeed");
let message = Message::try_from_circuit_output(vec![bridge_account_id], vec![], output) let message = Message::from_circuit_output(vec![], output);
.expect("Message construction should succeed");
let witness_set = let witness_set =
lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]);
let tx = let tx =

View File

@ -456,7 +456,6 @@ impl WalletCore {
let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else { let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else {
continue; continue;
}; };
pp_tx.message.validate_note_lengths()?;
// Sync updates while watching only the init nullifier. // Sync updates while watching only the init nullifier.
self.storage self.storage
.key_chain_mut() .key_chain_mut()
@ -677,7 +676,7 @@ impl WalletCore {
tx: &lee::privacy_preserving_transaction::PrivacyPreservingTransaction, tx: &lee::privacy_preserving_transaction::PrivacyPreservingTransaction,
acc_decode_mask: &[AccDecodeData], acc_decode_mask: &[AccDecodeData],
) -> Result<()> { ) -> Result<()> {
let note_count = tx.message.validate_note_lengths()?; let note_count = tx.message.private_actions.len();
anyhow::ensure!( anyhow::ensure!(
note_count >= acc_decode_mask.len(), note_count >= acc_decode_mask.len(),
"Decode mask has {} entries but the transaction has {note_count} notes", "Decode mask has {} entries but the transaction has {note_count} notes",
@ -785,12 +784,10 @@ impl WalletCore {
&program.to_owned(), &program.to_owned(),
)?; )?;
let message = let message = lee::privacy_preserving_transaction::message::Message::from_circuit_output(
lee::privacy_preserving_transaction::message::Message::try_from_circuit_output( acc_manager.public_account_nonces(),
acc_manager.public_account_ids(), output,
acc_manager.public_account_nonces(), );
output,
)?;
let message_hash = message.hash(); let message_hash = message.hash();
let signatures_public_keys = acc_manager let signatures_public_keys = acc_manager
@ -933,7 +930,6 @@ impl WalletCore {
let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else { let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else {
continue; continue;
}; };
pp_tx.message.validate_note_lengths()?;
// Eagerly decrypt note updates using expected nullifiers. // Eagerly decrypt note updates using expected nullifiers.
let handled = self let handled = self
.storage .storage
@ -972,18 +968,19 @@ impl WalletCore {
&key_chain.viewing_public_key, &key_chain.viewing_public_key,
); );
message message
.encrypted_private_post_states .private_actions
.iter() .iter()
.enumerate() .enumerate()
.filter(move |(ciph_id, encrypted_data)| { .filter(move |(ciph_id, action)| {
// If we have not decrypted the update using the nullifiers, // If we have not decrypted the update using the nullifiers,
// the note may be an initialized one, for which we should // the note may be an initialized one, for which we should
// scan. // 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)| { .filter_map(move |(ciph_id, action)| {
let shared_secret = let shared_secret = key_chain
key_chain.calculate_shared_secret_receiver(&encrypted_data.epk)?; .calculate_shared_secret_receiver(&action.encrypted_post_state.epk)?;
decrypt_note_at(message, ciph_id, &shared_secret).map(|(kind, res_acc)| { decrypt_note_at(message, ciph_id, &shared_secret).map(|(kind, res_acc)| {
let npk = &key_chain.nullifier_public_key; let npk = &key_chain.nullifier_public_key;
@ -1040,16 +1037,14 @@ impl WalletCore {
for (account_id, npk, vpk, vsk, nsk) in shared_keys { for (account_id, npk, vpk, vsk, nsk) in shared_keys {
let view_tag = EncryptedAccountData::compute_view_tag(&npk, &vpk); let view_tag = EncryptedAccountData::compute_view_tag(&npk, &vpk);
for (ciph_id, encrypted_data) in for (ciph_id, action) in message.private_actions.iter().enumerate() {
message.encrypted_private_post_states.iter().enumerate()
{
// If already decrypted or the tag does not match, skip. // 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; continue;
} }
let Some(shared_secret) = 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 { else {
continue; continue;
}; };
@ -1086,9 +1081,9 @@ fn decrypt_note_at(
secret: &SharedSecretKey, secret: &SharedSecretKey,
) -> Option<(lee_core::PrivateAccountKind, Account)> { ) -> Option<(lee_core::PrivateAccountKind, Account)> {
lee_core::EncryptionScheme::decrypt( lee_core::EncryptionScheme::decrypt(
&message.encrypted_private_post_states[i].ciphertext, &message.private_actions[i].encrypted_post_state.ciphertext,
secret, secret,
&message.new_nullifiers[i].0, &message.private_actions[i].nullifier,
) )
} }

View File

@ -390,9 +390,9 @@ impl UserKeyChain {
index: &mut NullifierIndex, index: &mut NullifierIndex,
) -> HashSet<usize> { ) -> HashSet<usize> {
let mut handled = HashSet::new(); 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. // 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; continue;
}; };
// Try decrypting the commitment connected to the nullifier and get the next // 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) { 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. // Update the index to await for the new state of the account, i.e.
// the new nullifier. // 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. // Record that this nullifier's position can be skipped for scanning.
handled.insert(i); handled.insert(i);
} }
@ -416,7 +416,7 @@ impl UserKeyChain {
message: &Message, message: &Message,
i: usize, i: usize,
) -> Option<Nullifier> { ) -> Option<Nullifier> {
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) let (nsk, secret, is_shared) = if let Some(entry) = self.shared_private_account(account_id)
{ {
@ -474,10 +474,9 @@ impl UserKeyChain {
pub fn locate_spend(&self, account_id: AccountId, message: &Message) -> Option<usize> { pub fn locate_spend(&self, account_id: AccountId, message: &Message) -> Option<usize> {
let init = Nullifier::for_account_initialization(&account_id); let init = Nullifier::for_account_initialization(&account_id);
let update = self.next_update_nullifier(account_id); let update = self.next_update_nullifier(account_id);
message message.private_actions.iter().position(|action| {
.new_nullifiers action.nullifier == init || Some(&action.nullifier) == update.as_ref()
.iter() })
.position(|(nullifier, _)| *nullifier == init || Some(nullifier) == update.as_ref())
} }
pub fn add_imported_public_account(&mut self, private_key: lee::PrivateKey) { pub fn add_imported_public_account(&mut self, private_key: lee::PrivateKey) {
@ -890,7 +889,7 @@ impl Default for UserKeyChain {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use lee_core::{EncryptionScheme, encryption::EncryptedAccountData}; use lee_core::{EncryptionScheme, PrivateAction, encryption::EncryptedAccountData};
use super::*; use super::*;
@ -935,9 +934,12 @@ mod tests {
); );
let message = Message { let message = Message {
encrypted_private_post_states: vec![note], private_actions: vec![PrivateAction {
new_commitments: vec![new_commitment], nullifier: old_nullifier,
new_nullifiers: vec![(old_nullifier, [0; 32])], commitment: new_commitment,
encrypted_post_state: note,
..Default::default()
}],
..Default::default() ..Default::default()
}; };
@ -999,9 +1001,12 @@ mod tests {
); );
let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk); let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk);
let message = Message { let message = Message {
encrypted_private_post_states: vec![note], private_actions: vec![PrivateAction {
new_commitments: vec![new_commitment], nullifier: old_nullifier,
new_nullifiers: vec![(old_nullifier, [0; 32])], commitment: new_commitment,
encrypted_post_state: note,
..Default::default()
}],
..Default::default() ..Default::default()
}; };
@ -1061,9 +1066,12 @@ mod tests {
); );
let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk); let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk);
Message { Message {
encrypted_private_post_states: vec![note], private_actions: vec![PrivateAction {
new_commitments: vec![commitment], nullifier: spent,
new_nullifiers: vec![(spent, [0; 32])], commitment,
encrypted_post_state: note,
..Default::default()
}],
..Default::default() ..Default::default()
} }
}; };
@ -1124,7 +1132,10 @@ mod tests {
&[9; 32], &[9; 32],
); );
let message = Message { let message = Message {
new_nullifiers: vec![(unindexed, [0; 32])], private_actions: vec![PrivateAction {
nullifier: unindexed,
..Default::default()
}],
..Default::default() ..Default::default()
}; };