mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-27 04:11:08 +00:00
feat(lee): decouple privacy proof verification from live public state (incremental update PR3)
The privacy-preserving circuit's output no longer commits materialized public-account post-states; it commits the raw AccountDiffs it produced instead, plus a signer_account_ids set the sequencer cross-checks against real signatures. Proof verification now checks the proof against exactly what the circuit witnessed, with no dependency on live sequencer state, fixing the race condition where an unrelated public transaction landing between proving and validation would invalidate an otherwise-valid proof. Materialization moves to a separate step that replays public_diffs against whatever the live account state actually is at apply time, mirroring how the public-transaction path already applies diffs. Also updates lez/indexer and lez/explorer_service for the new Message shape, and fixes the pre-existing clippy debt this surfaced across lez/programs (needless_pass_by_value/missing_const_for_fn/etc.) now that the workspace builds and lints clean end to end.
This commit is contained in:
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.
Binary file not shown.
@@ -4,7 +4,7 @@ use std::{
|
||||
};
|
||||
|
||||
use lee_core::{
|
||||
Identifier, InputAccountIdentity, NullifierPublicKey, PrivateWitness, WitnessKind,
|
||||
Identifier, InputAccountIdentity, NullifierPublicKey, PrivateWitness, PublicDiff, WitnessKind,
|
||||
account::{Account, AccountId, AccountWithMetadata, Data, apply_balance_diff},
|
||||
encryption::ViewingPublicKey,
|
||||
program::{
|
||||
@@ -54,6 +54,16 @@ pub struct ExecutionState {
|
||||
/// The set containing non-PDA accounts authorized at their first sight, anywhere in the
|
||||
/// call tree, remaining authorized throughout all calls.
|
||||
globally_authorized: HashSet<AccountId>,
|
||||
/// The canonical, committed signer set. `is_authorized` for a public account's first sighting
|
||||
/// is derived from membership in this list (or PDA-authorization via a caller's seeds) —
|
||||
/// never trusted as an independently-reported witness — and the list itself is echoed in the
|
||||
/// circuit's output so the sequencer can cross-check it against real signatures.
|
||||
signer_account_ids: Vec<AccountId>,
|
||||
/// Raw, per-call, unaggregated diffs for public accounts. 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. See `PrivacyPreservingCircuitOutput::public_diffs`.
|
||||
public_diffs: Vec<PublicDiff>,
|
||||
}
|
||||
|
||||
impl ExecutionState {
|
||||
@@ -62,6 +72,7 @@ impl ExecutionState {
|
||||
account_identities: &[InputAccountIdentity],
|
||||
program_id: ProgramId,
|
||||
program_outputs: Vec<ProgramOutput>,
|
||||
signer_account_ids: &[AccountId],
|
||||
update_from_diff_results: Vec<Data>,
|
||||
) -> Self {
|
||||
let mut update_from_diff_results: VecDeque<Data> = update_from_diff_results.into();
|
||||
@@ -117,6 +128,8 @@ impl ExecutionState {
|
||||
pda_family_binding: HashMap::new(),
|
||||
private_pda_by_position,
|
||||
globally_authorized: HashSet::new(),
|
||||
signer_account_ids: signer_account_ids.to_vec(),
|
||||
public_diffs: Vec::new(),
|
||||
};
|
||||
|
||||
let Some(first_output) = program_outputs.first() else {
|
||||
@@ -400,6 +413,24 @@ 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.
|
||||
let is_public = account_identities
|
||||
.get(pre_state_position)
|
||||
.is_some_and(InputAccountIdentity::is_public);
|
||||
if !has_private_pda_witness
|
||||
&& authorize_first_sight_without_pda_witness(
|
||||
&mut self.pda_family_binding,
|
||||
@@ -408,6 +439,8 @@ impl ExecutionState {
|
||||
caller_pda_seeds,
|
||||
pre_account_id,
|
||||
pre_is_authorized,
|
||||
is_public,
|
||||
self.signer_account_ids.contains(&pre_account_id),
|
||||
)
|
||||
{
|
||||
// authorize_first_sight_without_pda_witness is only true for PDAs
|
||||
@@ -428,6 +461,13 @@ impl ExecutionState {
|
||||
authorized_output_accounts.push(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];
|
||||
|
||||
let diff = diff_output.diff();
|
||||
|
||||
let balance = apply_balance_diff(pre_account.balance, diff.diff_balance)
|
||||
@@ -469,13 +509,6 @@ impl ExecutionState {
|
||||
"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 => {
|
||||
@@ -544,6 +577,14 @@ impl ExecutionState {
|
||||
pre_account.program_owner
|
||||
};
|
||||
|
||||
if account_identity.is_public() {
|
||||
self.public_diffs.push(PublicDiff {
|
||||
account_id: pre_account_id,
|
||||
executing_program_id: program_id,
|
||||
diff: diff_output,
|
||||
});
|
||||
}
|
||||
|
||||
post_states_entry.insert_entry(Account {
|
||||
program_owner: post_program_owner,
|
||||
balance,
|
||||
@@ -558,12 +599,16 @@ impl ExecutionState {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// (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.
|
||||
#[expect(
|
||||
clippy::type_complexity,
|
||||
reason = "tuple bundles four exit values from one consuming call so all fields stay private; a struct would only rename it"
|
||||
reason = "tuple bundles several exit values from one consuming call so all fields stay private; a struct would only rename it"
|
||||
)]
|
||||
pub fn into_parts(
|
||||
mut self,
|
||||
@@ -571,11 +616,15 @@ impl ExecutionState {
|
||||
BlockValidityWindow,
|
||||
TimestampValidityWindow,
|
||||
HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
Vec<AccountId>,
|
||||
Vec<PublicDiff>,
|
||||
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 signer_account_ids = std::mem::take(&mut self.signer_account_ids);
|
||||
let public_diffs = std::mem::take(&mut self.public_diffs);
|
||||
let states_iter = self.pre_states.into_iter().map(move |pre| {
|
||||
let post = self
|
||||
.post_states
|
||||
@@ -587,6 +636,8 @@ impl ExecutionState {
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
pda_seed_by_position,
|
||||
signer_account_ids,
|
||||
public_diffs,
|
||||
states_iter,
|
||||
)
|
||||
}
|
||||
@@ -689,6 +740,8 @@ fn authorize_first_sight_without_pda_witness(
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
pre_account_id: AccountId,
|
||||
pre_is_authorized: bool,
|
||||
is_public: bool,
|
||||
is_signer: bool,
|
||||
) -> bool {
|
||||
if let Some((seed, caller_program_id)) =
|
||||
match_caller_seed_as_public_pda(caller, caller_pda_seeds, pre_account_id)
|
||||
@@ -700,6 +753,17 @@ 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.
|
||||
assert!(
|
||||
!is_public || pre_is_authorized == is_signer,
|
||||
"is_authorized for account {pre_account_id} doesn't match the canonical signer/PDA-authorization sources",
|
||||
);
|
||||
// If an authorized account is a non-PDA one, it is globally authorized.
|
||||
if pre_is_authorized {
|
||||
globally_authorized.insert(pre_account_id);
|
||||
|
||||
@@ -11,12 +11,14 @@ fn main() {
|
||||
program_id,
|
||||
dummy_inputs,
|
||||
update_from_diff_results,
|
||||
signer_account_ids,
|
||||
} = env::read();
|
||||
|
||||
let execution_state = execution_state::ExecutionState::derive_from_outputs(
|
||||
&account_identities,
|
||||
program_id,
|
||||
program_outputs,
|
||||
&signer_account_ids,
|
||||
update_from_diff_results,
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use lee_core::{
|
||||
Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, NullifierWitness, PrivacyPreservingCircuitOutput, PrivateAccountKind,
|
||||
PrivateAction, PrivateWitness, PublicAction, SharedSecretKey, WitnessKind,
|
||||
PrivateAction, PrivateWitness, SharedSecretKey, WitnessKind,
|
||||
account::{Account, AccountId, Nonce},
|
||||
compute_digest_for_path,
|
||||
encryption::{ViewTag, ViewingPublicKey},
|
||||
@@ -15,13 +15,21 @@ pub fn compute_circuit_output(
|
||||
account_identities: &[InputAccountIdentity],
|
||||
dummy_inputs: Vec<DummyInput>,
|
||||
) -> PrivacyPreservingCircuitOutput {
|
||||
let (block_validity_window, timestamp_validity_window, pda_seed_by_position, states_iter) =
|
||||
execution_state.into_parts();
|
||||
let (
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
pda_seed_by_position,
|
||||
signer_account_ids,
|
||||
public_diffs,
|
||||
states_iter,
|
||||
) = execution_state.into_parts();
|
||||
let mut output = PrivacyPreservingCircuitOutput {
|
||||
public_actions: Vec::new(),
|
||||
public_pre_states: Vec::new(),
|
||||
public_diffs,
|
||||
private_actions: Vec::new(),
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
signer_account_ids,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -35,10 +43,7 @@ pub fn compute_circuit_output(
|
||||
{
|
||||
match account_identity {
|
||||
InputAccountIdentity::Public => {
|
||||
output.public_actions.push(PublicAction {
|
||||
pre: pre_state,
|
||||
post: post_state,
|
||||
});
|
||||
output.public_pre_states.push(pre_state);
|
||||
}
|
||||
InputAccountIdentity::Private(PrivateWitness {
|
||||
vpk,
|
||||
|
||||
@@ -123,7 +123,7 @@ pub struct Account {
|
||||
pub nonce: Nonce,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub struct AccountWithMetadata {
|
||||
pub account: Account,
|
||||
pub is_authorized: bool,
|
||||
|
||||
@@ -4,9 +4,12 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
AuthorizationSecretKey, Commitment, CommitmentSetDigest, Identifier, MembershipProof,
|
||||
Nullifier, NullifierPublicKey, NullifierSecretKey,
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
account::{AccountId, AccountWithMetadata, Data},
|
||||
encryption::{EncryptedAccountData, ViewTag, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
|
||||
program::{
|
||||
AccountDiffOutput, BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput,
|
||||
TimestampValidityWindow,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -30,6 +33,12 @@ 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.
|
||||
pub signer_account_ids: Vec<AccountId>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@@ -164,20 +173,40 @@ pub struct PrivateAction {
|
||||
pub encrypted_post_state: EncryptedAccountData,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
pub struct PublicAction {
|
||||
pub pre: AccountWithMetadata,
|
||||
pub post: Account,
|
||||
/// One call's raw, unaggregated 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.
|
||||
#[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.
|
||||
pub executing_program_id: ProgramId,
|
||||
pub diff: AccountDiffOutput,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq, Default))]
|
||||
pub struct PrivacyPreservingCircuitOutput {
|
||||
pub public_actions: Vec<PublicAction>,
|
||||
/// What the circuit witnessed as each public account's pre-state — deliberately *not*
|
||||
/// reconciled against live sequencer state. Used only to verify the proof is internally
|
||||
/// consistent (see `check_privacy_preserving_circuit_proof_is_valid`); materialization uses
|
||||
/// live state instead, via `public_diffs`, which is what actually avoids tying this
|
||||
/// transaction's validity to a specific public-account snapshot.
|
||||
pub public_pre_states: Vec<AccountWithMetadata>,
|
||||
pub public_diffs: Vec<PublicDiff>,
|
||||
pub private_actions: Vec<PrivateAction>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
pub timestamp_validity_window: TimestampValidityWindow,
|
||||
/// Committed so the sequencer can verify every account this circuit treated as authorized
|
||||
/// really did sign the transaction — see `PrivacyPreservingCircuitInput::signer_account_ids`.
|
||||
pub signer_account_ids: Vec<AccountId>,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "host", test))]
|
||||
@@ -216,51 +245,56 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
Commitment, Nullifier,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce},
|
||||
account::{Account, AccountDiff, AccountId, AccountWithMetadata, BalanceDiff, Nonce},
|
||||
encryption::{Ciphertext, EphemeralPublicKey},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn privacy_preserving_circuit_output_to_bytes_is_compatible_with_from_slice() {
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_actions: vec![
|
||||
PublicAction {
|
||||
pre: AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
|
||||
balance: 12_345_678_901_234_567_890,
|
||||
data: b"test data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFE),
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
),
|
||||
post: Account {
|
||||
public_pre_states: vec![
|
||||
AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
|
||||
balance: 100,
|
||||
data: b"post state data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF),
|
||||
balance: 12_345_678_901_234_567_890,
|
||||
data: b"test data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFE),
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
),
|
||||
AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [9, 9, 9, 8, 8, 8, 7, 7].into(),
|
||||
balance: 123_123_123_456_456_567_112,
|
||||
data: b"test data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(9_999_999_999_999_999_999_999),
|
||||
},
|
||||
false,
|
||||
AccountId::new([1; 32]),
|
||||
),
|
||||
],
|
||||
public_diffs: vec![
|
||||
PublicDiff {
|
||||
account_id: AccountId::new([0; 32]),
|
||||
executing_program_id: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
diff: AccountDiffOutput::new(AccountDiff {
|
||||
id: AccountId::new([0; 32]),
|
||||
diff_balance: BalanceDiff::Add(100),
|
||||
diff_data: Some(b"post state data".to_vec().try_into().unwrap()),
|
||||
}),
|
||||
},
|
||||
PublicAction {
|
||||
pre: AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [9, 9, 9, 8, 8, 8, 7, 7].into(),
|
||||
balance: 123_123_123_456_456_567_112,
|
||||
data: b"test data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(9_999_999_999_999_999_999_999),
|
||||
},
|
||||
false,
|
||||
AccountId::new([1; 32]),
|
||||
),
|
||||
post: Account {
|
||||
program_owner: [2, 3, 4, 5, 6, 7, 8, 9].into(),
|
||||
balance: 200,
|
||||
data: b"post state data 2".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFD),
|
||||
},
|
||||
PublicDiff {
|
||||
account_id: AccountId::new([1; 32]),
|
||||
executing_program_id: [2, 3, 4, 5, 6, 7, 8, 9],
|
||||
diff: AccountDiffOutput::new(AccountDiff {
|
||||
id: AccountId::new([1; 32]),
|
||||
diff_balance: BalanceDiff::Sub(200),
|
||||
diff_data: Some(b"post state data 2".to_vec().try_into().unwrap()),
|
||||
}),
|
||||
},
|
||||
],
|
||||
signer_account_ids: vec![AccountId::new([0; 32])],
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier: Nullifier::for_account_update(
|
||||
&Commitment::new(&AccountId::new([2; 32]), &Account::default()),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
pub use circuit_io::{
|
||||
DummyInput, InputAccountIdentity, NullifierWitness, PrivacyPreservingCircuitInput,
|
||||
PrivacyPreservingCircuitOutput, PrivateAction, PrivateWitness, PublicAction, WitnessKind,
|
||||
PrivacyPreservingCircuitOutput, PrivateAction, PrivateWitness, PublicDiff, WitnessKind,
|
||||
};
|
||||
pub use commitment::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof,
|
||||
|
||||
@@ -4,8 +4,11 @@ use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput,
|
||||
PrivacyPreservingCircuitOutput,
|
||||
account::AccountWithMetadata,
|
||||
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput, UpdateFromDiffOutput},
|
||||
account::{AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
ChainedCall, DEFAULT_PROGRAM_OWNER, InstructionData, ProgramId, ProgramOutput,
|
||||
UpdateFromDiffOutput,
|
||||
},
|
||||
};
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
@@ -95,6 +98,20 @@ 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.
|
||||
let signer_account_ids: Vec<AccountId> = pre_states
|
||||
.iter()
|
||||
.zip(&account_identities)
|
||||
.filter(|(pre, identity)| pre.is_authorized && identity.is_public())
|
||||
.map(|(pre, _)| pre.account_id)
|
||||
.collect();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: initial_program.id(),
|
||||
instruction_data,
|
||||
@@ -124,6 +141,9 @@ 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.
|
||||
for (pre, diff_output) in program_output
|
||||
.pre_states
|
||||
.iter()
|
||||
@@ -132,7 +152,21 @@ pub fn execute_and_prove_with_padded_inputs(
|
||||
let Some(diff_data) = diff_output.diff().diff_data.clone() else {
|
||||
continue;
|
||||
};
|
||||
let update_receipt = program.prove_update_from_diff(&pre.account, &diff_data)?;
|
||||
let owner_id: ProgramId = if pre.account.program_owner == DEFAULT_PROGRAM_OWNER {
|
||||
chained_call.program_id
|
||||
} else {
|
||||
pre.account.program_owner.into()
|
||||
};
|
||||
let owner_program = if owner_id == program.id() {
|
||||
program
|
||||
} else {
|
||||
dependencies.get(&owner_id).ok_or(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency {
|
||||
program_id: owner_id,
|
||||
},
|
||||
)?
|
||||
};
|
||||
let update_receipt = owner_program.prove_update_from_diff(&pre.account, &diff_data)?;
|
||||
let update_output: UpdateFromDiffOutput = update_receipt
|
||||
.journal
|
||||
.decode()
|
||||
@@ -167,6 +201,7 @@ pub fn execute_and_prove_with_padded_inputs(
|
||||
program_id: program_with_dependencies.program.id(),
|
||||
dummy_inputs,
|
||||
update_from_diff_results,
|
||||
signer_account_ids,
|
||||
};
|
||||
|
||||
env_builder.write(&circuit_input).unwrap();
|
||||
|
||||
@@ -4,7 +4,7 @@ use lee_core::{
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, EphemeralSecretKey,
|
||||
Nullifier, NullifierPublicKey, NullifierWitness, PrivacyPreservingCircuitOutput,
|
||||
PrivateWitness, SharedSecretKey, WitnessKind,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
account::{Account, AccountId, AccountWithMetadata, BalanceDiff, Nonce, data::Data},
|
||||
program::{PdaSeed, PrivateAccountKind},
|
||||
};
|
||||
|
||||
@@ -64,13 +64,6 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
|
||||
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
let expected_sender_post = Account {
|
||||
program_owner: program.id().into(),
|
||||
balance: 100 - balance_to_move,
|
||||
nonce: Nonce::default(),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let expected_recipient_post = Account {
|
||||
program_owner: program.id().into(),
|
||||
balance: balance_to_move,
|
||||
@@ -108,10 +101,23 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
|
||||
let [action] = output.public_actions.try_into().unwrap();
|
||||
let (sender_pre, sender_post) = (action.pre, action.post);
|
||||
let [sender_pre] = output.public_pre_states.try_into().unwrap();
|
||||
assert_eq!(sender_pre, expected_sender_pre);
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
|
||||
// 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.
|
||||
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());
|
||||
assert_eq!(
|
||||
public_diff.diff.diff().diff_balance,
|
||||
BalanceDiff::Sub(balance_to_move)
|
||||
);
|
||||
assert!(public_diff.diff.diff().diff_data.is_none());
|
||||
assert!(public_diff.diff.required_claim().is_none());
|
||||
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
@@ -230,7 +236,8 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
assert!(output.public_actions.is_empty());
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_diffs.is_empty());
|
||||
let sender_nullifier = expected_new_nullifiers[0].0;
|
||||
let recipient_nullifier = expected_new_nullifiers[1].0;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, PrivateAction,
|
||||
account::{Account, Nonce},
|
||||
PublicDiff,
|
||||
account::{AccountWithMetadata, Nonce},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
pub use lee_core::{EncryptedAccountData, ViewTag};
|
||||
@@ -11,19 +12,24 @@ use crate::AccountId;
|
||||
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PublicActionWithID {
|
||||
pub account_id: AccountId,
|
||||
pub post_state: Account,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub public_actions: Vec<PublicActionWithID>,
|
||||
/// What the circuit witnessed as each public account's pre-state — deliberately *not*
|
||||
/// reconciled against live sequencer state. Used only to verify the proof is internally
|
||||
/// consistent (see `check_privacy_preserving_circuit_proof_is_valid`); materialization uses
|
||||
/// live state instead, via `public_diffs` below, which is what actually avoids tying this
|
||||
/// transaction's validity to a specific public-account snapshot.
|
||||
pub public_pre_states: Vec<AccountWithMetadata>,
|
||||
/// Raw, per-call, unaggregated diffs for public accounts. See
|
||||
/// `PrivacyPreservingCircuitOutput::public_diffs`.
|
||||
pub public_diffs: Vec<PublicDiff>,
|
||||
pub nonces: Vec<Nonce>,
|
||||
pub private_actions: Vec<PrivateAction>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
pub timestamp_validity_window: TimestampValidityWindow,
|
||||
/// The accounts the circuit claims are signers — cross-checked by the sequencer against real
|
||||
/// signatures. See `PrivacyPreservingCircuitOutput::signer_account_ids`.
|
||||
pub signer_account_ids: Vec<AccountId>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Message {
|
||||
@@ -47,11 +53,13 @@ impl std::fmt::Debug for Message {
|
||||
})
|
||||
.collect();
|
||||
f.debug_struct("Message")
|
||||
.field("public_actions", &self.public_actions)
|
||||
.field("public_pre_states", &self.public_pre_states)
|
||||
.field("public_diffs", &self.public_diffs)
|
||||
.field("nonces", &self.nonces)
|
||||
.field("private_actions", &private_actions)
|
||||
.field("block_validity_window", &self.block_validity_window)
|
||||
.field("timestamp_validity_window", &self.timestamp_validity_window)
|
||||
.field("signer_account_ids", &self.signer_account_ids)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -59,20 +67,14 @@ impl std::fmt::Debug for Message {
|
||||
impl Message {
|
||||
#[must_use]
|
||||
pub fn from_circuit_output(nonces: Vec<Nonce>, output: PrivacyPreservingCircuitOutput) -> Self {
|
||||
let public_actions = output
|
||||
.public_actions
|
||||
.into_iter()
|
||||
.map(|action| PublicActionWithID {
|
||||
account_id: action.pre.account_id,
|
||||
post_state: action.post,
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
public_actions,
|
||||
public_pre_states: output.public_pre_states,
|
||||
public_diffs: output.public_diffs,
|
||||
nonces,
|
||||
private_actions: output.private_actions,
|
||||
block_validity_window: output.block_validity_window,
|
||||
timestamp_validity_window: output.timestamp_validity_window,
|
||||
signer_account_ids: output.signer_account_ids,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +94,15 @@ impl Message {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The unique set of public accounts this transaction touches — sourced from
|
||||
/// `public_pre_states`, not `public_diffs`, since a diff can legitimately repeat an account
|
||||
/// (multiple calls touching the same account within one transaction), while a pre-state is
|
||||
/// witnessed exactly once per account.
|
||||
#[must_use]
|
||||
pub fn public_account_ids(&self) -> Vec<AccountId> {
|
||||
self.public_actions
|
||||
self.public_pre_states
|
||||
.iter()
|
||||
.map(|action| action.account_id)
|
||||
.map(|pre| pre.account_id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -120,14 +126,14 @@ impl Message {
|
||||
pub mod tests {
|
||||
use lee_core::{
|
||||
Commitment, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey, Nullifier,
|
||||
NullifierPublicKey, PrivateAccountKind, PrivateAction, SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
NullifierPublicKey, PrivateAccountKind, PrivateAction, PublicDiff, SharedSecretKey,
|
||||
account::{Account, AccountDiff, AccountId, AccountWithMetadata, BalanceDiff, Nonce},
|
||||
encryption::{Ciphertext, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
program::{AccountDiffOutput, BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
use super::{EncryptedAccountData, Message, PREFIX, PublicActionWithID};
|
||||
use super::{EncryptedAccountData, Message, PREFIX};
|
||||
|
||||
#[must_use]
|
||||
pub fn message_for_tests() -> Message {
|
||||
@@ -150,10 +156,21 @@ pub mod tests {
|
||||
let old_commitment = Commitment::new(&account_id1, &account1);
|
||||
let nullifier = Nullifier::for_account_update(&old_commitment, &nsk1);
|
||||
|
||||
let public_account_id = AccountId::new([1; 32]);
|
||||
Message {
|
||||
public_actions: vec![PublicActionWithID {
|
||||
account_id: AccountId::new([1; 32]),
|
||||
post_state: Account::default(),
|
||||
public_pre_states: vec![AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
public_account_id,
|
||||
)],
|
||||
public_diffs: vec![PublicDiff {
|
||||
account_id: public_account_id,
|
||||
executing_program_id: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
diff: AccountDiffOutput::new(AccountDiff {
|
||||
id: public_account_id,
|
||||
diff_balance: BalanceDiff::Add(0),
|
||||
diff_data: None,
|
||||
}),
|
||||
}],
|
||||
nonces,
|
||||
private_actions: vec![PrivateAction {
|
||||
@@ -168,32 +185,36 @@ pub mod tests {
|
||||
}],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
signer_account_ids: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_privacy_pinned() {
|
||||
let msg = Message {
|
||||
public_actions: vec![],
|
||||
public_pre_states: vec![],
|
||||
public_diffs: vec![],
|
||||
nonces: vec![Nonce(5)],
|
||||
private_actions: vec![],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
signer_account_ids: vec![],
|
||||
};
|
||||
|
||||
// empty vec fields: u32 len=0
|
||||
let public_actions_bytes: &[u8] = &[0, 0, 0, 0];
|
||||
let empty_vec_bytes: &[u8] = &[0, 0, 0, 0];
|
||||
let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
let private_actions_bytes: &[u8] = &[0, 0, 0, 0];
|
||||
// validity windows: unbounded = {from: None (0_u8), to: None (0_u8)}
|
||||
let unbounded_window_bytes: &[u8] = &[0, 0];
|
||||
|
||||
let expected_borsh_vec: Vec<u8> = [
|
||||
public_actions_bytes,
|
||||
empty_vec_bytes, // public_pre_states
|
||||
empty_vec_bytes, // public_diffs
|
||||
nonces_bytes,
|
||||
private_actions_bytes,
|
||||
empty_vec_bytes, // private_actions
|
||||
unbounded_window_bytes, // block_validity_window
|
||||
unbounded_window_bytes, // timestamp_validity_window
|
||||
empty_vec_bytes, // signer_account_ids
|
||||
]
|
||||
.concat();
|
||||
let expected_borsh: &[u8] = &expected_borsh_vec;
|
||||
|
||||
@@ -53,12 +53,7 @@ impl PrivacyPreservingTransaction {
|
||||
.signer_account_ids()
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
acc_set.extend(
|
||||
self.message
|
||||
.public_actions
|
||||
.iter()
|
||||
.map(|action| action.account_id),
|
||||
);
|
||||
acc_set.extend(self.message.public_account_ids());
|
||||
|
||||
acc_set.into_iter().collect()
|
||||
}
|
||||
|
||||
@@ -448,7 +448,8 @@ fn private_pda_claim_succeeds() {
|
||||
|
||||
let (output, _proof) = result.expect("private PDA claim should succeed");
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
assert!(output.public_actions.is_empty());
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_diffs.is_empty());
|
||||
}
|
||||
|
||||
/// An npk is supplied that does not match the `pre_state`'s `account_id` under
|
||||
|
||||
@@ -306,7 +306,17 @@ fn data_changer_program_should_fail_for_non_owned_account_in_privacy_preserving_
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
// 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.
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency { .. }
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,11 +5,11 @@ use std::{
|
||||
};
|
||||
|
||||
use lee_core::{
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, PublicAction, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata, apply_balance_diff},
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata, BalanceDiff, apply_balance_diff},
|
||||
program::{
|
||||
CallerData, ChainedCall, Claim, DEFAULT_PROGRAM_OWNER, compute_public_authorized_pdas,
|
||||
validate_execution,
|
||||
CallerData, ChainedCall, Claim, DEFAULT_PROGRAM_OWNER, ExecutionValidationError,
|
||||
ProgramId, compute_public_authorized_pdas, validate_execution,
|
||||
},
|
||||
};
|
||||
use log::debug;
|
||||
@@ -224,8 +224,8 @@ impl ValidatedStateDiff {
|
||||
let balance = apply_balance_diff(pre.account.balance, diff.diff_balance)
|
||||
.map_err(InvalidProgramBehaviorError::BalanceDiffFailed)?;
|
||||
|
||||
let data = if let Some(diff_data) = diff.diff_data.clone() {
|
||||
program.execute_update_from_diff(&pre.account, &diff_data)?
|
||||
let data = if let Some(diff_data) = &diff.diff_data {
|
||||
program.execute_update_from_diff(&pre.account, diff_data)?
|
||||
} else {
|
||||
pre.account.data.clone()
|
||||
};
|
||||
@@ -420,24 +420,22 @@ impl ValidatedStateDiff {
|
||||
LeeError::OutOfValidityWindow
|
||||
);
|
||||
|
||||
// Build pre_states for proof verification
|
||||
let public_pre_states: Vec<_> = public_account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
state.get_account_by_id(*account_id),
|
||||
signer_account_ids.contains(account_id),
|
||||
*account_id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
// 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.
|
||||
ensure!(
|
||||
message
|
||||
.signer_account_ids
|
||||
.iter()
|
||||
.all(|id| signer_account_ids.contains(id)),
|
||||
LeeError::InvalidInput(
|
||||
"Circuit claims a signer account with no valid signature".into()
|
||||
)
|
||||
);
|
||||
|
||||
// 4. Proof verification
|
||||
check_privacy_preserving_circuit_proof_is_valid(
|
||||
&witness_set.proof,
|
||||
&public_pre_states,
|
||||
message,
|
||||
)?;
|
||||
check_privacy_preserving_circuit_proof_is_valid(&witness_set.proof, message)?;
|
||||
|
||||
// 5. Commitment freshness
|
||||
state.check_commitments_are_new(&commitments)?;
|
||||
@@ -445,11 +443,108 @@ impl ValidatedStateDiff {
|
||||
// 6. Nullifier uniqueness
|
||||
state.check_nullifiers_are_valid(&nullifiers)?;
|
||||
|
||||
let public_diff = message
|
||||
.public_actions
|
||||
.iter()
|
||||
.map(|action| (action.account_id, action.post_state.clone()))
|
||||
.collect();
|
||||
// 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.
|
||||
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;
|
||||
let executing_program_id = public_diff_entry.executing_program_id;
|
||||
let pre_account = public_diff
|
||||
.get(&account_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| state.get_account_by_id(account_id));
|
||||
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).
|
||||
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`).
|
||||
ensure!(
|
||||
!matches!(diff.diff_balance, BalanceDiff::Sub(amount) if amount > 0)
|
||||
|| account_program_owner == executing_account_id,
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedBalanceDecrease {
|
||||
account_id,
|
||||
owner_account_id: account_program_owner,
|
||||
executing_program_id,
|
||||
}
|
||||
)
|
||||
);
|
||||
ensure!(
|
||||
diff.diff_data.is_none()
|
||||
|| pre_account == Account::default()
|
||||
|| account_program_owner == executing_account_id,
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedDataModification {
|
||||
account_id,
|
||||
executing_program_id,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Claim-eligibility, same rules as the public-transaction path's — checked against
|
||||
// real signatures (`signer_account_ids`), not the circuit's witnessed claim.
|
||||
let claimed_owner = if let Some(claim) = public_diff_entry.diff.required_claim() {
|
||||
ensure!(
|
||||
account_program_owner == DEFAULT_PROGRAM_OWNER,
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
|
||||
);
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
ensure!(
|
||||
signer_account_ids.contains(&account_id),
|
||||
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
let pda = AccountId::for_public_pda(&executing_program_id, &seed);
|
||||
ensure!(
|
||||
account_id == pda,
|
||||
InvalidProgramBehaviorError::MismatchedPdaClaim {
|
||||
expected: pda,
|
||||
actual: account_id
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(executing_account_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Materialize: apply_balance_diff always; dispatch update_from_diff when diff_data
|
||||
// is Some, via the same unproven, sequencer-trusted mechanism the
|
||||
// public-transaction path already uses.
|
||||
let mut post = pre_account.clone();
|
||||
post.balance = apply_balance_diff(pre_account.balance, diff.diff_balance)
|
||||
.map_err(InvalidProgramBehaviorError::BalanceDiffFailed)?;
|
||||
if let Some(diff_data) = &diff.diff_data {
|
||||
let owner_id = claimed_owner.unwrap_or(account_program_owner);
|
||||
let Some(owner_program_account) = state.get_program(owner_id.into()) else {
|
||||
return Err(InvalidProgramBehaviorError::NoOwnerProgramForDataUpdate {
|
||||
account_id,
|
||||
}
|
||||
.into());
|
||||
};
|
||||
let owner_program = Program::new_unchecked(
|
||||
owner_id.into(),
|
||||
Cow::Owned(owner_program_account.data.to_vec()),
|
||||
);
|
||||
post.data = owner_program.execute_update_from_diff(&pre_account, diff_data)?;
|
||||
}
|
||||
if let Some(owner) = claimed_owner {
|
||||
post.program_owner = owner;
|
||||
}
|
||||
public_diff.insert(account_id, post);
|
||||
}
|
||||
|
||||
let new_nullifiers = nullifiers.iter().map(|(nullifier, _)| *nullifier).collect();
|
||||
|
||||
Ok(Self(StateDiff {
|
||||
@@ -524,24 +619,23 @@ fn authenticate_public_transaction_signers(
|
||||
Ok(signer_account_ids)
|
||||
}
|
||||
|
||||
/// Verifies the proof against exactly what the circuit witnessed and output — deliberately *not*
|
||||
/// reconciled against live sequencer state for public accounts. Reconciling `public_pre_states`
|
||||
/// against live state here is exactly the race condition `AccountDiff` exists to avoid: it would
|
||||
/// tie this proof's validity to a specific public-account snapshot, invalidating it the moment
|
||||
/// that account changes before this transaction is processed. Materialization (which *does* use
|
||||
/// live state) happens separately, later, via `message.public_diffs`.
|
||||
fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
proof: &Proof,
|
||||
public_pre_states: &[AccountWithMetadata],
|
||||
message: &Message,
|
||||
) -> Result<(), LeeError> {
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_actions: public_pre_states
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(&message.public_actions)
|
||||
.map(|(pre, action)| PublicAction {
|
||||
pre,
|
||||
post: action.post_state.clone(),
|
||||
})
|
||||
.collect(),
|
||||
public_pre_states: message.public_pre_states.clone(),
|
||||
public_diffs: message.public_diffs.clone(),
|
||||
private_actions: message.private_actions.clone(),
|
||||
block_validity_window: message.block_validity_window,
|
||||
timestamp_validity_window: message.timestamp_validity_window,
|
||||
signer_account_ids: message.signer_account_ids.clone(),
|
||||
};
|
||||
proof
|
||||
.is_valid_for(&output)
|
||||
|
||||
@@ -62,19 +62,18 @@ fn public_diff_reflects_a_successful_transfer() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Privacy-path version of the authorization-injection attack. The test passes when the
|
||||
/// attack is rejected and the victim's balance is left untouched.
|
||||
/// Privacy-path version of the authorization-injection attack. The test passes when the attack
|
||||
/// is rejected.
|
||||
///
|
||||
/// `execute_and_prove` succeeds because each inner receipt is individually valid and the
|
||||
/// outer circuit faithfully commits whatever the attacker's program output says, including
|
||||
/// `victim(is_authorized=true)`. The circuit has no access to chain state and cannot know
|
||||
/// the victim never signed.
|
||||
///
|
||||
/// The host-side validator is what catches the attack: it independently reconstructs
|
||||
/// `public_pre_states` from chain state using `signer_account_ids.contains(victim_id) = false`,
|
||||
/// so it expects `victim(is_authorized=false)`. The committed journal and the reconstructed
|
||||
/// expected output diverge, `receipt.verify` fails, and `from_privacy_preserving_transaction`
|
||||
/// returns an error before any state is applied.
|
||||
/// `signer_account_ids` is derived once, before the chain even starts, strictly from the
|
||||
/// *top-level* `pre_states` passed to `execute_and_prove` — here, just the attacker's own
|
||||
/// private account. The victim is only ever introduced later, inside `malicious_injector`'s
|
||||
/// chained call, so it can never be part of `signer_account_ids` regardless of what P1 forges.
|
||||
/// The circuit's own Vacant-branch consistency check (scoped to public accounts) derives the
|
||||
/// victim's expected `is_authorized` from `signer_account_ids` membership, finds it absent, and
|
||||
/// asserts that against the witnessed `is_authorized=true` — which fails, panicking inside the
|
||||
/// guest. So the attack is caught during proving itself: `execute_and_prove` returns
|
||||
/// `Err(CircuitProvingError)`, and never even reaches `from_privacy_preserving_transaction`.
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_public_victim() {
|
||||
use lee_core::{
|
||||
@@ -83,12 +82,7 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::{ProgramWithDependencies, execute_and_prove},
|
||||
message::Message,
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
privacy_preserving_transaction::circuit::{ProgramWithDependencies, execute_and_prove},
|
||||
state::{CommitmentSet, tests::test_private_account_keys_1},
|
||||
};
|
||||
|
||||
@@ -181,54 +175,32 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
|
||||
InputAccountIdentity::Public, // recipient
|
||||
];
|
||||
|
||||
// execute_and_prove succeeds: all inner receipts are valid.
|
||||
// The outer circuit commits victim(is_authorized=true) to its journal.
|
||||
let (circuit_output, proof) = execute_and_prove(
|
||||
let result = execute_and_prove(
|
||||
vec![attacker_pre],
|
||||
instruction_data,
|
||||
account_identities,
|
||||
&program_with_deps,
|
||||
)
|
||||
.expect("execute_and_prove should succeed \u{2014} the programs execute correctly");
|
||||
|
||||
// public_account_ids lists the Public entries from account_identities, in order.
|
||||
// The single ciphertext belongs to attacker's private account update.
|
||||
let message = Message::from_circuit_output(
|
||||
vec![], // no public signers, no nonces
|
||||
circuit_output,
|
||||
);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0);
|
||||
|
||||
assert!(
|
||||
matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)),
|
||||
"attack privacy transaction should be rejected with InvalidPrivacyPreservingProof"
|
||||
matches!(result, Err(LeeError::CircuitProvingError(_))),
|
||||
"forged victim(is_authorized=true) should be caught inside the circuit itself, since \
|
||||
signer_account_ids is derived from the top-level pre_states only, and the victim is \
|
||||
only ever introduced via a chained call"
|
||||
);
|
||||
assert_eq!(state.get_account_by_id(victim_id).balance, victim_balance);
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 0);
|
||||
}
|
||||
|
||||
/// Private-victim variant of the authorization-injection attack. The test passes when the
|
||||
/// attack is rejected and the recipient's balance remains zero.
|
||||
///
|
||||
/// After the circuit's Vacant branch accepts the injected `victim(is_authorized=true)`
|
||||
/// verbatim, the attacker must choose how to declare the victim in `account_identities`.
|
||||
/// There are two routes, both closed:
|
||||
///
|
||||
/// - **mask=1 (regular update)**: the circuit derives `account_id =
|
||||
/// AccountId::for_regular_private_account(&npk_from(nsk), identifier)` and asserts it matches
|
||||
/// `pre_state.account_id`. Passing this check requires the victim's `nsk`, which the attacker
|
||||
/// does not have. `execute_and_prove` panics inside the ZKVM and no proof is produced.
|
||||
///
|
||||
/// - **mask=0 (`Public`)**: the circuit places the account in `public_pre_states` and
|
||||
/// `execute_and_prove` succeeds. The host-side validator then reconstructs `public_pre_states`
|
||||
/// from chain state; `state.get_account_by_id(victim_id)` returns the default account (balance=0)
|
||||
/// because the victim has no public state entry. The committed journal and the reconstructed
|
||||
/// expected output diverge, `receipt.verify` fails, and `from_privacy_preserving_transaction`
|
||||
/// returns an error before any state is applied. This test exercises this route.
|
||||
/// Private-victim variant of the authorization-injection attack. The attacker has no `nsk` for
|
||||
/// the victim's private account, so a regular update isn't an option — the only route is to
|
||||
/// declare the victim `InputAccountIdentity::Public` and inject its data directly, since the
|
||||
/// circuit has no access to chain state and can't detect the values are fabricated. That's the
|
||||
/// exact same route `privacy_malicious_programs_cannot_drain_public_victim` exercises, so the
|
||||
/// same mechanism catches it: the victim is only ever introduced via `malicious_injector`'s
|
||||
/// chained call, never the top-level `pre_states` `signer_account_ids` is derived from, so the
|
||||
/// circuit's Vacant-branch consistency check rejects the forged `is_authorized=true` and
|
||||
/// `execute_and_prove` fails with `CircuitProvingError` before any proof is produced.
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
use lee_core::{
|
||||
@@ -237,12 +209,7 @@ fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::{ProgramWithDependencies, execute_and_prove},
|
||||
message::Message,
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
privacy_preserving_transaction::circuit::{ProgramWithDependencies, execute_and_prove},
|
||||
state::{
|
||||
CommitmentSet,
|
||||
tests::{test_private_account_keys_1, test_private_account_keys_2},
|
||||
@@ -273,15 +240,6 @@ fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
|
||||
let recipient_id = AccountId::new([42_u8; 32]);
|
||||
|
||||
// Victim has no public state entry; only recipient is registered at genesis.
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[(recipient_id, 0)]))
|
||||
.with_programs([
|
||||
crate::test_methods::simple_balance_transfer(),
|
||||
crate::test_methods::malicious_injector(),
|
||||
crate::test_methods::malicious_launderer(),
|
||||
]);
|
||||
|
||||
// Build attacker's private account and its local commitment tree.
|
||||
let attacker_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
|
||||
@@ -345,36 +303,18 @@ fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
InputAccountIdentity::Public, // recipient
|
||||
];
|
||||
|
||||
// execute_and_prove succeeds: simple_balance_transfer runs against the injected
|
||||
// victim(balance=5000, is_authorized=true) and produces valid inner receipts.
|
||||
// The outer circuit commits victim(is_authorized=true) to public_pre_states.
|
||||
let (circuit_output, proof) = execute_and_prove(
|
||||
let result = execute_and_prove(
|
||||
vec![attacker_pre],
|
||||
instruction_data,
|
||||
account_identities,
|
||||
&program_with_deps,
|
||||
)
|
||||
.expect("execute_and_prove should succeed \u{2014} the programs execute correctly");
|
||||
|
||||
// public_account_ids lists the Public entries from account_identities, in order.
|
||||
// The single ciphertext belongs to attacker's private account update.
|
||||
let message = Message::from_circuit_output(
|
||||
vec![], // no public signers, no nonces
|
||||
circuit_output,
|
||||
);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0);
|
||||
|
||||
assert!(
|
||||
matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)),
|
||||
"attack on private victim should be rejected with InvalidPrivacyPreservingProof"
|
||||
matches!(result, Err(LeeError::CircuitProvingError(_))),
|
||||
"forged victim(is_authorized=true) should be caught inside the circuit itself, the \
|
||||
same way as the public-victim variant of this attack"
|
||||
);
|
||||
// Victim has no public balance to check; confirming the recipient received nothing
|
||||
// is sufficient to show no funds moved.
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 0);
|
||||
}
|
||||
|
||||
/// Two malicious programs (injector + launderer) attempt to drain a victim's balance
|
||||
@@ -510,7 +450,8 @@ fn privacy_garbage_proof_is_rejected() {
|
||||
));
|
||||
let commitment = Commitment::new(&account_id, &Account::default());
|
||||
let message = Message {
|
||||
public_actions: vec![],
|
||||
public_pre_states: vec![],
|
||||
public_diffs: vec![],
|
||||
nonces: vec![],
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier: Nullifier::for_account_initialization(&account_id),
|
||||
@@ -524,6 +465,7 @@ fn privacy_garbage_proof_is_rejected() {
|
||||
}],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
signer_account_ids: vec![],
|
||||
};
|
||||
|
||||
// Garbage proof bytes: not a valid borsh-encoded `InnerReceipt`.
|
||||
@@ -539,3 +481,103 @@ fn privacy_garbage_proof_is_rejected() {
|
||||
Ok(_) => panic!("garbage proof was accepted instead of 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.
|
||||
#[test]
|
||||
fn privacy_transaction_survives_public_state_changing_after_proving() {
|
||||
use lee_core::{
|
||||
DUMMY_COMMITMENT_HASH, InputAccountIdentity, NullifierWitness, PrivateWitness, WitnessKind,
|
||||
account::AccountWithMetadata,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::execute_and_prove, message::Message, witness_set::WitnessSet,
|
||||
},
|
||||
state::tests::test_private_account_keys_1,
|
||||
};
|
||||
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
|
||||
let sender_key = PrivateKey::try_new([3_u8; 32]).unwrap();
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key));
|
||||
let balance_at_proving_time = 100_u128;
|
||||
let balance_to_move = 37_u128;
|
||||
|
||||
// State as it looked when the prover captured its pre-state.
|
||||
let state_at_proving_time = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[(
|
||||
sender_id,
|
||||
balance_at_proving_time,
|
||||
)]))
|
||||
.with_programs(std::iter::once(program.clone()));
|
||||
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
state_at_proving_time.get_account_by_id(sender_id),
|
||||
true,
|
||||
sender_id,
|
||||
);
|
||||
|
||||
let recipient_account_id =
|
||||
AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0);
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
|
||||
let (circuit_output, proof) = execute_and_prove(
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::Private(PrivateWitness {
|
||||
vpk: recipient_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
identifier: 0,
|
||||
kind: WitnessKind::Regular { ask: None },
|
||||
nullifier: NullifierWitness::Init {
|
||||
npk: recipient_keys.npk(),
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
}),
|
||||
],
|
||||
&program.clone().into(),
|
||||
)
|
||||
.expect("execute_and_prove should succeed");
|
||||
|
||||
let message = Message::from_circuit_output(vec![Nonce(0)], circuit_output);
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&sender_key]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
// Simulates an unrelated public transaction landing on the sender's account between
|
||||
// proving and sequencer validation: live state now disagrees with what the prover
|
||||
// witnessed as `sender_pre`.
|
||||
let balance_at_validation_time = 250_u128;
|
||||
let state_at_validation_time = state_at_proving_time.with_public_accounts(
|
||||
public_state_from_balances(&[(sender_id, balance_at_validation_time)]),
|
||||
);
|
||||
|
||||
let diff = ValidatedStateDiff::from_privacy_preserving_transaction(
|
||||
&tx,
|
||||
&state_at_validation_time,
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.expect(
|
||||
"proof validity must not depend on live public state matching the witnessed \
|
||||
pre-state",
|
||||
);
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
assert_eq!(
|
||||
public_diff[&sender_id].balance,
|
||||
balance_at_validation_time - balance_to_move,
|
||||
"the diff must be replayed against live state at validation time, not the stale \
|
||||
balance captured when the proof was generated",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,16 +68,18 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into
|
||||
witness_set,
|
||||
} = tx;
|
||||
let PrivacyPreservingMessage {
|
||||
public_actions,
|
||||
public_pre_states,
|
||||
public_diffs: _,
|
||||
nonces,
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
signer_account_ids: _,
|
||||
} = message;
|
||||
let private_action_count = private_actions.len();
|
||||
let public_account_ids: Vec<_> = public_actions
|
||||
let public_account_ids: Vec<_> = public_pre_states
|
||||
.into_iter()
|
||||
.map(|action| action.account_id)
|
||||
.map(|pre_state| pre_state.account_id)
|
||||
.collect();
|
||||
let public_account_count = public_account_ids.len();
|
||||
let WitnessSet {
|
||||
|
||||
@@ -40,7 +40,7 @@ pub fn TransactionPreview(transaction: Transaction) -> impl IntoView {
|
||||
} = tx;
|
||||
format!(
|
||||
"{} public accounts, {} commitments",
|
||||
message.public_actions.len(),
|
||||
message.public_pre_states.len(),
|
||||
message.private_actions.len()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -225,18 +225,27 @@ typedef struct FfiAccount {
|
||||
struct FfiU128 nonce;
|
||||
} FfiAccount;
|
||||
|
||||
typedef struct FfiPublicAction {
|
||||
typedef struct FfiAccountWithMetadata {
|
||||
struct FfiAccount account;
|
||||
bool is_authorized;
|
||||
FfiAccountId account_id;
|
||||
struct FfiAccount post_state;
|
||||
} FfiPublicAction;
|
||||
} FfiAccountWithMetadata;
|
||||
|
||||
typedef struct FfiVec_FfiPublicAction {
|
||||
struct FfiPublicAction *entries;
|
||||
typedef struct FfiVec_FfiAccountWithMetadata {
|
||||
struct FfiAccountWithMetadata *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiPublicAction;
|
||||
} FfiVec_FfiAccountWithMetadata;
|
||||
|
||||
typedef struct FfiVec_FfiPublicAction FfiPublicActionList;
|
||||
typedef struct FfiVec_FfiAccountWithMetadata FfiPublicPreStateList;
|
||||
|
||||
/**
|
||||
* C-compatible tagged `BalanceDiff`: `is_sub` selects `Sub` over `Add`.
|
||||
*/
|
||||
typedef struct FfiBalanceDiff {
|
||||
bool is_sub;
|
||||
struct FfiU128 amount;
|
||||
} FfiBalanceDiff;
|
||||
|
||||
typedef struct FfiVec_u8 {
|
||||
uint8_t *entries;
|
||||
@@ -246,6 +255,50 @@ typedef struct FfiVec_u8 {
|
||||
|
||||
typedef struct FfiVec_u8 FfiVecU8;
|
||||
|
||||
typedef struct FfiOption_FfiVecU8 {
|
||||
FfiVecU8 *value;
|
||||
bool is_some;
|
||||
} FfiOption_FfiVecU8;
|
||||
|
||||
typedef struct FfiAccountDiff {
|
||||
FfiAccountId id;
|
||||
struct FfiBalanceDiff diff_balance;
|
||||
struct FfiOption_FfiVecU8 diff_data;
|
||||
} FfiAccountDiff;
|
||||
|
||||
/**
|
||||
* C-compatible tagged `Claim`: `is_pda` selects `Pda(seed)` over `Authorized`, in which case
|
||||
* `pda_seed` is meaningless.
|
||||
*/
|
||||
typedef struct FfiClaim {
|
||||
bool is_pda;
|
||||
struct FfiBytes32 pda_seed;
|
||||
} FfiClaim;
|
||||
|
||||
typedef struct FfiOption_FfiClaim {
|
||||
struct FfiClaim *value;
|
||||
bool is_some;
|
||||
} FfiOption_FfiClaim;
|
||||
|
||||
typedef struct FfiAccountDiffOutput {
|
||||
struct FfiAccountDiff diff;
|
||||
struct FfiOption_FfiClaim claim;
|
||||
} FfiAccountDiffOutput;
|
||||
|
||||
typedef struct FfiPublicDiff {
|
||||
FfiAccountId account_id;
|
||||
struct FfiProgramId executing_program_id;
|
||||
struct FfiAccountDiffOutput diff;
|
||||
} FfiPublicDiff;
|
||||
|
||||
typedef struct FfiVec_FfiPublicDiff {
|
||||
struct FfiPublicDiff *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiPublicDiff;
|
||||
|
||||
typedef struct FfiVec_FfiPublicDiff FfiPublicDiffList;
|
||||
|
||||
typedef struct FfiEncryptedAccountData {
|
||||
FfiVecU8 ciphertext;
|
||||
FfiVecU8 epk;
|
||||
@@ -268,11 +321,13 @@ typedef struct FfiVec_FfiPrivateAction {
|
||||
typedef struct FfiVec_FfiPrivateAction FfiPrivateActionList;
|
||||
|
||||
typedef struct FfiPrivacyPreservingMessage {
|
||||
FfiPublicActionList public_actions;
|
||||
FfiPublicPreStateList public_pre_states;
|
||||
FfiPublicDiffList public_diffs;
|
||||
FfiNonceList nonces;
|
||||
FfiPrivateActionList private_actions;
|
||||
uint64_t block_validity_window[2];
|
||||
uint64_t timestamp_validity_window[2];
|
||||
FfiAccountIdList signer_account_ids;
|
||||
} FfiPrivacyPreservingMessage;
|
||||
|
||||
typedef FfiVecU8 FfiProof;
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use indexer_service_protocol::{
|
||||
AccountId, Ciphertext, Commitment, CommitmentSetDigest, EncryptedAccountData,
|
||||
EphemeralPublicKey, HashType, Nullifier, PrivacyPreservingMessage,
|
||||
PrivacyPreservingTransaction, PrivateAction, ProgramDeploymentMessage,
|
||||
ProgramDeploymentTransaction, ProgramId, Proof, PublicActionWithID, PublicKey, PublicMessage,
|
||||
PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet,
|
||||
AccountDiff, AccountDiffOutput, AccountId, AccountWithMetadata, BalanceDiff, Ciphertext, Claim,
|
||||
Commitment, CommitmentSetDigest, EncryptedAccountData, EphemeralPublicKey, HashType, Nullifier,
|
||||
PdaSeed, PrivacyPreservingMessage, PrivacyPreservingTransaction, PrivateAction,
|
||||
ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, Proof, PublicDiff,
|
||||
PublicKey, PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow,
|
||||
WitnessSet,
|
||||
};
|
||||
|
||||
use crate::api::types::{
|
||||
FfiAccountId, FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature,
|
||||
FfiVec,
|
||||
FfiU128, FfiVec,
|
||||
account::FfiAccount,
|
||||
vectors::{
|
||||
FfiAccountIdList, FfiInstructionDataList, FfiNonceList, FfiPrivateActionList,
|
||||
FfiProgramDeploymentMessage, FfiProof, FfiPublicActionList, FfiSignaturePubKeyList,
|
||||
FfiVecU8,
|
||||
FfiProgramDeploymentMessage, FfiProof, FfiPublicDiffList, FfiPublicPreStateList,
|
||||
FfiSignaturePubKeyList, FfiVecU8,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -158,17 +159,13 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
|
||||
Self {
|
||||
hash: HashType(value.hash.data),
|
||||
message: PrivacyPreservingMessage {
|
||||
public_actions: {
|
||||
let std_vec: Vec<_> = value.message.public_actions.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| PublicActionWithID {
|
||||
account_id: AccountId {
|
||||
value: ffi_val.account_id.data,
|
||||
},
|
||||
post_state: ffi_val.post_state.into(),
|
||||
})
|
||||
.collect()
|
||||
public_pre_states: {
|
||||
let std_vec: Vec<_> = value.message.public_pre_states.into();
|
||||
std_vec.into_iter().map(Into::into).collect()
|
||||
},
|
||||
public_diffs: {
|
||||
let std_vec: Vec<_> = value.message.public_diffs.into();
|
||||
std_vec.into_iter().map(Into::into).collect()
|
||||
},
|
||||
nonces: {
|
||||
let std_vec: Vec<_> = value.message.nonces.into();
|
||||
@@ -198,6 +195,15 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
|
||||
timestamp_validity_window: cast_ffi_validity_window(
|
||||
value.message.timestamp_validity_window,
|
||||
),
|
||||
signer_account_ids: {
|
||||
let std_vec: Vec<_> = value.message.signer_account_ids.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| AccountId {
|
||||
value: ffi_val.data,
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
},
|
||||
witness_set: WitnessSet {
|
||||
signatures_and_public_keys: {
|
||||
@@ -219,20 +225,228 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPublicAction {
|
||||
pub struct FfiAccountWithMetadata {
|
||||
pub account: FfiAccount,
|
||||
pub is_authorized: bool,
|
||||
pub account_id: FfiAccountId,
|
||||
pub post_state: FfiAccount,
|
||||
}
|
||||
|
||||
impl From<PublicActionWithID> for FfiPublicAction {
|
||||
fn from(value: PublicActionWithID) -> Self {
|
||||
let post_state: lee::Account = value
|
||||
.post_state
|
||||
.try_into()
|
||||
.expect("Source is in blocks, must fit");
|
||||
impl From<AccountWithMetadata> for FfiAccountWithMetadata {
|
||||
fn from(value: AccountWithMetadata) -> Self {
|
||||
let AccountWithMetadata {
|
||||
account,
|
||||
is_authorized,
|
||||
account_id,
|
||||
} = value;
|
||||
let account: lee::Account = account.try_into().expect("Source is in blocks, must fit");
|
||||
Self {
|
||||
account_id: value.account_id.into(),
|
||||
post_state: post_state.into(),
|
||||
account: account.into(),
|
||||
is_authorized,
|
||||
account_id: account_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FfiAccountWithMetadata> for AccountWithMetadata {
|
||||
fn from(value: FfiAccountWithMetadata) -> Self {
|
||||
let FfiAccountWithMetadata {
|
||||
account,
|
||||
is_authorized,
|
||||
account_id,
|
||||
} = value;
|
||||
Self {
|
||||
account: account.into(),
|
||||
is_authorized,
|
||||
account_id: AccountId {
|
||||
value: account_id.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// C-compatible tagged `BalanceDiff`: `is_sub` selects `Sub` over `Add`.
|
||||
#[repr(C)]
|
||||
pub struct FfiBalanceDiff {
|
||||
pub is_sub: bool,
|
||||
pub amount: FfiU128,
|
||||
}
|
||||
|
||||
impl From<BalanceDiff> for FfiBalanceDiff {
|
||||
fn from(value: BalanceDiff) -> Self {
|
||||
match value {
|
||||
BalanceDiff::Add(amount) => Self {
|
||||
is_sub: false,
|
||||
amount: amount.into(),
|
||||
},
|
||||
BalanceDiff::Sub(amount) => Self {
|
||||
is_sub: true,
|
||||
amount: amount.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FfiBalanceDiff> for BalanceDiff {
|
||||
fn from(value: FfiBalanceDiff) -> Self {
|
||||
let amount: u128 = value.amount.into();
|
||||
if value.is_sub {
|
||||
Self::Sub(amount)
|
||||
} else {
|
||||
Self::Add(amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// C-compatible tagged `Claim`: `is_pda` selects `Pda(seed)` over `Authorized`, in which case
|
||||
/// `pda_seed` is meaningless.
|
||||
#[repr(C)]
|
||||
pub struct FfiClaim {
|
||||
pub is_pda: bool,
|
||||
pub pda_seed: FfiBytes32,
|
||||
}
|
||||
|
||||
impl From<Claim> for FfiClaim {
|
||||
fn from(value: Claim) -> Self {
|
||||
match value {
|
||||
Claim::Authorized => Self {
|
||||
is_pda: false,
|
||||
pda_seed: FfiBytes32::default(),
|
||||
},
|
||||
Claim::Pda(seed) => Self {
|
||||
is_pda: true,
|
||||
pda_seed: FfiBytes32 { data: seed.0 },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FfiClaim> for Claim {
|
||||
fn from(value: FfiClaim) -> Self {
|
||||
if value.is_pda {
|
||||
Self::Pda(PdaSeed(value.pda_seed.data))
|
||||
} else {
|
||||
Self::Authorized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiAccountDiff {
|
||||
pub id: FfiAccountId,
|
||||
pub diff_balance: FfiBalanceDiff,
|
||||
pub diff_data: FfiOption<FfiVecU8>,
|
||||
}
|
||||
|
||||
impl From<AccountDiff> for FfiAccountDiff {
|
||||
fn from(value: AccountDiff) -> Self {
|
||||
let AccountDiff {
|
||||
id,
|
||||
diff_balance,
|
||||
diff_data,
|
||||
} = value;
|
||||
Self {
|
||||
id: id.into(),
|
||||
diff_balance: diff_balance.into(),
|
||||
diff_data: match diff_data {
|
||||
Some(bytes) => FfiOption::from_value(bytes.into()),
|
||||
None => FfiOption::from_none(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FfiAccountDiff> for AccountDiff {
|
||||
fn from(value: FfiAccountDiff) -> Self {
|
||||
let FfiAccountDiff {
|
||||
id,
|
||||
diff_balance,
|
||||
diff_data,
|
||||
} = value;
|
||||
let diff_data = if diff_data.is_some {
|
||||
let boxed = unsafe { Box::from_raw(diff_data.value) };
|
||||
let bytes: Vec<u8> = (*boxed).into();
|
||||
Some(bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
id: AccountId { value: id.data },
|
||||
diff_balance: diff_balance.into(),
|
||||
diff_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiAccountDiffOutput {
|
||||
pub diff: FfiAccountDiff,
|
||||
pub claim: FfiOption<FfiClaim>,
|
||||
}
|
||||
|
||||
impl From<AccountDiffOutput> for FfiAccountDiffOutput {
|
||||
fn from(value: AccountDiffOutput) -> Self {
|
||||
let AccountDiffOutput { diff, claim } = value;
|
||||
Self {
|
||||
diff: diff.into(),
|
||||
claim: match claim {
|
||||
Some(claim) => FfiOption::from_value(claim.into()),
|
||||
None => FfiOption::from_none(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FfiAccountDiffOutput> for AccountDiffOutput {
|
||||
fn from(value: FfiAccountDiffOutput) -> Self {
|
||||
let FfiAccountDiffOutput { diff, claim } = value;
|
||||
let claim = if claim.is_some {
|
||||
let boxed = unsafe { Box::from_raw(claim.value) };
|
||||
Some((*boxed).into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
diff: diff.into(),
|
||||
claim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPublicDiff {
|
||||
pub account_id: FfiAccountId,
|
||||
pub executing_program_id: FfiProgramId,
|
||||
pub diff: FfiAccountDiffOutput,
|
||||
}
|
||||
|
||||
impl From<PublicDiff> for FfiPublicDiff {
|
||||
fn from(value: PublicDiff) -> Self {
|
||||
let PublicDiff {
|
||||
account_id,
|
||||
executing_program_id,
|
||||
diff,
|
||||
} = value;
|
||||
Self {
|
||||
account_id: account_id.into(),
|
||||
executing_program_id: executing_program_id.into(),
|
||||
diff: diff.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FfiPublicDiff> for PublicDiff {
|
||||
fn from(value: FfiPublicDiff) -> Self {
|
||||
let FfiPublicDiff {
|
||||
account_id,
|
||||
executing_program_id,
|
||||
diff,
|
||||
} = value;
|
||||
Self {
|
||||
account_id: AccountId {
|
||||
value: account_id.data,
|
||||
},
|
||||
executing_program_id: ProgramId(executing_program_id.data),
|
||||
diff: diff.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,25 +476,34 @@ impl From<PrivateAction> for FfiPrivateAction {
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPrivacyPreservingMessage {
|
||||
pub public_actions: FfiPublicActionList,
|
||||
pub public_pre_states: FfiPublicPreStateList,
|
||||
pub public_diffs: FfiPublicDiffList,
|
||||
pub nonces: FfiNonceList,
|
||||
pub private_actions: FfiPrivateActionList,
|
||||
pub block_validity_window: [u64; 2],
|
||||
pub timestamp_validity_window: [u64; 2],
|
||||
pub signer_account_ids: FfiAccountIdList,
|
||||
}
|
||||
|
||||
impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
|
||||
fn from(value: PrivacyPreservingMessage) -> Self {
|
||||
let PrivacyPreservingMessage {
|
||||
public_actions,
|
||||
public_pre_states,
|
||||
public_diffs,
|
||||
nonces,
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
signer_account_ids,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
public_actions: public_actions
|
||||
public_pre_states: public_pre_states
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
public_diffs: public_diffs
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
@@ -297,6 +520,11 @@ impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
|
||||
.into(),
|
||||
block_validity_window: cast_validity_window(block_validity_window),
|
||||
timestamp_validity_window: cast_validity_window(timestamp_validity_window),
|
||||
signer_account_ids: signer_account_ids
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::api::types::{
|
||||
FfiAccountId, FfiNonce, FfiVec,
|
||||
transaction::{FfiPrivateAction, FfiPublicAction, FfiSignaturePubKeyEntry, FfiTransaction},
|
||||
transaction::{
|
||||
FfiAccountWithMetadata, FfiPrivateAction, FfiPublicDiff, FfiSignaturePubKeyEntry,
|
||||
FfiTransaction,
|
||||
},
|
||||
};
|
||||
|
||||
pub type FfiVecU8 = FfiVec<u8>;
|
||||
@@ -19,6 +22,8 @@ pub type FfiProof = FfiVecU8;
|
||||
|
||||
pub type FfiProgramDeploymentMessage = FfiVecU8;
|
||||
|
||||
pub type FfiPublicActionList = FfiVec<FfiPublicAction>;
|
||||
pub type FfiPublicPreStateList = FfiVec<FfiAccountWithMetadata>;
|
||||
|
||||
pub type FfiPublicDiffList = FfiVec<FfiPublicDiff>;
|
||||
|
||||
pub type FfiPrivateActionList = FfiVec<FfiPrivateAction>;
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
use lee_core::account::Nonce;
|
||||
|
||||
use crate::{
|
||||
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockIngestError, Ciphertext,
|
||||
Commitment, CommitmentSetDigest, CrossZoneHalt, Data, EncryptedAccountData, EphemeralPublicKey,
|
||||
HashType, IndexerStatus, IndexerSyncState, Nullifier, PeerHealth, PeerStatus,
|
||||
Account, AccountDiff, AccountDiffOutput, AccountId, AccountWithMetadata, BalanceDiff,
|
||||
BedrockStatus, Block, BlockBody, BlockHeader, BlockIngestError, Ciphertext, Claim, Commitment,
|
||||
CommitmentSetDigest, CrossZoneHalt, Data, EncryptedAccountData, EphemeralPublicKey, HashType,
|
||||
IndexerStatus, IndexerSyncState, Nullifier, PdaSeed, PeerHealth, PeerStatus,
|
||||
PrivacyPreservingMessage, PrivacyPreservingTransaction, PrivateAction,
|
||||
ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, Proof, PublicActionWithID,
|
||||
ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, Proof, PublicDiff,
|
||||
PublicKey, PublicMessage, PublicTransaction, Signature, StallReason, Transaction,
|
||||
ValidityWindow, WitnessSet,
|
||||
};
|
||||
@@ -280,11 +281,162 @@ impl From<PublicMessage> for lee::public_transaction::Message {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee::privacy_preserving_transaction::message::PublicActionWithID> for PublicActionWithID {
|
||||
fn from(value: lee::privacy_preserving_transaction::message::PublicActionWithID) -> Self {
|
||||
impl From<lee_core::account::AccountWithMetadata> for AccountWithMetadata {
|
||||
fn from(value: lee_core::account::AccountWithMetadata) -> Self {
|
||||
let lee_core::account::AccountWithMetadata {
|
||||
account,
|
||||
is_authorized,
|
||||
account_id,
|
||||
} = value;
|
||||
Self {
|
||||
account_id: value.account_id.into(),
|
||||
post_state: value.post_state.into(),
|
||||
account: account.into(),
|
||||
is_authorized,
|
||||
account_id: account_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AccountWithMetadata> for lee_core::account::AccountWithMetadata {
|
||||
type Error = lee_core::account::data::DataTooBigError;
|
||||
|
||||
fn try_from(value: AccountWithMetadata) -> Result<Self, Self::Error> {
|
||||
let AccountWithMetadata {
|
||||
account,
|
||||
is_authorized,
|
||||
account_id,
|
||||
} = value;
|
||||
Ok(Self {
|
||||
account: account.try_into()?,
|
||||
is_authorized,
|
||||
account_id: account_id.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee_core::account::BalanceDiff> for BalanceDiff {
|
||||
fn from(value: lee_core::account::BalanceDiff) -> Self {
|
||||
match value {
|
||||
lee_core::account::BalanceDiff::Add(amount) => Self::Add(amount),
|
||||
lee_core::account::BalanceDiff::Sub(amount) => Self::Sub(amount),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BalanceDiff> for lee_core::account::BalanceDiff {
|
||||
fn from(value: BalanceDiff) -> Self {
|
||||
match value {
|
||||
BalanceDiff::Add(amount) => Self::Add(amount),
|
||||
BalanceDiff::Sub(amount) => Self::Sub(amount),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee_core::program::PdaSeed> for PdaSeed {
|
||||
fn from(value: lee_core::program::PdaSeed) -> Self {
|
||||
Self(*value.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PdaSeed> for lee_core::program::PdaSeed {
|
||||
fn from(value: PdaSeed) -> Self {
|
||||
Self::new(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee_core::program::Claim> for Claim {
|
||||
fn from(value: lee_core::program::Claim) -> Self {
|
||||
match value {
|
||||
lee_core::program::Claim::Authorized => Self::Authorized,
|
||||
lee_core::program::Claim::Pda(seed) => Self::Pda(seed.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Claim> for lee_core::program::Claim {
|
||||
fn from(value: Claim) -> Self {
|
||||
match value {
|
||||
Claim::Authorized => Self::Authorized,
|
||||
Claim::Pda(seed) => Self::Pda(seed.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee_core::account::AccountDiff> for AccountDiff {
|
||||
fn from(value: lee_core::account::AccountDiff) -> Self {
|
||||
let lee_core::account::AccountDiff {
|
||||
id,
|
||||
diff_balance,
|
||||
diff_data,
|
||||
} = value;
|
||||
Self {
|
||||
id: id.into(),
|
||||
diff_balance: diff_balance.into(),
|
||||
diff_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AccountDiff> for lee_core::account::AccountDiff {
|
||||
fn from(value: AccountDiff) -> Self {
|
||||
let AccountDiff {
|
||||
id,
|
||||
diff_balance,
|
||||
diff_data,
|
||||
} = value;
|
||||
Self {
|
||||
id: id.into(),
|
||||
diff_balance: diff_balance.into(),
|
||||
diff_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee_core::program::AccountDiffOutput> for AccountDiffOutput {
|
||||
fn from(value: lee_core::program::AccountDiffOutput) -> Self {
|
||||
let claim = value.required_claim().map(Into::into);
|
||||
Self {
|
||||
diff: value.into_diff().into(),
|
||||
claim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AccountDiffOutput> for lee_core::program::AccountDiffOutput {
|
||||
fn from(value: AccountDiffOutput) -> Self {
|
||||
let AccountDiffOutput { diff, claim } = value;
|
||||
match claim {
|
||||
Some(claim) => Self::new_claimed(diff.into(), claim.into()),
|
||||
None => Self::new(diff.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee_core::PublicDiff> for PublicDiff {
|
||||
fn from(value: lee_core::PublicDiff) -> Self {
|
||||
let lee_core::PublicDiff {
|
||||
account_id,
|
||||
executing_program_id,
|
||||
diff,
|
||||
} = value;
|
||||
Self {
|
||||
account_id: account_id.into(),
|
||||
executing_program_id: executing_program_id.into(),
|
||||
diff: diff.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicDiff> for lee_core::PublicDiff {
|
||||
fn from(value: PublicDiff) -> Self {
|
||||
let PublicDiff {
|
||||
account_id,
|
||||
executing_program_id,
|
||||
diff,
|
||||
} = value;
|
||||
Self {
|
||||
account_id: account_id.into(),
|
||||
executing_program_id: executing_program_id.into(),
|
||||
diff: diff.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,38 +455,26 @@ impl From<lee_core::PrivateAction> for PrivateAction {
|
||||
impl From<lee::privacy_preserving_transaction::message::Message> for PrivacyPreservingMessage {
|
||||
fn from(value: lee::privacy_preserving_transaction::message::Message) -> Self {
|
||||
let lee::privacy_preserving_transaction::message::Message {
|
||||
public_actions,
|
||||
public_pre_states,
|
||||
public_diffs,
|
||||
nonces,
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
signer_account_ids,
|
||||
} = value;
|
||||
Self {
|
||||
public_actions: public_actions.into_iter().map(Into::into).collect(),
|
||||
public_pre_states: public_pre_states.into_iter().map(Into::into).collect(),
|
||||
public_diffs: public_diffs.into_iter().map(Into::into).collect(),
|
||||
nonces: nonces.iter().map(|x| x.0).collect(),
|
||||
private_actions: private_actions.into_iter().map(Into::into).collect(),
|
||||
block_validity_window: block_validity_window.into(),
|
||||
timestamp_validity_window: timestamp_validity_window.into(),
|
||||
signer_account_ids: signer_account_ids.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PublicActionWithID>
|
||||
for lee::privacy_preserving_transaction::message::PublicActionWithID
|
||||
{
|
||||
type Error = lee::error::LeeError;
|
||||
|
||||
fn try_from(value: PublicActionWithID) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
account_id: value.account_id.into(),
|
||||
post_state: value
|
||||
.post_state
|
||||
.try_into()
|
||||
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrivateAction> for lee_core::PrivateAction {
|
||||
fn from(value: PrivateAction) -> Self {
|
||||
Self {
|
||||
@@ -351,21 +491,26 @@ impl TryFrom<PrivacyPreservingMessage> for lee::privacy_preserving_transaction::
|
||||
|
||||
fn try_from(value: PrivacyPreservingMessage) -> Result<Self, Self::Error> {
|
||||
let PrivacyPreservingMessage {
|
||||
public_actions,
|
||||
public_pre_states,
|
||||
public_diffs,
|
||||
nonces,
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
signer_account_ids,
|
||||
} = value;
|
||||
|
||||
let public_actions = public_actions
|
||||
let public_pre_states = public_pre_states
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?;
|
||||
let public_diffs = public_diffs.into_iter().map(Into::into).collect();
|
||||
let private_actions = private_actions.into_iter().map(Into::into).collect();
|
||||
|
||||
Ok(Self {
|
||||
public_actions,
|
||||
public_pre_states,
|
||||
public_diffs,
|
||||
nonces: nonces
|
||||
.iter()
|
||||
.map(|x| lee_core::account::Nonce(*x))
|
||||
@@ -377,6 +522,7 @@ impl TryFrom<PrivacyPreservingMessage> for lee::privacy_preserving_transaction::
|
||||
timestamp_validity_window: timestamp_validity_window
|
||||
.try_into()
|
||||
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?,
|
||||
signer_account_ids: signer_account_ids.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,35 @@ mod base64 {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod opt {
|
||||
use base64::prelude::{BASE64_STANDARD, Engine as _};
|
||||
|
||||
use super::{Deserializer, Serializer};
|
||||
|
||||
// `Option<&Vec<u8>>` isn't usable here: `#[serde(with = "base64::opt")]` always calls
|
||||
// this with `&self.field`, i.e. `&Option<Vec<u8>>` verbatim — serde has no equivalent of
|
||||
// `Option::as_ref` to redistribute the reference inward before the call.
|
||||
#[expect(
|
||||
clippy::ref_option,
|
||||
reason = "signature is fixed by serde's `with` calling convention"
|
||||
)]
|
||||
pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
|
||||
let encoded = v.as_ref().map(|bytes| BASE64_STANDARD.encode(bytes));
|
||||
serde::Serialize::serialize(&encoded, s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
|
||||
let encoded: Option<String> = serde::Deserialize::deserialize(d)?;
|
||||
encoded
|
||||
.map(|s| {
|
||||
BASE64_STANDARD
|
||||
.decode(s.as_bytes())
|
||||
.map_err(serde::de::Error::custom)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
|
||||
let base64 = BASE64_STANDARD.encode(v);
|
||||
String::serialize(&base64, s)
|
||||
@@ -227,9 +256,54 @@ pub struct PublicMessage {
|
||||
pub type InstructionData = Vec<u32>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PublicActionWithID {
|
||||
pub struct AccountWithMetadata {
|
||||
pub account: Account,
|
||||
pub is_authorized: bool,
|
||||
pub account_id: AccountId,
|
||||
pub post_state: Account,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum BalanceDiff {
|
||||
Add(u128),
|
||||
Sub(u128),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PdaSeed(
|
||||
#[serde(with = "base64::arr")]
|
||||
#[schemars(with = "String", description = "base64-encoded PDA seed")]
|
||||
pub [u8; 32],
|
||||
);
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Claim {
|
||||
Authorized,
|
||||
Pda(PdaSeed),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AccountDiff {
|
||||
pub id: AccountId,
|
||||
pub diff_balance: BalanceDiff,
|
||||
#[serde(with = "base64::opt")]
|
||||
#[schemars(
|
||||
with = "Option<String>",
|
||||
description = "base64-encoded account data diff"
|
||||
)]
|
||||
pub diff_data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AccountDiffOutput {
|
||||
pub diff: AccountDiff,
|
||||
pub claim: Option<Claim>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PublicDiff {
|
||||
pub account_id: AccountId,
|
||||
pub executing_program_id: ProgramId,
|
||||
pub diff: AccountDiffOutput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -245,11 +319,13 @@ pub struct PrivateAction {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PrivacyPreservingMessage {
|
||||
pub public_actions: Vec<PublicActionWithID>,
|
||||
pub public_pre_states: Vec<AccountWithMetadata>,
|
||||
pub public_diffs: Vec<PublicDiff>,
|
||||
pub nonces: Vec<Nonce>,
|
||||
pub private_actions: Vec<PrivateAction>,
|
||||
pub block_validity_window: ValidityWindow,
|
||||
pub timestamp_validity_window: ValidityWindow,
|
||||
pub signer_account_ids: Vec<AccountId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use indexer_service_protocol::{
|
||||
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment,
|
||||
CommitmentSetDigest, Data, EncryptedAccountData, HashType, IndexerStatus, IndexerSyncState,
|
||||
PrivacyPreservingMessage, PrivacyPreservingTransaction, PrivateAction,
|
||||
ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, PublicActionWithID,
|
||||
PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet,
|
||||
Account, AccountDiff, AccountDiffOutput, AccountId, AccountWithMetadata, BalanceDiff,
|
||||
BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment, CommitmentSetDigest, Data,
|
||||
EncryptedAccountData, HashType, IndexerStatus, IndexerSyncState, PrivacyPreservingMessage,
|
||||
PrivacyPreservingTransaction, PrivateAction, ProgramDeploymentMessage,
|
||||
ProgramDeploymentTransaction, ProgramId, PublicDiff, PublicMessage, PublicTransaction,
|
||||
Signature, Transaction, ValidityWindow, WitnessSet,
|
||||
};
|
||||
use jsonrpsee::{
|
||||
core::{SubscriptionResult, async_trait},
|
||||
@@ -304,9 +305,9 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
|
||||
Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id),
|
||||
Transaction::PrivacyPreserving(priv_tx) => priv_tx
|
||||
.message
|
||||
.public_actions
|
||||
.public_pre_states
|
||||
.iter()
|
||||
.any(|action| action.account_id == account_id),
|
||||
.any(|pre_state| pre_state.account_id == account_id),
|
||||
Transaction::ProgramDeployment(_) => false,
|
||||
})
|
||||
.cloned()
|
||||
@@ -384,17 +385,31 @@ fn mock_privacy_preserving_tx(
|
||||
tx_idx: u64,
|
||||
account_ids: &[AccountId],
|
||||
) -> Transaction {
|
||||
let public_account_id = account_ids[tx_idx as usize % account_ids.len()];
|
||||
Transaction::PrivacyPreserving(PrivacyPreservingTransaction {
|
||||
hash: tx_hash,
|
||||
message: PrivacyPreservingMessage {
|
||||
public_actions: vec![PublicActionWithID {
|
||||
account_id: account_ids[tx_idx as usize % account_ids.len()],
|
||||
post_state: Account {
|
||||
public_pre_states: vec![AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: AccountId { value: [1_u8; 32] },
|
||||
balance: 500,
|
||||
data: Data(vec![0xdd, 0xee]),
|
||||
nonce: block_id as u128,
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: public_account_id,
|
||||
}],
|
||||
public_diffs: vec![PublicDiff {
|
||||
account_id: public_account_id,
|
||||
executing_program_id: ProgramId([1_u32; 8]),
|
||||
diff: AccountDiffOutput {
|
||||
diff: AccountDiff {
|
||||
id: public_account_id,
|
||||
diff_balance: BalanceDiff::Add(0),
|
||||
diff_data: None,
|
||||
},
|
||||
claim: None,
|
||||
},
|
||||
}],
|
||||
nonces: vec![block_id as u128],
|
||||
private_actions: vec![PrivateAction {
|
||||
@@ -409,6 +424,7 @@ fn mock_privacy_preserving_tx(
|
||||
}],
|
||||
block_validity_window: ValidityWindow((None, None)),
|
||||
timestamp_validity_window: ValidityWindow((None, None)),
|
||||
signer_account_ids: vec![public_account_id],
|
||||
},
|
||||
witness_set: WitnessSet {
|
||||
signatures_and_public_keys: vec![],
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user