docs(lee): trim comments in PR3 diff, rebuild artifacts

Cut duplicated/wordy comments introduced across the PR3 rebase to a
single canonical explanation each, remove stray "see X" pointers in
favor of self-contained comments, and drop a task-label reference.
Rebuild every guest ELF/program artifact and the prebuilt sequencer
fixture to match (comments don't affect codegen, but keeping the
artifacts freshly rebuilt after each source touch).
This commit is contained in:
Marvin Jones
2026-08-22 14:27:26 -04:00
parent 75f2d7648a
commit 4c65fd0dd3
29 changed files with 72 additions and 117 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -55,9 +55,9 @@ pub struct ExecutionState {
/// call tree, remaining authorized throughout all calls.
globally_authorized: HashSet<AccountId>,
signer_account_ids: Vec<AccountId>,
/// This, not `post_states`, is what the circuit ultimately outputs for public accounts:
/// `post_states` is only ever used internally, to give a later call in the same chain a
/// concrete `pre_state` for an account an earlier call already touched.
/// One diff per public account touch; diffs are not aggregated for the same public account.
/// This is the circuit's real output for public accounts and replayed by the sequencer against
/// live state.
public_diffs: Vec<PublicDiff>,
}
@@ -408,21 +408,12 @@ impl ExecutionState {
pre_is_authorized,
);
}
// First sighting of a non-PDA-init account (`!has_private_pda_witness`):
// this still runs unconditionally for its caller-PDA-seed-matching side
// effects (a private PDA without an external seed legitimately authorizes
// via the caller's `pda_seeds`), but the signer-list-derived result is only
// *enforced* against `pre.is_authorized` for public accounts, inside
// `authorize_first_sight_without_pda_witness`: `is_authorized` has no
// downstream security consequence for private accounts (claim semantics are
// entirely bypassed for them), and a private `AccountId` can never
// legitimately appear in `signer_account_ids` (it's derived from `npk`/`vpk`,
// not a real-world signature).
//
// Replaces the guarantee live-state reconstruction used to provide: once the
// sequencer stops reconstructing pre-states from live state (PR3), this is
// the circuit's only independent check that `is_authorized` for a top-level
// public account was derived honestly rather than self-reported.
// Enforces that a first-sighted public account's `is_authorized` claim
// matches the real signer set instead of being trusted at face value: now
// that the sequencer no longer reconstructs pre-states from live state, this
// is the circuit's only remaining check that the claim was derived honestly
// rather than self-reported. Also covers a private PDA with no external seed,
// which can still be authorized via the caller's `pda_seeds`.
let is_public = account_identities
.get(pre_state_position)
.is_some_and(InputAccountIdentity::is_public);
@@ -476,11 +467,10 @@ impl ExecutionState {
// journal we can't derive locally — the resulting `data` — in the same order the host
// proved these receipts, matching this function's own traversal.
let data = if let Some(diff_data) = diff.diff_data.clone() {
// The diff's materialization logic belongs to the account's *owner* program, not
// necessarily the calling program — falling back to the caller only when the
// account is still unclaimed (default owner). Must match the host's own
// resolution in `circuit::execute_and_prove`, since that's the program whose ELF
// actually produced the receipt being checked below.
// Owned by the account's *owner* program, not necessarily the caller — falls
// back to the caller only when unclaimed (default owner). Must match the host's
// resolution in `circuit::execute_and_prove`, the program whose ELF actually
// produced the receipt checked below.
let owner_id: ProgramId = if pre_account.program_owner == DEFAULT_PROGRAM_OWNER {
program_id
} else {
@@ -603,14 +593,10 @@ impl ExecutionState {
authorized_accounts
}
/// Consume self and yield the validity windows, the per-position PDA seed/program map
/// (recorded during `derive_from_outputs`), the committed signer set, the raw per-call
/// public diffs, and an iterator over pre and (internally materialized) post states of every
/// account involved in the execution. The materialized post state is only authoritative for
/// private accounts — for public ones it was only ever needed internally, for
/// chain-threading; `public_diffs` is the real output for those. Returning everything
/// together keeps the fields module-private rather than forcing them visible to downstream
/// consumers.
/// Consumes self into a tuple so the fields stay module-private rather than exposed
/// individually. The returned post state is only authoritative for private accounts — for
/// public ones it was only ever needed internally, for chain-threading; `public_diffs` is
/// the real output for those.
#[expect(
clippy::type_complexity,
reason = "tuple bundles several exit values from one consuming call so all fields stay private; a struct would only rename it"
@@ -765,13 +751,11 @@ fn authorize_first_sight_without_pda_witness(
assert_family_binding(pda_family_binding, caller_program_id, seed, pre_account_id);
true
} else {
// Replaces the guarantee live-state reconstruction used to provide: once the sequencer
// stops reconstructing pre-states from live state, this is the circuit's only
// independent check that `is_authorized` for a top-level public account was derived
// honestly rather than self-reported. `is_authorized` has no downstream security
// consequence for private accounts (claim semantics are entirely bypassed for them),
// and a private `AccountId` can never legitimately appear in `signer_account_ids` (it's
// derived from `npk`/`vpk`, not a real-world signature), so the check is public-only.
// Replaces the check live-state reconstruction used to provide: this is the circuit's
// only remaining proof that a public account's `is_authorized` was derived honestly, not
// self-reported. Private accounts are exempt — `is_authorized` has no security
// consequence for them, and a private `AccountId` (derived from `npk`/`vpk`) can never
// legitimately appear in `signer_account_ids` anyway.
assert!(
!is_public || pre_is_authorized == is_signer,
"is_authorized for account {pre_account_id} doesn't match the canonical signer/PDA-authorization sources",
+2 -3
View File
@@ -42,9 +42,8 @@ pub fn compute_circuit_output(
{
match account_identity {
InputAccountIdentity::Public => {
// No longer exported: `pre_state` for public accounts is now witness-only. This
// account's effect is captured entirely by `execution_state`'s `public_diffs`
// (already merged into `output` above via `into_parts`).
// Nothing to add: `pre_state` is witness-only now, and the account's effect is
// already in `output.public_diffs` (merged above via `into_parts`).
}
InputAccountIdentity::Private(PrivateWitness {
vpk,
+10 -14
View File
@@ -33,11 +33,9 @@ pub struct PrivacyPreservingCircuitInput {
/// the pre-state), `diff_data` (known from the diff), and this `data`, then checks it via
/// `env::verify` — so this value is untrusted input, made trustworthy only by that check.
pub update_from_diff_results: Vec<Data>,
/// The accounts this transaction claims are signers. `is_authorized` for every account is
/// *derived* from membership in this single list — never accepted as an independent
/// per-account witness — and the list itself is committed to the output so the sequencer can
/// cross-check it against real signatures. Without that, a prover could satisfy
/// claim-eligibility's authorization check for an account it never actually controls.
/// Accounts this transaction claims as signers. Every account's `is_authorized` is derived
/// from membership here, not trusted individually — and the list is committed to the output
/// so the sequencer can cross-check it against real signatures.
pub signer_account_ids: Vec<AccountId>,
}
@@ -173,20 +171,18 @@ pub struct PrivateAction {
pub encrypted_post_state: EncryptedAccountData,
}
/// One call's raw, unaggregated diff to a public account.
/// One call's raw diff to a public account.
///
/// Deliberately not collapsed into one diff per account: `AccountDiff` has no "combine two
/// diffs" operation, especially for `diff_data`, which only composes by being applied in
/// sequence. The sequencer replays these one at a time against its own live state — never
/// trusting anything the circuit internally materialized for a public account, which is why the
/// account's *value* (as opposed to its diffs) never appears here.
/// Never aggregated per-account, since `AccountDiff` only composes by sequential application
/// (no "combine" operation, especially for `diff_data`). The sequencer replays these one at a
/// time against live state, never trusting what the circuit materialized, which is why only
/// diffs (not values) appear here.
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))]
pub struct PublicDiff {
pub account_id: AccountId,
/// Carried alongside the diff because the sequencer's replay-time authorization re-check
/// (and PDA claim resolution) needs to know which program produced it — the same role
/// `chained_call.program_id` plays in the public-transaction path's live materialize loop.
/// Needed for the sequencer's replay-time authorization re-check and PDA claim resolution —
/// the same role `chained_call.program_id` plays in the public-transaction path.
pub executing_program_id: ProgramId,
pub diff: AccountDiffOutput,
}
@@ -98,13 +98,10 @@ pub fn execute_and_prove_with_padded_inputs(
let mut program_outputs = Vec::new();
let mut update_from_diff_results = Vec::new();
// Captured before `pre_states` moves into `initial_call` below — this is the only place
// `is_authorized` is caller-supplied (every later pre_state's authorization is resolved
// inside the circuit from this same list), so it's also the only place we need to derive
// the committed signer set from. Scoped to public accounts only: this list is committed and
// cross-checked by the sequencer against real, signature-derived `AccountId`s, which a
// private (`npk`/`vpk`-derived) `AccountId` can never match — including a private account
// here would make it permanently unverifiable.
// Captured before `pre_states` moves below — this is the only place `is_authorized` is
// caller-supplied; every later pre_state's authorization is derived inside the circuit from
// this same list. Public accounts only: a private (`npk`/`vpk`-derived) `AccountId` could
// never match a real signature, so including one would make it permanently unverifiable.
let signer_account_ids: Vec<AccountId> = pre_states
.iter()
.zip(&account_identities)
@@ -141,9 +138,8 @@ pub fn execute_and_prove_with_padded_inputs(
// Prove `update_from_diff` for every account this call's diff writes data to, in the
// same order `execution_state::derive_from_outputs` will visit them, so
// `update_from_diff_results` lines up positionally with the circuit's own traversal.
// The diff's materialization logic belongs to the account's *owner* program, not
// necessarily the calling program — falling back to the caller only when the account
// is still unclaimed (default owner), mirroring the claim-eligibility rule elsewhere.
// Dispatched to the account's *owner* program, not necessarily the caller — falls back
// to the caller only when the account is still unclaimed (default owner).
for (pre, diff_output) in program_output
.pre_states
.iter()
@@ -101,10 +101,8 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
assert!(proof.is_valid_for(&output));
// The sender's `AccountDiff`, not a materialized post-state — this is the whole point of
// `AccountDiff`: the circuit never commits to a specific public post-state, only to what
// changed, so the sequencer can replay it against whatever the account's live state is by
// the time it processes this transaction.
// The sender's diff, not a materialized post-state — the whole point of `PublicDiff`: commit
// only to what changed, so the sequencer can replay it against live state.
let [public_diff] = output.public_diffs.try_into().unwrap();
assert_eq!(public_diff.account_id, expected_sender_pre.account_id);
assert_eq!(public_diff.executing_program_id, program.id());
@@ -16,8 +16,7 @@ const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00";
#[derive(Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct Message {
/// Raw, per-call, unaggregated diffs for public accounts. See
/// `PrivacyPreservingCircuitOutput::public_diffs`.
/// Per-call, unaggregated diffs for public accounts, copied from the circuit's output.
pub public_diffs: Vec<PublicDiff>,
pub nonces: Vec<Nonce>,
pub private_actions: Vec<PrivateAction>,
@@ -88,10 +87,8 @@ impl Message {
.collect()
}
/// The unique set of public accounts this transaction touches. `public_diffs` can
/// legitimately repeat an account (multiple calls touching the same account within one
/// transaction), so this dedups — callers rely on "each affected account listed once" (see
/// `PrivacyPreservingTransaction::affected_public_account_ids`).
/// The unique set of public accounts this transaction touches. `public_diffs` can repeat an
/// account across calls, so this dedups — callers depend on each account being listed once.
#[must_use]
pub fn public_account_ids(&self) -> Vec<AccountId> {
let mut seen = HashSet::new();
+7 -12
View File
@@ -865,10 +865,9 @@ fn delegated_public_pda_first_seen_in_callee_is_authorized() {
)
.expect("a caller's pda_seeds must authorize a public PDA it delegates at first sight");
// The callee ran with the PDA authorized (auth_asserting_noop did not panic). The
// pre-state's is_authorized view is no longer exported to the journal at all (removed
// along with public_pre_states — nothing left to audit once it isn't part of the proven
// output), so only the touched account's identity is left to check here.
// The callee ran with the PDA authorized (auth_asserting_noop didn't panic). `is_authorized`
// is no longer exported to the journal at all, so only the touched account's identity is
// left to check here.
assert_eq!(output.public_diffs.len(), 1);
assert_eq!(output.public_diffs[0].account_id, pda);
}
@@ -908,11 +907,8 @@ fn public_pda_first_sight_grant_does_not_extend_to_sibling_calls() {
fn public_account_first_sight_authorization_without_signer_backing_is_rejected() {
let seed = PdaSeed::new([77; 32]);
// Regression pin for the forgery PR3's signer check closes: `undeclaring_pda_delegator`
// self-declares `is_authorized = true` on a plain public account (not the seed's derived
// PDA, so no caller-seed match either) with no backing signer. Once `public_pre_states`
// stopped being exported, the circuit's signer-set check became the only thing standing
// between a self-reported claim and the proof succeeding — it must reject this.
// Regression pin: `undeclaring_pda_delegator` self-declares `is_authorized = true` with no
// caller-seed match and no backing signer — the signer-set check must reject this claim.
let result = undeclaring_public_delegation(
AccountId::new([9; 32]),
Some(seed),
@@ -931,9 +927,8 @@ fn wrong_seed_public_pda_first_sight_without_signer_backing_is_rejected() {
let delegator_id = crate::test_methods::undeclaring_pda_delegator().id();
let pda = AccountId::for_public_pda(&delegator_id, &seed);
// An unmatched seed falls back to the credential-claim path, which is subject to the same
// signer-backing requirement as any other first-sighted public account — regression pin,
// see `public_account_first_sight_authorization_without_signer_backing_is_rejected`.
// An unmatched seed falls back to the credential-claim path, which needs the same
// signer backing as any first-sighted public account.
let result = undeclaring_public_delegation(
pda,
Some(wrong_seed),
@@ -306,11 +306,9 @@ fn data_changer_program_should_fail_for_non_owned_account_in_privacy_preserving_
&program.into(),
);
// The account's owner ([0,1,2,3,4,5,6,7]) is not `data_changer` itself and isn't declared
// as a dependency, so the host can't resolve whose `update_from_diff` logic should interpret
// this diff — caught here, before the outer circuit is even proven, rather than surfacing as
// a `validate_execution` panic inside the circuit (`CircuitProvingError`) the way it would if
// the account were instead owned by some other *declared* program.
// The account's owner isn't `data_changer` and isn't a declared dependency, so the host
// can't resolve whose `update_from_diff` should run — caught here, before proving, rather
// than as a `validate_execution` panic inside the circuit.
assert!(matches!(
result,
Err(LeeError::InvalidProgramBehavior(
@@ -413,10 +413,9 @@ impl ValidatedStateDiff {
LeeError::OutOfValidityWindow
);
// Anchor the circuit's claimed signer set against real, cryptographically-verified
// signatures. Without this, a prover could satisfy claim-eligibility's authorization
// check for an account it never actually controls, since `is_authorized` inside the
// circuit is derived entirely from this list.
// Cross-checks the circuit's claimed signers against real signatures. `is_authorized`
// inside the circuit is derived entirely from this list, so without this check a prover
// could claim control of an account it doesn't own.
ensure!(
message
.signer_account_ids
@@ -436,12 +435,9 @@ impl ValidatedStateDiff {
// 5. Nullifier uniqueness
state.check_nullifiers_are_valid(&nullifiers)?;
// Replay each public diff against live state, one at a time never trusting anything
// the circuit internally materialized for a public account. This, not proof
// verification above, is what actually avoids tying this transaction's validity to a
// stale public-account snapshot: this step only cares about the diff's shape and this
// program's ownership, both independent of whatever pre-state the circuit witnessed
// while proving.
// Replay each public diff against live state, one at a time, never trusting what the
// circuit materialized — this is what actually decouples validity from a stale
// snapshot, checking only the diff's shape and ownership, not the witnessed pre-state.
let mut public_diff: HashMap<AccountId, Account> = HashMap::new();
for public_diff_entry in &message.public_diffs {
let account_id = public_diff_entry.account_id;
@@ -453,12 +449,11 @@ impl ValidatedStateDiff {
let diff = public_diff_entry.diff.diff();
let account_program_owner = pre_account.program_owner;
// `program_owner` is `AccountId`-typed; convert once up front rather than at each
// comparison below (see `From<ProgramId> for AccountId`'s doc comment).
// comparison below.
let executing_account_id = AccountId::from(executing_program_id);
// Re-check authorization against *live* state the circuit's own
// `validate_execution` check ran against a witnessed pre-state that isn't trusted
// for this purpose (see `check_privacy_preserving_circuit_proof_is_valid`).
// Re-check authorization against *live* state: the circuit's own `validate_execution`
// ran against a witnessed pre-state, which isn't trusted for this purpose.
ensure!(
!matches!(diff.diff_balance, BalanceDiff::Sub(amount) if amount > 0)
|| account_program_owner == executing_account_id,
@@ -612,10 +607,9 @@ fn authenticate_public_transaction_signers(
Ok(signer_account_ids)
}
/// Verifies the proof against exactly what the circuit witnessed and output. Public account
/// pre-states are a circuit-internal secret witness never committed to the journal at all, so
/// there is nothing here to reconcile against live state in the first place. Materialization
/// (which *does* use live state) happens separately, later, via `message.public_diffs`.
/// Verifies the proof against exactly what the circuit witnessed and output. Public pre-states
/// are a circuit-internal secret witness, never committed to the journal — materialization
/// (which *does* use live state) happens later, via `message.public_diffs`.
fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof,
message: &Message,
@@ -469,13 +469,11 @@ fn privacy_garbage_proof_is_rejected() {
}
}
/// The race condition this whole `AccountDiff` design exists to fix: a public account touched
/// by a privacy transaction changes on-chain *after* the proof was generated but *before* the
/// sequencer validates it (e.g. an unrelated public transfer into/out of the same account
/// landing first). Proof validity no longer depends on a specific public-account snapshot — only
/// on what the circuit itself witnessed and output — so the proof still verifies, and the
/// diff it carries gets replayed against whatever the live balance actually is by the time the
/// sequencer processes it, not the stale balance captured at proving time.
/// The race condition this whole `AccountDiff` design fixes: a public account changes on-chain
/// after proving but before sequencer validation (e.g. an unrelated transfer lands first). Proof
/// validity no longer depends on a snapshot — only on what the circuit witnessed — so the proof
/// still verifies, and its diff replays against the live balance at validation time, not the
/// stale one captured while proving.
#[test]
fn privacy_transaction_survives_public_state_changing_after_proving() {
use lee_core::{
Binary file not shown.