mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-25 03:11:21 +00:00
feat(ppc)!: introduce protocol-level changes for vpk binding
BREAKING: Before: The epk and the vpk of the receiver were not bound to the ss that was directly fed to the circuit. After: The ss, epk, tag fields are removed as explicit arguments per-account and instead replaced by supplying a vpk, esk per account. The ss, epk, tag all constructed in-circuit. Account ID generation now uses vpk as additional argument. Mitigation: Change Account ID generation to include the vpk, change proving inputs.
This commit is contained in:
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -63,11 +65,13 @@ impl ExecutionState {
|
||||
// in `account_identities`. The vec is documented as 1:1 with the program's pre_state
|
||||
// order, so position here matches `pre_state_position` used downstream in
|
||||
// `validate_and_sync_states`.
|
||||
let mut private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)> =
|
||||
HashMap::new();
|
||||
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, identifier)) = account_identity.npk_if_private_pda() {
|
||||
private_pda_npk_by_position.insert(pos, (npk, identifier));
|
||||
if let Some((npk, vpk, identifier)) = account_identity.npk_vpk_if_private_pda() {
|
||||
private_pda_by_position.insert(pos, (npk, vpk, identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,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(),
|
||||
};
|
||||
|
||||
@@ -289,7 +293,7 @@ impl ExecutionState {
|
||||
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,
|
||||
&self.private_pda_by_position,
|
||||
&mut self.authorized_accounts,
|
||||
pre_account_id,
|
||||
pre_state_position,
|
||||
@@ -309,6 +313,7 @@ impl ExecutionState {
|
||||
let external_seed = match account_identities.get(pre_state_position) {
|
||||
Some(InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
..
|
||||
@@ -317,6 +322,7 @@ impl ExecutionState {
|
||||
authority_program_id,
|
||||
seed,
|
||||
npk,
|
||||
vpk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -327,6 +333,7 @@ impl ExecutionState {
|
||||
}
|
||||
Some(InputAccountIdentity::PrivatePdaUpdate {
|
||||
nsk,
|
||||
vpk,
|
||||
identifier,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
..
|
||||
@@ -336,6 +343,7 @@ impl ExecutionState {
|
||||
authority_program_id,
|
||||
seed,
|
||||
&npk,
|
||||
vpk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -416,14 +424,19 @@ impl ExecutionState {
|
||||
match claim {
|
||||
Claim::Authorized => {}
|
||||
Claim::Pda(seed) => {
|
||||
let (npk, identifier) = self
|
||||
.private_pda_npk_by_position
|
||||
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, *identifier);
|
||||
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}"
|
||||
@@ -548,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,
|
||||
@@ -562,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));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralPublicKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
compute_digest_for_path,
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
|
||||
use crate::execution_state::ExecutionState;
|
||||
@@ -40,14 +41,13 @@ pub fn compute_circuit_output(
|
||||
output.public_post_states.push(post_state);
|
||||
}
|
||||
InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
vpk,
|
||||
esk,
|
||||
nsk,
|
||||
identifier,
|
||||
} => {
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
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, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@@ -72,23 +72,22 @@ pub fn compute_circuit_output(
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
&npk,
|
||||
vpk,
|
||||
esk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
vpk,
|
||||
esk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier,
|
||||
} => {
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
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, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@@ -110,21 +109,20 @@ pub fn compute_circuit_output(
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
&npk,
|
||||
vpk,
|
||||
esk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag,
|
||||
vpk,
|
||||
esk,
|
||||
npk,
|
||||
ssk,
|
||||
identifier,
|
||||
} => {
|
||||
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, pre_state.account_id, "AccountId mismatch");
|
||||
assert_eq!(
|
||||
@@ -149,25 +147,24 @@ pub fn compute_circuit_output(
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
npk,
|
||||
vpk,
|
||||
esk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag,
|
||||
npk: _,
|
||||
ssk,
|
||||
vpk,
|
||||
esk,
|
||||
npk,
|
||||
identifier,
|
||||
seed: _,
|
||||
} => {
|
||||
// The npk-to-account_id binding is established upstream in
|
||||
// `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds`
|
||||
// match. Here we only enforce the init pre-conditions. The supplied npk on
|
||||
// the variant has been recorded into `private_pda_npk_by_position` and used
|
||||
// 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!(
|
||||
@@ -200,17 +197,16 @@ pub fn compute_circuit_output(
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
npk,
|
||||
vpk,
|
||||
esk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
vpk,
|
||||
esk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier,
|
||||
@@ -235,6 +231,7 @@ pub fn compute_circuit_output(
|
||||
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");
|
||||
@@ -248,9 +245,9 @@ pub fn compute_circuit_output(
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
&npk,
|
||||
vpk,
|
||||
esk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
@@ -271,9 +268,9 @@ fn emit_private_output(
|
||||
post_state: Account,
|
||||
account_id: &AccountId,
|
||||
kind: &PrivateAccountKind,
|
||||
shared_secret: &SharedSecretKey,
|
||||
epk: &EphemeralPublicKey,
|
||||
view_tag: u8,
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
esk: &EphemeralSecretKey,
|
||||
new_nullifier: (Nullifier, CommitmentSetDigest),
|
||||
new_nonce: Nonce,
|
||||
) {
|
||||
@@ -283,10 +280,14 @@ fn emit_private_output(
|
||||
post_with_updated_nonce.nonce = new_nonce;
|
||||
|
||||
let commitment_post = Commitment::new(account_id, &post_with_updated_nonce);
|
||||
|
||||
let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(vpk, esk, *output_index);
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk);
|
||||
|
||||
let encrypted_account = EncryptionScheme::encrypt(
|
||||
&post_with_updated_nonce,
|
||||
kind,
|
||||
shared_secret,
|
||||
&shared_secret,
|
||||
&commitment_post,
|
||||
*output_index,
|
||||
);
|
||||
@@ -296,7 +297,7 @@ fn emit_private_output(
|
||||
.encrypted_private_post_states
|
||||
.push(EncryptedAccountData {
|
||||
ciphertext: encrypted_account,
|
||||
epk: epk.clone(),
|
||||
epk,
|
||||
view_tag,
|
||||
});
|
||||
*output_index = output_index
|
||||
|
||||
Reference in New Issue
Block a user