Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions packages/privacy/src/tests/test_e2e.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ use privacy::actions::{
ClientAction, ComputeAndInvokeInput, CreateEncNoteInput, DepositInput, InvokeExternalInput,
OpenChannelInput, OpenSubchannelInput, SetViewingKeyInput, UseNoteInput, WithdrawInput,
};
use privacy::objects::OpenNoteDeposit;
use privacy::objects::{OpenNoteDeposit, OpenNoteScreeningPolicy};
use privacy::tests::utils_for_tests::{
PrivacyCfgTrait, Test, TestTrait, User, UserTrait, VesuTrait,
build_ekubo_swap_anonymizer_calldata, deploy_shadow_account_anonymizer,
deploy_shadow_account_mock_dapp, pool_key_for_tokens,
deploy_shadow_account_mock_dapp, pool_key_for_tokens, sign_screening_attestation,
};
use privacy::utils::constants::OPEN_NOTE_SALT;
use privacy::utils::{encrypt_channel_info, unpack};
Expand Down Expand Up @@ -1922,6 +1922,14 @@ fn test_e2e_shadow_account_anonymizer_compute_invoke() {
let mut user = test.new_user();

let anonymizer = deploy_shadow_account_anonymizer(privacy_address: test.privacy.address);
// The anonymizer names the shadow account its deposits are associated with, so it is listed
// `Delegated` — the policy it is deployed under in production, and the only one under which
// its invokes are accepted at all.
test
.privacy
.set_open_note_screening_policy(
depositor: anonymizer, policy: OpenNoteScreeningPolicy::Delegated,
);
let mock_dapp = deploy_shadow_account_mock_dapp();
// Fund the dapp so its `transfer_to_caller` can transfer `amount` to the shadow account.
token.supply(address: mock_dapp, :amount);
Expand Down Expand Up @@ -1962,16 +1970,29 @@ fn test_e2e_shadow_account_anonymizer_compute_invoke() {
assert_eq!(token.balance_of(address: mock_dapp), amount.into());
assert_eq!(token.balance_of(address: anonymizer), 0);

// The address to attest is predicted before the interaction runs, exactly as the prover's
// interceptor does: the shadow account for this identity and dapp at nonce 0.
let anonymizer_disp = IShadowAccountAnonymizerDispatcher { contract_address: anonymizer };
let identity_key = user.compute_identity_key(contract_address: anonymizer);
let predicted_shadow_account = *anonymizer_disp
.get_shadow_accounts(partial_commitment(:identity_key, :dapp_name), 0, 1, false)[0];
assert!(!predicted_shadow_account.is_deployed);

test
.privacy
.execute_actions_e2e(
.execute_actions_e2e_screened(
:user,
client_actions: [
set_viewing_key_action(), open_channel_action(from: user, to: user, index: 0),
open_subchannel_action(from: user, to: user, :token_addr, index: 0),
ClientAction::CreateOpenNote(create_open_note), compute_and_invoke,
]
.span(),
screening: Option::Some(
sign_screening_attestation(
depositor: predicted_shadow_account.address, issued_at: 0,
),
),
);

// The dapp payout, collected via the shadow account and anonymizer, settled into the open note.
Expand All @@ -1986,12 +2007,12 @@ fn test_e2e_shadow_account_anonymizer_compute_invoke() {
assert_eq!(token.balance_of(address: mock_dapp), 0);
assert_eq!(token.balance_of(address: anonymizer), 0);

// A shadow account was deployed for the derived commitment and holds nothing after collection.
let anonymizer_disp = IShadowAccountAnonymizerDispatcher { contract_address: anonymizer };
let identity_key = user.compute_identity_key(contract_address: anonymizer);
// The shadow account the attestation was minted for is the one that was deployed, and it holds
// nothing after collection.
let shadow_account_info = *anonymizer_disp
.get_shadow_accounts(partial_commitment(:identity_key, :dapp_name), 0, 1, false)[0];
assert!(shadow_account_info.is_deployed);
assert_eq!(shadow_account_info.address, predicted_shadow_account.address);
assert_eq!(token.balance_of(address: shadow_account_info.address), 0);
}

Expand Down
18 changes: 17 additions & 1 deletion packages/privacy/src/tests/utils_for_tests.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2151,6 +2151,19 @@ pub(crate) impl PrivacyCfgImpl of PrivacyCfgTrait {
}

fn execute_actions_e2e(self: @PrivacyCfg, user: User, client_actions: Span<ClientAction>) {
self.execute_actions_e2e_screened(:user, :client_actions, screening: Option::None)
}

/// Drives the client and server halves end to end, screening with `screening` when given and
/// otherwise with whatever the harness can derive. A `Delegated` depositor needs the explicit
/// form: the address to attest is the one its invoke returns, which is not knowable before the
/// invoke runs — off chain it is predicted, which is what `get_shadow_accounts` is for.
fn execute_actions_e2e_screened(
self: @PrivacyCfg,
user: User,
client_actions: Span<ClientAction>,
screening: Option<ScreeningAttestation>,
) {
let calls = self
.wrap_inputs_into_calls(
user_addr: user.address, user_private_key: user.private_key, :client_actions,
Expand All @@ -2167,7 +2180,10 @@ pub(crate) impl PrivacyCfgImpl of PrivacyCfgTrait {
let message_hash = compute_hash_from_message(:from, :message);
let mut proof_facts: ProofFacts = Default::default();
proof_facts.message_to_l1_hashes = [message_hash].span();
let screening = self._auto_screening(actions: server_actions);
let screening = match screening {
Option::Some(attestation) => Option::Some(attestation),
Option::None => self._auto_screening(actions: server_actions),
};
self._cheat_proof_facts(:proof_facts);
self.server.apply_actions(actions: server_actions, :screening);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/shadow_account_anonymizer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ Each user interaction is identified by a commitment. The anonymizer keeps a regi
commitment to a dedicated shadow account contract that performs the dapp calls and holds the resulting
funds, which are then settled back into the privacy pool's open notes. Driving interactions is
restricted to the privacy contract the anonymizer is configured for.

The funds reach an open note through a sub-account rather than through the user's own address, so
the anonymizer also answers the pool's screening query: given an interaction's calldata, it names
the sub-account that interaction settles its notes through, which is the address screening must
cover.
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ pub trait IShadowAccountAnonymizer<T> {
/// #### Returns
/// - ([`Span<OpenNoteDeposit>`](privacy::objects::OpenNoteDeposit)) - one deposit per open
/// note, for the privacy contract to apply.
/// - (`Span<ContractAddress>`) - the shadow account those deposits passed through, which the
/// privacy contract screens for them when this anonymizer's policy is `Delegated`. It is
/// named whether or not it was already deployed, its address being deterministic. Empty only
/// when there are no deposits either, since the privacy contract reads this span only after
/// deposits it has to attribute.
///
/// #### Preconditions
/// - Caller must be the configured privacy contract.
Expand All @@ -140,7 +145,7 @@ pub trait IShadowAccountAnonymizer<T> {
identity_commitment: IdentityCommitment,
calls: Array<Call>,
open_notes: Span<OpenNote>,
) -> Span<OpenNoteDeposit>;
) -> (Span<OpenNoteDeposit>, Span<ContractAddress>);

/// Resolves the shadow accounts for nonces `[start_nonce, end_nonce)` under
/// `partial_commitment`, one [`ShadowAccountInfo`](ShadowAccountInfo) per nonce in ascending
Expand Down Expand Up @@ -323,7 +328,7 @@ pub mod ShadowAccountAnonymizer {
identity_commitment: IdentityCommitment,
calls: Array<Call>,
open_notes: Span<OpenNote>,
) -> Span<OpenNoteDeposit> {
) -> (Span<OpenNoteDeposit>, Span<ContractAddress>) {
assert(
get_caller_address() == self.privacy_contract.read(), errors::UNAUTHORIZED_CALLER,
);
Expand All @@ -333,7 +338,16 @@ pub mod ShadowAccountAnonymizer {
shadow_account: shadow_account.contract_address, :open_notes,
);
shadow_account.execute(calls);
self.collect_open_notes(:shadow_account, :note_balance_snapshots)
let deposits = self.collect_open_notes(:shadow_account, :note_balance_snapshots);
// The privacy contract screens the account the funds passed through, and reads this
// span only behind deposits — so an interaction settling no note names nobody, which
// is the same thing as returning no deposits.
let associated_addresses = if deposits.is_empty() {
array![]
} else {
array![shadow_account.contract_address]
};
(deposits, associated_addresses.span())
}

fn get_shadow_accounts(
Expand Down Expand Up @@ -470,6 +484,7 @@ pub mod ShadowAccountAnonymizer {
}
}


/// Pairs `CollectPolicy::Diff` notes with the shadow account's `token` balance before the
/// interaction. Other policies are paired with (unused) zero.
fn snapshot_open_notes(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ use snforge_std::{
DeclareResultTrait, EventSpyTrait, EventsFilterTrait, TokenTrait, declare, spy_events,
};
use starknet::account::Call;
use starknet::{ContractAddress, SyscallResultTrait};
use starknet::syscalls::call_contract_syscall;
use starknet::{ContractAddress, SyscallResult, SyscallResultTrait};
use starkware_accounts::shadow_account::{IShadowAccountDispatcher, IShadowAccountDispatcherTrait};
use starkware_utils_testing::test_utils::{
TokenHelperTrait, assert_expected_event_emitted, assert_panic_with_felt_error,
Expand Down Expand Up @@ -735,3 +736,104 @@ fn test_get_shadow_accounts_inverted_range_reverts() {
anonymizer_disp(components.anonymizer)
.get_shadow_accounts(partial_commitment('USER', 'DAPP'), 5, 3, false);
}

/// A single open note collecting the shadow account's whole `token` balance.
fn collect_all_note(token: ContractAddress) -> Span<OpenNote> {
array![OpenNote { note_id: NOTE_ID, token, collect_policy: CollectPolicy::All }].span()
}

/// The calldata `privacy_invoke_with_computation` is called with, which the privacy contract
/// forwards verbatim when it asks a delegated depositor which addresses its deposits are
/// associated with.
fn invoke_calldata(
identity_commitment: felt252, calls: Array<Call>, open_notes: Span<OpenNote>,
) -> Span<felt252> {
let mut calldata = array![identity_commitment];
calls.serialize(ref calldata);
open_notes.serialize(ref calldata);
calldata.span()
}

/// Runs the invoke over a raw syscall, the way the privacy contract does, and hands back its
/// undecoded return data.
fn invoke_return_data(
anonymizer: ContractAddress, invoke_calldata: Span<felt252>,
) -> SyscallResult<Span<felt252>> {
cheat_caller_address_once(contract_address: anonymizer, caller_address: PRIVACY);
call_contract_syscall(
address: anonymizer,
entry_point_selector: selector!("privacy_invoke_with_computation"),
calldata: invoke_calldata,
)
}

#[test]
fn test_invoke_names_the_shadow_account_it_ran_through() {
let components = deploy_components();
let anonymizer = anonymizer_disp(components.anonymizer);
let token = components.token.contract_address();
let identity_commitment = anonymizer.privacy_compute('USER', 'DAPP', 1);
// The address is advertised before the account exists, so an attestation can be minted ahead of
// the interaction.
let predicted = shadow_account_info(components.anonymizer, 1).address;
components.token.supply(address: components.mock_dapp, amount: AMOUNT);

let (deposits, associated_addresses) = components
.invoke_naming_addresses(
:identity_commitment,
calls: array![transfer_to_caller_call(components.mock_dapp, token, AMOUNT)],
open_notes: collect_all_note(token),
);

assert_eq!(deposits.len(), 1);
assert_eq!(associated_addresses.len(), 1);
// The funds passed through this account, and it is the one the prediction named.
assert_eq!(*associated_addresses[0], anonymizer.get_shadow_account(identity_commitment));
assert_eq!(*associated_addresses[0], predicted);
}

#[test]
fn test_invoke_without_open_notes_names_nobody() {
let components = deploy_components();
let anonymizer = anonymizer_disp(components.anonymizer);
let token = components.token.contract_address();
components.token.supply(address: components.mock_dapp, amount: AMOUNT);

// An interaction settling no note funds no deposit, so it puts no one up for screening — and
// the privacy contract, stopping at the deposits, never looks for an address behind them. The
// dapp call still runs; what it leaves in the shadow account simply goes uncollected.
let (deposits, associated_addresses) = components
.invoke_naming_addresses(
identity_commitment: anonymizer.privacy_compute('USER', 'DAPP', 1),
calls: array![transfer_to_caller_call(components.mock_dapp, token, AMOUNT)],
open_notes: array![].span(),
);

assert!(deposits.is_empty());
assert!(associated_addresses.is_empty());
}

#[test]
fn test_invoke_return_data_is_the_deposits_then_the_addresses() {
let components = deploy_components();
let anonymizer = anonymizer_disp(components.anonymizer);
let token = components.token.contract_address();
let identity_commitment = anonymizer.privacy_compute('USER', 'DAPP', 1);
components.token.supply(address: components.mock_dapp, amount: AMOUNT);
let calldata = invoke_calldata(
identity_commitment,
calls: array![transfer_to_caller_call(components.mock_dapp, token, AMOUNT)],
open_notes: collect_all_note(token),
);

// The privacy contract deserializes the deposits, then — for a `Delegated` depositor — the
// addresses behind them, and rejects anything left over. Read the raw return data in that same
// order to pin the layout it depends on.
let mut return_data = invoke_return_data(components.anonymizer, calldata).unwrap_syscall();
let deposits: Span<OpenNoteDeposit> = Serde::deserialize(ref return_data).unwrap();
let associated_addresses: Span<ContractAddress> = Serde::deserialize(ref return_data).unwrap();
assert!(return_data.is_empty());
assert_eq!(deposits.len(), 1);
assert_eq!(associated_addresses.len(), 1);
assert_eq!(*associated_addresses[0], anonymizer.get_shadow_account(identity_commitment));
}
14 changes: 14 additions & 0 deletions packages/shadow_account_anonymizer/src/tests/test_utils.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,26 @@ pub struct Components {
#[generate_trait]
pub impl ComponentsImpl of ComponentsTrait {
/// Calls `privacy_invoke_with_computation` cheating the caller to be the privacy contract.
/// Runs the invoke and returns only its deposits, which is all most tests look at. The
/// addresses it also returns are the subject of `invoke_naming_addresses`.
fn invoke(
self: @Components,
identity_commitment: felt252,
calls: Array<Call>,
open_notes: Span<OpenNote>,
) -> Span<OpenNoteDeposit> {
let (deposits, _) = self.invoke_naming_addresses(:identity_commitment, :calls, :open_notes);
deposits
}

/// Runs the invoke and returns both halves of what it answers: the deposits, and the addresses
/// the privacy contract screens for them.
fn invoke_naming_addresses(
self: @Components,
identity_commitment: felt252,
calls: Array<Call>,
open_notes: Span<OpenNote>,
) -> (Span<OpenNoteDeposit>, Span<ContractAddress>) {
cheat_caller_address_once(contract_address: *self.anonymizer, caller_address: PRIVACY);
anonymizer_disp(*self.anonymizer)
.privacy_invoke_with_computation(:identity_commitment, :calls, :open_notes)
Expand Down
5 changes: 5 additions & 0 deletions sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

### Added

- Regenerated `ShadowAccountAnonymizerABI` against the anonymizer's new
`privacy_invoke_with_computation` return shape, `(Span<OpenNoteDeposit>, Span<ContractAddress>)`:
the interaction now names the shadow account its deposits passed through — the address the pool
screens for them — after those deposits, so no separate query is needed. Calldata is unchanged, so
code that only builds invoke calldata is unaffected.
- `exemptOpenNoteDepositor(admin, provider, pool, depositor)` in `starknet-privacy-sdk/testing`,
which lists a depositor as screening-exempt on a devnet pool. An Invoke target that funds open
notes is the transaction's screening subject unless it is listed, so a harness deploying its own
Expand Down
22 changes: 11 additions & 11 deletions sdk/src/internal/anonymizer-abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ export const ShadowAccountAnonymizerABI = [
},
],
},
{
type: "struct",
name: "core::array::Span::<core::starknet::contract_address::ContractAddress>",
members: [
{
name: "snapshot",
type: "@core::array::Array::<core::starknet::contract_address::ContractAddress>",
},
],
},
{
type: "enum",
name: "core::bool",
Expand Down Expand Up @@ -203,7 +213,7 @@ export const ShadowAccountAnonymizerABI = [
],
outputs: [
{
type: "core::array::Span::<privacy::objects::OpenNoteDeposit>",
type: "(core::array::Span::<privacy::objects::OpenNoteDeposit>, core::array::Span::<core::starknet::contract_address::ContractAddress>)",
},
],
state_mutability: "external",
Expand Down Expand Up @@ -471,16 +481,6 @@ export const ShadowAccountAnonymizerABI = [
},
],
},
{
type: "struct",
name: "core::array::Span::<core::starknet::contract_address::ContractAddress>",
members: [
{
name: "snapshot",
type: "@core::array::Array::<core::starknet::contract_address::ContractAddress>",
},
],
},
{
type: "interface",
name: "starkware_utils::components::roles::interface::ICommonRoles",
Expand Down
Loading