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

This commit is contained in:
Artem Gureev 2026-07-08 12:33:52 +00:00 committed by agureev
parent 2e669e76f8
commit e409b3ac16
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")?;
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(())

View File

@ -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(

View File

@ -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);
}

View File

@ -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);

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;
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(())
}

View File

@ -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)
}

View File

@ -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
);
}
}
}

View File

@ -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<AccountWithMetadata>,
pub public_post_states: Vec<Account>,
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub public_actions: Vec<PublicAction>,
pub private_actions: Vec<PrivateAction>,
pub block_validity_window: BlockValidityWindow,
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.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(),
};

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,
];
#[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]);

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
/// 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 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<u8>);
#[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,

View File

@ -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,

View File

@ -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]);

View File

@ -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

View File

@ -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<AccountId>,
pub public_actions: Vec<PublicActionWithID>,
pub nonces: Vec<Nonce>,
pub public_post_states: Vec<Account>,
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub private_actions: Vec<PrivateAction>,
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,55 @@ impl std::fmt::Debug for Message {
}
impl Message {
pub fn try_from_circuit_output(
public_account_ids: Vec<AccountId>,
nonces: Vec<Nonce>,
output: PrivacyPreservingCircuitOutput,
) -> Result<Self, LeeError> {
Ok(Self {
public_account_ids,
#[must_use]
pub fn from_circuit_output(nonces: Vec<Nonce>, 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<Commitment> {
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<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]
@ -84,34 +122,20 @@ impl Message {
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)]
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 +149,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<u8> = [
&[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
]

View File

@ -53,7 +53,12 @@ impl PrivacyPreservingTransaction {
.signer_account_ids()
.into_iter()
.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()
}

View File

@ -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(

View File

@ -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);

View File

@ -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, &[]);

View File

@ -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!(

View File

@ -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)

View File

@ -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<Self, LeeError> {
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::<Vec<_>>()
) == message.new_nullifiers.len(),
n_unique(&nullifiers.iter().map(|(n, _)| n).collect::<Vec<_>>()) == 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
let public_diff = public_account_ids
.iter()
.copied()
.zip(message.public_post_states.clone())
.collect();
let new_nullifiers = message
.new_nullifiers
.iter()
.copied()
.map(|(nullifier, _)| nullifier)
.zip(message.public_post_states())
.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,13 @@ 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_post_states())
.map(|(pre, post)| PublicAction { pre, post })
.collect(),
private_actions: message.private_actions.clone(),
block_validity_window: message.block_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.
// 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(),
};

View File

@ -282,27 +282,30 @@ impl From<PublicMessage> for lee::public_transaction::Message {
impl From<lee::privacy_preserving_transaction::message::Message> 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_account_ids: public_actions.iter().map(|a| a.account_id.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
public_post_states: public_actions
.into_iter()
.map(Into::into)
.map(|a| a.post_state.into())
.collect(),
new_commitments: new_commitments.into_iter().map(Into::into).collect(),
new_nullifiers: new_nullifiers
encrypted_private_post_states: private_actions
.iter()
.map(|a| a.encrypted_post_state.clone().into())
.collect(),
new_commitments: private_actions
.iter()
.map(|a| a.commitment.clone().into())
.collect(),
new_nullifiers: private_actions
.into_iter()
.map(|(n, d)| (n.into(), d.into()))
.map(|a| (a.nullifier.into(), a.root.into()))
.collect(),
block_validity_window: block_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,
timestamp_validity_window,
} = 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 {
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::<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(),
private_actions,
block_validity_window: block_validity_window
.try_into()
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?,

View File

@ -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 =

View File

@ -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,
)
}

View File

@ -390,9 +390,9 @@ impl UserKeyChain {
index: &mut NullifierIndex,
) -> HashSet<usize> {
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<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)
{
@ -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()
};