Skip to content

Commit 08e2bd7

Browse files
nchamoaztec-bot
authored andcommitted
fix(aztec-nr): check_notes_order lexicographic multi-key sort (#21973)
1 parent fc60a05 commit 08e2bd7

2 files changed

Lines changed: 111 additions & 15 deletions

File tree

noir-projects/aztec-nr/aztec/src/note/note_getter.nr

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,46 @@ fn check_packed_note<let N: u32>(packed_note: [Field; N], selects: BoundedVec<Op
5555
}
5656
}
5757

58-
fn check_notes_order<let N: u32>(fields_0: [Field; N], fields_1: [Field; N], sorts: BoundedVec<Option<Sort>, N>) {
59-
for i in 0..sorts.max_len() {
60-
if i < sorts.len() {
61-
let sort = sorts.get_unchecked(i).unwrap_unchecked();
62-
let field_0 = extract_property_value_from_selector(fields_0, sort.property_selector);
63-
let field_1 = extract_property_value_from_selector(fields_1, sort.property_selector);
64-
let eq = field_0 == field_1;
65-
let lt = field_0.lt(field_1);
66-
if sort.order == SortOrder.ASC {
67-
assert(eq | lt, "Return notes not sorted in ascending order.");
68-
} else if !eq {
69-
assert(!lt, "Return notes not sorted in descending order.");
58+
// Asserts that two notes are correctly ordered given the sort criteria.
59+
//
60+
// Only the provided criteria are checked -- fields not covered by any criterion are never compared, and if
61+
// `sort_criteria` is empty no ordering is enforced at all. Criteria are evaluated in priority order: the first
62+
// criterion where the two notes differ determines whether the pair is correctly ordered. If all sorted fields are
63+
// equal, any order is accepted.
64+
//
65+
// For example, with criteria `[field_0 ASC, field_1 DESC]`:
66+
// - `[1, 10], [2, 999]` -> passes: field_0 (1 < 2) satisfies ASC, field_1 is not checked.
67+
// - `[1, 10], [1, 5]` -> passes: field_0 ties, so field_1 (10 > 5) satisfies DESC.
68+
// - `[1, 5], [1, 10]` -> fails: field_0 ties, so field_1 (5 < 10) violates DESC.
69+
// - `[1, 10], [1, 10]` -> passes: all fields tie, any order criteria is valid.
70+
//
71+
// With a single criterion, `[field_0 DESC]`:
72+
// - `[10, 1], [5, 20]` -> passes: field_0 (10 > 5) satisfies DESC, field_1 is never compared.
73+
// - `[5, 1], [5, 20]` -> passes: field_0 ties and there are no more criteria, so any order is accepted.
74+
//
75+
// With no criteria:
76+
// - `[2, 5], [1, 10]` -> passes: nothing to check.
77+
//
78+
// This is called by [`confirm_hinted_notes`] to validate that the oracle returned notes in the requested order.
79+
fn assert_notes_order<let N: u32>(note_1: [Field; N], note_2: [Field; N], sort_criteria: BoundedVec<Option<Sort>, N>) {
80+
// Tracks whether a prior criterion already determined the ordering. Once true, all subsequent
81+
// asserts become trivially satisfied. We fold this into the assert condition instead of
82+
// predicating the loop body to avoid gating field extraction behind a branch, which would be
83+
// more expensive in a circuit.
84+
let mut order_decided = false;
85+
for i in 0..sort_criteria.max_len() {
86+
if i < sort_criteria.len() {
87+
let sort = sort_criteria.get_unchecked(i).unwrap_unchecked();
88+
let field_1 = extract_property_value_from_selector(note_1, sort.property_selector);
89+
let field_2 = extract_property_value_from_selector(note_2, sort.property_selector);
90+
if field_1 != field_2 {
91+
let lt = field_1.lt(field_2);
92+
if sort.order == SortOrder.ASC {
93+
assert(lt | order_decided, "Return notes not sorted in ascending order.");
94+
} else {
95+
assert(!lt | order_decided, "Return notes not sorted in descending order.");
96+
}
97+
order_decided = true;
7098
}
7199
}
72100
}
@@ -219,7 +247,7 @@ where
219247
let packed_note = hinted_note.note.pack();
220248
check_packed_note(packed_note, options.selects);
221249
if i != 0 {
222-
check_notes_order(prev_packed_note, packed_note, options.sorts);
250+
assert_notes_order(prev_packed_note, packed_note, options.sorts);
223251
}
224252
prev_packed_note = packed_note;
225253

noir-projects/aztec-nr/aztec/src/note/note_getter/test.nr

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@ use crate::{
22
note::{
33
ConfirmedNote,
44
HintedNote,
5-
note_getter::{confirm_hinted_note, confirm_hinted_notes, extract_property_value_from_selector},
6-
note_getter_options::{NoteGetterOptions, PropertySelector, SortOrder},
5+
note_getter::{
6+
assert_notes_order, confirm_hinted_note, confirm_hinted_notes, extract_property_value_from_selector,
7+
},
8+
note_getter_options::{NoteGetterOptions, PropertySelector, Sort, SortOrder},
79
},
810
oracle::random::random,
911
test::{helpers::test_environment::TestEnvironment, mocks::mock_note::MockNote},
1012
utils::comparison::Comparator,
1113
};
1214

15+
fn sort_criterion(index: u8, order: u8) -> Option<Sort> {
16+
Option::some(
17+
Sort::new(PropertySelector { index, offset: 0, length: 32 }, order),
18+
)
19+
}
20+
1321
use crate::protocol::{address::AztecAddress, traits::FromField};
1422

1523
global storage_slot: Field = 42;
@@ -388,3 +396,63 @@ unconstrained fn extract_property_value_fails_on_oob_index() {
388396
let selector = PropertySelector { index: 1, offset: 0, length: 1 };
389397
let _ = extract_property_value_from_selector(packed, selector);
390398
}
399+
400+
global ASC_DESC_CRITERIA: BoundedVec<Option<Sort>, 2> = BoundedVec::from_array([
401+
sort_criterion(0, SortOrder.ASC),
402+
sort_criterion(1, SortOrder.DESC),
403+
]);
404+
405+
#[test]
406+
unconstrained fn assert_notes_order_passes_when_first_criterion_satisfied() {
407+
assert_notes_order([1, 10], [2, 999], ASC_DESC_CRITERIA);
408+
}
409+
410+
#[test]
411+
unconstrained fn assert_notes_order_passes_when_second_criterion_satisfied_after_tie() {
412+
assert_notes_order([1, 10], [1, 5], ASC_DESC_CRITERIA);
413+
}
414+
415+
#[test(should_fail_with = "Return notes not sorted in descending order.")]
416+
unconstrained fn assert_notes_order_fails_when_second_criterion_violated_after_tie() {
417+
assert_notes_order([1, 5], [1, 10], ASC_DESC_CRITERIA);
418+
}
419+
420+
#[test]
421+
unconstrained fn assert_notes_order_passes_when_all_fields_equal() {
422+
assert_notes_order([1, 10], [1, 10], ASC_DESC_CRITERIA);
423+
}
424+
425+
global DESC_ONLY_CRITERIA: BoundedVec<Option<Sort>, 2> = BoundedVec::from_array([sort_criterion(0, SortOrder.DESC)]);
426+
427+
#[test]
428+
unconstrained fn assert_notes_order_passes_with_single_criterion() {
429+
assert_notes_order([10, 1], [5, 20], DESC_ONLY_CRITERIA);
430+
}
431+
432+
#[test]
433+
unconstrained fn assert_notes_order_passes_with_single_criterion_when_field_ties() {
434+
assert_notes_order([5, 1], [5, 20], DESC_ONLY_CRITERIA);
435+
}
436+
437+
global REVERSED_CRITERIA: BoundedVec<Option<Sort>, 2> = BoundedVec::from_array([
438+
sort_criterion(1, SortOrder.ASC),
439+
sort_criterion(0, SortOrder.DESC),
440+
]);
441+
442+
#[test]
443+
unconstrained fn assert_notes_order_uses_criterion_order_not_field_order() {
444+
// field_0 is 10 > 5 (would fail ASC), but the first criterion targets field_1
445+
assert_notes_order([10, 1], [5, 2], REVERSED_CRITERIA);
446+
}
447+
448+
#[test(should_fail_with = "Return notes not sorted in ascending order.")]
449+
unconstrained fn assert_notes_order_fails_when_criterion_order_not_field_order() {
450+
// First criterion is field_1 ASC: 5 > 2 violates ASC
451+
assert_notes_order([1, 5], [2, 2], REVERSED_CRITERIA);
452+
}
453+
454+
#[test]
455+
unconstrained fn assert_notes_order_passes_with_no_criteria() {
456+
let criteria: BoundedVec<Option<Sort>, 2> = BoundedVec::new();
457+
assert_notes_order([2, 5], [1, 10], criteria);
458+
}

0 commit comments

Comments
 (0)