Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use aztec::macros::aztec;
mod packed_note;

/// Used to test note getter in e2e_note_getter.test.ts
#[aztec]
Expand All @@ -16,9 +17,12 @@ pub contract NoteGetter {

use field_note::FieldNote;

use crate::packed_note::PackedNote;

#[storage]
struct Storage<Context> {
set: Owned<PrivateSet<FieldNote, Context>, Context>,
packed_set: Owned<PrivateSet<PackedNote, Context>, Context>,
}

#[external("private")]
Expand All @@ -42,4 +46,42 @@ pub contract NoteGetter {
));
notes.map(|note| note.value)
}

#[external("private")]
fn insert_packed_note(high: u8, low: u8) {
let owner = self.msg_sender();
let note = PackedNote { high, low };

self.storage.packed_set.at(owner).insert(note).deliver(MessageDelivery.ONCHAIN_CONSTRAINED);
}

#[external("utility")]
unconstrained fn select_packed_notes_by_high(
owner: AztecAddress,
comparator: u8,
value: Field,
) -> BoundedVec<[u8; 2], 10> {
let selector = PackedNote::high_selector();
let notes = self.storage.packed_set.at(owner).view_notes(NoteViewerOptions::new().select(
selector,
comparator,
value,
));
notes.map(|note: PackedNote| [note.high, note.low])
}

#[external("utility")]
unconstrained fn select_packed_notes_by_low(
owner: AztecAddress,
comparator: u8,
value: Field,
) -> BoundedVec<[u8; 2], 10> {
let selector = PackedNote::low_selector();
let notes = self.storage.packed_set.at(owner).view_notes(NoteViewerOptions::new().select(
selector,
comparator,
value,
));
notes.map(|note: PackedNote| [note.high, note.low])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use aztec::{
macros::notes::note,
note::note_getter_options::PropertySelector,
protocol::traits::{Deserialize, Packable, Serialize},
};

/// A note that packs two u8 fields (high, low) into a single Field.
/// The layout in the packed Field is: (high << 8) + low.
/// This means in the big-endian 32-byte representation:
/// - low occupies offset=0, length=1 (the least significant byte)
/// - high occupies offset=1, length=1 (the second least significant byte)
#[derive(Deserialize, Eq, Serialize)]
#[note]
pub struct PackedNote {
pub high: u8,
pub low: u8,
}

impl Packable for PackedNote {
let N: u32 = 1;

fn pack(self) -> [Field; Self::N] {
[(self.high as Field) * 256 + (self.low as Field)]
}

fn unpack(packed: [Field; Self::N]) -> Self {
let low = packed[0] as u8;
let high = ((packed[0] - low as Field) / 256) as u8;
Self { high, low }
}
}

impl PackedNote {
pub fn high_selector() -> PropertySelector {
PropertySelector { index: 0, offset: 1, length: 1 }
}

pub fn low_selector() -> PropertySelector {
PropertySelector { index: 0, offset: 0, length: 1 }
}
}

mod test {
use super::{Packable, PackedNote};

#[test]
fn test_pack_unpack() {
let note = PackedNote { high: 42, low: 7 };
let unpacked = PackedNote::unpack(note.pack());
assert_eq(unpacked.high, 42);
assert_eq(unpacked.low, 7);
}

#[test]
fn test_pack_unpack_max() {
let note = PackedNote { high: 255, low: 255 };
let unpacked = PackedNote::unpack(note.pack());
assert_eq(unpacked.high, 255);
assert_eq(unpacked.low, 255);
}
}
66 changes: 66 additions & 0 deletions yarn-project/end-to-end/src/e2e_note_getter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,72 @@ describe('e2e_note_getter', () => {
});
});

describe('sub-field property selector', () => {
let contract: NoteGetterContract;

beforeAll(async () => {
({ contract } = await NoteGetterContract.deploy(wallet).send({ from: defaultAddress }));

// Insert packed notes with (high, low) pairs.
// PackedNote packs two u8s into one Field: (high << 8) + low.
// Sub-field selectors use Noir's LSB convention to extract individual u8 values.
await Promise.all([
contract.methods.insert_packed_note(1, 10).send({ from: defaultAddress }),
contract.methods.insert_packed_note(2, 10).send({ from: defaultAddress }),
contract.methods.insert_packed_note(1, 20).send({ from: defaultAddress }),
contract.methods.insert_packed_note(3, 30).send({ from: defaultAddress }),
]);
});

it('filters by high sub-field', async () => {
// high occupies offset=1, length=1 in the packed Field (second LSB)
const { result } = await contract.methods
.select_packed_notes_by_high(defaultAddress, Comparator.EQ, 1)
.simulate({ from: defaultAddress });

const notes = boundedVecToArray(result) as bigint[][];
expect(notes).toHaveLength(2);
expect(notes.map(([h, l]) => [Number(h), Number(l)]).sort()).toEqual(
[
[1, 10],
[1, 20],
].sort(),
);
});

it('filters by low sub-field', async () => {
// low occupies offset=0, length=1 in the packed Field (LSB)
const { result } = await contract.methods
.select_packed_notes_by_low(defaultAddress, Comparator.EQ, 10)
.simulate({ from: defaultAddress });

const notes = boundedVecToArray(result) as bigint[][];
expect(notes).toHaveLength(2);
expect(notes.map(([h, l]) => [Number(h), Number(l)]).sort()).toEqual(
[
[1, 10],
[2, 10],
].sort(),
);
});

it('filters with GT comparator on sub-field', async () => {
// low > 10 should match (1,20) and (3,30)
const { result } = await contract.methods
.select_packed_notes_by_low(defaultAddress, Comparator.GT, 10)
.simulate({ from: defaultAddress });

const notes = boundedVecToArray(result) as bigint[][];
expect(notes).toHaveLength(2);
expect(notes.map(([h, l]) => [Number(h), Number(l)]).sort()).toEqual(
[
[1, 20],
[3, 30],
].sort(),
);
});
});

describe('status filter', () => {
let contract: TestContract;
let owner: AztecAddress;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,50 @@ describe('getNotes', () => {
]);
});

it('should select using sub-field byte range with LSB offset convention', () => {
// Each note has a single field. We'll use values where the low bytes differ:
// 0x0102 = 258: low byte (offset=0, len=1) = 0x02, second byte (offset=1, len=1) = 0x01
// 0x0302 = 770: low byte = 0x02, second byte = 0x03
// 0x0105 = 261: low byte = 0x05, second byte = 0x01
const notes = [createNote([258n]), createNote([770n]), createNote([261n])];

// Select by low byte (offset=0, length=1) == 0x02
{
const options = {
selects: [{ selector: { index: 0, offset: 0, length: 1 }, value: new Fr(0x02n), comparator: Comparator.EQ }],
};
const result = pickNotes(notes, options);
expectNotes(result, [[258n], [770n]]);
}

// Select by second byte (offset=1, length=1) == 0x01
{
const options = {
selects: [{ selector: { index: 0, offset: 1, length: 1 }, value: new Fr(0x01n), comparator: Comparator.EQ }],
};
const result = pickNotes(notes, options);
expectNotes(result, [[258n], [261n]]);
}

// Select by two low bytes (offset=0, length=2) == 0x0102 = 258
{
const options = {
selects: [{ selector: { index: 0, offset: 0, length: 2 }, value: new Fr(0x0102n), comparator: Comparator.EQ }],
};
const result = pickNotes(notes, options);
expectNotes(result, [[258n]]);
}

// GT on low byte: low byte > 0x02 matches only 261 (low byte = 0x05)
{
const options = {
selects: [{ selector: { index: 0, offset: 0, length: 1 }, value: new Fr(0x02n), comparator: Comparator.GT }],
};
const result = pickNotes(notes, options);
expectNotes(result, [[261n]]);
}
});

it('should get sorted matching notes with GTE and LTE', () => {
const notes = [
createNote([2n, 1n, 1n]),
Expand Down
11 changes: 9 additions & 2 deletions yarn-project/pxe/src/contract_function_simulator/pick_notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,15 @@ interface ContainsNote {

const selectPropertyFromPackedNoteContent = (noteData: Fr[], selector: PropertySelector): Fr => {
const noteValueBuffer = noteData[selector.index].toBuffer();
const noteValue = noteValueBuffer.subarray(selector.offset, selector.offset + selector.length);
return Fr.fromBuffer(noteValue);
// Noir's PropertySelector counts offset from the LSB (last byte of the big-endian buffer),
// so offset=0,length=Fr.SIZE_IN_BYTES reads the entire field, and offset=0,length=1 reads the last byte.
const start = Fr.SIZE_IN_BYTES - selector.offset - selector.length;
const end = Fr.SIZE_IN_BYTES - selector.offset;
const noteValue = noteValueBuffer.subarray(start, end);
// Left-pad to Fr.SIZE_IN_BYTES so Fr.fromBuffer interprets the value correctly.
const padded = Buffer.alloc(Fr.SIZE_IN_BYTES);
noteValue.copy(padded, Fr.SIZE_IN_BYTES - noteValue.length);
return Fr.fromBuffer(padded);
};
Comment on lines 87 to 98
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First time I see this code, so might be redundant or unnecessary, but shouldn't we place a bunch of bounds check assertions here?


const selectNotes = <T extends ContainsNote>(noteDatas: T[], selects: Select[]): T[] =>
Expand Down
Loading