refactor(privacy_preserving_circuit): introduce helper functions to shorten long functions (#545)

* refactor(privacy_preserving_circuit): extract functions for readability

* refactor(privacy_preserving_circuit): address PR review comments

Bundle shared handle_* arguments into PrivateOutputHandler struct in
output.rs and fix misplaced docstring on resolve_external_seed in
execution_state.rs.

* feat: update commitment mechanism for new private account (#546)

* refactor(privacy_preserving_circuit): extract functions for readability

* feat: update commitment mechanism for new private accounts

Allow init accounts to optionally use a real membership proof for
DUMMY_COMMITMENT instead of hardcoding DUMMY_COMMITMENT_HASH as the
CommitmentSetDigest. The wallet fetches the proof from the sequencer
and passes it through the circuit.

* fix: address clippy lints and fix integration test visibility

* add tests

* refactor: removed duplicated code

* refactor: simplify init nullifier mechanism

Replace Option<MembershipProof> with Option<CommitmentSetDigest> on init
variants (PrivateAuthorizedInit, PrivateUnauthorized, PrivatePdaInit).
The circuit now receives the commitment tree root directly instead of
recomputing it from a Merkle proof.

* refactor: use CommitmentSetDigest directly instead of Option for init commitment root

Address PR #546 review feedback: the circuit now accepts CommitmentSetDigest
directly on init variants (PrivateAuthorizedInit, PrivateUnauthorized,
PrivatePdaInit), with callers providing DUMMY_COMMITMENT_HASH as the default.
Also fixes duplicate resolve_external_seed from rebase and rebuilds artifacts.

* style: run cargo +nightly fmt
This commit is contained in:
jonesmarvin8 2026-07-01 10:14:32 -04:00 committed by GitHub
parent 3b3857594f
commit f8d859394b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 792 additions and 547 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

@ -11,8 +11,10 @@ use lee::{
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
};
use lee_core::{
EncryptedAccountData, InputAccountIdentity, ML_KEM_768_CIPHERTEXT_LEN, NullifierPublicKey,
account::AccountWithMetadata,
DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, Nullifier,
NullifierPublicKey,
account::{Account, AccountWithMetadata},
compute_digest_for_path,
encryption::{EphemeralPublicKey, ViewingPublicKey},
};
use log::info;
@ -710,6 +712,7 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
npk,
ssk,
identifier: 1337,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
},
],
@ -720,3 +723,100 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
Ok(())
}
async fn prove_init_with_commitment_root(
ctx: &TestContext,
commitment_root: lee_core::CommitmentSetDigest,
) -> Result<lee_core::PrivacyPreservingCircuitOutput> {
let program = programs::authenticated_transfer();
let sender_id = ctx.existing_public_accounts()[0];
let sender_pre = AccountWithMetadata::new(
ctx.sequencer_client().get_account(sender_id).await?,
true,
sender_id,
);
let nsk: lee_core::NullifierSecretKey = [7; 32];
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
let ssk = SharedSecretKey([55_u8; 32]);
let recipient_account_id = AccountId::for_regular_private_account(&npk, 0);
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
let (output, _) = execute_and_prove(
vec![sender_pre, recipient],
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount: 1,
})?,
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
npk,
ssk,
identifier: 0,
commitment_root,
},
],
&program.into(),
)?;
Ok(output)
}
#[test]
async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> {
let ctx = TestContext::new().await?;
let dummy_proof = ctx
.sequencer_client()
.get_proof_for_commitment(DUMMY_COMMITMENT)
.await?
.expect("DUMMY_COMMITMENT must be in genesis commitment set");
let expected_digest = compute_digest_for_path(&DUMMY_COMMITMENT, &dummy_proof);
let nsk: lee_core::NullifierSecretKey = [7; 32];
let npk = NullifierPublicKey::from(&nsk);
let recipient_account_id = AccountId::for_regular_private_account(&npk, 0);
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!(
*nullifier,
Nullifier::for_account_initialization(&recipient_account_id)
);
assert_eq!(*digest, expected_digest);
assert_ne!(*digest, DUMMY_COMMITMENT_HASH);
Ok(())
}
#[test]
async fn init_nullifier_digest_is_bound_to_commitment_root() -> Result<()> {
let ctx = TestContext::new().await?;
let dummy_proof = ctx
.sequencer_client()
.get_proof_for_commitment(DUMMY_COMMITMENT)
.await?
.expect("DUMMY_COMMITMENT must be in genesis commitment set");
let expected_digest = compute_digest_for_path(&DUMMY_COMMITMENT, &dummy_proof);
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_without_root.new_nullifiers[0].1,
DUMMY_COMMITMENT_HASH
);
assert_ne!(
output_with_root.new_nullifiers[0].1,
output_without_root.new_nullifiers[0].1,
);
Ok(())
}

View File

@ -22,7 +22,7 @@ use lee::{
program::Program,
};
use lee_core::{
EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
account::{Account, AccountWithMetadata},
encryption::ViewingPublicKey,
program::PdaSeed,
@ -78,6 +78,7 @@ async fn fund_private_pda(
npk,
ssk,
identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: Some((seed, authority_program_id)),
},
];

View File

@ -23,7 +23,8 @@ use lee::{
public_transaction as putx,
};
use lee_core::{
EncryptedAccountData, InputAccountIdentity, MembershipProof, NullifierPublicKey,
DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, MembershipProof,
NullifierPublicKey,
account::{AccountWithMetadata, Nonce, data::Data},
encryption::ViewingPublicKey,
};
@ -314,6 +315,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
npk: recipient_npk,
ssk: recipient_ss,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),

View File

@ -59,46 +59,9 @@ impl ExecutionState {
program_id: ProgramId,
program_outputs: Vec<ProgramOutput>,
) -> Self {
// Build position → (npk, identifier) map for private-PDA pre_states, indexed by position
// in `account_identities`. The vec is documented as 1:1 with the program's pre_state
// order, so position here matches `pre_state_position` used downstream in
// `validate_and_sync_states`.
let mut private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)> =
HashMap::new();
for (pos, account_identity) in account_identities.iter().enumerate() {
if let Some((npk, identifier)) = account_identity.npk_if_private_pda() {
private_pda_npk_by_position.insert(pos, (npk, identifier));
}
}
let block_valid_from = program_outputs
.iter()
.filter_map(|output| output.block_validity_window.start())
.max();
let block_valid_until = program_outputs
.iter()
.filter_map(|output| output.block_validity_window.end())
.min();
let ts_valid_from = program_outputs
.iter()
.filter_map(|output| output.timestamp_validity_window.start())
.max();
let ts_valid_until = program_outputs
.iter()
.filter_map(|output| output.timestamp_validity_window.end())
.min();
let block_validity_window: BlockValidityWindow = (block_valid_from, block_valid_until)
.try_into()
.expect(
"There should be non empty intersection in the program output block validity windows",
);
let timestamp_validity_window: TimestampValidityWindow =
(ts_valid_from, ts_valid_until)
.try_into()
.expect(
"There should be non empty intersection in the program output timestamp validity windows",
);
let private_pda_npk_by_position = build_private_pda_npk_map(account_identities);
let (block_validity_window, timestamp_validity_window) =
intersect_validity_windows(&program_outputs);
let mut execution_state = Self {
pre_states: Vec::new(),
@ -136,49 +99,7 @@ impl ExecutionState {
panic!("Insufficient program outputs for chained calls");
};
// Check that instruction data in chained call is the instruction data in program output
assert_eq!(
chained_call.instruction_data, program_output.instruction_data,
"Mismatched instruction data between chained call and program output"
);
// Check that `program_output` is consistent with the execution of the corresponding
// program.
let program_output_words =
&to_vec(&program_output).expect("program_output must be serializable");
env::verify(chained_call.program_id, program_output_words).unwrap_or_else(
|_: Infallible| unreachable!("Infallible error is never constructed"),
);
// Verify that the program output's self_program_id matches the expected program ID.
// This ensures the proof commits to which program produced the output.
assert_eq!(
program_output.self_program_id, chained_call.program_id,
"Program output self_program_id does not match chained call program_id"
);
// Verify that the program output's caller_program_id matches the actual caller.
// This prevents a malicious user from privately executing an internal function
// by spoofing caller_program_id (e.g. passing caller_program_id = self_program_id
// to bypass access control checks).
assert_eq!(
program_output.caller_program_id, caller_program_id,
"Program output caller_program_id does not match actual caller"
);
// Check that the program is well behaved.
// See the # Programs section for the definition of the `validate_execution` method.
let validated_execution = validate_execution(
&program_output.pre_states,
&program_output.post_states,
chained_call.program_id,
);
if let Err(err) = validated_execution {
panic!(
"Invalid program behavior in program {:?}: {err}",
chained_call.program_id
);
}
verify_program_output(&chained_call, caller_program_id, &program_output);
for next_call in program_output.chained_calls.iter().rev() {
chained_calls.push_front((next_call.clone(), Some(chained_call.program_id)));
@ -202,41 +123,8 @@ impl ExecutionState {
"Inner call without a chained call found",
);
// Every private-PDA pre_state must have had its npk bound to its account_id, either via
// a `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds`
// matching the private derivation. An unbound private-PDA pre_state has no
// cryptographic link between the supplied npk and the account_id, and must be rejected.
for (pos, account_identity) in account_identities.iter().enumerate() {
if account_identity.is_private_pda() {
assert!(
execution_state
.private_pda_bound_positions
.contains_key(&pos),
"private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds"
);
}
}
// Check that all modified uninitialized accounts were claimed
for (account_id, post) in execution_state
.pre_states
.iter()
.filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID)
.map(|a| {
let post = execution_state
.post_states
.get(&a.account_id)
.expect("Post state must exist for pre state");
(a, post)
})
.filter(|(pre_default, post)| pre_default.account != **post)
.map(|(pre, post)| (pre.account_id, post))
{
assert_ne!(
post.program_owner, DEFAULT_PROGRAM_ID,
"Account {account_id} was modified but not claimed"
);
}
execution_state.assert_all_pda_positions_bound(account_identities);
execution_state.assert_modified_accounts_claimed();
execution_state
}
@ -254,200 +142,185 @@ impl ExecutionState {
for (pre, mut post) in output_pre_states.into_iter().zip(output_post_states) {
let pre_account_id = pre.account_id;
let pre_is_authorized = pre.is_authorized;
let post_states_entry = self.post_states.entry(pre.account_id);
match &post_states_entry {
Entry::Occupied(occupied) => {
#[expect(
clippy::shadow_unrelated,
reason = "Shadowing is intentional to use all fields"
)]
let AccountWithMetadata {
account: pre_account,
account_id: pre_account_id,
is_authorized: pre_is_authorized,
} = pre;
// Ensure that new pre state is the same as known post state
assert_eq!(
occupied.get(),
&pre_account,
"Inconsistent pre state for account {pre_account_id}",
if let Some(existing) = self.post_states.get(&pre.account_id) {
#[expect(
clippy::shadow_unrelated,
reason = "Shadowing is intentional to use all fields"
)]
let AccountWithMetadata {
account: pre_account,
account_id: pre_account_id,
is_authorized: pre_is_authorized,
} = pre;
assert_eq!(
existing, &pre_account,
"Inconsistent pre state for account {pre_account_id}",
);
let (previous_is_authorized, pre_state_position) = self
.pre_states
.iter()
.enumerate()
.find(|(_, acc)| acc.account_id == pre_account_id)
.map_or_else(
|| panic!(
"Pre state must exist in execution state for account {pre_account_id}",
),
|(pos, acc)| (acc.is_authorized, pos),
);
let (previous_is_authorized, pre_state_position) = self
.pre_states
.iter()
.enumerate()
.find(|(_, acc)| acc.account_id == pre_account_id)
.map_or_else(
|| panic!(
"Pre state must exist in execution state for account {pre_account_id}",
),
|(pos, acc)| (acc.is_authorized, pos)
);
let is_authorized = resolve_authorization_and_record_bindings(
&mut self.pda_family_binding,
&mut self.private_pda_bound_positions,
&self.private_pda_npk_by_position,
&mut self.authorized_accounts,
pre_account_id,
pre_state_position,
caller_program_id,
caller_pda_seeds,
previous_is_authorized,
);
let is_authorized = resolve_authorization_and_record_bindings(
&mut self.pda_family_binding,
&mut self.private_pda_bound_positions,
&self.private_pda_npk_by_position,
&mut self.authorized_accounts,
pre_account_id,
pre_state_position,
caller_program_id,
caller_pda_seeds,
previous_is_authorized,
);
assert_eq!(
pre_is_authorized, is_authorized,
"Inconsistent authorization for account {pre_account_id}",
);
}
Entry::Vacant(_) => {
// Pre state for the initial call
let pre_state_position = self.pre_states.len();
let external_seed = match account_identities.get(pre_state_position) {
Some(InputAccountIdentity::PrivatePdaInit {
npk,
identifier,
seed: Some((seed, authority_program_id)),
..
}) => {
let expected = AccountId::for_private_pda(
authority_program_id,
seed,
npk,
*identifier,
);
assert_eq!(
pre_account_id, expected,
"External seed mismatch for PrivatePdaInit at position {pre_state_position}"
);
Some((*seed, *authority_program_id))
}
Some(InputAccountIdentity::PrivatePdaUpdate {
nsk,
identifier,
seed: Some((seed, authority_program_id)),
..
}) => {
let npk = NullifierPublicKey::from(nsk);
let expected = AccountId::for_private_pda(
authority_program_id,
seed,
&npk,
*identifier,
);
assert_eq!(
pre_account_id, expected,
"External seed mismatch for PrivatePdaUpdate at position {pre_state_position}"
);
Some((*seed, *authority_program_id))
}
_ => None,
};
// External seed is only consulted the first time the account is seen.
// Subsequent calls need no re-check because the entry is already recorded on
// private_pda_bound_positions.
if let Some((seed, authority_program_id)) = external_seed {
assert!(
!pre.is_authorized,
"Private PDA with externally-provided seed must not be authorized at position {pre_state_position}"
);
bind_private_pda_position(
&mut self.private_pda_bound_positions,
pre_state_position,
authority_program_id,
seed,
);
assert_family_binding(
&mut self.pda_family_binding,
authority_program_id,
seed,
pre_account_id,
);
}
self.pre_states.push(pre);
}
assert_eq!(
pre_is_authorized, is_authorized,
"Inconsistent authorization for account {pre_account_id}",
);
} else {
let pre_state_position = self.pre_states.len();
resolve_external_seed(
account_identities,
pre_state_position,
pre_account_id,
pre.is_authorized,
&mut self.private_pda_bound_positions,
&mut self.pda_family_binding,
);
self.pre_states.push(pre);
}
if let Some(claim) = post.required_claim() {
// The invoked program can only claim accounts with default program id.
assert_eq!(
post.account().program_owner,
DEFAULT_PROGRAM_ID,
"Cannot claim an initialized account {pre_account_id}"
self.process_claim(
account_identities,
&mut post,
pre_account_id,
pre_is_authorized,
program_id,
claim,
);
let pre_state_position = self
.pre_states
.iter()
.position(|acc| acc.account_id == pre_account_id)
.expect("Pre state must exist at this point");
let account_identity = &account_identities[pre_state_position];
if account_identity.is_public() {
match claim {
Claim::Authorized => {
// Note: no need to check authorized pdas because we have already
// checked consistency of authorization above.
assert!(
pre_is_authorized,
"Cannot claim unauthorized account {pre_account_id}"
);
}
Claim::Pda(seed) => {
let pda = AccountId::for_public_pda(&program_id, &seed);
assert_eq!(
pre_account_id, pda,
"Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}"
);
assert_family_binding(
&mut self.pda_family_binding,
program_id,
seed,
pre_account_id,
);
}
}
} else {
// Private accounts: don't enforce the claim semantics. Unauthorized private
// claiming is intentionally allowed
match claim {
Claim::Authorized => {}
Claim::Pda(seed) => {
let (npk, identifier) = self
.private_pda_npk_by_position
.get(&pre_state_position)
.expect(
"private PDA pre_state must have an npk in the position map",
);
let pda =
AccountId::for_private_pda(&program_id, &seed, npk, *identifier);
assert_eq!(
pre_account_id, pda,
"Invalid private PDA claim for account {pre_account_id}"
);
bind_private_pda_position(
&mut self.private_pda_bound_positions,
pre_state_position,
program_id,
seed,
);
assert_family_binding(
&mut self.pda_family_binding,
program_id,
seed,
pre_account_id,
);
}
}
}
post.account_mut().program_owner = program_id;
}
post_states_entry.insert_entry(post.into_account());
self.post_states.insert(pre_account_id, post.into_account());
}
}
fn process_claim(
&mut self,
account_identities: &[InputAccountIdentity],
post: &mut AccountPostState,
pre_account_id: AccountId,
pre_is_authorized: bool,
program_id: ProgramId,
claim: Claim,
) {
assert_eq!(
post.account().program_owner,
DEFAULT_PROGRAM_ID,
"Cannot claim an initialized account {pre_account_id}"
);
let pre_state_position = self
.pre_states
.iter()
.position(|acc| acc.account_id == pre_account_id)
.expect("Pre state must exist at this point");
let account_identity = &account_identities[pre_state_position];
if account_identity.is_public() {
match claim {
Claim::Authorized => {
assert!(
pre_is_authorized,
"Cannot claim unauthorized account {pre_account_id}"
);
}
Claim::Pda(seed) => {
let pda = AccountId::for_public_pda(&program_id, &seed);
assert_eq!(
pre_account_id, pda,
"Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}"
);
assert_family_binding(
&mut self.pda_family_binding,
program_id,
seed,
pre_account_id,
);
}
}
} else {
match claim {
Claim::Authorized => {}
Claim::Pda(seed) => {
let (npk, identifier) = self
.private_pda_npk_by_position
.get(&pre_state_position)
.expect("private PDA pre_state must have an npk in the position map");
let pda = AccountId::for_private_pda(&program_id, &seed, npk, *identifier);
assert_eq!(
pre_account_id, pda,
"Invalid private PDA claim for account {pre_account_id}"
);
bind_private_pda_position(
&mut self.private_pda_bound_positions,
pre_state_position,
program_id,
seed,
);
assert_family_binding(
&mut self.pda_family_binding,
program_id,
seed,
pre_account_id,
);
}
}
}
post.account_mut().program_owner = program_id;
}
fn assert_all_pda_positions_bound(&self, account_identities: &[InputAccountIdentity]) {
for (pos, account_identity) in account_identities.iter().enumerate() {
if account_identity.is_private_pda() {
assert!(
self.private_pda_bound_positions.contains_key(&pos),
"private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds"
);
}
}
}
fn assert_modified_accounts_claimed(&self) {
for (account_id, post) in self
.pre_states
.iter()
.filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID)
.map(|a| {
let post = self
.post_states
.get(&a.account_id)
.expect("Post state must exist for pre state");
(a, post)
})
.filter(|(pre_default, post)| pre_default.account != **post)
.map(|(pre, post)| (pre.account_id, post))
{
assert_ne!(
post.program_owner, DEFAULT_PROGRAM_ID,
"Account {account_id} was modified but not claimed"
);
}
}
@ -486,6 +359,150 @@ impl ExecutionState {
}
}
fn verify_program_output(
chained_call: &ChainedCall,
caller_program_id: Option<ProgramId>,
program_output: &ProgramOutput,
) {
assert_eq!(
chained_call.instruction_data, program_output.instruction_data,
"Mismatched instruction data between chained call and program output"
);
let program_output_words =
&to_vec(program_output).expect("program_output must be serializable");
env::verify(chained_call.program_id, program_output_words)
.unwrap_or_else(|_: Infallible| unreachable!("Infallible error is never constructed"));
assert_eq!(
program_output.self_program_id, chained_call.program_id,
"Program output self_program_id does not match chained call program_id"
);
assert_eq!(
program_output.caller_program_id, caller_program_id,
"Program output caller_program_id does not match actual caller"
);
if let Err(err) = validate_execution(
&program_output.pre_states,
&program_output.post_states,
chained_call.program_id,
) {
panic!(
"Invalid program behavior in program {:?}: {err}",
chained_call.program_id
);
}
}
fn build_private_pda_npk_map(
account_identities: &[InputAccountIdentity],
) -> HashMap<usize, (NullifierPublicKey, Identifier)> {
account_identities
.iter()
.enumerate()
.filter_map(|(pos, identity)| {
identity
.npk_if_private_pda()
.map(|(npk, identifier)| (pos, (npk, identifier)))
})
.collect()
}
fn intersect_validity_windows(
program_outputs: &[ProgramOutput],
) -> (BlockValidityWindow, TimestampValidityWindow) {
let block_valid_from = program_outputs
.iter()
.filter_map(|output| output.block_validity_window.start())
.max();
let block_valid_until = program_outputs
.iter()
.filter_map(|output| output.block_validity_window.end())
.min();
let ts_valid_from = program_outputs
.iter()
.filter_map(|output| output.timestamp_validity_window.start())
.max();
let ts_valid_until = program_outputs
.iter()
.filter_map(|output| output.timestamp_validity_window.end())
.min();
let block_validity_window: BlockValidityWindow =
(block_valid_from, block_valid_until).try_into().expect(
"There should be non empty intersection in the program output block validity windows",
);
let timestamp_validity_window: TimestampValidityWindow = (ts_valid_from, ts_valid_until)
.try_into()
.expect(
"There should be non empty intersection in the program output timestamp validity windows",
);
(block_validity_window, timestamp_validity_window)
}
fn resolve_external_seed(
account_identities: &[InputAccountIdentity],
pre_state_position: usize,
pre_account_id: AccountId,
is_authorized: bool,
private_pda_bound_positions: &mut HashMap<usize, (ProgramId, PdaSeed)>,
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
) {
let external_seed = match account_identities.get(pre_state_position) {
Some(InputAccountIdentity::PrivatePdaInit {
npk,
identifier,
seed: Some((seed, authority_program_id)),
..
}) => {
let expected = AccountId::for_private_pda(authority_program_id, seed, npk, *identifier);
assert_eq!(
pre_account_id, expected,
"External seed mismatch for PrivatePdaInit at position {pre_state_position}"
);
Some((*seed, *authority_program_id))
}
Some(InputAccountIdentity::PrivatePdaUpdate {
nsk,
identifier,
seed: Some((seed, authority_program_id)),
..
}) => {
let npk = NullifierPublicKey::from(nsk);
let expected =
AccountId::for_private_pda(authority_program_id, seed, &npk, *identifier);
assert_eq!(
pre_account_id, expected,
"External seed mismatch for PrivatePdaUpdate at position {pre_state_position}"
);
Some((*seed, *authority_program_id))
}
_ => None,
};
if let Some((seed, authority_program_id)) = external_seed {
assert!(
!is_authorized,
"Private PDA with externally-provided seed must not be authorized at position {pre_state_position}"
);
bind_private_pda_position(
private_pda_bound_positions,
pre_state_position,
authority_program_id,
seed,
);
assert_family_binding(
pda_family_binding,
authority_program_id,
seed,
pre_account_id,
);
}
}
/// Record or re-verify the `(program_id, seed) → account_id` family binding for the
/// transaction. Any claim or caller-seed authorization that resolves a `pre_state` under
/// `(program_id, seed)` must agree with every prior resolution of the same pair; otherwise a

View File

@ -1,13 +1,244 @@
use lee_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
EphemeralPublicKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
Commitment, CommitmentSetDigest, EncryptedAccountData, EncryptionScheme, EphemeralPublicKey,
InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
account::{Account, AccountId, Nonce},
compute_digest_for_path,
};
use crate::execution_state::ExecutionState;
struct PrivateOutputHandler<'ctx> {
output: &'ctx mut PrivacyPreservingCircuitOutput,
output_index: &'ctx mut u32,
pre_state: &'ctx lee_core::account::AccountWithMetadata,
post_state: Account,
epk: &'ctx EphemeralPublicKey,
view_tag: u8,
ssk: &'ctx SharedSecretKey,
identifier: u128,
}
impl PrivateOutputHandler<'_> {
fn authorized_init(self, nsk: &NullifierSecretKey, commitment_root: &CommitmentSetDigest) {
let npk = NullifierPublicKey::from(nsk);
let account_id =
derive_and_verify_account_id(&npk, self.identifier, self.pre_state.account_id);
assert!(
self.pre_state.is_authorized,
"Pre-state not authorized for authenticated private account"
);
assert_eq!(
self.pre_state.account,
Account::default(),
"Found new private account with non default values"
);
let (new_nullifier, new_nonce) = init_nullifier_and_nonce(&account_id, commitment_root);
let kind = PrivateAccountKind::Regular(self.identifier);
self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce);
}
fn authorized_update(self, nsk: &NullifierSecretKey, membership_proof: &MembershipProof) {
let npk = NullifierPublicKey::from(nsk);
let account_id =
derive_and_verify_account_id(&npk, self.identifier, self.pre_state.account_id);
assert!(
self.pre_state.is_authorized,
"Pre-state not authorized for authenticated private account"
);
let new_nullifier = compute_update_nullifier_and_set_digest(
membership_proof,
&self.pre_state.account,
&account_id,
nsk,
);
let new_nonce = self
.pre_state
.account
.nonce
.private_account_nonce_increment(nsk);
let kind = PrivateAccountKind::Regular(self.identifier);
self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce);
}
fn unauthorized(self, npk: &NullifierPublicKey, commitment_root: &CommitmentSetDigest) {
let account_id =
derive_and_verify_account_id(npk, self.identifier, self.pre_state.account_id);
assert_eq!(
self.pre_state.account,
Account::default(),
"Found new private account with non default values",
);
assert!(
!self.pre_state.is_authorized,
"Found new private account marked as authorized."
);
let (new_nullifier, new_nonce) = init_nullifier_and_nonce(&account_id, commitment_root);
let kind = PrivateAccountKind::Regular(self.identifier);
self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce);
}
fn pda_init(
self,
commitment_root: &CommitmentSetDigest,
pos: usize,
pda_seed_by_position: &std::collections::HashMap<
usize,
(lee_core::program::ProgramId, lee_core::program::PdaSeed),
>,
) {
// The npk-to-account_id binding is established upstream in
// `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds`
// match. Here we only enforce the init pre-conditions.
assert!(
!self.pre_state.is_authorized,
"PrivatePdaInit requires unauthorized pre_state"
);
assert_eq!(
self.pre_state.account,
Account::default(),
"New private PDA must be default"
);
let (new_nullifier, new_nonce) =
init_nullifier_and_nonce(&self.pre_state.account_id, commitment_root);
let account_id = self.pre_state.account_id;
let (authority_program_id, seed) = pda_seed_by_position
.get(&pos)
.expect("PrivatePdaInit position must be in pda_seed_by_position");
let kind = PrivateAccountKind::Pda {
program_id: *authority_program_id,
seed: *seed,
identifier: self.identifier,
};
self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce);
}
fn pda_update(
self,
nsk: &NullifierSecretKey,
membership_proof: &MembershipProof,
external_seed: Option<&(lee_core::program::PdaSeed, lee_core::program::ProgramId)>,
pos: usize,
pda_seed_by_position: &std::collections::HashMap<
usize,
(lee_core::program::ProgramId, lee_core::program::PdaSeed),
>,
) {
// With an external seed the binding comes from the circuit input and the
// pre_state is intentionally unauthorized; without one the binding comes from
// a Claim or caller pda_seeds, so the pre_state must already be authorized.
assert!(
self.pre_state.is_authorized ^ external_seed.is_some(),
"PrivatePdaUpdate requires authorized pre_state or external seed"
);
let new_nullifier = compute_update_nullifier_and_set_digest(
membership_proof,
&self.pre_state.account,
&self.pre_state.account_id,
nsk,
);
let new_nonce = self
.pre_state
.account
.nonce
.private_account_nonce_increment(nsk);
let account_id = self.pre_state.account_id;
let (authority_program_id, seed) = pda_seed_by_position
.get(&pos)
.expect("PrivatePdaUpdate position must be in pda_seed_by_position");
let kind = PrivateAccountKind::Pda {
program_id: *authority_program_id,
seed: *seed,
identifier: self.identifier,
};
self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce);
}
fn emit_private_output(
self,
account_id: &AccountId,
kind: &PrivateAccountKind,
new_nullifier: (Nullifier, CommitmentSetDigest),
new_nonce: Nonce,
) {
self.output.new_nullifiers.push(new_nullifier);
let mut post_with_updated_nonce = self.post_state;
post_with_updated_nonce.nonce = new_nonce;
let commitment_post = Commitment::new(account_id, &post_with_updated_nonce);
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
kind,
self.ssk,
&commitment_post,
*self.output_index,
);
self.output.new_commitments.push(commitment_post);
self.output
.encrypted_private_post_states
.push(EncryptedAccountData {
ciphertext: encrypted_account,
epk: self.epk.clone(),
view_tag: self.view_tag,
});
*self.output_index = self
.output_index
.checked_add(1)
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
}
}
fn init_nullifier_and_nonce(
account_id: &AccountId,
commitment_root: &CommitmentSetDigest,
) -> ((Nullifier, CommitmentSetDigest), Nonce) {
let nullifier = (
Nullifier::for_account_initialization(account_id),
*commitment_root,
);
let nonce = Nonce::private_account_nonce_init(account_id);
(nullifier, nonce)
}
fn derive_and_verify_account_id(
npk: &NullifierPublicKey,
identifier: u128,
pre_state_account_id: AccountId,
) -> AccountId {
let account_id = AccountId::for_regular_private_account(npk, identifier);
assert_eq!(account_id, pre_state_account_id, "AccountId mismatch");
account_id
}
fn compute_update_nullifier_and_set_digest(
membership_proof: &MembershipProof,
pre_account: &Account,
account_id: &AccountId,
nsk: &NullifierSecretKey,
) -> (Nullifier, CommitmentSetDigest) {
let commitment_pre = Commitment::new(account_id, pre_account);
let set_digest = compute_digest_for_path(&commitment_pre, membership_proof);
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk);
(nullifier, set_digest)
}
pub fn compute_circuit_output(
execution_state: ExecutionState,
account_identities: &[InputAccountIdentity],
@ -45,40 +276,18 @@ pub fn compute_circuit_output(
ssk,
nsk,
identifier,
} => {
let npk = NullifierPublicKey::from(nsk);
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
assert!(
pre_state.is_authorized,
"Pre-state not authorized for authenticated private account"
);
assert_eq!(
pre_state.account,
Account::default(),
"Found new private account with non default values"
);
let new_nullifier = (
Nullifier::for_account_initialization(&account_id),
DUMMY_COMMITMENT_HASH,
);
let new_nonce = Nonce::private_account_nonce_init(&account_id);
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Regular(*identifier),
ssk,
epk,
*view_tag,
new_nullifier,
new_nonce,
);
commitment_root,
} => PrivateOutputHandler {
output: &mut output,
output_index: &mut output_index,
pre_state: &pre_state,
post_state,
epk,
view_tag: *view_tag,
ssk,
identifier: *identifier,
}
.authorized_init(nsk, commitment_root),
InputAccountIdentity::PrivateAuthorizedUpdate {
epk,
view_tag,
@ -86,127 +295,54 @@ pub fn compute_circuit_output(
nsk,
membership_proof,
identifier,
} => {
let npk = NullifierPublicKey::from(nsk);
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
assert!(
pre_state.is_authorized,
"Pre-state not authorized for authenticated private account"
);
let new_nullifier = compute_update_nullifier_and_set_digest(
membership_proof,
&pre_state.account,
&account_id,
nsk,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Regular(*identifier),
ssk,
epk,
*view_tag,
new_nullifier,
new_nonce,
);
} => PrivateOutputHandler {
output: &mut output,
output_index: &mut output_index,
pre_state: &pre_state,
post_state,
epk,
view_tag: *view_tag,
ssk,
identifier: *identifier,
}
.authorized_update(nsk, membership_proof),
InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag,
npk,
ssk,
identifier,
} => {
let account_id = AccountId::for_regular_private_account(npk, *identifier);
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
assert_eq!(
pre_state.account,
Account::default(),
"Found new private account with non default values",
);
assert!(
!pre_state.is_authorized,
"Found new private account marked as authorized."
);
let new_nullifier = (
Nullifier::for_account_initialization(&account_id),
DUMMY_COMMITMENT_HASH,
);
let new_nonce = Nonce::private_account_nonce_init(&account_id);
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Regular(*identifier),
ssk,
epk,
*view_tag,
new_nullifier,
new_nonce,
);
commitment_root,
} => PrivateOutputHandler {
output: &mut output,
output_index: &mut output_index,
pre_state: &pre_state,
post_state,
epk,
view_tag: *view_tag,
ssk,
identifier: *identifier,
}
.unauthorized(npk, commitment_root),
InputAccountIdentity::PrivatePdaInit {
epk,
view_tag,
npk: _,
ssk,
identifier,
commitment_root,
seed: _,
} => {
// The npk-to-account_id binding is established upstream in
// `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds`
// match. Here we only enforce the init pre-conditions. The supplied npk on
// the variant has been recorded into `private_pda_npk_by_position` and used
// for the binding check; we use `pre_state.account_id` directly for nullifier
// and commitment derivation.
assert!(
!pre_state.is_authorized,
"PrivatePdaInit requires unauthorized pre_state"
);
assert_eq!(
pre_state.account,
Account::default(),
"New private PDA must be default"
);
let new_nullifier = (
Nullifier::for_account_initialization(&pre_state.account_id),
DUMMY_COMMITMENT_HASH,
);
let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
let account_id = pre_state.account_id;
let (authority_program_id, seed) = pda_seed_by_position
.get(&pos)
.expect("PrivatePdaInit position must be in pda_seed_by_position");
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Pda {
program_id: *authority_program_id,
seed: *seed,
identifier: *identifier,
},
ssk,
epk,
*view_tag,
new_nullifier,
new_nonce,
);
} => PrivateOutputHandler {
output: &mut output,
output_index: &mut output_index,
pre_state: &pre_state,
post_state,
epk,
view_tag: *view_tag,
ssk,
identifier: *identifier,
}
.pda_init(commitment_root, pos, &pda_seed_by_position),
InputAccountIdentity::PrivatePdaUpdate {
epk,
view_tag,
@ -215,103 +351,25 @@ pub fn compute_circuit_output(
membership_proof,
identifier,
seed: external_seed,
} => {
// With an external seed the binding comes from the circuit input and the
// pre_state is intentionally unauthorized; without one the binding comes from
// a Claim or caller pda_seeds, so the pre_state must already be authorized.
// When `external_seed` is `Some`, execution_state already asserted
// `!pre_state.is_authorized`.
assert!(
pre_state.is_authorized ^ external_seed.is_some(),
"PrivatePdaUpdate requires authorized pre_state or external seed"
);
let new_nullifier = compute_update_nullifier_and_set_digest(
membership_proof,
&pre_state.account,
&pre_state.account_id,
nsk,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
let account_id = pre_state.account_id;
let (authority_program_id, seed) = pda_seed_by_position
.get(&pos)
.expect("PrivatePdaUpdate position must be in pda_seed_by_position");
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Pda {
program_id: *authority_program_id,
seed: *seed,
identifier: *identifier,
},
ssk,
epk,
*view_tag,
new_nullifier,
new_nonce,
);
} => PrivateOutputHandler {
output: &mut output,
output_index: &mut output_index,
pre_state: &pre_state,
post_state,
epk,
view_tag: *view_tag,
ssk,
identifier: *identifier,
}
.pda_update(
nsk,
membership_proof,
external_seed.as_ref(),
pos,
&pda_seed_by_position,
),
}
}
output
}
#[expect(
clippy::too_many_arguments,
reason = "Inputs are distinct concerns from the variant arms; bundling would be artificial"
)]
fn emit_private_output(
output: &mut PrivacyPreservingCircuitOutput,
output_index: &mut u32,
post_state: Account,
account_id: &AccountId,
kind: &PrivateAccountKind,
shared_secret: &SharedSecretKey,
epk: &EphemeralPublicKey,
view_tag: u8,
new_nullifier: (Nullifier, CommitmentSetDigest),
new_nonce: Nonce,
) {
output.new_nullifiers.push(new_nullifier);
let mut post_with_updated_nonce = post_state;
post_with_updated_nonce.nonce = new_nonce;
let commitment_post = Commitment::new(account_id, &post_with_updated_nonce);
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
kind,
shared_secret,
&commitment_post,
*output_index,
);
output.new_commitments.push(commitment_post);
output
.encrypted_private_post_states
.push(EncryptedAccountData {
ciphertext: encrypted_account,
epk: epk.clone(),
view_tag,
});
*output_index = output_index
.checked_add(1)
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
}
fn compute_update_nullifier_and_set_digest(
membership_proof: &MembershipProof,
pre_account: &Account,
account_id: &AccountId,
nsk: &NullifierSecretKey,
) -> (Nullifier, CommitmentSetDigest) {
let commitment_pre = Commitment::new(account_id, pre_account);
let set_digest = compute_digest_for_path(&commitment_pre, membership_proof);
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk);
(nullifier, set_digest)
}

View File

@ -38,6 +38,7 @@ pub enum InputAccountIdentity {
ssk: SharedSecretKey,
nsk: NullifierSecretKey,
identifier: Identifier,
commitment_root: CommitmentSetDigest,
},
/// Update of an authorized standalone private account: existing on-chain commitment, with
/// membership proof.
@ -57,6 +58,7 @@ pub enum InputAccountIdentity {
npk: NullifierPublicKey,
ssk: SharedSecretKey,
identifier: Identifier,
commitment_root: CommitmentSetDigest,
},
/// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream
/// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. The identifier diversifies the
@ -68,6 +70,7 @@ pub enum InputAccountIdentity {
npk: NullifierPublicKey,
ssk: SharedSecretKey,
identifier: Identifier,
commitment_root: CommitmentSetDigest,
/// When `Some((seed, authority_program_id))`, the circuit binds this position via the
/// external derivation check
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) ==

View File

@ -273,6 +273,7 @@ mod tests {
npk: recipient_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&crate::test_methods::simple_balance_transfer().into(),
@ -387,6 +388,7 @@ mod tests {
npk: recipient_keys.npk(),
ssk: shared_secret_2,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),
@ -460,6 +462,7 @@ mod tests {
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
&program_with_deps,
);
@ -491,6 +494,7 @@ mod tests {
npk,
ssk: shared_secret,
identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
}],
&program.clone().into(),
@ -540,6 +544,7 @@ mod tests {
npk,
ssk: shared_secret_pda,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
}],
&program_with_deps,
@ -595,6 +600,7 @@ mod tests {
npk,
ssk: shared_secret_pda,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
},
InputAccountIdentity::Public,
@ -653,6 +659,7 @@ mod tests {
npk: shared_npk,
ssk: shared_secret,
identifier: shared_identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),
@ -683,6 +690,7 @@ mod tests {
ssk,
nsk: keys.nsk,
identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
&program.into(),
)
@ -714,6 +722,7 @@ mod tests {
npk: keys.npk(),
ssk,
identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
&program.into(),
)
@ -848,6 +857,7 @@ mod tests {
npk,
ssk: shared_secret,
identifier: 99,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
}],
&program.into(),

View File

@ -327,8 +327,8 @@ pub mod tests {
use std::collections::HashMap;
use lee_core::{
BlockId, Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier,
NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp,
BlockId, Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity,
Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, ViewingPublicKey},
program::{
@ -1192,6 +1192,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&crate::test_methods::simple_balance_transfer().into(),
@ -1259,6 +1260,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: shared_secret_2,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),
@ -1908,6 +1910,7 @@ pub mod tests {
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
},
],
@ -1974,6 +1977,7 @@ pub mod tests {
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
},
],
@ -2040,6 +2044,7 @@ pub mod tests {
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
},
],
@ -2106,6 +2111,7 @@ pub mod tests {
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
},
],
@ -2172,6 +2178,7 @@ pub mod tests {
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
},
],
@ -2236,6 +2243,7 @@ pub mod tests {
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
},
],
@ -2279,6 +2287,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
},
],
@ -2314,6 +2323,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
}],
&program.into(),
@ -2357,6 +2367,7 @@ pub mod tests {
npk: npk_b,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
}],
&program.into(),
@ -2396,6 +2407,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
}],
&program_with_deps,
@ -2438,6 +2450,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
}],
&program_with_deps,
@ -2479,6 +2492,7 @@ pub mod tests {
npk: keys_a.npk(),
ssk: shared_a,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
},
InputAccountIdentity::PrivatePdaInit {
@ -2487,6 +2501,7 @@ pub mod tests {
npk: keys_b.npk(),
ssk: shared_b,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
},
],
@ -2531,6 +2546,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
}],
&program.into(),
@ -3302,6 +3318,7 @@ pub mod tests {
ssk: shared_secret,
nsk: private_keys.nsk,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
&program.into(),
)
@ -3349,6 +3366,7 @@ pub mod tests {
npk: private_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
&program.into(),
)
@ -3400,6 +3418,7 @@ pub mod tests {
ssk: shared_secret,
nsk: private_keys.nsk,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
&claimer_program.into(),
)
@ -3446,6 +3465,7 @@ pub mod tests {
ssk: shared_secret2,
nsk: private_keys.nsk,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
&noop_program.into(),
);
@ -3788,6 +3808,7 @@ pub mod tests {
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
&validity_window_program.into(),
)
@ -3856,6 +3877,7 @@ pub mod tests {
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
&validity_window_program.into(),
)
@ -4200,6 +4222,7 @@ pub mod tests {
npk: alice_npk,
ssk: alice_shared_0,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: Some((seed, proxy_id)),
},
],
@ -4240,6 +4263,7 @@ pub mod tests {
npk: alice_npk,
ssk: alice_shared_1,
identifier: 1,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: Some((seed, proxy_id)),
},
],

View File

@ -5,8 +5,8 @@ use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
use keycard_wallet::{KeycardWallet, python_path};
use lee::{AccountId, PrivateKey, PublicKey, Signature};
use lee_core::{
Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey,
SharedSecretKey,
CommitmentSetDigest, DUMMY_COMMITMENT_HASH, Identifier, InputAccountIdentity, MembershipProof,
NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
account::{AccountWithMetadata, Nonce},
encryption::{EncryptedAccountData, EphemeralPublicKey, ViewingPublicKey},
};
@ -187,6 +187,7 @@ enum State {
pub struct AccountManager {
states: Vec<State>,
pin: Option<String>,
dummy_commitment_root: CommitmentSetDigest,
}
impl AccountManager {
@ -340,7 +341,24 @@ impl AccountManager {
states.push(state);
}
Ok(Self { states, pin })
let has_init_account = states
.iter()
.any(|s| matches!(s, State::Private(pre) if pre.proof.is_none()));
let dummy_commitment_root = if has_init_account {
wallet
.get_commitment_root()
.await
.map_err(ExecutionFailureKind::SequencerError)?
.unwrap_or(DUMMY_COMMITMENT_HASH)
} else {
DUMMY_COMMITMENT_HASH
};
Ok(Self {
states,
pin,
dummy_commitment_root,
})
}
pub fn pre_states(&self) -> Vec<AccountWithMetadata> {
@ -404,6 +422,7 @@ impl AccountManager {
npk: pre.npk,
ssk: pre.ssk,
identifier: pre.identifier,
commitment_root: self.dummy_commitment_root,
seed: None,
},
},
@ -424,6 +443,7 @@ impl AccountManager {
ssk: pre.ssk,
nsk,
identifier: pre.identifier,
commitment_root: self.dummy_commitment_root,
},
(None, _) => InputAccountIdentity::PrivateUnauthorized {
epk: pre.epk.clone(),
@ -431,6 +451,7 @@ impl AccountManager {
npk: pre.npk,
ssk: pre.ssk,
identifier: pre.identifier,
commitment_root: self.dummy_commitment_root,
},
},
})

View File

@ -22,7 +22,8 @@ use lee::{
},
};
use lee_core::{
Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData,
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, SharedSecretKey,
account::Nonce, compute_digest_for_path, program::InstructionData,
};
use log::info;
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
@ -508,6 +509,14 @@ impl WalletCore {
}
}
pub async fn get_commitment_root(&self) -> Result<Option<CommitmentSetDigest>> {
let proof = self
.sequencer_client
.get_proof_for_commitment(DUMMY_COMMITMENT)
.await?;
Ok(proof.map(|p| compute_digest_for_path(&DUMMY_COMMITMENT, &p)))
}
pub fn decode_insert_privacy_preserving_transaction_results(
&mut self,
tx: &lee::privacy_preserving_transaction::PrivacyPreservingTransaction,