Skip to content

Commit 2558027

Browse files
committed
feat(sub-account-anonymizer): until_undeployed flag on get_sub_accounts
Add a trailing `until_undeployed: bool` to the anonymizer's `get_sub_accounts` view. When true, the view stops at the first undeployed nonce and returns only the contiguous deployed prefix; when false it resolves every nonce in [start, end) (unchanged). This lets the client resolve a user's deployed sub-accounts in one view call instead of scanning window-by-window client-side. Regenerating SubAccountAnonymizerABI also picks up the OpenNote.collect_policy field, from which the committed ABI had drifted (never regenerated after CollectPolicy landed in Cairo).
1 parent 1ee4a25 commit 2558027

5 files changed

Lines changed: 62 additions & 13 deletions

File tree

packages/privacy/src/tests/test_e2e.cairo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1990,7 +1990,7 @@ fn test_e2e_sub_account_anonymizer_compute_invoke() {
19901990
let anonymizer_disp = ISubAccountAnonymizerDispatcher { contract_address: anonymizer };
19911991
let identity_key = user.compute_identity_key(contract_address: anonymizer);
19921992
let sub_account_info = *anonymizer_disp
1993-
.get_sub_accounts(partial_commitment(:identity_key, :dapp_name), 0, 1)[0];
1993+
.get_sub_accounts(partial_commitment(:identity_key, :dapp_name), 0, 1, false)[0];
19941994
assert!(sub_account_info.is_deployed);
19951995
assert_eq!(token.balance_of(address: sub_account_info.address), 0);
19961996
}

packages/sub_account_anonymizer/src/sub_account_anonymizer.cairo

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,24 @@ pub trait ISubAccountAnonymizer<T> {
141141
/// the commitment, `hash(identity_key, dapp_name)`.
142142
/// - `start_nonce` (`u64`) - the first nonce to resolve (inclusive).
143143
/// - `end_nonce` (`u64`) - the upper bound (exclusive).
144+
/// - `until_undeployed` (`bool`) - When true, resolution stops at the first undeployed nonce
145+
/// and that nonce is not returned, so the result is the contiguous deployed prefix of the range.
146+
/// When false, every nonce in the range is returned regardless of deployment.
144147
///
145148
/// #### Returns
146149
/// - ([`Span<SubAccountInfo>`](SubAccountInfo)) - one entry per nonce in `[start_nonce,
147-
/// end_nonce)`.
150+
/// end_nonce)`, or the deployed prefix of that range when `until_undeployed` is true.
148151
///
149152
/// #### Reverts
150153
/// - [`INVALID_RANGE`](errors::INVALID_RANGE): Thrown if `end_nonce < start_nonce`.
151154
/// - [`RANGE_TOO_LARGE`](errors::RANGE_TOO_LARGE): Thrown if `end_nonce - start_nonce` exceeds
152155
/// [`MAX_SCAN_RANGE`](MAX_SCAN_RANGE).
153156
fn get_sub_accounts(
154-
self: @T, partial_commitment: PartialCommitment, start_nonce: u64, end_nonce: u64,
157+
self: @T,
158+
partial_commitment: PartialCommitment,
159+
start_nonce: u64,
160+
end_nonce: u64,
161+
until_undeployed: bool,
155162
) -> Span<SubAccountInfo>;
156163

157164
/// Returns the deployed sub-account address bound to `identity_commitment`.
@@ -318,6 +325,7 @@ pub mod SubAccountAnonymizer {
318325
partial_commitment: PartialCommitment,
319326
start_nonce: u64,
320327
end_nonce: u64,
328+
until_undeployed: bool,
321329
) -> Span<SubAccountInfo> {
322330
assert(end_nonce >= start_nonce, errors::INVALID_RANGE);
323331
assert(
@@ -330,6 +338,9 @@ pub mod SubAccountAnonymizer {
330338
let commitment = commitment_from_partial(partial_commitment, nonce.into());
331339
let stored = self.get_sub_account(commitment);
332340
let is_deployed = stored.is_non_zero();
341+
if until_undeployed && !is_deployed {
342+
break;
343+
}
333344
// Undeployed sub-accounts resolve to the address the deploy syscall would derive.
334345
let address = if is_deployed {
335346
stored

packages/sub_account_anonymizer/src/tests/test_sub_account_anonymizer.cairo

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const NOTE_ID: felt252 = 'NOTE_ID';
2929
/// Resolves the `('USER', 'DAPP', nonce)` sub-account via the range view.
3030
fn sub_account_info(anonymizer: ContractAddress, nonce: u64) -> SubAccountInfo {
3131
let infos = anonymizer_disp(anonymizer)
32-
.get_sub_accounts(partial_commitment('USER', 'DAPP'), nonce, nonce + 1);
32+
.get_sub_accounts(partial_commitment('USER', 'DAPP'), nonce, nonce + 1, false);
3333
*infos[0]
3434
}
3535

@@ -620,7 +620,7 @@ fn test_get_sub_accounts_resolves_deployed_and_undeployed() {
620620
deploy_nonce(components, 1);
621621

622622
// Nonce 2 is undeployed; the view resolves every nonce in range with an is_deployed flag.
623-
let infos = anonymizer.get_sub_accounts(partial, 0, 3);
623+
let infos = anonymizer.get_sub_accounts(partial, 0, 3, false);
624624
assert_eq!(infos.len(), 3);
625625
assert_eq!((*infos[0]).nonce, 0);
626626
assert!((*infos[0]).is_deployed);
@@ -632,16 +632,42 @@ fn test_get_sub_accounts_resolves_deployed_and_undeployed() {
632632
assert!(undeployed.address.is_non_zero());
633633
}
634634

635+
#[test]
636+
fn test_get_sub_accounts_until_undeployed_returns_deployed_prefix() {
637+
let components = deploy_components();
638+
let anonymizer = anonymizer_disp(components.anonymizer);
639+
let partial = partial_commitment('USER', 'DAPP');
640+
deploy_nonce(components, 0);
641+
deploy_nonce(components, 1);
642+
643+
// Nonce 2 is the first gap; until_undeployed resolves the deployed prefix and omits the gap on.
644+
let infos = anonymizer.get_sub_accounts(partial, 0, 5, true);
645+
assert_eq!(infos.len(), 2);
646+
assert_eq!((*infos[0]).nonce, 0);
647+
assert_eq!((*infos[1]).nonce, 1);
648+
}
649+
650+
#[test]
651+
fn test_get_sub_accounts_until_undeployed_on_leading_gap_is_empty() {
652+
let components = deploy_components();
653+
let anonymizer = anonymizer_disp(components.anonymizer);
654+
let partial = partial_commitment('USER', 'DAPP');
655+
656+
// The range's first nonce is undeployed, so the deployed prefix is empty.
657+
let infos = anonymizer.get_sub_accounts(partial, 0, 5, true);
658+
assert_eq!(infos.len(), 0);
659+
}
660+
635661
#[test]
636662
fn test_get_sub_accounts_computed_address_matches_deploy() {
637663
let components = deploy_components();
638664
let anonymizer = anonymizer_disp(components.anonymizer);
639665
let partial = partial_commitment('USER', 'DAPP');
640666

641-
let before = *anonymizer.get_sub_accounts(partial, 0, 1)[0];
667+
let before = *anonymizer.get_sub_accounts(partial, 0, 1, false)[0];
642668
assert!(!before.is_deployed);
643669
deploy_nonce(components, 0);
644-
let after = *anonymizer.get_sub_accounts(partial, 0, 1)[0];
670+
let after = *anonymizer.get_sub_accounts(partial, 0, 1, false)[0];
645671
assert!(after.is_deployed);
646672
// The address computed before deployment matches the actual on-chain deploy address.
647673
assert_eq!(before.address, after.address);
@@ -651,10 +677,11 @@ fn test_get_sub_accounts_computed_address_matches_deploy() {
651677
fn test_get_sub_accounts_scopes_by_dapp_and_nonce() {
652678
let components = deploy_components();
653679
let anonymizer = anonymizer_disp(components.anonymizer);
654-
let dapp = anonymizer.get_sub_accounts(partial_commitment('USER', 'DAPP'), 0, 2);
680+
let dapp = anonymizer.get_sub_accounts(partial_commitment('USER', 'DAPP'), 0, 2, false);
655681
let nonce0 = *dapp[0];
656682
let nonce1 = *dapp[1];
657-
let other_dapp = *anonymizer.get_sub_accounts(partial_commitment('USER', 'OTHER'), 0, 1)[0];
683+
let other_dapp = *anonymizer
684+
.get_sub_accounts(partial_commitment('USER', 'OTHER'), 0, 1, false)[0];
658685
assert!(nonce0.address != nonce1.address);
659686
assert!(nonce0.address != other_dapp.address);
660687
}
@@ -668,7 +695,7 @@ fn test_get_sub_accounts_from_start_nonce() {
668695
deploy_nonce(components, 1);
669696

670697
// Scanning from nonce 1 skips nonce 0; entries carry their own nonce.
671-
let infos = anonymizer.get_sub_accounts(partial, 1, 3);
698+
let infos = anonymizer.get_sub_accounts(partial, 1, 3, false);
672699
assert_eq!(infos.len(), 2);
673700
assert_eq!((*infos[0]).nonce, 1);
674701
assert!((*infos[0]).is_deployed);
@@ -680,7 +707,7 @@ fn test_get_sub_accounts_from_start_nonce() {
680707
fn test_get_sub_accounts_empty_range() {
681708
let components = deploy_components();
682709
let infos = anonymizer_disp(components.anonymizer)
683-
.get_sub_accounts(partial_commitment('USER', 'DAPP'), 5, 5);
710+
.get_sub_accounts(partial_commitment('USER', 'DAPP'), 5, 5, false);
684711
assert_eq!(infos.len(), 0);
685712
}
686713

@@ -689,13 +716,13 @@ fn test_get_sub_accounts_empty_range() {
689716
fn test_get_sub_accounts_range_too_large_reverts() {
690717
let components = deploy_components();
691718
anonymizer_disp(components.anonymizer)
692-
.get_sub_accounts(partial_commitment('USER', 'DAPP'), 0, MAX_SCAN_RANGE + 1);
719+
.get_sub_accounts(partial_commitment('USER', 'DAPP'), 0, MAX_SCAN_RANGE + 1, false);
693720
}
694721

695722
#[test]
696723
#[should_panic(expected: 'INVALID_RANGE')]
697724
fn test_get_sub_accounts_inverted_range_reverts() {
698725
let components = deploy_components();
699726
anonymizer_disp(components.anonymizer)
700-
.get_sub_accounts(partial_commitment('USER', 'DAPP'), 5, 3);
727+
.get_sub_accounts(partial_commitment('USER', 'DAPP'), 5, 3, false);
701728
}

sdk/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@
4141
`collect_policy`), and would have produced calldata the anonymizer rejects. The ABI is regenerated
4242
to include `collect_policy`.
4343

44+
### Changed
45+
46+
- `SubAccountAnonymizerABI`: `get_sub_accounts` gains a trailing `until_undeployed: bool` argument.
47+
When true the view stops at the first undeployed nonce and returns only the contiguous deployed
48+
prefix; when false it resolves every nonce in the range (the prior behavior). Regenerating the ABI
49+
also picks up the `OpenNote.collect_policy` field, from which the generated ABI had drifted.
50+
4451
## 0.14.3-RC.3
4552

4653
### Breaking

sdk/src/internal/anonymizer-abi.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ export const SubAccountAnonymizerABI = [
223223
name: "end_nonce",
224224
type: "core::integer::u64",
225225
},
226+
{
227+
name: "until_undeployed",
228+
type: "core::bool",
229+
},
226230
],
227231
outputs: [
228232
{

0 commit comments

Comments
 (0)