Merge remote-tracking branch 'origin/dev' into artem/change-wallet-balance-fetch

This commit is contained in:
agureev 2026-07-08 22:55:15 +04:00
commit bf86d0c05b
60 changed files with 1455 additions and 1509 deletions

View File

@ -60,6 +60,7 @@ allow-git = [
"https://github.com/logos-blockchain/logos-blockchain.git",
"https://github.com/logos-blockchain/logos-blockchain-circuits.git",
"https://github.com/logos-blockchain/logos-blockchain-rust-rapidsnark.git",
"https://github.com/logos-blockchain/sponges",
"https://github.com/arkworks-rs/spongefish.git",
]
unknown-git = "deny"

View File

@ -28,10 +28,6 @@ jobs:
- uses: ./.github/actions/install-risc0
- uses: ./.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install active toolchain
run: rustup install

9
Cargo.lock generated
View File

@ -1914,7 +1914,6 @@ dependencies = [
"criterion",
"key_protocol",
"lee_core",
"rand 0.8.6",
]
[[package]]
@ -3850,7 +3849,6 @@ dependencies = [
"anyhow",
"arc-swap",
"async-stream",
"authenticated_transfer_core",
"borsh",
"common",
"futures",
@ -4441,11 +4439,11 @@ dependencies = [
[[package]]
name = "keccak"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
source = "git+https://github.com/logos-blockchain/sponges?rev=3a56e99771beedf04946eab21a4a62adc2951377#3a56e99771beedf04946eab21a4a62adc2951377"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"risc0-zkvm",
]
[[package]]
@ -4553,7 +4551,6 @@ dependencies = [
"anyhow",
"borsh",
"build_utils",
"env_logger",
"hex",
"hex-literal 1.1.0",
"k256",
@ -4568,7 +4565,6 @@ dependencies = [
"test-case",
"test_methods",
"thiserror 2.0.18",
"token_core",
]
[[package]]
@ -10890,7 +10886,6 @@ dependencies = [
"risc0-zkvm",
"sequencer_service_rpc",
"serde_json",
"tempfile",
"tokio",
"vault_core",
"wallet",

View File

@ -194,6 +194,10 @@ opt-level = 'z'
lto = true
codegen-units = 1
# Keccak speedup for in-guest ML KEM
[patch.crates-io]
keccak = { git = "https://github.com/logos-blockchain/sponges", rev = "3a56e99771beedf04946eab21a4a62adc2951377" }
# Keep backtraces but drop full DWARF type info to avoid LLD OOM/SIGBUS when
# linking large integration-test binaries on resource-constrained CI runners.
[profile.dev]
@ -339,4 +343,4 @@ clippy.cargo = { level = "deny", priority = -1 }
clippy.cargo-common-metadata = "allow"
# Reason: hard to address right now and mostly comes from dependencies
# so the fix would be just a long list of exceptions.
clippy.multiple-crate-versions = "allow"
clippy.multiple-crate-versions = "allow"

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

@ -141,7 +141,11 @@ async fn import_private_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let key_chain = KeyChain::new_os_random();
let account_id = lee::AccountId::from((&key_chain.nullifier_public_key, 0));
let account_id = lee::AccountId::from((
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
0,
));
let account = lee::Account {
program_owner: programs::authenticated_transfer().id(),
balance: 777,
@ -198,7 +202,11 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<(
let mut ctx = TestContext::new().await?;
let key_chain = KeyChain::new_os_random();
let account_id = lee::AccountId::from((&key_chain.nullifier_public_key, 0));
let account_id = lee::AccountId::from((
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
0,
));
let key_chain_json =
serde_json::to_string(&key_chain).context("Failed to serialize key chain")?;

View File

@ -8,15 +8,14 @@ use integration_tests::{
private_mention, public_mention, send, sync_private, verify_commitment_is_in_state,
};
use lee::{
AccountId, SharedSecretKey, execute_and_prove,
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
AccountId, execute_and_prove, privacy_preserving_transaction::circuit::ProgramWithDependencies,
program::Program,
};
use lee_core::{
DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity,
ML_KEM_768_CIPHERTEXT_LEN, Nullifier, NullifierPublicKey,
DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, NullifierPublicKey,
account::{Account, AccountWithMetadata},
compute_digest_for_path,
encryption::{EphemeralPublicKey, ViewingPublicKey},
encryption::ViewingPublicKey,
};
use log::info;
use sequencer_service_rpc::RpcClient as _;
@ -499,14 +498,14 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
sync_private(&mut ctx).await?;
// Both accounts must be discovered with the correct balances.
let account_id_1 = AccountId::for_regular_private_account(&npk, identifier_1);
let account_id_1 = AccountId::for_regular_private_account(&npk, &vpk, identifier_1);
let acc_1 = ctx
.wallet()
.get_account_private(account_id_1)
.context("account for identifier 1 not found after sync")?;
assert_eq!(acc_1.balance, 100);
let account_id_2 = AccountId::for_regular_private_account(&npk, identifier_2);
let account_id_2 = AccountId::for_regular_private_account(&npk, &vpk, identifier_2);
let acc_2 = ctx
.wallet()
.get_account_private(account_id_2)
@ -562,11 +561,9 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
let nsk: lee_core::NullifierSecretKey = [3; 32];
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
let ssk = SharedSecretKey([55_u8; 32]);
let epk = EphemeralPublicKey(vec![55_u8; ML_KEM_768_CIPHERTEXT_LEN]);
let attacker_vault_id = {
let seed = vault_core::compute_vault_seed(attacker_id);
AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337)
AccountId::for_private_pda(&vault_program_id, &seed, &npk, &vpk, 1337)
};
let amount: u128 = 1;
@ -600,10 +597,9 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
epk,
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
vpk,
random_seed: [0; 32],
npk,
ssk,
identifier: 1337,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -632,8 +628,7 @@ async fn prove_init_with_commitment_root(
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_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
let (output, _) = execute_and_prove(
@ -644,10 +639,9 @@ async fn prove_init_with_commitment_root(
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
vpk,
random_seed: [0; 32],
npk,
ssk,
identifier: 0,
commitment_root,
},
@ -671,7 +665,8 @@ async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> {
let nsk: lee_core::NullifierSecretKey = [7; 32];
let npk = NullifierPublicKey::from(&nsk);
let recipient_account_id = AccountId::for_regular_private_account(&npk, 0);
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
let output = prove_init_with_commitment_root(&ctx, expected_digest).await?;

View File

@ -9,7 +9,7 @@ use std::time::Duration;
use anyhow::{Context as _, Result};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, new_account, private_mention,
public_mention, sync_private, verify_commitment_is_in_state,
public_mention, sync_private, verify_commitment_is_in_state, wait_for_indexer_to_catch_up,
};
use log::info;
use tokio::test;
@ -115,6 +115,41 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> {
Ok(())
}
#[test]
async fn claim_pinata_indexer_keeps_up() -> Result<()> {
let mut ctx = TestContext::new().await?;
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: public_mention(ctx.existing_public_accounts()[0]),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Waiting for indexer to parse blocks");
wait_for_indexer_to_catch_up(&ctx).await?;
let winner_ind_state = indexer_service_rpc::RpcClient::get_account(
&**ctx.indexer_client(),
ctx.existing_public_accounts()[0].into(),
)
.await
.unwrap();
let winner_seq_state = sequencer_service_rpc::RpcClient::get_account(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
)
.await?;
assert_eq!(winner_ind_state, winner_seq_state.into());
info!("Indexer correctly indexed the pinata claim");
Ok(())
}
#[test]
async fn claim_pinata_to_existing_private_account() -> Result<()> {
let mut ctx = TestContext::new().await?;

View File

@ -11,7 +11,6 @@ use common::transaction::LeeTransaction;
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, sync_private, verify_commitment_is_in_state,
};
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
use lee::{
AccountId, PrivacyPreservingTransaction, ProgramId,
privacy_preserving_transaction::{
@ -22,7 +21,7 @@ use lee::{
program::Program,
};
use lee_core::{
DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
DUMMY_COMMITMENT_HASH, InputAccountIdentity, NullifierPublicKey,
account::{Account, AccountWithMetadata},
encryption::ViewingPublicKey,
program::PdaSeed,
@ -48,7 +47,8 @@ async fn fund_private_pda(
amount: u128,
auth_transfer: &ProgramWithDependencies,
) -> Result<()> {
let pda_account_id = AccountId::for_private_pda(&authority_program_id, &seed, &npk, identifier);
let pda_account_id =
AccountId::for_private_pda(&authority_program_id, &seed, &npk, &vpk, identifier);
let sender_account = wallet
.get_account_public(sender)
.await
@ -60,20 +60,15 @@ async fn fund_private_pda(
let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender);
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_account_id);
let eph_holder = EphemeralKeyHolder::new(&vpk);
let ssk = eph_holder.calculate_shared_secret_sender();
let epk = eph_holder.ephemeral_public_key().clone();
let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
.context("failed to serialize auth_transfer instruction")?;
let account_identities = vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
epk,
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
vpk,
random_seed: [0; 32],
npk,
ssk,
identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: Some((seed, authority_program_id)),
@ -173,8 +168,8 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
let spend_program =
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0);
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1);
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 0);
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 1);
// Use two different public senders to avoid nonce conflicts between the back-to-back txs.
let senders = ctx.existing_public_accounts();

View File

@ -15,7 +15,6 @@ use anyhow::{Context as _, Result};
use bytesize::ByteSize;
use common::transaction::LeeTransaction;
use integration_tests::{TestContext, config::SequencerPartialConfig};
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
use lee::{
Account, AccountId, PrivacyPreservingTransaction, PrivateKey, PublicKey, PublicTransaction,
privacy_preserving_transaction::{self as pptx, circuit},
@ -23,8 +22,7 @@ use lee::{
public_transaction as putx,
};
use lee_core::{
DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, MembershipProof,
NullifierPublicKey,
DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof, NullifierPublicKey,
account::{AccountWithMetadata, Nonce, data::Data},
encryption::ViewingPublicKey,
};
@ -267,7 +265,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
data: Data::default(),
},
true,
AccountId::for_regular_private_account(&sender_npk, 0),
AccountId::for_regular_private_account(&sender_npk, &sender_vpk, 0),
);
let recipient_nsk = [2; 32];
let recipient_vpk = ViewingPublicKey::from_seed(&[101_u8; 32], &[102_u8; 32]);
@ -275,17 +273,9 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
let recipient_pre = AccountWithMetadata::new(
Account::default(),
false,
AccountId::for_regular_private_account(&recipient_npk, 0),
AccountId::for_regular_private_account(&recipient_npk, &recipient_vpk, 0),
);
let eph_holder_from = EphemeralKeyHolder::new(&sender_vpk);
let sender_ss = eph_holder_from.calculate_shared_secret_sender();
let sender_epk = eph_holder_from.ephemeral_public_key().clone();
let eph_holder_to = EphemeralKeyHolder::new(&recipient_vpk);
let recipient_ss = eph_holder_to.calculate_shared_secret_sender();
let recipient_epk = eph_holder_to.ephemeral_public_key().clone();
let balance_to_move: u128 = 1;
let proof: MembershipProof = (
1,
@ -302,18 +292,16 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
.unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: sender_epk,
view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk),
ssk: sender_ss,
vpk: sender_vpk,
random_seed: [0; 32],
nsk: sender_nsk,
membership_proof: proof,
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: recipient_epk,
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
vpk: recipient_vpk,
random_seed: [0; 32],
npk: recipient_npk,
ssk: recipient_ss,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},

View File

@ -1012,7 +1012,11 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
let (to, to_keys) = unsafe {
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
let account_id = lee::AccountId::for_regular_private_account(
&out_keys.npk(),
&out_keys.vpk().unwrap(),
0_u128,
);
let to: FfiBytes32 = account_id.into();
(to, out_keys)
};
@ -1155,7 +1159,11 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
let (to, to_keys) = unsafe {
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
let account_id = lee::AccountId::for_regular_private_account(
&out_keys.npk(),
&out_keys.vpk().unwrap(),
0_u128,
);
let to: FfiBytes32 = account_id.into();
(to, out_keys)
};
@ -1236,7 +1244,11 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
let (private_account_id_1, private_account_1_keys) = unsafe {
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
let account_id = lee::AccountId::for_regular_private_account(
&out_keys.npk(),
&out_keys.vpk().unwrap(),
0_u128,
);
let to: FfiBytes32 = account_id.into();
(to, out_keys)
};
@ -1244,7 +1256,11 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
let (private_account_id_2, private_account_2_keys) = unsafe {
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
let account_id = lee::AccountId::for_regular_private_account(
&out_keys.npk(),
&out_keys.vpk().unwrap(),
0_u128,
);
let to: FfiBytes32 = account_id.into();
(to, out_keys)
};

View File

@ -351,10 +351,10 @@ mod tests {
let program_id: ProgramId = [9; 8];
let holder = GroupKeyHolder::from_gms(gms);
let npk = holder
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
.generate_nullifier_public_key();
let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX);
let keys = holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed);
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX);
let expected_npk = NullifierPublicKey([
136, 176, 234, 71, 208, 8, 143, 142, 126, 155, 132, 18, 71, 27, 88, 56, 100, 90, 79,
@ -363,7 +363,7 @@ mod tests {
// AccountId is derived from (program_id, seed, npk), so it changes when npk changes.
// We verify npk is pinned, and AccountId is deterministically derived from it.
let expected_account_id =
AccountId::for_private_pda(&program_id, &seed, &expected_npk, u128::MAX);
AccountId::for_private_pda(&program_id, &seed, &expected_npk, &vpk, u128::MAX);
assert_eq!(npk, expected_npk);
assert_eq!(account_id, expected_account_id);
@ -560,13 +560,16 @@ mod tests {
let bob_holder =
GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS");
let bob_npk = bob_holder
.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed)
.generate_nullifier_public_key();
let bob_group_keys = bob_holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed);
let bob_npk = bob_group_keys.generate_nullifier_public_key();
assert_eq!(alice_npk, bob_npk);
let alice_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, 0);
let bob_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, 0);
let alice_vpk = alice_keys.generate_viewing_public_key();
let bob_group_vpk = bob_group_keys.generate_viewing_public_key();
let alice_account_id =
AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, &alice_vpk, 0);
let bob_account_id =
AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, &bob_group_vpk, 0);
assert_eq!(alice_account_id, bob_account_id);
}

View File

@ -94,10 +94,11 @@ impl KeyTreeNode for ChildKeysPrivate {
fn account_ids(&self) -> impl Iterator<Item = lee::AccountId> {
let npk = self.value.0.nullifier_public_key;
let vpk = self.value.0.viewing_public_key.clone();
self.value
.1
.keys()
.map(move |kind| lee::AccountId::for_private_account(&npk, kind))
.map(move |kind| lee::AccountId::for_private_account(&npk, &vpk, kind))
}
}

View File

@ -268,6 +268,7 @@ impl KeyTree<ChildKeysPrivate> {
let node = self.key_map.get(cci)?;
let account_id = lee::AccountId::for_regular_private_account(
&node.value.0.nullifier_public_key,
&node.value.0.viewing_public_key,
identifier,
);
if self.account_id_map.contains_key(&account_id) {

View File

@ -6,6 +6,7 @@ use std::{
use lee_core::{
Identifier, InputAccountIdentity, NullifierPublicKey,
account::{Account, AccountId, AccountWithMetadata},
encryption::ViewingPublicKey,
program::{
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID,
MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow,
@ -21,7 +22,7 @@ pub struct ExecutionState {
block_validity_window: BlockValidityWindow,
timestamp_validity_window: TimestampValidityWindow,
/// Positions (in `pre_states`) of private-PDA accounts whose supplied npk has been bound to
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk,
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk, vpk,
/// identifier)` check.
/// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on
/// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state`
@ -43,12 +44,13 @@ pub struct ExecutionState {
/// `AccountId` entry or as an equality check against the existing one, making the rule: one
/// `(program, seed)` → one account per tx.
pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>,
/// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk,
/// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk, vpk,
/// identifier) supplied for that position. Built once in `derive_from_outputs` by walking
/// `account_identities` and consulting `npk_if_private_pda`. Used later by the claim and
/// `account_identities` and consulting `npk_vpk_if_private_pda`. Used later by the claim and
/// caller-seeds authorization paths to verify
/// `AccountId::for_private_pda(program_id, seed, npk, identifier) == pre_state.account_id`.
private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)>,
/// `AccountId::for_private_pda(program_id, seed, npk, vpk, identifier) ==
/// pre_state.account_id`.
private_pda_by_position: HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
authorized_accounts: HashSet<AccountId>,
}
@ -59,9 +61,48 @@ impl ExecutionState {
program_id: ProgramId,
program_outputs: Vec<ProgramOutput>,
) -> Self {
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);
// 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_by_position: HashMap<
usize,
(NullifierPublicKey, ViewingPublicKey, Identifier),
> = HashMap::new();
for (pos, account_identity) in account_identities.iter().enumerate() {
if let Some((npk, vpk, identifier)) = account_identity.npk_vpk_if_private_pda() {
private_pda_by_position.insert(pos, (npk, vpk, 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 mut execution_state = Self {
pre_states: Vec::new(),
@ -70,7 +111,7 @@ impl ExecutionState {
timestamp_validity_window,
private_pda_bound_positions: HashMap::new(),
pda_family_binding: HashMap::new(),
private_pda_npk_by_position,
private_pda_by_position,
authorized_accounts: HashSet::new(),
};
@ -99,7 +140,49 @@ impl ExecutionState {
panic!("Insufficient program outputs for chained calls");
};
verify_program_output(&chained_call, caller_program_id, &program_output);
// 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
);
}
for next_call in program_output.chained_calls.iter().rev() {
chained_calls.push_front((next_call.clone(), Some(chained_call.program_id)));
@ -123,8 +206,41 @@ impl ExecutionState {
"Inner call without a chained call found",
);
execution_state.assert_all_pda_positions_bound(account_identities);
execution_state.assert_modified_accounts_claimed();
// 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
}
@ -142,185 +258,209 @@ 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;
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),
// 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}",
);
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 (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)
);
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);
let is_authorized = resolve_authorization_and_record_bindings(
&mut self.pda_family_binding,
&mut self.private_pda_bound_positions,
&self.private_pda_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,
vpk,
identifier,
seed: Some((seed, authority_program_id)),
..
}) => {
let expected = AccountId::for_private_pda(
authority_program_id,
seed,
npk,
vpk,
*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,
vpk,
identifier,
seed: Some((seed, authority_program_id)),
..
}) => {
let npk = NullifierPublicKey::from(nsk);
let expected = AccountId::for_private_pda(
authority_program_id,
seed,
&npk,
vpk,
*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);
}
}
if let Some(claim) = post.required_claim() {
self.process_claim(
account_identities,
&mut post,
pre_account_id,
pre_is_authorized,
program_id,
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.post_states.insert(pre_account_id, post.into_account());
}
}
let pre_state_position = self
.pre_states
.iter()
.position(|acc| acc.account_id == pre_account_id)
.expect("Pre state must exist at this point");
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}"
);
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, vpk, identifier) = self
.private_pda_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,
vpk,
*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,
);
}
}
}
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"
);
post.account_mut().program_owner = program_id;
}
}
}
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"
);
post_states_entry.insert_entry(post.into_account());
}
}
@ -359,150 +499,6 @@ 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
@ -565,7 +561,7 @@ fn bind_private_pda_position(
fn resolve_authorization_and_record_bindings(
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
private_pda_bound_positions: &mut HashMap<usize, (ProgramId, PdaSeed)>,
private_pda_npk_by_position: &HashMap<usize, (NullifierPublicKey, Identifier)>,
private_pda_by_position: &HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
authorized_accounts: &mut HashSet<AccountId>,
pre_account_id: AccountId,
pre_state_position: usize,
@ -579,9 +575,10 @@ fn resolve_authorization_and_record_bindings(
if AccountId::for_public_pda(&caller, seed) == pre_account_id {
return Some((*seed, false, caller));
}
if let Some((npk, identifier)) =
private_pda_npk_by_position.get(&pre_state_position)
&& AccountId::for_private_pda(&caller, seed, npk, *identifier) == pre_account_id
if let Some((npk, vpk, identifier)) =
private_pda_by_position.get(&pre_state_position)
&& AccountId::for_private_pda(&caller, seed, npk, vpk, *identifier)
== pre_account_id
{
return Some((*seed, true, caller));
}

View File

@ -1,244 +1,14 @@
use lee_core::{
Commitment, CommitmentSetDigest, EncryptedAccountData, EncryptionScheme, EphemeralPublicKey,
Commitment, CommitmentSetDigest, EncryptedAccountData, EncryptionScheme, EphemeralSecretKey,
InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
account::{Account, AccountId, Nonce},
compute_digest_for_path,
encryption::ViewingPublicKey,
};
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],
@ -271,105 +41,290 @@ pub fn compute_circuit_output(
output.public_post_states.push(post_state);
}
InputAccountIdentity::PrivateAuthorizedInit {
epk,
view_tag,
ssk,
vpk,
random_seed,
nsk,
identifier,
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,
} => {
let npk = NullifierPublicKey::from(nsk);
let account_id = AccountId::for_regular_private_account(&npk, vpk, *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),
*commitment_root,
);
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),
&npk,
vpk,
random_seed,
new_nullifier,
new_nonce,
);
}
.authorized_init(nsk, commitment_root),
InputAccountIdentity::PrivateAuthorizedUpdate {
epk,
view_tag,
ssk,
vpk,
random_seed,
nsk,
membership_proof,
identifier,
} => PrivateOutputHandler {
output: &mut output,
output_index: &mut output_index,
pre_state: &pre_state,
post_state,
epk,
view_tag: *view_tag,
ssk,
identifier: *identifier,
} => {
let npk = NullifierPublicKey::from(nsk);
let account_id = AccountId::for_regular_private_account(&npk, vpk, *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),
&npk,
vpk,
random_seed,
new_nullifier,
new_nonce,
);
}
.authorized_update(nsk, membership_proof),
InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag,
vpk,
random_seed,
npk,
ssk,
identifier,
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,
} => {
let account_id = AccountId::for_regular_private_account(npk, vpk, *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),
*commitment_root,
);
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),
npk,
vpk,
random_seed,
new_nullifier,
new_nonce,
);
}
.unauthorized(npk, commitment_root),
InputAccountIdentity::PrivatePdaInit {
epk,
view_tag,
npk: _,
ssk,
vpk,
random_seed,
npk,
identifier,
commitment_root,
seed: _,
} => PrivateOutputHandler {
output: &mut output,
output_index: &mut output_index,
pre_state: &pre_state,
post_state,
epk,
view_tag: *view_tag,
ssk,
identifier: *identifier,
} => {
// 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_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),
*commitment_root,
);
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,
},
npk,
vpk,
random_seed,
new_nullifier,
new_nonce,
);
}
.pda_init(commitment_root, pos, &pda_seed_by_position),
InputAccountIdentity::PrivatePdaUpdate {
epk,
view_tag,
ssk,
vpk,
random_seed,
nsk,
membership_proof,
identifier,
seed: external_seed,
} => PrivateOutputHandler {
output: &mut output,
output_index: &mut output_index,
pre_state: &pre_state,
post_state,
epk,
view_tag: *view_tag,
ssk,
identifier: *identifier,
} => {
// 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 npk = NullifierPublicKey::from(nsk);
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,
},
&npk,
vpk,
random_seed,
new_nullifier,
new_nonce,
);
}
.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,
npk: &NullifierPublicKey,
vpk: &ViewingPublicKey,
random_seed: &[u8; 32],
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 esk = EphemeralSecretKey::new(account_id, random_seed, &new_nonce);
let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(vpk, &esk);
// Currently the view tag is properlty generated for all accounts.
// To increase privacy, this will be changed in the later version
// to only be generated explicitly for initialized accounts and
// fed by the prover directly for updated accounts.
//
// See issue 573:
// https://github.com/logos-blockchain/logos-execution-zone/issues/573
let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk);
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,
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

@ -28,10 +28,8 @@ build_utils.workspace = true
[dev-dependencies]
lee_core = { workspace = true, features = ["test_utils"] }
token_core.workspace = true
test_methods = { path = "test_methods" }
env_logger.workspace = true
hex-literal = "1.0.0"
test-case = "3.3.1"

View File

@ -16,7 +16,7 @@ thiserror.workspace = true
bytemuck.workspace = true
bytesize.workspace = true
base58.workspace = true
ml-kem = { workspace = true, optional = true, features = ["getrandom"] }
ml-kem = { workspace = true }
chacha20 = { version = "0.10" }
[dev-dependencies]
@ -24,5 +24,5 @@ serde_json.workspace = true
[features]
default = []
host = ["dep:ml-kem"]
host = ["ml-kem/getrandom"]
test_utils = ["host"]

View File

@ -2,9 +2,9 @@ use serde::{Deserialize, Serialize};
use crate::{
Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey,
NullifierSecretKey, SharedSecretKey,
NullifierSecretKey,
account::{Account, AccountWithMetadata},
encryption::{EncryptedAccountData, EphemeralPublicKey, ViewTag},
encryption::{EncryptedAccountData, ViewingPublicKey},
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
};
@ -14,15 +14,13 @@ pub struct PrivacyPreservingCircuitInput {
pub program_outputs: Vec<ProgramOutput>,
/// One entry per `pre_state`, in the same order as the program's `pre_states`.
/// Length must equal the number of `pre_states` derived from `program_outputs`.
/// The guest's `private_pda_npk_by_position` and `private_pda_bound_positions`
/// The guest's `private_pda_by_position` and `private_pda_bound_positions`
/// rely on this position alignment.
pub account_identities: Vec<InputAccountIdentity>,
/// Program ID.
pub program_id: ProgramId,
}
/// Per-account input to the privacy-preserving circuit. Each variant carries exactly the fields
/// the guest needs for that account's code path.
#[derive(Serialize, Deserialize, Clone)]
pub enum InputAccountIdentity {
/// Public account. The guest reads pre/post state from `program_outputs` and emits no
@ -30,12 +28,11 @@ pub enum InputAccountIdentity {
Public,
/// Init of an authorized standalone private account: no membership proof. The `pre_state`
/// must be `Account::default()`. The `account_id` is derived as
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and
/// matched against `pre_state.account_id`.
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), vpk, identifier)`
/// and matched against `pre_state.account_id`.
PrivateAuthorizedInit {
epk: EphemeralPublicKey,
view_tag: ViewTag,
ssk: SharedSecretKey,
vpk: ViewingPublicKey,
random_seed: [u8; 32],
nsk: NullifierSecretKey,
identifier: Identifier,
commitment_root: CommitmentSetDigest,
@ -43,9 +40,8 @@ pub enum InputAccountIdentity {
/// Update of an authorized standalone private account: existing on-chain commitment, with
/// membership proof.
PrivateAuthorizedUpdate {
epk: EphemeralPublicKey,
view_tag: ViewTag,
ssk: SharedSecretKey,
vpk: ViewingPublicKey,
random_seed: [u8; 32],
nsk: NullifierSecretKey,
membership_proof: MembershipProof,
identifier: Identifier,
@ -53,10 +49,9 @@ pub enum InputAccountIdentity {
/// Init of a standalone private account the caller does not own (e.g. a recipient who
/// doesn't yet exist on chain). No `nsk`, no membership proof.
PrivateUnauthorized {
epk: EphemeralPublicKey,
view_tag: ViewTag,
vpk: ViewingPublicKey,
random_seed: [u8; 32],
npk: NullifierPublicKey,
ssk: SharedSecretKey,
identifier: Identifier,
commitment_root: CommitmentSetDigest,
},
@ -65,15 +60,14 @@ pub enum InputAccountIdentity {
/// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it
/// as the 4th input.
PrivatePdaInit {
epk: EphemeralPublicKey,
view_tag: ViewTag,
vpk: ViewingPublicKey,
random_seed: [u8; 32],
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) ==
/// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) ==
/// pre_state.account_id` rather than requiring a `Claim::Pda` or caller
/// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized
/// == false`.
@ -83,15 +77,14 @@ pub enum InputAccountIdentity {
/// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a
/// previously-seen authorization in a chained call.
PrivatePdaUpdate {
epk: EphemeralPublicKey,
view_tag: ViewTag,
ssk: SharedSecretKey,
vpk: ViewingPublicKey,
random_seed: [u8; 32],
nsk: NullifierSecretKey,
membership_proof: MembershipProof,
identifier: Identifier,
/// 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) ==
/// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) ==
/// pre_state.account_id` rather than requiring a caller `pda_seeds` to establish
/// the binding. The `pre_state` must have `is_authorized == false`.
seed: Option<(PdaSeed, ProgramId)>,
@ -112,17 +105,23 @@ impl InputAccountIdentity {
)
}
/// For private PDA variants, return the `(npk, identifier)` pair. `Init` carries both
/// directly; `Update` derives `npk` from `nsk`. For non-PDA variants returns `None`.
#[must_use]
pub fn npk_if_private_pda(&self) -> Option<(NullifierPublicKey, Identifier)> {
pub fn npk_vpk_if_private_pda(
&self,
) -> Option<(NullifierPublicKey, ViewingPublicKey, Identifier)> {
match self {
Self::PrivatePdaInit {
npk, identifier, ..
} => Some((*npk, *identifier)),
npk,
vpk,
identifier,
..
} => Some((*npk, vpk.clone(), *identifier)),
Self::PrivatePdaUpdate {
nsk, identifier, ..
} => Some((NullifierPublicKey::from(nsk), *identifier)),
nsk,
vpk,
identifier,
..
} => Some((NullifierPublicKey::from(nsk), vpk.clone(), *identifier)),
Self::Public
| Self::PrivateAuthorizedInit { .. }
| Self::PrivateAuthorizedUpdate { .. }
@ -161,7 +160,7 @@ mod tests {
use crate::{
Commitment, Nullifier,
account::{Account, AccountId, AccountWithMetadata, Nonce},
encryption::Ciphertext,
encryption::{Ciphertext, EphemeralPublicKey},
};
#[test]

View File

@ -5,11 +5,9 @@ use chacha20::{
};
use risc0_zkvm::sha::{Impl, Sha256 as _};
use serde::{Deserialize, Serialize};
#[cfg(feature = "host")]
pub use shared_key_derivation::{MlKem768EncapsulationKey, ViewingPublicKey};
use crate::{Commitment, account::Account, program::PrivateAccountKind};
#[cfg(feature = "host")]
pub mod shared_key_derivation;
/// Length in bytes of an ML-KEM-768 ciphertext (the `EphemeralPublicKey` payload).
@ -17,6 +15,31 @@ pub const ML_KEM_768_CIPHERTEXT_LEN: usize = 1088;
pub type Scalar = [u8; 32];
#[derive(Serialize, Deserialize, Clone, Copy)]
pub struct EphemeralSecretKey(pub [u8; 32]);
impl EphemeralSecretKey {
/// Derives an ephemeral secret key from OS randomness and account-specific values.
///
/// For updates, `nonce` carries `nsk`-derived entropy, making `esk` strong even
/// with a compromised RNG. For inits, `nonce` is deterministic, so `random_seed`
/// is the sole entropy source.
#[must_use]
pub fn new(
account_id: &crate::account::AccountId,
random_seed: &[u8; 32],
nonce: &crate::account::Nonce,
) -> Self {
const PREFIX: &[u8; 14] = b"/LEE/v0.3/esk/";
let mut input = [0_u8; 14 + 32 + 32 + 16];
input[0..14].copy_from_slice(PREFIX);
input[14..46].copy_from_slice(account_id.value());
input[46..78].copy_from_slice(random_seed);
input[78..94].copy_from_slice(&nonce.0.to_le_bytes());
Self(Impl::hash_bytes(&input).as_bytes().try_into().unwrap())
}
}
#[derive(Serialize, Deserialize, Clone, Copy)]
pub struct SharedSecretKey(pub [u8; 32]);
@ -55,6 +78,18 @@ pub struct EncryptedAccountData {
pub view_tag: ViewTag,
}
impl EncryptedAccountData {
#[must_use]
pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag {
const PREFIX: &[u8; 18] = b"/LEE/v0.3/ViewTag/";
let mut bytes = [0_u8; 18 + 32 + ViewingPublicKey::LEN];
bytes[0..18].copy_from_slice(PREFIX);
bytes[18..50].copy_from_slice(&npk.to_byte_array());
bytes[50..].copy_from_slice(vpk.to_bytes());
Impl::hash_bytes(&bytes).as_bytes()[0]
}
}
#[cfg(feature = "host")]
impl EncryptedAccountData {
#[must_use]
@ -71,16 +106,6 @@ impl EncryptedAccountData {
view_tag,
}
}
/// Computes the tag as the first byte of SHA256("/LEE/v0.3/ViewTag/" || npk || vpk).
#[must_use]
pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"/LEE/v0.3/ViewTag/");
bytes.extend_from_slice(&npk.to_byte_array());
bytes.extend_from_slice(vpk.to_bytes());
Impl::hash_bytes(&bytes).as_bytes()[0]
}
}
impl EncryptionScheme {
@ -116,12 +141,12 @@ impl EncryptionScheme {
commitment: &Commitment,
output_index: u32,
) -> [u8; 32] {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"LEE/v0.2/KDF-SHA256/");
bytes.extend_from_slice(&shared_secret.0);
bytes.extend_from_slice(&commitment.to_byte_array());
bytes.extend_from_slice(&output_index.to_le_bytes());
const PREFIX: &[u8; 20] = b"LEE/v0.2/KDF-SHA256/";
let mut bytes = [0_u8; 20 + 32 + 32 + 4];
bytes[0..20].copy_from_slice(PREFIX);
bytes[20..52].copy_from_slice(&shared_secret.0);
bytes[52..84].copy_from_slice(&commitment.to_byte_array());
bytes[84..88].copy_from_slice(&output_index.to_le_bytes());
Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()
}
@ -238,4 +263,41 @@ mod tests {
"wrong shared secret must not produce the correct plaintext"
);
}
#[test]
fn esk_is_deterministic() {
let account_id = AccountId::new([1_u8; 32]);
let random_seed = [2_u8; 32];
let nonce = crate::account::Nonce(42);
let esk1 = EphemeralSecretKey::new(&account_id, &random_seed, &nonce);
let esk2 = EphemeralSecretKey::new(&account_id, &random_seed, &nonce);
assert_eq!(esk1.0, esk2.0);
}
#[test]
fn esk_differs_for_different_account_id() {
let random_seed = [2_u8; 32];
let nonce = crate::account::Nonce(42);
let esk_a = EphemeralSecretKey::new(&AccountId::new([0_u8; 32]), &random_seed, &nonce);
let esk_b = EphemeralSecretKey::new(&AccountId::new([1_u8; 32]), &random_seed, &nonce);
assert_ne!(esk_a.0, esk_b.0);
}
#[test]
fn esk_differs_for_different_random_seed() {
let account_id = AccountId::new([1_u8; 32]);
let nonce = crate::account::Nonce(42);
let esk_a = EphemeralSecretKey::new(&account_id, &[0_u8; 32], &nonce);
let esk_b = EphemeralSecretKey::new(&account_id, &[1_u8; 32], &nonce);
assert_ne!(esk_a.0, esk_b.0);
}
#[test]
fn esk_differs_for_different_nonce() {
let account_id = AccountId::new([1_u8; 32]);
let random_seed = [2_u8; 32];
let esk_a = EphemeralSecretKey::new(&account_id, &random_seed, &crate::account::Nonce(0));
let esk_b = EphemeralSecretKey::new(&account_id, &random_seed, &crate::account::Nonce(1));
assert_ne!(esk_a.0, esk_b.0);
}
}

View File

@ -1,5 +1,7 @@
use borsh::{BorshDeserialize, BorshSerialize};
use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed};
#[cfg(feature = "host")]
use ml_kem::Encapsulate as _;
use ml_kem::{Decapsulate as _, KeyExport as _, Seed};
use serde::{Deserialize, Serialize};
use crate::{EphemeralPublicKey, SharedSecretKey};
@ -26,6 +28,7 @@ impl MlKem768EncapsulationKey {
pub const LEN: usize = 1184;
/// Construct from raw bytes, returning an error if the length is not [`Self::LEN`].
#[cfg(feature = "host")]
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, crate::error::LeeCoreError> {
if bytes.len() != Self::LEN {
return Err(crate::error::LeeCoreError::DeserializationError(format!(
@ -59,6 +62,7 @@ impl SharedSecretKey {
/// Returns `(shared_secret, ciphertext)`. The ciphertext must be included in the transaction
/// as the `EphemeralPublicKey`; the receiver recovers the same shared secret via
/// [`Self::decapsulate`].
#[cfg(feature = "host")]
#[must_use]
pub fn encapsulate(ek: &MlKem768EncapsulationKey) -> (Self, EphemeralPublicKey) {
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> =
@ -76,30 +80,18 @@ impl SharedSecretKey {
(Self(ss_bytes), EphemeralPublicKey(ct.to_vec()))
}
/// Deterministically encapsulate a shared secret toward `ek` for use in tests.
/// Deterministically encapsulate a shared secret toward `ek` using a
/// pre-derived `esk` as the ML-KEM encapsulation randomness.
///
/// The shared secret has no secret entropy — it is fully determined by `ek`,
/// `message_hash`, and `output_index`, all of which are public. This makes it
/// unsuitable for real encryption but useful for producing stable, reproducible
/// shared secrets in unit tests. Use a distinct `output_index` per output to
/// avoid EPK collisions across multiple outputs in the same test.
///
/// For production use [`Self::encapsulate`], which draws randomness from the OS.
#[cfg(any(test, feature = "test_utils"))]
/// The `esk` must be derived via `derive_esk(account_id, random_seed, nonce)`
/// which binds it to the account and incorporates OS entropy.
#[must_use]
pub fn encapsulate_deterministic(
ek: &MlKem768EncapsulationKey,
message_hash: &[u8; 32],
output_index: u32,
esk: &crate::encryption::EphemeralSecretKey,
) -> (Self, EphemeralPublicKey) {
use risc0_zkvm::sha::{Impl, Sha256 as _};
let mut input = Vec::with_capacity(36);
input.extend_from_slice(message_hash);
input.extend_from_slice(&output_index.to_le_bytes());
let hash = Impl::hash_bytes(&input);
let m: ml_kem::B32 =
ml_kem::array::Array::try_from(hash.as_bytes()).expect("SHA-256 output is 32 bytes");
let m: ml_kem::B32 = ml_kem::array::Array::try_from(esk.0.as_slice())
.expect("EphemeralSecretKey is 32 bytes");
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> =
ek.0.as_slice()

View File

@ -11,8 +11,8 @@ pub use commitment::{
compute_digest_for_path,
};
pub use encryption::{
EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN,
SharedSecretKey, ViewTag,
EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey,
ML_KEM_768_CIPHERTEXT_LEN, SharedSecretKey, ViewTag,
};
pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
pub use program::PrivateAccountKind;

View File

@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use risc0_zkvm::sha::{Impl, Sha256 as _};
use serde::{Deserialize, Serialize};
use crate::{Commitment, account::AccountId};
use crate::{Commitment, account::AccountId, encryption::ViewingPublicKey};
const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00";
@ -16,12 +16,16 @@ impl AccountId {
/// Derives an [`AccountId`] for a regular (non-PDA) private account from the nullifier public
/// key and identifier.
#[must_use]
pub fn for_regular_private_account(npk: &NullifierPublicKey, identifier: Identifier) -> Self {
// 32 bytes prefix || 32 bytes npk || 16 bytes identifier
let mut bytes = [0; 80];
pub fn for_regular_private_account(
npk: &NullifierPublicKey,
vpk: &ViewingPublicKey,
identifier: Identifier,
) -> Self {
let mut bytes = [0_u8; 32 + 32 + ViewingPublicKey::LEN + 16];
bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX);
bytes[32..64].copy_from_slice(&npk.0);
bytes[64..80].copy_from_slice(&identifier.to_le_bytes());
bytes[64..64 + ViewingPublicKey::LEN].copy_from_slice(vpk.to_bytes());
bytes[64 + ViewingPublicKey::LEN..].copy_from_slice(&identifier.to_le_bytes());
Self::new(
Impl::hash_bytes(&bytes)
@ -32,9 +36,9 @@ impl AccountId {
}
}
impl From<(&NullifierPublicKey, Identifier)> for AccountId {
fn from((npk, identifier): (&NullifierPublicKey, Identifier)) -> Self {
Self::for_regular_private_account(npk, identifier)
impl From<(&NullifierPublicKey, &ViewingPublicKey, Identifier)> for AccountId {
fn from((npk, vpk, identifier): (&NullifierPublicKey, &ViewingPublicKey, Identifier)) -> Self {
Self::for_regular_private_account(npk, vpk, identifier)
}
}
@ -158,12 +162,13 @@ mod tests {
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
];
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
let expected_account_id = AccountId::new([
165, 52, 40, 32, 231, 171, 113, 10, 65, 241, 156, 72, 154, 207, 122, 192, 15, 46, 50,
253, 105, 164, 89, 84, 40, 191, 182, 119, 64, 255, 67, 142,
242, 239, 57, 244, 89, 109, 65, 201, 223, 100, 43, 87, 205, 83, 148, 161, 176, 22, 208,
220, 68, 135, 10, 171, 182, 80, 54, 74, 228, 244, 236, 7,
]);
let account_id = AccountId::for_regular_private_account(&npk, 0);
let account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
assert_eq!(account_id, expected_account_id);
}
@ -175,12 +180,13 @@ mod tests {
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
];
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
let expected_account_id = AccountId::new([
203, 201, 109, 245, 40, 54, 195, 12, 55, 33, 0, 86, 245, 65, 70, 156, 24, 249, 26, 95,
56, 247, 99, 121, 165, 182, 234, 255, 19, 127, 191, 72,
149, 125, 157, 109, 119, 81, 9, 163, 231, 181, 214, 43, 57, 113, 221, 72, 180, 149,
189, 170, 32, 181, 255, 231, 19, 92, 235, 59, 153, 185, 172, 206,
]);
let account_id = AccountId::for_regular_private_account(&npk, 1);
let account_id = AccountId::for_regular_private_account(&npk, &vpk, 1);
assert_eq!(account_id, expected_account_id);
}
@ -193,12 +199,13 @@ mod tests {
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
];
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
let expected_account_id = AccountId::new([
178, 16, 226, 206, 217, 38, 38, 45, 155, 240, 226, 253, 168, 87, 146, 70, 72, 32, 174,
19, 245, 25, 214, 162, 209, 135, 252, 82, 27, 2, 174, 196,
30, 232, 222, 201, 233, 125, 124, 194, 58, 39, 121, 96, 185, 84, 168, 109, 80, 111,
159, 112, 84, 100, 133, 244, 16, 34, 221, 35, 128, 131, 98, 159,
]);
let account_id = AccountId::for_regular_private_account(&npk, identifier);
let account_id = AccountId::for_regular_private_account(&npk, &vpk, identifier);
assert_eq!(account_id, expected_account_id);
}

View File

@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::{
BlockId, Identifier, NullifierPublicKey, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
encryption::ViewingPublicKey,
};
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
@ -154,19 +155,21 @@ impl AccountId {
program_id: &ProgramId,
seed: &PdaSeed,
npk: &NullifierPublicKey,
vpk: &ViewingPublicKey,
identifier: Identifier,
) -> Self {
use risc0_zkvm::sha::{Impl, Sha256 as _};
const PRIVATE_PDA_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/PrivatePDA/\x00";
let mut bytes = [0_u8; 144];
let mut bytes = [0_u8; 32 + 32 + 32 + 32 + ViewingPublicKey::LEN + 16];
bytes[0..32].copy_from_slice(PRIVATE_PDA_PREFIX);
let program_id_bytes: &[u8] =
bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]");
bytes[32..64].copy_from_slice(program_id_bytes);
bytes[64..96].copy_from_slice(&seed.0);
bytes[96..128].copy_from_slice(&npk.to_byte_array());
bytes[128..144].copy_from_slice(&identifier.to_le_bytes());
bytes[128..128 + ViewingPublicKey::LEN].copy_from_slice(vpk.to_bytes());
bytes[128 + ViewingPublicKey::LEN..].copy_from_slice(&identifier.to_le_bytes());
Self::new(
Impl::hash_bytes(&bytes)
.as_bytes()
@ -177,16 +180,20 @@ impl AccountId {
/// Derives the [`AccountId`] for a private account from the nullifier public key and kind.
#[must_use]
pub fn for_private_account(npk: &NullifierPublicKey, kind: &PrivateAccountKind) -> Self {
pub fn for_private_account(
npk: &NullifierPublicKey,
vpk: &ViewingPublicKey,
kind: &PrivateAccountKind,
) -> Self {
match kind {
PrivateAccountKind::Regular(identifier) => {
Self::for_regular_private_account(npk, *identifier)
Self::for_regular_private_account(npk, vpk, *identifier)
}
PrivateAccountKind::Pda {
program_id,
seed,
identifier,
} => Self::for_private_pda(program_id, seed, npk, *identifier),
} => Self::for_private_pda(program_id, seed, npk, vpk, *identifier),
}
}
}

View File

@ -184,13 +184,14 @@ fn for_private_pda_matches_pinned_value() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
let identifier: Identifier = u128::MAX;
let expected = AccountId::new([
59, 239, 182, 97, 14, 220, 96, 115, 238, 133, 143, 33, 234, 82, 237, 255, 148, 110, 54,
124, 98, 159, 245, 101, 146, 182, 150, 54, 37, 62, 25, 17,
5, 87, 128, 244, 206, 244, 65, 130, 178, 88, 225, 183, 0, 159, 201, 201, 212, 206, 6, 156,
13, 55, 32, 139, 91, 222, 209, 83, 172, 148, 123, 179,
]);
assert_eq!(
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier),
expected
);
}
@ -202,9 +203,10 @@ fn for_private_pda_differs_for_different_npk() {
let seed = PdaSeed::new([2; 32]);
let npk_a = NullifierPublicKey([3; 32]);
let npk_b = NullifierPublicKey([4; 32]);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id, &seed, &npk_a, u128::MAX),
AccountId::for_private_pda(&program_id, &seed, &npk_b, u128::MAX),
AccountId::for_private_pda(&program_id, &seed, &npk_a, &vpk, u128::MAX),
AccountId::for_private_pda(&program_id, &seed, &npk_b, &vpk, u128::MAX),
);
}
@ -215,9 +217,10 @@ fn for_private_pda_differs_for_different_seed() {
let seed_a = PdaSeed::new([2; 32]);
let seed_b = PdaSeed::new([5; 32]);
let npk = NullifierPublicKey([3; 32]);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id, &seed_a, &npk, u128::MAX),
AccountId::for_private_pda(&program_id, &seed_b, &npk, u128::MAX),
AccountId::for_private_pda(&program_id, &seed_a, &npk, &vpk, u128::MAX),
AccountId::for_private_pda(&program_id, &seed_b, &npk, &vpk, u128::MAX),
);
}
@ -228,9 +231,10 @@ fn for_private_pda_differs_for_different_program_id() {
let program_id_b: ProgramId = [9; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id_a, &seed, &npk, u128::MAX),
AccountId::for_private_pda(&program_id_b, &seed, &npk, u128::MAX),
AccountId::for_private_pda(&program_id_a, &seed, &npk, &vpk, u128::MAX),
AccountId::for_private_pda(&program_id_b, &seed, &npk, &vpk, u128::MAX),
);
}
@ -241,13 +245,14 @@ fn for_private_pda_differs_for_different_identifier() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
AccountId::for_private_pda(&program_id, &seed, &npk, 1),
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0),
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 1),
);
assert_ne!(
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX),
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0),
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX),
);
}
@ -258,7 +263,8 @@ fn for_private_pda_differs_from_public_pda() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX);
let public_id = AccountId::for_public_pda(&program_id, &seed);
assert_ne!(private_id, public_id);
}
@ -295,22 +301,24 @@ fn for_private_account_dispatches_correctly() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
let identifier: Identifier = 77;
assert_eq!(
AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)),
AccountId::for_regular_private_account(&npk, identifier),
AccountId::for_private_account(&npk, &vpk, &PrivateAccountKind::Regular(identifier)),
AccountId::for_regular_private_account(&npk, &vpk, identifier),
);
assert_eq!(
AccountId::for_private_account(
&npk,
&vpk,
&PrivateAccountKind::Pda {
program_id,
seed,
identifier
}
),
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier),
);
}

View File

@ -1,8 +1,8 @@
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
use lee_core::{
Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, EphemeralPublicKey,
Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey,
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, EphemeralSecretKey, Nullifier,
PrivacyPreservingCircuitOutput, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
program::{PdaSeed, PrivateAccountKind},
};
@ -58,7 +58,8 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
AccountId::new([0; 32]),
);
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
let recipient_account_id =
AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0);
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
let balance_to_move: u128 = 37;
@ -79,8 +80,9 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
let expected_sender_pre = sender.clone();
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0;
let init_nonce = Nonce::private_account_nonce_init(&recipient_account_id);
let esk = EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &init_nonce);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &esk).0;
let (output, proof) = execute_and_prove(
vec![sender, recipient],
@ -88,13 +90,9 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
@ -138,12 +136,14 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
data: Data::default(),
},
true,
AccountId::for_regular_private_account(&sender_keys.npk(), 0),
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
let sender_account_id =
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account);
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
let recipient_account_id =
AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0);
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
let balance_to_move: u128 = 37;
@ -179,23 +179,25 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
Commitment::new(&recipient_account_id, &expected_private_account_2),
];
let shared_secret_1 =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0;
let esk_1 = EphemeralSecretKey::new(
&sender_account_id,
&[0; 32],
&sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
);
let shared_secret_1 = SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &esk_1).0;
let init_nonce_2 = Nonce::private_account_nonce_init(&recipient_account_id);
let esk_2 = EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &init_nonce_2);
let shared_secret_2 =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1).0;
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &esk_2).0;
let (output, proof) = execute_and_prove(
vec![sender_pre, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret_1,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: commitment_set
.get_proof_for(&commitment_sender)
@ -203,13 +205,9 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: shared_secret_2,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
@ -250,7 +248,7 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
let pre = AccountWithMetadata::new(
Account::default(),
false,
AccountId::for_regular_private_account(&account_keys.npk(), 0),
AccountId::for_regular_private_account(&account_keys.npk(), &account_keys.vpk(), 0),
);
let validity_window_chain_caller = crate::test_methods::validity_window_chain_caller();
@ -265,9 +263,6 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
))
.unwrap();
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0).0;
let program_with_deps = ProgramWithDependencies::new(
validity_window_chain_caller,
[(validity_window.id(), validity_window)].into(),
@ -277,13 +272,9 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
vec![pre],
instruction,
vec![InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&account_keys.npk(),
&account_keys.vpk(),
),
vpk: account_keys.vpk(),
random_seed: [0; 32],
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
@ -302,19 +293,21 @@ fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() {
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let identifier: u128 = 99;
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id =
AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier);
let init_nonce = Nonce::private_account_nonce_init(&account_id);
let esk = EphemeralSecretKey::new(&account_id, &[0; 32], &init_nonce);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
let (output, _proof) = execute_and_prove(
vec![pre_state],
Program::serialize_instruction(seed).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk,
ssk: shared_secret,
identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -343,11 +336,8 @@ fn private_pda_init() {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret_pda =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
// PDA (new, private PDA)
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0);
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
let auth_id = simple_transfer.id();
@ -361,10 +351,9 @@ fn private_pda_init() {
vec![pda_pre],
instruction,
vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk,
ssk: shared_secret_pda,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -386,11 +375,8 @@ fn private_pda_withdraw() {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret_pda =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
// PDA (new, private PDA)
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0);
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
// Recipient (public)
@ -417,10 +403,9 @@ fn private_pda_withdraw() {
instruction,
vec![
InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk,
ssk: shared_secret_pda,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -445,8 +430,6 @@ fn shared_account_receives_via_simple_transfer() {
let shared_keys = test_private_account_keys_1();
let shared_npk = shared_keys.npk();
let shared_identifier: u128 = 42;
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0_u8; 32], 0).0;
// Sender: public account with balance, owned by auth-transfer
let sender_id = AccountId::new([99; 32]);
@ -461,7 +444,7 @@ fn shared_account_receives_via_simple_transfer() {
);
// Recipient: shared private account (new, unauthorized)
let shared_account_id = AccountId::from((&shared_npk, shared_identifier));
let shared_account_id = AccountId::from((&shared_npk, &shared_keys.vpk(), shared_identifier));
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
let balance_to_move: u128 = 100;
@ -473,10 +456,9 @@ fn shared_account_receives_via_simple_transfer() {
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&shared_npk, &shared_keys.vpk()),
vpk: shared_keys.vpk(),
random_seed: [0; 32],
npk: shared_npk,
ssk: shared_secret,
identifier: shared_identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
},
@ -496,17 +478,21 @@ fn private_authorized_init_encrypts_regular_kind_with_identifier() {
let program = crate::test_methods::claimer();
let keys = test_private_account_keys_1();
let identifier: u128 = 99;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier);
let esk = EphemeralSecretKey::new(
&account_id,
&[0; 32],
&Nonce::private_account_nonce_init(&account_id),
);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
let pre = AccountWithMetadata::new(Account::default(), true, account_id);
let (output, _) = execute_and_prove(
vec![pre],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
ssk,
vpk: keys.vpk(),
random_seed: [0; 32],
nsk: keys.nsk,
identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
@ -528,18 +514,22 @@ fn private_unauthorized_init_encrypts_regular_kind_with_identifier() {
let program = crate::test_methods::claimer();
let keys = test_private_account_keys_1();
let identifier: u128 = 99;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let recipient_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
let recipient_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier);
let esk = EphemeralSecretKey::new(
&recipient_id,
&[0; 32],
&Nonce::private_account_nonce_init(&recipient_id),
);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id);
let (output, _) = execute_and_prove(
vec![recipient],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk: keys.npk(),
ssk,
identifier,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
@ -560,8 +550,13 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() {
let program = crate::test_methods::noop();
let keys = test_private_account_keys_1();
let identifier: u128 = 99;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier);
let esk = EphemeralSecretKey::new(
&account_id,
&[0; 32],
&Nonce::default().private_account_nonce_increment(&keys.nsk),
);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
let account = Account {
program_owner: program.id(),
balance: 1,
@ -577,9 +572,8 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() {
vec![sender],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
ssk,
vpk: keys.vpk(),
random_seed: [0; 32],
nsk: keys.nsk,
membership_proof: commitment_set.get_proof_for(&commitment).unwrap(),
identifier,
@ -604,10 +598,14 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() {
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let identifier: u128 = 99;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let simple_transfer_id = simple_transfer.id();
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier);
let esk = EphemeralSecretKey::new(
&pda_id,
&[0; 32],
&Nonce::default().private_account_nonce_increment(&keys.nsk),
);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
let pda_account = Account {
program_owner: simple_transfer_id,
balance: 1,
@ -630,9 +628,8 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() {
Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(),
vec![
InputAccountIdentity::PrivatePdaUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
ssk,
vpk: keys.vpk(),
random_seed: [0; 32],
nsk: keys.nsk,
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
identifier,
@ -660,19 +657,16 @@ fn private_pda_init_identifier_mismatch_fails() {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
let result = execute_and_prove(
vec![pre_state],
Program::serialize_instruction(seed).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk,
ssk: shared_secret,
identifier: 99,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -690,10 +684,8 @@ fn private_pda_update_identifier_mismatch_fails() {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let simple_transfer_id = simple_transfer.id();
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5);
let pda_account = Account {
program_owner: simple_transfer_id,
balance: 1,
@ -714,9 +706,8 @@ fn private_pda_update_identifier_mismatch_fails() {
Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(),
vec![
InputAccountIdentity::PrivatePdaUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
ssk,
vpk: keys.vpk(),
random_seed: [0; 32],
nsk: keys.nsk,
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
identifier: 99,

View File

@ -103,8 +103,8 @@ impl Message {
#[cfg(test)]
pub mod tests {
use lee_core::{
Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, PrivateAccountKind,
SharedSecretKey,
Commitment, EncryptionScheme, EphemeralSecretKey, Nullifier, NullifierPublicKey,
PrivateAccountKind, SharedSecretKey,
account::{Account, AccountId, Nonce},
encryption::ViewingPublicKey,
program::{BlockValidityWindow, TimestampValidityWindow},
@ -123,6 +123,7 @@ pub mod tests {
let npk1 = NullifierPublicKey::from(&nsk1);
let npk2 = NullifierPublicKey::from(&nsk2);
let vpk = ViewingPublicKey::from_seed(&[7; 32], &[8; 32]);
let public_account_ids = vec![AccountId::new([1; 32])];
@ -132,10 +133,10 @@ pub mod tests {
let encrypted_private_post_states = Vec::new();
let account_id2 = lee_core::account::AccountId::for_regular_private_account(&npk2, 0);
let account_id2 = lee_core::account::AccountId::for_regular_private_account(&npk2, &vpk, 0);
let new_commitments = vec![Commitment::new(&account_id2, &account2)];
let account_id1 = lee_core::account::AccountId::for_regular_private_account(&npk1, 0);
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),
@ -225,9 +226,10 @@ pub mod tests {
let npk = NullifierPublicKey::from(&[1; 32]);
let vpk = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]);
let account = Account::default();
let account_id = lee_core::account::AccountId::for_regular_private_account(&npk, 0);
let account_id = lee_core::account::AccountId::for_regular_private_account(&npk, &vpk, 0);
let commitment = Commitment::new(&account_id, &account);
let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &[0_u8; 32], 0);
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&vpk, &EphemeralSecretKey([0_u8; 32]));
let ciphertext = EncryptionScheme::encrypt(
&account,
&PrivateAccountKind::Regular(0),

View File

@ -60,8 +60,11 @@ fn public_changer_claimer_data_change_no_claim_fails() {
fn private_changer_claimer_no_data_change_no_claim_succeeds() {
let program = crate::test_methods::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0));
let private_account = AccountWithMetadata::new(
Account::default(),
true,
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
// Don't change data (None) and don't claim (false)
let instruction: (Option<Vec<u8>>, bool) = (None, false);
@ -69,12 +72,8 @@ fn private_changer_claimer_no_data_change_no_claim_succeeds() {
vec![private_account],
Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: (0, vec![]),
identifier: 0,
@ -90,8 +89,11 @@ fn private_changer_claimer_no_data_change_no_claim_succeeds() {
fn private_changer_claimer_data_change_no_claim_fails() {
let program = crate::test_methods::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0));
let private_account = AccountWithMetadata::new(
Account::default(),
true,
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
// Change data but don't claim (false) - should fail
let new_data = vec![1, 2, 3, 4, 5];
let instruction: (Option<Vec<u8>>, bool) = (Some(new_data), false);
@ -100,12 +102,8 @@ fn private_changer_claimer_data_change_no_claim_fails() {
vec![private_account],
Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: (0, vec![]),
identifier: 0,

View File

@ -45,10 +45,13 @@ fn circuit_fails_if_invalid_auth_keys_are_provided() {
..Account::default()
},
true,
(&sender_keys.npk(), 0),
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account::default(),
false,
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
// Setting the recipient nsk to authorize the sender.
// This should be set to the sender private account in a normal circumstance.
@ -59,32 +62,18 @@ fn circuit_fails_if_invalid_auth_keys_are_provided() {
Program::serialize_instruction(10_u128).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: recipient_keys.nsk,
membership_proof: (0, vec![]),
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0_u8; 32],
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),
@ -105,7 +94,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid
..Account::default()
},
true,
(&sender_keys.npk(), 0),
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -114,7 +103,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid
..Account::default()
},
false,
(&recipient_keys.npk(), 0),
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
);
let result = execute_and_prove(
@ -122,32 +111,18 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid
Program::serialize_instruction(10_u128).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: (0, vec![]),
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0_u8; 32],
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),
@ -168,7 +143,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_
..Account::default()
},
true,
(&sender_keys.npk(), 0),
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -177,7 +152,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_
..Account::default()
},
false,
(&recipient_keys.npk(), 0),
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
);
let result = execute_and_prove(
@ -185,32 +160,18 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_
Program::serialize_instruction(10_u128).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: (0, vec![]),
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0_u8; 32],
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),
@ -231,7 +192,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided(
..Account::default()
},
true,
(&sender_keys.npk(), 0),
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -240,7 +201,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided(
..Account::default()
},
false,
(&recipient_keys.npk(), 0),
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
);
let result = execute_and_prove(
@ -248,32 +209,18 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided(
Program::serialize_instruction(10_u128).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: (0, vec![]),
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0_u8; 32],
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),
@ -294,7 +241,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided
..Account::default()
},
true,
(&sender_keys.npk(), 0),
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -303,7 +250,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided
..Account::default()
},
false,
(&recipient_keys.npk(), 0),
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
);
let result = execute_and_prove(
@ -311,32 +258,18 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided
Program::serialize_instruction(10_u128).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: (0, vec![]),
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0_u8; 32],
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),
@ -358,13 +291,13 @@ fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_bu
..Account::default()
},
true,
(&sender_keys.npk(), 0),
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account::default(),
// This should be set to false in normal circumstances
true,
(&recipient_keys.npk(), 0),
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
);
let result = execute_and_prove(
@ -372,32 +305,18 @@ fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_bu
Program::serialize_instruction(10_u128).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: (0, vec![]),
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0_u8; 32],
0,
)
.0,
commitment_root: DUMMY_COMMITMENT_HASH,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
],
&program.into(),
@ -415,7 +334,6 @@ fn private_pda_without_binding_fails() {
let program = crate::test_methods::simple_balance_transfer();
let keys = test_private_account_keys_1();
let npk = keys.npk();
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let public_account_1 = AccountWithMetadata::new(
Account {
program_owner: program.id(),
@ -434,10 +352,9 @@ fn private_pda_without_binding_fails() {
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -460,19 +377,17 @@ fn private_pda_claim_succeeds() {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX);
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
let result = execute_and_prove(
vec![pre_state],
Program::serialize_instruction(seed).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -500,22 +415,21 @@ fn private_pda_npk_mismatch_fails() {
let npk_a = keys_a.npk();
let npk_b = keys_b.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0;
// `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state.
// `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in
// the circuit must reject.
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk_a, u128::MAX);
let account_id =
AccountId::for_private_pda(&program.id(), &seed, &npk_a, &keys_a.vpk(), u128::MAX);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
let result = execute_and_prove(
vec![pre_state],
Program::serialize_instruction(seed).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk_b, &keys_b.vpk()),
vpk: keys_b.vpk(),
random_seed: [0; 32],
npk: npk_b,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -538,9 +452,9 @@ fn caller_pda_seeds_authorize_private_pda_for_callee() {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([77; 32]);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk, u128::MAX);
let account_id =
AccountId::for_private_pda(&delegator.id(), &seed, &npk, &keys.vpk(), u128::MAX);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
let callee_id = callee.id();
@ -550,10 +464,9 @@ fn caller_pda_seeds_authorize_private_pda_for_callee() {
vec![pre_state],
Program::serialize_instruction((seed, seed, callee_id)).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -579,9 +492,9 @@ fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() {
let npk = keys.npk();
let claim_seed = PdaSeed::new([77; 32]);
let wrong_delegated_seed = PdaSeed::new([88; 32]);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, u128::MAX);
let account_id =
AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, &keys.vpk(), u128::MAX);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
let callee_id = callee.id();
@ -591,10 +504,9 @@ fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() {
vec![pre_state],
Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -619,11 +531,21 @@ fn two_private_pda_claims_under_same_seed_are_rejected() {
let keys_a = test_private_account_keys_1();
let keys_b = test_private_account_keys_2();
let seed = PdaSeed::new([55; 32]);
let shared_a = SharedSecretKey::encapsulate_deterministic(&keys_a.vpk(), &[0_u8; 32], 0).0;
let shared_b = SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0;
let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk(), u128::MAX);
let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk(), u128::MAX);
let account_a = AccountId::for_private_pda(
&program.id(),
&seed,
&keys_a.npk(),
&keys_a.vpk(),
u128::MAX,
);
let account_b = AccountId::for_private_pda(
&program.id(),
&seed,
&keys_b.npk(),
&keys_b.vpk(),
u128::MAX,
);
let pre_a = AccountWithMetadata::new(Account::default(), false, account_a);
let pre_b = AccountWithMetadata::new(Account::default(), false, account_b);
@ -633,19 +555,17 @@ fn two_private_pda_claims_under_same_seed_are_rejected() {
Program::serialize_instruction(seed).unwrap(),
vec![
InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys_a.npk(), &keys_a.vpk()),
vpk: keys_a.vpk(),
random_seed: [0; 32],
npk: keys_a.npk(),
ssk: shared_a,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
},
InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys_b.npk(), &keys_b.vpk()),
vpk: keys_b.vpk(),
random_seed: [0; 32],
npk: keys_b.npk(),
ssk: shared_b,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -667,12 +587,11 @@ fn private_pda_top_level_reuse_rejected_by_binding_check() {
let program = crate::test_methods::noop();
let keys = test_private_account_keys_1();
let npk = keys.npk();
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let seed = PdaSeed::new([99; 32]);
// Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized =
// true, account_id derived via the private formula.
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX);
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX);
let owned_pre_state = AccountWithMetadata::new(
Account {
program_owner: program.id(),
@ -686,10 +605,9 @@ fn private_pda_top_level_reuse_rejected_by_binding_check() {
vec![owned_pre_state],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
vpk: keys.vpk(),
random_seed: [0; 32],
npk,
ssk: shared_secret,
identifier: u128::MAX,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: None,
@ -766,33 +684,23 @@ fn circuit_should_fail_if_there_are_repeated_ids() {
..Account::default()
},
true,
(&sender_keys.npk(), 0),
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0;
let result = execute_and_prove(
vec![private_account_1.clone(), private_account_1],
Program::serialize_instruction(100_u128).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: (1, vec![]),
identifier: 0,
},
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: (1, vec![]),
identifier: 0,
@ -812,14 +720,15 @@ fn private_authorized_uninitialized_account() {
let private_keys = test_private_account_keys_1();
// Create an authorized private account with default values (new account being initialized)
let authorized_account =
AccountWithMetadata::new(Account::default(), true, (&private_keys.npk(), 0));
let authorized_account = AccountWithMetadata::new(
Account::default(),
true,
(&private_keys.npk(), &private_keys.vpk(), 0),
);
let program = crate::test_methods::simple_balance_transfer();
// Set up parameters for the new account
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0);
let instruction: u128 = 0;
@ -828,12 +737,8 @@ fn private_authorized_uninitialized_account() {
vec![authorized_account],
Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedInit {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&private_keys.npk(),
&private_keys.vpk(),
),
ssk: shared_secret,
vpk: private_keys.vpk(),
random_seed: [0; 32],
nsk: private_keys.nsk,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
@ -851,7 +756,8 @@ fn private_authorized_uninitialized_account() {
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0);
assert!(result.is_ok());
let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0);
let account_id =
AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0);
let nullifier = Nullifier::for_account_initialization(&account_id);
assert!(state.private_state.1.contains(&nullifier));
}
@ -865,24 +771,21 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() {
// especially PDAs. Private PDAs are not useful in practice because there is no way to
// operate them without the corresponding private keys, so unauthorized private claiming
// remains allowed.
let unauthorized_account =
AccountWithMetadata::new(Account::default(), false, (&private_keys.npk(), 0));
let unauthorized_account = AccountWithMetadata::new(
Account::default(),
false,
(&private_keys.npk(), &private_keys.vpk(), 0),
);
let program = crate::test_methods::claimer();
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0);
let (output, proof) = execute_and_prove(
vec![unauthorized_account],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&private_keys.npk(),
&private_keys.vpk(),
),
vpk: private_keys.vpk(),
random_seed: [0; 32],
npk: private_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
@ -899,7 +802,8 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() {
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0);
let account_id =
AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0);
let nullifier = Nullifier::for_account_initialization(&account_id);
assert!(state.private_state.1.contains(&nullifier));
}
@ -912,14 +816,15 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() {
let private_keys = test_private_account_keys_1();
// Step 1: Create a new private account with authorization
let authorized_account =
AccountWithMetadata::new(Account::default(), true, (&private_keys.npk(), 0));
let authorized_account = AccountWithMetadata::new(
Account::default(),
true,
(&private_keys.npk(), &private_keys.vpk(), 0),
);
let claimer_program = crate::test_methods::claimer();
// Set up parameters for claiming the new account
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0);
let instruction = ();
@ -928,12 +833,8 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() {
vec![authorized_account.clone()],
Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedInit {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&private_keys.npk(),
&private_keys.vpk(),
),
ssk: shared_secret,
vpk: private_keys.vpk(),
random_seed: [0; 32],
nsk: private_keys.nsk,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
@ -955,7 +856,8 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() {
);
// Verify the account is now initialized (nullifier exists)
let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0);
let account_id =
AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0);
let nullifier = Nullifier::for_account_initialization(&account_id);
assert!(state.private_state.1.contains(&nullifier));
@ -967,20 +869,14 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() {
};
let noop_program = crate::test_methods::noop();
let shared_secret2 =
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0).0;
// Step 3: Try to execute noop program with authentication but without initialization
let res = execute_and_prove(
vec![account_metadata],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&private_keys.npk(),
&private_keys.vpk(),
),
ssk: shared_secret2,
vpk: private_keys.vpk(),
random_seed: [0; 32],
nsk: private_keys.nsk,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
@ -1010,8 +906,10 @@ fn two_private_pda_family_members_receive_and_spend() {
);
let funder_id = funder_keys.account_id();
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0);
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1);
let alice_pda_0_id =
AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_keys.vpk(), 0);
let alice_pda_1_id =
AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_keys.vpk(), 1);
let recipient_id = test_public_account_keys_2().account_id();
let recipient_signing_key = test_public_account_keys_2().signing_key;
@ -1031,11 +929,6 @@ fn two_private_pda_family_members_receive_and_spend() {
..Account::default()
};
let (alice_shared_0, alice_epk_0) =
SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 0);
let (alice_shared_1, alice_epk_1) =
SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 1);
// Fund alice_pda_0 via authenticated_transfer directly.
{
let funder_account = state.get_account_by_id(funder_id);
@ -1049,10 +942,9 @@ fn two_private_pda_family_members_receive_and_spend() {
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
epk: alice_epk_0.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()),
vpk: alice_keys.vpk(),
random_seed: [0; 32],
npk: alice_npk,
ssk: alice_shared_0,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: Some((seed, proxy_id)),
@ -1086,10 +978,9 @@ fn two_private_pda_family_members_receive_and_spend() {
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
epk: alice_epk_1.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()),
vpk: alice_keys.vpk(),
random_seed: [0; 32],
npk: alice_npk,
ssk: alice_shared_1,
identifier: 1,
commitment_root: DUMMY_COMMITMENT_HASH,
seed: Some((seed, proxy_id)),
@ -1127,9 +1018,8 @@ fn two_private_pda_family_members_receive_and_spend() {
Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(),
vec![
InputAccountIdentity::PrivatePdaUpdate {
epk: alice_epk_0,
view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()),
ssk: alice_shared_0,
vpk: alice_keys.vpk(),
random_seed: [0; 32],
nsk: alice_keys.nsk,
membership_proof: state
.get_proof_for_commitment(&commitment_pda_0)
@ -1165,9 +1055,8 @@ fn two_private_pda_family_members_receive_and_spend() {
Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(),
vec![
InputAccountIdentity::PrivatePdaUpdate {
epk: alice_epk_1,
view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()),
ssk: alice_shared_1,
vpk: alice_keys.vpk(),
random_seed: [0; 32],
nsk: alice_keys.nsk,
membership_proof: state
.get_proof_for_commitment(&commitment_pda_1)
@ -1205,7 +1094,6 @@ fn two_private_pda_family_members_receive_and_spend() {
};
let commitment_pda_1_after_spend =
Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend);
let alice_shared_1_refund = SharedSecretKey([12; 32]);
{
let recipient_account = state.get_account_by_id(recipient_id);
let recipient_nonce = recipient_account.nonce;
@ -1218,10 +1106,9 @@ fn two_private_pda_family_members_receive_and_spend() {
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaUpdate {
epk: EphemeralPublicKey(vec![12_u8; ML_KEM_768_CIPHERTEXT_LEN]),
view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()),
vpk: alice_keys.vpk(),
random_seed: [0; 32],
nsk: alice_keys.nsk,
ssk: alice_shared_1_refund,
membership_proof: state
.get_proof_for_commitment(&commitment_pda_1_after_spend)
.expect("pda_1 after spend must be in state"),

View File

@ -303,19 +303,21 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() {
balance: 100,
..Account::default()
};
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
let sender_account_id =
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id);
let mut state =
V03State::new().with_private_accounts([(sender_commitment.clone(), sender_init_nullifier)]);
let sender_pre =
AccountWithMetadata::new(sender_private_account, true, (&sender_keys.npk(), 0));
let sender_pre = AccountWithMetadata::new(
sender_private_account,
true,
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap();
let recipient_account_id =
AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key));
let recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_account_id);
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0);
let balance = 37;
@ -324,12 +326,8 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() {
Program::serialize_instruction(balance).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: state
.get_proof_for_commitment(&sender_commitment)
@ -383,7 +381,7 @@ fn private_chained_call(number_of_calls: u32) {
..Account::default()
},
true,
(&from_keys.npk(), 0),
(&from_keys.npk(), &from_keys.vpk(), 0),
);
let to_account = AccountWithMetadata::new(
Account {
@ -391,11 +389,12 @@ fn private_chained_call(number_of_calls: u32) {
..Account::default()
},
true,
(&to_keys.npk(), 0),
(&to_keys.npk(), &to_keys.vpk(), 0),
);
let from_account_id = AccountId::for_regular_private_account(&from_keys.npk(), 0);
let to_account_id = AccountId::for_regular_private_account(&to_keys.npk(), 0);
let from_account_id =
AccountId::for_regular_private_account(&from_keys.npk(), &from_keys.vpk(), 0);
let to_account_id = AccountId::for_regular_private_account(&to_keys.npk(), &to_keys.vpk(), 0);
let from_commitment = Commitment::new(&from_account_id, &from_account.account);
let to_commitment = Commitment::new(&to_account_id, &to_account.account);
let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id);
@ -414,12 +413,6 @@ fn private_chained_call(number_of_calls: u32) {
None,
);
let (from_ss, from_epk) =
SharedSecretKey::encapsulate_deterministic(&from_keys.vpk(), &[0_u8; 32], 0);
let (to_ss, to_epk) =
SharedSecretKey::encapsulate_deterministic(&to_keys.vpk(), &[0_u8; 32], 1);
let mut dependencies = HashMap::new();
dependencies.insert(simple_transfers.id(), simple_transfers);
@ -448,9 +441,8 @@ fn private_chained_call(number_of_calls: u32) {
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: to_epk,
view_tag: EncryptedAccountData::compute_view_tag(&to_keys.npk(), &to_keys.vpk()),
ssk: to_ss,
vpk: from_keys.vpk(),
random_seed: [0; 32],
nsk: from_keys.nsk,
membership_proof: state
.get_proof_for_commitment(&from_commitment)
@ -458,12 +450,8 @@ fn private_chained_call(number_of_calls: u32) {
identifier: 0,
},
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: from_epk,
view_tag: EncryptedAccountData::compute_view_tag(
&from_keys.npk(),
&from_keys.vpk(),
),
ssk: from_ss,
vpk: to_keys.vpk(),
random_seed: [0; 32],
nsk: to_keys.nsk,
membership_proof: state
.get_proof_for_commitment(&to_commitment)

View File

@ -43,8 +43,8 @@ fn new_includes_nullifiers_for_private_accounts() {
..Account::default()
};
let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), 0);
let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), 0);
let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), &keys1.vpk(), 0);
let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), &keys2.vpk(), 0);
let init_commitment1 = Commitment::new(&account_id1, &account);
let init_commitment2 = Commitment::new(&account_id2, &account);

View File

@ -7,10 +7,10 @@
use std::collections::HashMap;
use lee_core::{
BlockId, Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity,
Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp,
BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier,
NullifierPublicKey, NullifierSecretKey, Timestamp,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, ViewingPublicKey},
encryption::ViewingPublicKey,
program::{
BlockValidityWindow, ExecutionValidationError, MAX_NUMBER_CHAINED_CALLS, PdaSeed,
ProgramId, TimestampValidityWindow, WrappedBalanceSum,
@ -118,7 +118,7 @@ impl V03State {
#[must_use]
pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self {
let account_id = AccountId::for_regular_private_account(&keys.npk(), 0);
let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), 0);
let commitment = Commitment::new(&account_id, account);
self.private_state.0.extend(&[commitment]);
self
@ -267,10 +267,11 @@ fn shielded_balance_transfer_for_tests(
let sender_nonce = sender.account.nonce;
let recipient = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0);
let recipient = AccountWithMetadata::new(
Account::default(),
false,
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
);
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
vec![sender, recipient],
@ -278,13 +279,9 @@ fn shielded_balance_transfer_for_tests(
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
@ -312,33 +309,27 @@ fn private_balance_transfer_for_tests(
state: &V03State,
) -> PrivacyPreservingTransaction {
let program = crate::test_methods::simple_balance_transfer();
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
let sender_account_id =
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
let sender_pre = AccountWithMetadata::new(
sender_private_account.clone(),
true,
(&sender_keys.npk(), 0),
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let recipient_pre = AccountWithMetadata::new(
Account::default(),
false,
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
);
let recipient_pre =
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let (shared_secret_1, epk_1) =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0);
let (shared_secret_2, epk_2) =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1);
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: epk_1,
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret_1,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: state
.get_proof_for_commitment(&sender_commitment)
@ -346,13 +337,9 @@ fn private_balance_transfer_for_tests(
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: epk_2,
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
npk: recipient_keys.npk(),
ssk: shared_secret_2,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
},
@ -376,12 +363,13 @@ fn deshielded_balance_transfer_for_tests(
state: &V03State,
) -> PrivacyPreservingTransaction {
let program = crate::test_methods::simple_balance_transfer();
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
let sender_account_id =
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
let sender_pre = AccountWithMetadata::new(
sender_private_account.clone(),
true,
(&sender_keys.npk(), 0),
(&sender_keys.npk(), &sender_keys.vpk(), 0),
);
let recipient_pre = AccountWithMetadata::new(
state.get_account_by_id(*recipient_account_id),
@ -389,20 +377,13 @@ fn deshielded_balance_transfer_for_tests(
*recipient_account_id,
);
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0);
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret,
vpk: sender_keys.vpk(),
random_seed: [0; 32],
nsk: sender_keys.nsk,
membership_proof: state
.get_proof_for_commitment(&sender_commitment)

View File

@ -68,8 +68,10 @@ fn transition_from_privacy_preserving_transaction_private() {
&state,
);
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
let sender_account_id =
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
let recipient_account_id =
AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0);
let expected_new_commitment_1 = Commitment::new(
&sender_account_id,
&Account {
@ -203,7 +205,8 @@ fn transition_from_privacy_preserving_transaction_deshielded() {
&state,
);
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
let sender_account_id =
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
let expected_new_commitment = Commitment::new(
&sender_account_id,
&Account {
@ -487,10 +490,14 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
false,
sender_keys.account_id(),
);
let recipient_account =
AccountWithMetadata::new(Account::default(), true, (&recipient_keys.npk(), 0));
let recipient_account = AccountWithMetadata::new(
Account::default(),
true,
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
);
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
let recipient_account_id =
AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0);
let recipient_commitment = Commitment::new(&recipient_account_id, &recipient_account.account);
let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id);
let state = V03State::new()
@ -504,9 +511,6 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
let balance_to_transfer = 10_u128;
let instruction = (balance_to_transfer, simple_transfers.id());
let recipient =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0;
let mut dependencies = HashMap::new();
dependencies.insert(simple_transfers.id(), simple_transfers);
let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies);
@ -518,12 +522,8 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
ssk: recipient,
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
nsk: recipient_keys.nsk,
membership_proof: state
.get_proof_for_commitment(&recipient_commitment)

View File

@ -124,12 +124,13 @@ fn validity_window_works_in_privacy_preserving_transactions(
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
let validity_window_program = crate::test_methods::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0));
let pre = AccountWithMetadata::new(
Account::default(),
false,
(&account_keys.npk(), &account_keys.vpk(), 0),
);
let mut state = V03State::new().with_test_programs();
let tx = {
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0);
let instruction = (
block_validity_window,
TimestampValidityWindow::new_unbounded(),
@ -138,13 +139,9 @@ fn validity_window_works_in_privacy_preserving_transactions(
vec![pre],
Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&account_keys.npk(),
&account_keys.vpk(),
),
vpk: account_keys.vpk(),
random_seed: [0; 32],
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],
@ -192,12 +189,13 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions(
let timestamp_validity_window: TimestampValidityWindow = validity_window.try_into().unwrap();
let validity_window_program = crate::test_methods::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0));
let pre = AccountWithMetadata::new(
Account::default(),
false,
(&account_keys.npk(), &account_keys.vpk(), 0),
);
let mut state = V03State::new().with_test_programs();
let tx = {
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0);
let instruction = (
BlockValidityWindow::new_unbounded(),
timestamp_validity_window,
@ -206,13 +204,9 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions(
vec![pre],
Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&account_keys.npk(),
&account_keys.vpk(),
),
vpk: account_keys.vpk(),
random_seed: [0; 32],
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
commitment_root: DUMMY_COMMITMENT_HASH,
}],

View File

@ -78,7 +78,7 @@ fn public_diff_reflects_a_successful_transfer() {
#[test]
fn privacy_malicious_programs_cannot_drain_public_victim() {
use lee_core::{
Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
Commitment, InputAccountIdentity,
account::{Account, AccountWithMetadata},
};
@ -105,8 +105,8 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
// Attacker controls a private account.
let attacker_keys = test_private_account_keys_1();
let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0);
let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk());
let attacker_id =
AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0);
let victim_id = AccountId::new([20_u8; 32]);
let recipient_id = AccountId::new([42_u8; 32]);
@ -165,12 +165,8 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
// [2] recipient — first seen in simple_balance_transfer's program_output.pre_states
let account_identities = vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: attacker_epk,
view_tag: EncryptedAccountData::compute_view_tag(
&attacker_keys.npk(),
&attacker_keys.vpk(),
),
ssk: attacker_ssk,
vpk: attacker_keys.vpk(),
random_seed: [0; 32],
nsk: attacker_keys.nsk,
membership_proof,
identifier: 0,
@ -232,7 +228,7 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
#[test]
fn privacy_malicious_programs_cannot_drain_private_victim() {
use lee_core::{
Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
Commitment, InputAccountIdentity,
account::{Account, AccountWithMetadata},
};
@ -262,12 +258,13 @@ fn privacy_malicious_programs_cannot_drain_private_victim() {
// Attacker controls a private account.
let attacker_keys = test_private_account_keys_1();
let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0);
let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk());
let attacker_id =
AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0);
// Victim is a private account — not registered in public chain state.
let victim_keys = test_private_account_keys_2();
let victim_id = AccountId::for_regular_private_account(&victim_keys.npk(), 0);
let victim_id =
AccountId::for_regular_private_account(&victim_keys.npk(), &victim_keys.vpk(), 0);
let victim_balance = 5_000_u128;
let recipient_id = AccountId::new([42_u8; 32]);
@ -328,12 +325,8 @@ fn privacy_malicious_programs_cannot_drain_private_victim() {
// so PrivateAuthorizedUpdate is not an option.
let account_identities = vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: attacker_epk,
view_tag: EncryptedAccountData::compute_view_tag(
&attacker_keys.npk(),
&attacker_keys.vpk(),
),
ssk: attacker_ssk,
vpk: attacker_keys.vpk(),
random_seed: [0; 32],
nsk: attacker_keys.nsk,
membership_proof,
identifier: 0,

View File

@ -7,6 +7,10 @@ license = { workspace = true }
[lints]
workspace = true
[features]
default = []
testnet = []
[dependencies]
common.workspace = true
logos-blockchain-zone-sdk.workspace = true
@ -30,4 +34,3 @@ tokio.workspace = true
[dev-dependencies]
tempfile.workspace = true
authenticated_transfer_core.workspace = true

View File

@ -23,7 +23,12 @@ impl IndexerStore {
/// Starting database at the start of new chain.
/// Creates files if necessary.
pub fn open_db(location: &Path) -> Result<Self> {
#[cfg(not(feature = "testnet"))]
let initial_state = testnet_initial_state::initial_state();
#[cfg(feature = "testnet")]
let initial_state = testnet_initial_state::initial_state_testnet();
let dbio = RocksDBIO::open_or_create(location, &initial_state)?;
let current_state = dbio.final_state()?;

View File

@ -6,7 +6,7 @@ version = "0.1.0"
[dependencies]
lee.workspace = true
indexer_core.workspace = true
indexer_core = { workspace = true, features = ["testnet"] }
indexer_service_protocol = { workspace = true, features = ["convert"] }
env_logger.workspace = true

View File

@ -10,7 +10,7 @@ workspace = true
[dependencies]
indexer_service_protocol = { workspace = true, features = ["convert"] }
indexer_service_rpc = { workspace = true, features = ["server"] }
indexer_core.workspace = true
indexer_core = { workspace = true, features = ["testnet"] }
clap = { workspace = true, features = ["derive"] }
anyhow.workspace = true

View File

@ -10,15 +10,15 @@ use common::{
};
use key_protocol::key_management::KeyChain;
use lee::{
Account, AccountId, Data, EphemeralPublicKey, PrivacyPreservingTransaction, PrivateKey,
PublicKey, PublicTransaction, SharedSecretKey, V03State,
Account, AccountId, Data, PrivacyPreservingTransaction, PrivateKey, PublicKey,
PublicTransaction, V03State,
error::LeeError,
execute_and_prove,
privacy_preserving_transaction::{Message, circuit::ProgramWithDependencies},
program::Program,
};
use lee_core::{
Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier,
Commitment, InputAccountIdentity, Nullifier,
account::{AccountWithMetadata, Nonce},
program::PdaSeed,
};
@ -790,8 +790,11 @@ async fn block_production_aborts_when_clock_account_data_is_corrupted() {
#[test]
fn private_bridge_withdraw_invocation_is_dropped() {
let sender_keys = KeyChain::new_os_random();
let sender_account_id =
AccountId::for_regular_private_account(&sender_keys.nullifier_public_key, 0);
let sender_account_id = AccountId::for_regular_private_account(
&sender_keys.nullifier_public_key,
&sender_keys.viewing_public_key,
0,
);
let sender_private_account = Account {
program_owner: programs::authenticated_transfer().id(),
balance: 100,
@ -812,7 +815,11 @@ fn private_bridge_withdraw_invocation_is_dropped() {
let sender_pre = AccountWithMetadata::new(
sender_private_account,
true,
(&sender_keys.nullifier_public_key, 0),
(
&sender_keys.nullifier_public_key,
&sender_keys.viewing_public_key,
0,
),
);
let bridge_pre = AccountWithMetadata::new(
state.get_account_by_id(bridge_account_id),
@ -820,8 +827,6 @@ fn private_bridge_withdraw_invocation_is_dropped() {
bridge_account_id,
);
let shared_secret = SharedSecretKey::encapsulate(&sender_keys.viewing_public_key).0;
let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw {
amount: 1,
bedrock_account_pk: [0; 32],
@ -842,12 +847,8 @@ fn private_bridge_withdraw_invocation_is_dropped() {
instruction,
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(vec![12_u8; 1088]),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.nullifier_public_key,
&sender_keys.viewing_public_key,
),
ssk: shared_secret,
vpk: sender_keys.viewing_public_key.clone(),
random_seed: [0; 32],
nsk: sender_keys.private_key_holder.nullifier_secret_key,
membership_proof: state
.get_proof_for_commitment(&sender_commitment)

View File

@ -86,6 +86,7 @@ pub struct PublicAccountPublicInitialData {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PrivateAccountPublicInitialData {
pub npk: lee_core::NullifierPublicKey,
pub vpk: lee_core::encryption::ViewingPublicKey,
pub account: lee_core::account::Account,
}
@ -108,6 +109,7 @@ impl PrivateAccountPrivateInitialData {
pub fn account_id(&self) -> lee::AccountId {
lee::AccountId::for_regular_private_account(
&self.key_chain.nullifier_public_key,
&self.key_chain.viewing_public_key,
self.identifier,
)
}
@ -183,6 +185,7 @@ fn initial_commitments() -> Vec<PrivateAccountPublicInitialData> {
.into_iter()
.map(|data| PrivateAccountPublicInitialData {
npk: data.key_chain.nullifier_public_key,
vpk: data.key_chain.viewing_public_key.clone(),
account: data.account,
})
.collect()
@ -193,7 +196,8 @@ fn initial_private_accounts() -> Vec<(lee_core::Commitment, lee_core::Nullifier)
.iter()
.map(|init_comm_data| {
let npk = &init_comm_data.npk;
let account_id = lee::AccountId::for_regular_private_account(npk, 0);
let account_id =
lee::AccountId::for_regular_private_account(npk, &init_comm_data.vpk, 0);
let mut acc = init_comm_data.account.clone();
@ -304,8 +308,8 @@ mod tests {
const PUB_ACC_A_TEXT_ADDR: &str = "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV";
const PUB_ACC_B_TEXT_ADDR: &str = "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo";
const PRIV_ACC_A_TEXT_ADDR: &str = "4eGX3M3rgjHsme8n3sSp89af8JRZtYVTesbJjLqaX1VQ";
const PRIV_ACC_B_TEXT_ADDR: &str = "3m6HQmCgmAvsxZtxAHPqqEqoBG4335fCG8TzxigyW7rE";
const PRIV_ACC_A_TEXT_ADDR: &str = "EVesBKsYRVtkjnTcsbk8tWHkBn2xZmzAXzwgrP3ZaVoZ";
const PRIV_ACC_B_TEXT_ADDR: &str = "94MXhZnueurjX6v37CYDKVEKYBiyhYArvtEdceq2XDQP";
#[test]
fn pub_state_consistency() {
@ -440,6 +444,10 @@ mod tests {
init_comms[0],
PrivateAccountPublicInitialData {
npk: NullifierPublicKey(NPK_PRIV_ACC_A),
vpk: init_private_accs_keys[0]
.key_chain
.viewing_public_key
.clone(),
account: Account {
program_owner: DEFAULT_PROGRAM_OWNER,
balance: PRIV_ACC_A_INITIAL_BALANCE,
@ -453,6 +461,10 @@ mod tests {
init_comms[1],
PrivateAccountPublicInitialData {
npk: NullifierPublicKey(NPK_PRIV_ACC_B),
vpk: init_private_accs_keys[1]
.key_chain
.viewing_public_key
.clone(),
account: Account {
program_owner: DEFAULT_PROGRAM_OWNER,
balance: PRIV_ACC_B_INITIAL_BALANCE,

View File

@ -28,9 +28,6 @@ vault_core.workspace = true
[build-dependencies]
cbindgen = "0.29"
[dev-dependencies]
tempfile = "3"
[features]
default = ["prove"]
prove = ["lee/prove"]

View File

@ -645,9 +645,10 @@ mod tests {
let identifier = u128::from_le_bytes([45; 16]);
let private_reg_acc_id =
AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier));
AccountId::for_private_account(&npk, &vpk, &PrivateAccountKind::Regular(identifier));
let private_pda_acc_id = AccountId::for_private_account(
&npk,
&vpk,
&PrivateAccountKind::Pda {
program_id: [46; 8],
seed: PdaSeed::new([47; 32]),

View File

@ -1,15 +1,15 @@
use core::fmt;
use anyhow::Result;
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
use keycard_wallet::{KeycardWallet, python_path};
use lee::{AccountId, PrivateKey, PublicKey, Signature};
use lee_core::{
CommitmentSetDigest, DUMMY_COMMITMENT_HASH, Identifier, InputAccountIdentity, MembershipProof,
NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
account::{AccountWithMetadata, Nonce},
encryption::{EncryptedAccountData, EphemeralPublicKey, ViewingPublicKey},
encryption::ViewingPublicKey,
};
use rand::{RngCore as _, rngs::OsRng};
use crate::{ExecutionFailureKind, WalletCore};
@ -201,15 +201,38 @@ impl AccountManager {
for account in accounts {
let state = match account {
AccountIdentity::Public(account_id) => {
prepare_public_state(wallet, account_id, true).await?
let acc = wallet
.get_account_public(account_id)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let sk = wallet.get_account_public_signing_key(account_id).cloned();
let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id);
State::Public { account, sk }
}
AccountIdentity::PublicNoSign(account_id) => {
prepare_public_state(wallet, account_id, false).await?
let acc = wallet
.get_account_public(account_id)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let sk = None;
let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id);
State::Public { account, sk }
}
AccountIdentity::PublicKeycard {
account_id,
key_path,
} => {
let acc = wallet
.get_account_public(account_id)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let account = AccountWithMetadata::new(acc.clone(), true, account_id);
if pin.is_none() {
pin = Some(
crate::helperfunctions::read_pin()
@ -225,40 +248,75 @@ impl AccountManager {
.to_owned(),
);
}
prepare_public_keycard_state(wallet, account_id, key_path).await?
State::PublicKeycard { account, key_path }
}
AccountIdentity::PrivateOwned(account_id) => {
let pre = private_key_tree_acc_preparation(wallet, account_id, false).await?;
State::Private(pre)
}
AccountIdentity::PrivateOwned(account_id) => State::Private(
private_key_tree_acc_preparation(wallet, account_id, false).await?,
),
AccountIdentity::PrivateForeign {
npk,
vpk,
identifier,
} => State::Private(private_foreign_acc_preparation(npk, vpk, identifier)),
AccountIdentity::PrivatePdaOwned(account_id) => State::Private(
private_key_tree_acc_preparation(wallet, account_id, true).await?,
),
} => {
let acc = lee_core::account::Account::default();
let auth_acc = AccountWithMetadata::new(acc, false, (&npk, &vpk, identifier));
let mut random_seed: [u8; 32] = [0; 32];
OsRng.fill_bytes(&mut random_seed);
let pre = AccountPreparedData {
nsk: None,
npk,
identifier,
vpk,
pre_state: auth_acc,
proof: None,
random_seed,
is_pda: false,
};
State::Private(pre)
}
AccountIdentity::PrivatePdaOwned(account_id) => {
let pre = private_key_tree_acc_preparation(wallet, account_id, true).await?;
State::Private(pre)
}
AccountIdentity::PrivatePdaForeign {
account_id,
npk,
vpk,
identifier,
} => State::Private(private_pda_foreign_acc_preparation(
account_id, npk, vpk, identifier,
)),
} => {
let acc = lee_core::account::Account::default();
let auth_acc = AccountWithMetadata::new(acc, false, account_id);
let mut random_seed: [u8; 32] = [0; 32];
OsRng.fill_bytes(&mut random_seed);
let pre = AccountPreparedData {
nsk: None,
npk,
identifier,
vpk,
pre_state: auth_acc,
proof: None,
random_seed,
is_pda: true,
};
State::Private(pre)
}
AccountIdentity::PrivateShared {
nsk,
npk,
vpk,
identifier,
} => {
let account_id = lee::AccountId::from((&npk, identifier));
State::Private(
private_shared_acc_preparation(
wallet, account_id, nsk, npk, vpk, identifier, false,
)
.await?,
let account_id = lee::AccountId::from((&npk, &vpk, identifier));
let pre = private_shared_acc_preparation(
wallet, account_id, nsk, npk, vpk, identifier, false,
)
.await?;
State::Private(pre)
}
AccountIdentity::PrivatePdaShared {
account_id,
@ -266,12 +324,14 @@ impl AccountManager {
npk,
vpk,
identifier,
} => State::Private(
private_shared_acc_preparation(
} => {
let pre = private_shared_acc_preparation(
wallet, account_id, nsk, npk, vpk, identifier, true,
)
.await?,
),
.await?;
State::Private(pre)
}
};
states.push(state);
@ -327,9 +387,26 @@ impl AccountManager {
self.states
.iter()
.filter_map(|state| match state {
State::Private(pre) => Some(PrivateAccountKeys { ssk: pre.ssk }),
State::Private(pre) => Some(pre),
State::Public { .. } | State::PublicKeycard { .. } => None,
})
.map(|pre| {
let nonce = if pre.proof.is_some() {
pre.pre_state.account.nonce.private_account_nonce_increment(
pre.nsk.as_ref().expect("update variant must have nsk"),
)
} else {
lee_core::account::Nonce::private_account_nonce_init(&pre.pre_state.account_id)
};
let esk = lee_core::EphemeralSecretKey::new(
&pre.pre_state.account_id,
&pre.random_seed,
&nonce,
);
PrivateAccountKeys {
ssk: SharedSecretKey::encapsulate_deterministic(&pre.vpk, &esk).0,
}
})
.collect()
}
@ -344,19 +421,17 @@ impl AccountManager {
State::Public { .. } | State::PublicKeycard { .. } => InputAccountIdentity::Public,
State::Private(pre) if pre.is_pda => match (pre.nsk, pre.proof.clone()) {
(Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
ssk: pre.ssk,
vpk: pre.vpk.clone(),
random_seed: pre.random_seed,
nsk,
membership_proof,
identifier: pre.identifier,
seed: None,
},
_ => InputAccountIdentity::PrivatePdaInit {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
vpk: pre.vpk.clone(),
random_seed: pre.random_seed,
npk: pre.npk,
ssk: pre.ssk,
identifier: pre.identifier,
commitment_root: self.dummy_commitment_root,
seed: None,
@ -365,27 +440,24 @@ impl AccountManager {
State::Private(pre) => match (pre.nsk, pre.proof.clone()) {
(Some(nsk), Some(membership_proof)) => {
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
ssk: pre.ssk,
vpk: pre.vpk.clone(),
random_seed: pre.random_seed,
nsk,
membership_proof,
identifier: pre.identifier,
}
}
(Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
ssk: pre.ssk,
vpk: pre.vpk.clone(),
random_seed: pre.random_seed,
nsk,
identifier: pre.identifier,
commitment_root: self.dummy_commitment_root,
},
(None, _) => InputAccountIdentity::PrivateUnauthorized {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
vpk: pre.vpk.clone(),
random_seed: pre.random_seed,
npk: pre.npk,
ssk: pre.ssk,
identifier: pre.identifier,
commitment_root: self.dummy_commitment_root,
},
@ -462,49 +534,12 @@ struct AccountPreparedData {
vpk: ViewingPublicKey,
pre_state: AccountWithMetadata,
proof: Option<MembershipProof>,
/// Cached shared-secret key derived once at `AccountManager::new`. Reused for both the
/// circuit input variant (`account_identities()`) and the message ephemeral-key tuples
/// (`private_account_keys()`), so all consumers see the same key. The corresponding
/// `EphemeralKeyHolder` uses `OsRng` and would produce a different value on a second call.
ssk: SharedSecretKey,
/// Cached ephemeral public key, paired with `ssk`.
epk: EphemeralPublicKey,
random_seed: [u8; 32],
/// True when this account is a private PDA (owned or foreign). Used by `account_identities()`
/// to select `PrivatePdaInit`/`PrivatePdaUpdate` rather than the standalone private variants.
is_pda: bool,
}
async fn prepare_public_state(
wallet: &WalletCore,
account_id: AccountId,
lookup_signing_key: bool,
) -> Result<State, ExecutionFailureKind> {
let acc = wallet
.get_account_public(account_id)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let sk = if lookup_signing_key {
wallet.get_account_public_signing_key(account_id).cloned()
} else {
None
};
let account = AccountWithMetadata::new(acc, sk.is_some(), account_id);
Ok(State::Public { account, sk })
}
async fn prepare_public_keycard_state(
wallet: &WalletCore,
account_id: AccountId,
key_path: String,
) -> Result<State, ExecutionFailureKind> {
let acc = wallet
.get_account_public(account_id)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let account = AccountWithMetadata::new(acc, true, account_id);
Ok(State::PublicKeycard { account, key_path })
}
async fn private_key_tree_acc_preparation(
wallet: &WalletCore,
account_id: AccountId,
@ -530,9 +565,8 @@ async fn private_key_tree_acc_preparation(
// support from that in the wallet.
let sender_pre = AccountWithMetadata::new(from_acc.account.clone(), true, account_id);
let eph_holder = EphemeralKeyHolder::new(&from_vpk);
let ssk = eph_holder.calculate_shared_secret_sender();
let epk = eph_holder.ephemeral_public_key().clone();
let mut random_seed: [u8; 32] = [0; 32];
OsRng.fill_bytes(&mut random_seed);
Ok(AccountPreparedData {
nsk: Some(nsk),
@ -541,8 +575,7 @@ async fn private_key_tree_acc_preparation(
vpk: from_vpk,
pre_state: sender_pre,
proof,
ssk,
epk,
random_seed,
is_pda,
})
}
@ -570,9 +603,8 @@ async fn private_shared_acc_preparation(
.await
.unwrap_or(None);
let eph_holder = EphemeralKeyHolder::new(&vpk);
let ssk = eph_holder.calculate_shared_secret_sender();
let epk = eph_holder.ephemeral_public_key().clone();
let mut random_seed: [u8; 32] = [0; 32];
OsRng.fill_bytes(&mut random_seed);
Ok(AccountPreparedData {
nsk: Some(nsk),
@ -581,61 +613,11 @@ async fn private_shared_acc_preparation(
vpk,
pre_state,
proof,
ssk,
epk,
random_seed,
is_pda,
})
}
fn private_foreign_acc_preparation(
npk: NullifierPublicKey,
vpk: ViewingPublicKey,
identifier: Identifier,
) -> AccountPreparedData {
let acc = lee_core::account::Account::default();
let pre_state = AccountWithMetadata::new(acc, false, (&npk, identifier));
let eph_holder = EphemeralKeyHolder::new(&vpk);
let ssk = eph_holder.calculate_shared_secret_sender();
let epk = eph_holder.ephemeral_public_key().clone();
AccountPreparedData {
nsk: None,
npk,
identifier,
vpk,
pre_state,
proof: None,
ssk,
epk,
is_pda: false,
}
}
fn private_pda_foreign_acc_preparation(
account_id: AccountId,
npk: NullifierPublicKey,
vpk: ViewingPublicKey,
identifier: Identifier,
) -> AccountPreparedData {
let acc = lee_core::account::Account::default();
let pre_state = AccountWithMetadata::new(acc, false, account_id);
let eph_holder = EphemeralKeyHolder::new(&vpk);
let ssk = eph_holder.calculate_shared_secret_sender();
let epk = eph_holder.ephemeral_public_key().clone();
AccountPreparedData {
nsk: None,
npk,
identifier,
vpk,
pre_state,
proof: None,
ssk,
epk,
is_pda: true,
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -581,8 +581,11 @@ impl WalletSubcommand for ImportSubcommand {
let key_chain: KeyChain = serde_json::from_str(&key_chain_json)
.map_err(|err| anyhow::anyhow!("Invalid key chain JSON: {err}"))?;
let account = lee::Account::from(account_state);
let account_id =
lee::AccountId::from((&key_chain.nullifier_public_key, identifier));
let account_id = lee::AccountId::from((
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
identifier,
));
wallet_core
.storage_mut()

View File

@ -369,7 +369,7 @@ impl WalletCore {
let keys = holder.derive_keys_for_pda(&program_id, &pda_seed);
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let account_id = AccountId::for_private_pda(&program_id, &pda_seed, &npk, identifier);
let account_id = AccountId::for_private_pda(&program_id, &pda_seed, &npk, &vpk, identifier);
self.register_shared_account(
account_id,
@ -403,7 +403,7 @@ impl WalletCore {
let keys = holder.derive_regular_shared_account_keys_from_identifier(identifier);
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let account_id = AccountId::from((&npk, identifier));
let account_id = AccountId::from((&npk, &vpk, identifier));
self.register_shared_account(account_id, group_name, identifier, None, None);
@ -803,7 +803,11 @@ impl WalletCore {
)
.map(|(kind, res_acc)| {
let npk = &key_chain.nullifier_public_key;
let account_id = lee::AccountId::for_private_account(npk, &kind);
let account_id = lee::AccountId::for_private_account(
npk,
&key_chain.viewing_public_key,
&kind,
);
let nsk = key_chain.private_key_holder.nullifier_secret_key;
(account_id, kind, res_acc, nsk)
})

View File

@ -212,7 +212,8 @@ impl UserKeyChain {
.1
.first_key_value()
.expect("Newly created key chain node must have at least one account");
let account_id = AccountId::for_private_account(&npk, kind);
let account_id =
AccountId::for_private_account(&npk, &node.value.0.viewing_public_key, kind);
(account_id, chain_index)
}
@ -248,8 +249,11 @@ impl UserKeyChain {
#[must_use]
pub fn private_account(&self, account_id: AccountId) -> Option<FoundPrivateAccount<'_>> {
self.private_accounts().find_map(|found| {
let expected_id =
AccountId::for_private_account(&found.key_chain.nullifier_public_key, found.kind);
let expected_id = AccountId::for_private_account(
&found.key_chain.nullifier_public_key,
&found.key_chain.viewing_public_key,
found.kind,
);
(expected_id == account_id).then_some(found)
})
}
@ -305,8 +309,11 @@ impl UserKeyChain {
.iter()
.flat_map(|(key, data)| {
data.accounts.keys().map(|kind| {
let account_id =
AccountId::for_private_account(&key.key_chain.nullifier_public_key, kind);
let account_id = AccountId::for_private_account(
&key.key_chain.nullifier_public_key,
&key.key_chain.viewing_public_key,
kind,
);
(account_id, &key.key_chain, key.chain_index.as_ref())
})
})
@ -346,8 +353,11 @@ impl UserKeyChain {
// For each (regular) found account the user owns, compute its nullifier and put
// into the map. This is the next nullifier it will look for.
for found in self.private_accounts() {
let account_id =
AccountId::for_private_account(&found.key_chain.nullifier_public_key, found.kind);
let account_id = AccountId::for_private_account(
&found.key_chain.nullifier_public_key,
&found.key_chain.viewing_public_key,
found.kind,
);
let nsk = found.key_chain.private_key_holder.nullifier_secret_key;
index.track(account_id, found.account, &nsk);
}
@ -496,8 +506,11 @@ impl UserKeyChain {
// Then try to update imported account
for (key, data) in &mut self.imported_private_accounts {
for (kind, imported_account) in &mut data.accounts {
let expected_id =
AccountId::for_private_account(&key.key_chain.nullifier_public_key, kind);
let expected_id = AccountId::for_private_account(
&key.key_chain.nullifier_public_key,
&key.key_chain.viewing_public_key,
kind,
);
if expected_id == account_id {
debug!("Updating imported private account {account_id}");
*imported_account = account;
@ -534,8 +547,11 @@ impl UserKeyChain {
// Node not yet in account_id_map — find it by checking all nodes
for (ci, node) in &mut self.private_key_tree.key_map {
let expected_id =
lee::AccountId::for_private_account(&node.value.0.nullifier_public_key, &kind);
let expected_id = lee::AccountId::for_private_account(
&node.value.0.nullifier_public_key,
&node.value.0.viewing_public_key,
&kind,
);
if expected_id == account_id {
match node.value.1.entry(kind) {
Entry::Occupied(mut occupied) => {
@ -585,8 +601,11 @@ impl UserKeyChain {
.iter()
.flat_map(|(key, data)| {
data.accounts.keys().map(|kind| {
let account_id =
AccountId::for_private_account(&key.key_chain.nullifier_public_key, kind);
let account_id = AccountId::for_private_account(
&key.key_chain.nullifier_public_key,
&key.key_chain.viewing_public_key,
kind,
);
(account_id, key.chain_index.as_ref())
})
})
@ -851,6 +870,7 @@ mod tests {
let identifier = 0;
let account_id = AccountId::for_private_account(
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
&PrivateAccountKind::Regular(identifier),
);
@ -913,7 +933,7 @@ mod tests {
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let nsk = keys.nullifier_secret_key;
let account_id = AccountId::from((&npk, identifier));
let account_id = AccountId::from((&npk, &vpk, identifier));
kc.insert_group_key_holder(label.clone(), holder);
let old_account = Account::default();
@ -975,6 +995,7 @@ mod tests {
let identifier = 0;
let account_id = AccountId::for_private_account(
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
&PrivateAccountKind::Regular(identifier),
);
let account = Account::default();
@ -1032,7 +1053,11 @@ mod tests {
let mut user_data = UserKeyChain::default();
let key_chain = KeyChain::new_os_random();
let account_id = AccountId::from((&key_chain.nullifier_public_key, 0));
let account_id = AccountId::from((
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
0,
));
let account = lee_core::account::Account::default();
user_data.add_imported_private_account(key_chain, None, 0, account);
@ -1047,7 +1072,11 @@ mod tests {
let mut user_data = UserKeyChain::default();
let key_chain = KeyChain::new_os_random();
let account_id = AccountId::from((&key_chain.nullifier_public_key, 0));
let account_id = AccountId::from((
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
0,
));
let account = lee_core::account::Account::default();
user_data.add_imported_private_account(key_chain, None, 0, account.clone());
@ -1092,7 +1121,11 @@ mod tests {
let mut user_data = UserKeyChain::default();
let key_chain = KeyChain::new_os_random();
let account_id = AccountId::from((&key_chain.nullifier_public_key, 0));
let account_id = AccountId::from((
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
0,
));
let new_account = lee_core::account::Account {
balance: 100,
@ -1113,7 +1146,11 @@ mod tests {
let mut user_data = UserKeyChain::default();
let key_chain = KeyChain::new_os_random();
let account_id1 = AccountId::from((&key_chain.nullifier_public_key, 0));
let account_id1 = AccountId::from((
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
0,
));
let account = lee_core::account::Account::default();
user_data.add_imported_private_account(key_chain, None, 0, account);

View File

@ -23,7 +23,11 @@ pub struct InitialPrivateAccountForWallet {
impl InitialPrivateAccountForWallet {
#[must_use]
pub fn account_id(&self) -> AccountId {
AccountId::from((&self.key_chain.nullifier_public_key, self.identifier))
AccountId::from((
&self.key_chain.nullifier_public_key,
&self.key_chain.viewing_public_key,
self.identifier,
))
}
}

View File

@ -11,7 +11,6 @@ workspace = true
[dev-dependencies]
key_protocol.workspace = true
lee_core = { workspace = true, features = ["host"] }
rand = { workspace = true }
criterion.workspace = true
[[bench]]

View File

@ -48,7 +48,8 @@ fn bench_encryption(c: &mut Criterion) {
let recipient_kc = KeyChain::new_os_random();
let npk = recipient_kc.nullifier_public_key;
let account = Account::default();
let account_id = AccountId::for_regular_private_account(&npk, 0);
let account_id =
AccountId::for_regular_private_account(&npk, &recipient_kc.viewing_public_key, 0);
let commitment = Commitment::new(&account_id, &account);
let (shared, _epk) = SharedSecretKey::encapsulate(&recipient_kc.viewing_public_key);
let kind = PrivateAccountKind::Regular(0_u128);