mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-25 03:11:21 +00:00
merge: resolve conflicts with main
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque, hash_map::Entry},
|
||||
collections::{HashMap, VecDeque, hash_map::Entry},
|
||||
convert::Infallible,
|
||||
};
|
||||
|
||||
use nssa_core::{
|
||||
InputAccountIdentity, NullifierPublicKey,
|
||||
Identifier, InputAccountIdentity, NullifierPublicKey,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID,
|
||||
@@ -20,17 +20,20 @@ pub struct ExecutionState {
|
||||
post_states: HashMap<AccountId, Account>,
|
||||
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)`
|
||||
/// check.
|
||||
/// 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,
|
||||
/// 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`
|
||||
/// under the private derivation. Binding is an idempotent property, not an event: the same
|
||||
/// position can legitimately be bound through both paths in the same tx (e.g. a program
|
||||
/// claims a private PDA and then delegates it to a callee), and the set uses `contains`,
|
||||
/// not `assert!(insert)`. After the main loop, every private-PDA position must appear in
|
||||
/// this set; otherwise the npk is unbound and the circuit rejects.
|
||||
private_pda_bound_positions: HashSet<usize>,
|
||||
/// claims a private PDA and then delegates it to a callee), and the map uses `contains_key`,
|
||||
/// not `assert!(insert)`. After the main loop, every private-PDA position must appear in this
|
||||
/// map; otherwise the npk is unbound and the circuit rejects.
|
||||
/// The stored `(ProgramId, PdaSeed)` is the owner program and seed, used in
|
||||
/// `compute_circuit_output` to construct `PrivateAccountKind::Pda { program_id, seed,
|
||||
/// identifier }`.
|
||||
private_pda_bound_positions: HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
/// Across the whole transaction, each `(program_id, seed)` pair may resolve to at most one
|
||||
/// `AccountId`. A seed under a program can derive a family of accounts, one public PDA and
|
||||
/// one private PDA per distinct npk. Without this check, a single `pda_seeds: [S]` entry in
|
||||
@@ -40,12 +43,12 @@ 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 that
|
||||
/// variant supplies for that position. Populated once in `derive_from_outputs` by walking
|
||||
/// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk,
|
||||
/// 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
|
||||
/// caller-seeds authorization paths to verify
|
||||
/// `AccountId::for_private_pda(program_id, seed, npk) == pre_state.account_id`.
|
||||
private_pda_npk_by_position: HashMap<usize, NullifierPublicKey>,
|
||||
/// `AccountId::for_private_pda(program_id, seed, npk, identifier) == pre_state.account_id`.
|
||||
private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)>,
|
||||
}
|
||||
|
||||
impl ExecutionState {
|
||||
@@ -55,14 +58,15 @@ impl ExecutionState {
|
||||
program_id: ProgramId,
|
||||
program_outputs: Vec<ProgramOutput>,
|
||||
) -> Self {
|
||||
// Build position → npk 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
|
||||
// Build position → (npk, identifier) map for private-PDA pre_states, indexed by position
|
||||
// in `account_identities`. The vec is documented as 1:1 with the program's pre_state
|
||||
// order, so position here matches `pre_state_position` used downstream in
|
||||
// `validate_and_sync_states`.
|
||||
let mut private_pda_npk_by_position: HashMap<usize, NullifierPublicKey> = HashMap::new();
|
||||
let mut private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)> =
|
||||
HashMap::new();
|
||||
for (pos, account_identity) in account_identities.iter().enumerate() {
|
||||
if let Some(npk) = account_identity.npk_if_private_pda() {
|
||||
private_pda_npk_by_position.insert(pos, npk);
|
||||
if let Some((npk, identifier)) = account_identity.npk_if_private_pda() {
|
||||
private_pda_npk_by_position.insert(pos, (npk, identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +104,7 @@ impl ExecutionState {
|
||||
post_states: HashMap::new(),
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
private_pda_bound_positions: HashSet::new(),
|
||||
private_pda_bound_positions: HashMap::new(),
|
||||
pda_family_binding: HashMap::new(),
|
||||
private_pda_npk_by_position,
|
||||
};
|
||||
@@ -203,7 +207,9 @@ impl ExecutionState {
|
||||
for (pos, account_identity) in account_identities.iter().enumerate() {
|
||||
if account_identity.is_private_pda() {
|
||||
assert!(
|
||||
execution_state.private_pda_bound_positions.contains(&pos),
|
||||
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"
|
||||
);
|
||||
}
|
||||
@@ -348,18 +354,24 @@ impl ExecutionState {
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
let npk = self
|
||||
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);
|
||||
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}"
|
||||
);
|
||||
self.private_pda_bound_positions.insert(pre_state_position);
|
||||
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,
|
||||
@@ -381,18 +393,21 @@ impl ExecutionState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume self and yield the validity windows alongside an iterator over pre and post
|
||||
/// states of each account involved in the execution. Returning the windows here keeps the
|
||||
/// Consume self and yield the validity windows, the per-position PDA seed/program map
|
||||
/// (recorded during `derive_from_outputs`), and an iterator over pre and post states of each
|
||||
/// account involved in the execution. Returning everything together keeps the
|
||||
/// fields module-private rather than forcing them visible to downstream consumers.
|
||||
pub fn into_parts(
|
||||
mut self,
|
||||
) -> (
|
||||
BlockValidityWindow,
|
||||
TimestampValidityWindow,
|
||||
HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
impl ExactSizeIterator<Item = (AccountWithMetadata, Account)>,
|
||||
) {
|
||||
let block_validity_window = self.block_validity_window;
|
||||
let timestamp_validity_window = self.timestamp_validity_window;
|
||||
let pda_seed_by_position = std::mem::take(&mut self.private_pda_bound_positions);
|
||||
let states_iter = self.pre_states.into_iter().map(move |pre| {
|
||||
let post = self
|
||||
.post_states
|
||||
@@ -403,6 +418,7 @@ impl ExecutionState {
|
||||
(
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
pda_seed_by_position,
|
||||
states_iter,
|
||||
)
|
||||
}
|
||||
@@ -436,6 +452,24 @@ fn assert_family_binding(
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_private_pda_position(
|
||||
map: &mut HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
position: usize,
|
||||
program_id: ProgramId,
|
||||
seed: PdaSeed,
|
||||
) {
|
||||
match map.entry(position) {
|
||||
Entry::Occupied(e) => assert_eq!(
|
||||
*e.get(),
|
||||
(program_id, seed),
|
||||
"Duplicate binding at position {position}: conflicting (program_id, seed)"
|
||||
),
|
||||
Entry::Vacant(e) => {
|
||||
e.insert((program_id, seed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the authorization state of a `pre_state` seen again in a chained call and record
|
||||
/// any resulting bindings. Returns `true` if the `pre_state` is authorized through either a
|
||||
/// previously-seen authorization or a matching caller seed (under the public or private
|
||||
@@ -451,8 +485,8 @@ fn assert_family_binding(
|
||||
)]
|
||||
fn resolve_authorization_and_record_bindings(
|
||||
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
private_pda_bound_positions: &mut HashSet<usize>,
|
||||
private_pda_npk_by_position: &HashMap<usize, NullifierPublicKey>,
|
||||
private_pda_bound_positions: &mut HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
private_pda_npk_by_position: &HashMap<usize, (NullifierPublicKey, Identifier)>,
|
||||
pre_account_id: AccountId,
|
||||
pre_state_position: usize,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
@@ -465,8 +499,9 @@ 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) = private_pda_npk_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk) == pre_account_id
|
||||
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
|
||||
{
|
||||
return Some((*seed, true, caller));
|
||||
}
|
||||
@@ -477,7 +512,12 @@ fn resolve_authorization_and_record_bindings(
|
||||
if let Some((seed, is_private_form, caller)) = matched_caller_seed {
|
||||
assert_family_binding(pda_family_binding, caller, seed, pre_account_id);
|
||||
if is_private_form {
|
||||
private_pda_bound_positions.insert(pre_state_position);
|
||||
bind_private_pda_position(
|
||||
private_pda_bound_positions,
|
||||
pre_state_position,
|
||||
caller,
|
||||
seed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
use nssa_core::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier,
|
||||
InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
|
||||
PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, InputAccountIdentity,
|
||||
MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
|
||||
PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
compute_digest_for_path,
|
||||
};
|
||||
|
||||
use crate::execution_state::ExecutionState;
|
||||
|
||||
// SECURITY: the non-PDA private variants below assert that the prover-supplied `identifier` is
|
||||
// not equal to this constant; the PDA variants pass it as the fixed identifier. This keeps the
|
||||
// `(npk, identifier)` account-id space disjoint from private-PDA accounts. Single source of
|
||||
// truth, do not redefine in another module.
|
||||
const PRIVATE_PDA_FIXED_IDENTIFIER: Identifier = u128::MAX;
|
||||
|
||||
pub fn compute_circuit_output(
|
||||
execution_state: ExecutionState,
|
||||
account_identities: &[InputAccountIdentity],
|
||||
) -> PrivacyPreservingCircuitOutput {
|
||||
let (block_validity_window, timestamp_validity_window, states_iter) =
|
||||
let (block_validity_window, timestamp_validity_window, pda_seed_by_position, states_iter) =
|
||||
execution_state.into_parts();
|
||||
let mut output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: Vec::new(),
|
||||
@@ -37,7 +31,9 @@ pub fn compute_circuit_output(
|
||||
);
|
||||
|
||||
let mut output_index = 0;
|
||||
for (account_identity, (pre_state, post_state)) in account_identities.iter().zip(states_iter) {
|
||||
for (pos, (account_identity, (pre_state, post_state))) in
|
||||
account_identities.iter().zip(states_iter).enumerate()
|
||||
{
|
||||
match account_identity {
|
||||
InputAccountIdentity::Public => {
|
||||
output.public_pre_states.push(pre_state);
|
||||
@@ -48,12 +44,8 @@ pub fn compute_circuit_output(
|
||||
nsk,
|
||||
identifier,
|
||||
} => {
|
||||
assert_ne!(
|
||||
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
|
||||
);
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let account_id = AccountId::from((&npk, *identifier));
|
||||
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@@ -77,7 +69,7 @@ pub fn compute_circuit_output(
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
*identifier,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
@@ -89,12 +81,8 @@ pub fn compute_circuit_output(
|
||||
membership_proof,
|
||||
identifier,
|
||||
} => {
|
||||
assert_ne!(
|
||||
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
|
||||
);
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let account_id = AccountId::from((&npk, *identifier));
|
||||
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@@ -115,7 +103,7 @@ pub fn compute_circuit_output(
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
*identifier,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
@@ -126,11 +114,7 @@ pub fn compute_circuit_output(
|
||||
ssk,
|
||||
identifier,
|
||||
} => {
|
||||
assert_ne!(
|
||||
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
|
||||
);
|
||||
let account_id = AccountId::from((npk, *identifier));
|
||||
let account_id = AccountId::for_regular_private_account(npk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert_eq!(
|
||||
@@ -154,13 +138,17 @@ pub fn compute_circuit_output(
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
*identifier,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaInit { npk: _, ssk } => {
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk: _,
|
||||
ssk,
|
||||
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
|
||||
@@ -184,12 +172,19 @@ pub fn compute_circuit_output(
|
||||
let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
|
||||
|
||||
let account_id = pre_state.account_id;
|
||||
let (pda_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,
|
||||
PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id: *pda_program_id,
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
@@ -199,6 +194,7 @@ pub fn compute_circuit_output(
|
||||
ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier,
|
||||
} => {
|
||||
// The npk binding is established upstream. Authorization must already be set;
|
||||
// an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an
|
||||
@@ -218,12 +214,19 @@ 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 (pda_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,
|
||||
PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id: *pda_program_id,
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
@@ -244,7 +247,7 @@ fn emit_private_output(
|
||||
output_index: &mut u32,
|
||||
post_state: Account,
|
||||
account_id: &AccountId,
|
||||
identifier: Identifier,
|
||||
kind: &PrivateAccountKind,
|
||||
shared_secret: &SharedSecretKey,
|
||||
new_nullifier: (Nullifier, CommitmentSetDigest),
|
||||
new_nonce: Nonce,
|
||||
@@ -257,7 +260,7 @@ fn emit_private_output(
|
||||
let commitment_post = Commitment::new(account_id, &post_with_updated_nonce);
|
||||
let encrypted_account = EncryptionScheme::encrypt(
|
||||
&post_with_updated_nonce,
|
||||
identifier,
|
||||
kind,
|
||||
shared_secret,
|
||||
&commitment_post,
|
||||
*output_index,
|
||||
|
||||
Reference in New Issue
Block a user