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.
This commit is contained in:
Marvin Jones 2026-06-24 13:47:17 -04:00
parent b7e715fa0d
commit f9d80cd67a
8 changed files with 68 additions and 66 deletions

View File

@ -712,7 +712,7 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
npk,
ssk,
identifier: 1337,
membership_proof: None,
commitment_root: None,
seed: None,
},
],
@ -724,9 +724,9 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
Ok(())
}
async fn prove_init_with_membership_proof(
async fn prove_init_with_commitment_root(
ctx: &TestContext,
membership_proof: Option<lee_core::MembershipProof>,
commitment_root: Option<lee_core::CommitmentSetDigest>,
) -> Result<lee_core::PrivacyPreservingCircuitOutput> {
let program = Program::authenticated_transfer_program();
let sender_id = ctx.existing_public_accounts()[0];
@ -756,7 +756,7 @@ async fn prove_init_with_membership_proof(
npk,
ssk,
identifier: 0,
membership_proof,
commitment_root,
},
],
&program.into(),
@ -766,7 +766,7 @@ async fn prove_init_with_membership_proof(
}
#[test]
async fn init_with_dummy_commitment_membership_proof_produces_valid_root() -> Result<()> {
async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> {
let ctx = TestContext::new().await?;
let dummy_proof = ctx
@ -780,7 +780,7 @@ async fn init_with_dummy_commitment_membership_proof_produces_valid_root() -> Re
let npk = NullifierPublicKey::from(&nsk);
let recipient_account_id = AccountId::for_regular_private_account(&npk, 0);
let output = prove_init_with_membership_proof(&ctx, Some(dummy_proof)).await?;
let output = prove_init_with_commitment_root(&ctx, Some(expected_digest)).await?;
assert_eq!(output.new_nullifiers.len(), 1);
let (nullifier, digest) = &output.new_nullifiers[0];
@ -795,7 +795,7 @@ async fn init_with_dummy_commitment_membership_proof_produces_valid_root() -> Re
}
#[test]
async fn init_nullifier_digest_is_bound_to_membership_proof() -> Result<()> {
async fn init_nullifier_digest_is_bound_to_commitment_root() -> Result<()> {
let ctx = TestContext::new().await?;
let dummy_proof = ctx
@ -805,17 +805,17 @@ async fn init_nullifier_digest_is_bound_to_membership_proof() -> Result<()> {
.expect("DUMMY_COMMITMENT must be in genesis commitment set");
let expected_digest = compute_digest_for_path(&DUMMY_COMMITMENT, &dummy_proof);
let output_with_proof = prove_init_with_membership_proof(&ctx, Some(dummy_proof)).await?;
let output_without_proof = prove_init_with_membership_proof(&ctx, None).await?;
let output_with_root = prove_init_with_commitment_root(&ctx, Some(expected_digest)).await?;
let output_without_root = prove_init_with_commitment_root(&ctx, None).await?;
assert_eq!(output_with_proof.new_nullifiers[0].1, expected_digest);
assert_eq!(output_with_root.new_nullifiers[0].1, expected_digest);
assert_eq!(
output_without_proof.new_nullifiers[0].1,
output_without_root.new_nullifiers[0].1,
DUMMY_COMMITMENT_HASH
);
assert_ne!(
output_with_proof.new_nullifiers[0].1,
output_without_proof.new_nullifiers[0].1,
output_with_root.new_nullifiers[0].1,
output_without_root.new_nullifiers[0].1,
);
Ok(())

View File

@ -78,7 +78,7 @@ async fn fund_private_pda(
npk,
ssk,
identifier,
membership_proof: None,
commitment_root: None,
seed: Some((seed, authority_program_id)),
},
];

View File

@ -314,7 +314,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
npk: recipient_npk,
ssk: recipient_ss,
identifier: 0,
membership_proof: None,
commitment_root: None,
},
],
&program.into(),

View File

@ -38,7 +38,7 @@ pub enum InputAccountIdentity {
ssk: SharedSecretKey,
nsk: NullifierSecretKey,
identifier: Identifier,
membership_proof: Option<MembershipProof>,
commitment_root: Option<CommitmentSetDigest>,
},
/// Update of an authorized standalone private account: existing on-chain commitment, with
/// membership proof.
@ -58,7 +58,7 @@ pub enum InputAccountIdentity {
npk: NullifierPublicKey,
ssk: SharedSecretKey,
identifier: Identifier,
membership_proof: Option<MembershipProof>,
commitment_root: Option<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
@ -70,7 +70,7 @@ pub enum InputAccountIdentity {
npk: NullifierPublicKey,
ssk: SharedSecretKey,
identifier: Identifier,
membership_proof: Option<MembershipProof>,
commitment_root: Option<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,7 +273,7 @@ mod tests {
npk: recipient_keys.npk(),
ssk: shared_secret,
identifier: 0,
membership_proof: None,
commitment_root: None,
},
],
&crate::test_methods::simple_balance_transfer().into(),
@ -388,7 +388,7 @@ mod tests {
npk: recipient_keys.npk(),
ssk: shared_secret_2,
identifier: 0,
membership_proof: None,
commitment_root: None,
},
],
&program.into(),
@ -462,7 +462,7 @@ mod tests {
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
membership_proof: None,
commitment_root: None,
}],
&program_with_deps,
);
@ -494,7 +494,7 @@ mod tests {
npk,
ssk: shared_secret,
identifier,
membership_proof: None,
commitment_root: None,
seed: None,
}],
&program.clone().into(),
@ -544,7 +544,7 @@ mod tests {
npk,
ssk: shared_secret_pda,
identifier: 0,
membership_proof: None,
commitment_root: None,
seed: None,
}],
&program_with_deps,
@ -600,7 +600,7 @@ mod tests {
npk,
ssk: shared_secret_pda,
identifier: 0,
membership_proof: None,
commitment_root: None,
seed: None,
},
InputAccountIdentity::Public,
@ -659,7 +659,7 @@ mod tests {
npk: shared_npk,
ssk: shared_secret,
identifier: shared_identifier,
membership_proof: None,
commitment_root: None,
},
],
&program.into(),
@ -690,7 +690,7 @@ mod tests {
ssk,
nsk: keys.nsk,
identifier,
membership_proof: None,
commitment_root: None,
}],
&program.into(),
)
@ -737,7 +737,7 @@ mod tests {
npk: keys.npk(),
ssk,
identifier,
membership_proof: None,
commitment_root: None,
},
],
&program.into(),
@ -873,7 +873,7 @@ mod tests {
npk,
ssk: shared_secret,
identifier: 99,
membership_proof: None,
commitment_root: None,
seed: None,
}],
&program.into(),

View File

@ -1192,7 +1192,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: shared_secret,
identifier: 0,
membership_proof: None,
commitment_root: None,
},
],
&crate::test_methods::simple_balance_transfer().into(),
@ -1260,7 +1260,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: shared_secret_2,
identifier: 0,
membership_proof: None,
commitment_root: None,
},
],
&program.into(),
@ -1910,7 +1910,7 @@ pub mod tests {
0,
)
.0,
membership_proof: None,
commitment_root: None,
identifier: 0,
},
],
@ -1977,7 +1977,7 @@ pub mod tests {
0,
)
.0,
membership_proof: None,
commitment_root: None,
identifier: 0,
},
],
@ -2044,7 +2044,7 @@ pub mod tests {
0,
)
.0,
membership_proof: None,
commitment_root: None,
identifier: 0,
},
],
@ -2111,7 +2111,7 @@ pub mod tests {
0,
)
.0,
membership_proof: None,
commitment_root: None,
identifier: 0,
},
],
@ -2178,7 +2178,7 @@ pub mod tests {
0,
)
.0,
membership_proof: None,
commitment_root: None,
identifier: 0,
},
],
@ -2243,7 +2243,7 @@ pub mod tests {
0,
)
.0,
membership_proof: None,
commitment_root: None,
identifier: 0,
},
],
@ -2287,7 +2287,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
membership_proof: None,
commitment_root: None,
seed: None,
},
],
@ -2323,7 +2323,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
membership_proof: None,
commitment_root: None,
seed: None,
}],
&program.into(),
@ -2367,7 +2367,7 @@ pub mod tests {
npk: npk_b,
ssk: shared_secret,
identifier: u128::MAX,
membership_proof: None,
commitment_root: None,
seed: None,
}],
&program.into(),
@ -2407,7 +2407,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
membership_proof: None,
commitment_root: None,
seed: None,
}],
&program_with_deps,
@ -2450,7 +2450,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
membership_proof: None,
commitment_root: None,
seed: None,
}],
&program_with_deps,
@ -2492,7 +2492,7 @@ pub mod tests {
npk: keys_a.npk(),
ssk: shared_a,
identifier: u128::MAX,
membership_proof: None,
commitment_root: None,
seed: None,
},
InputAccountIdentity::PrivatePdaInit {
@ -2501,7 +2501,7 @@ pub mod tests {
npk: keys_b.npk(),
ssk: shared_b,
identifier: u128::MAX,
membership_proof: None,
commitment_root: None,
seed: None,
},
],
@ -2546,7 +2546,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
membership_proof: None,
commitment_root: None,
seed: None,
}],
&program.into(),
@ -3318,7 +3318,7 @@ pub mod tests {
ssk: shared_secret,
nsk: private_keys.nsk,
identifier: 0,
membership_proof: None,
commitment_root: None,
}],
&program.into(),
)
@ -3366,7 +3366,7 @@ pub mod tests {
npk: private_keys.npk(),
ssk: shared_secret,
identifier: 0,
membership_proof: None,
commitment_root: None,
}],
&program.into(),
)
@ -3418,7 +3418,7 @@ pub mod tests {
ssk: shared_secret,
nsk: private_keys.nsk,
identifier: 0,
membership_proof: None,
commitment_root: None,
}],
&claimer_program.into(),
)
@ -3465,7 +3465,7 @@ pub mod tests {
ssk: shared_secret2,
nsk: private_keys.nsk,
identifier: 0,
membership_proof: None,
commitment_root: None,
}],
&noop_program.into(),
);
@ -3808,7 +3808,7 @@ pub mod tests {
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
membership_proof: None,
commitment_root: None,
}],
&validity_window_program.into(),
)
@ -3877,7 +3877,7 @@ pub mod tests {
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
membership_proof: None,
commitment_root: None,
}],
&validity_window_program.into(),
)
@ -4222,7 +4222,7 @@ pub mod tests {
npk: alice_npk,
ssk: alice_shared_0,
identifier: 0,
membership_proof: None,
commitment_root: None,
seed: Some((seed, proxy_id)),
},
],
@ -4263,7 +4263,7 @@ pub mod tests {
npk: alice_npk,
ssk: alice_shared_1,
identifier: 1,
membership_proof: None,
commitment_root: None,
seed: Some((seed, proxy_id)),
},
],

View File

@ -5,7 +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,
CommitmentSetDigest, Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey,
NullifierSecretKey,
SharedSecretKey,
account::{AccountWithMetadata, Nonce},
encryption::{EncryptedAccountData, EphemeralPublicKey, ViewingPublicKey},
@ -187,7 +188,7 @@ enum State {
pub struct AccountManager {
states: Vec<State>,
pin: Option<String>,
dummy_commitment_proof: Option<MembershipProof>,
dummy_commitment_root: Option<CommitmentSetDigest>,
}
impl AccountManager {
@ -344,9 +345,9 @@ impl AccountManager {
let has_init_account = states
.iter()
.any(|s| matches!(s, State::Private(pre) if pre.proof.is_none()));
let dummy_commitment_proof = if has_init_account {
let dummy_commitment_root = if has_init_account {
wallet
.get_dummy_commitment_proof()
.get_commitment_root()
.await
.map_err(ExecutionFailureKind::SequencerError)?
} else {
@ -356,7 +357,7 @@ impl AccountManager {
Ok(Self {
states,
pin,
dummy_commitment_proof,
dummy_commitment_root,
})
}
@ -421,7 +422,7 @@ impl AccountManager {
npk: pre.npk,
ssk: pre.ssk,
identifier: pre.identifier,
membership_proof: self.dummy_commitment_proof.clone(),
commitment_root: self.dummy_commitment_root,
seed: None,
},
},
@ -442,7 +443,7 @@ impl AccountManager {
ssk: pre.ssk,
nsk,
identifier: pre.identifier,
membership_proof: self.dummy_commitment_proof.clone(),
commitment_root: self.dummy_commitment_root,
},
(None, _) => InputAccountIdentity::PrivateUnauthorized {
epk: pre.epk.clone(),
@ -450,7 +451,7 @@ impl AccountManager {
npk: pre.npk,
ssk: pre.ssk,
identifier: pre.identifier,
membership_proof: self.dummy_commitment_proof.clone(),
commitment_root: self.dummy_commitment_root,
},
},
})

View File

@ -22,8 +22,8 @@ use lee::{
},
};
use lee_core::{
Commitment, DUMMY_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};
@ -509,11 +509,12 @@ impl WalletCore {
}
}
pub async fn get_dummy_commitment_proof(&self) -> Result<Option<MembershipProof>> {
self.sequencer_client
pub async fn get_commitment_root(&self) -> Result<Option<CommitmentSetDigest>> {
let proof = self
.sequencer_client
.get_proof_for_commitment(DUMMY_COMMITMENT)
.await
.map_err(Into::into)
.await?;
Ok(proof.map(|p| compute_digest_for_path(&DUMMY_COMMITMENT, &p)))
}
pub fn decode_insert_privacy_preserving_transaction_results(