Skip to content

Commit

Permalink
test: improve aztec-nr testing infra (#6611)
Browse files Browse the repository at this point in the history
Some time ago @sklppy88 created some note getter tests in noir. These
could not be placed in the same crate as `NoteInterface` due to
noir-lang/noir#4502. However, this prevented
us from importing the `TestNote` in the original aztec-nr crate as that
would've created cyclical imports, which are not allowed.

Now that noir-lang/noir#4502 is fixed, we can
have the `TestNote` live alongisde `NoteInterface`. This PR deletes the
`test` crate and creates instead a `test` module inside the `aztec_nr`
crate. I moved the note getter tests into a `test` submodule, meaning we
can access internal functions (i.e. no more `pub` internals) while still
having the tests live in separate files.

I also created a context builder object, which is something I've had to
manually craft multiple times, and is now slightly more complicated due
to the new AVM opcodes.

Hopefully this will be a better foundation over which to continue
writing Noir tests for aztec-nr.
  • Loading branch information
nventuro authored and AztecBot committed May 24, 2024
1 parent e6cff35 commit 0d2a0b8
Show file tree
Hide file tree
Showing 14 changed files with 334 additions and 155 deletions.
1 change: 0 additions & 1 deletion Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@ members = [
"compressed-string",
"easy-private-state",
"value-note",
"tests",
]
2 changes: 2 additions & 0 deletions aztec/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ mod prelude;
mod public_storage;
mod encrypted_logs;
use dep::protocol_types;

mod test;
6 changes: 4 additions & 2 deletions aztec/src/note/note_getter.nr
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use crate::note::{
};
use crate::oracle;

mod test;

fn extract_property_value_from_selector<N>(
serialized_note: [Field; N],
selector: PropertySelector
Expand Down Expand Up @@ -105,10 +107,10 @@ pub fn get_notes<Note, N, M, FILTER_ARGS>(
) -> [Option<Note>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL] where Note: NoteInterface<N, M> {
let opt_notes = get_notes_internal(storage_slot, options);

_get_notes_constrain_get_notes_internal(context, storage_slot, opt_notes, options)
constrain_get_notes_internal(context, storage_slot, opt_notes, options)
}

pub fn _get_notes_constrain_get_notes_internal<Note, N, M, FILTER_ARGS>(
fn constrain_get_notes_internal<Note, N, M, FILTER_ARGS>(
context: &mut PrivateContext,
storage_slot: Field,
opt_notes: [Option<Note>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL],
Expand Down
150 changes: 150 additions & 0 deletions aztec/src/note/note_getter/test.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use dep::protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL;
use crate::{
context::PrivateContext,
note::{
note_header::NoteHeader,
note_getter_options::{NoteGetterOptions, Sort, SortOrder, Comparator, PropertySelector},
note_getter::constrain_get_notes_internal
}
};
use dep::protocol_types::address::AztecAddress;

use crate::test::{helpers::context_builder::ContextBuilder, mocks::mock_note::MockNote};

global contract_address = AztecAddress::from_field(69);
global storage_slot: Field = 42;

fn setup() -> PrivateContext {
ContextBuilder::new().contract_address(contract_address).private()
}

fn build_valid_note(value: Field) -> MockNote {
MockNote::new(value).contract_address(contract_address).storage_slot(storage_slot).build()
}

#[test]
fn validates_note_and_returns_it() {
let mut context = setup();

let value = 1337;
let test_note = build_valid_note(value);

let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL];
opt_notes[0] = Option::some(test_note);

let options = NoteGetterOptions::new();
let returned = constrain_get_notes_internal(&mut context, storage_slot, opt_notes, options);

assert_eq(returned[0].unwrap().value, value);
}

#[test(should_fail)]
fn cannot_return_zero_notes() {
let mut context = setup();

let mut opt_notes: [Option<MockNote>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL] = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL];

let mut options = NoteGetterOptions::new();
let _ = constrain_get_notes_internal(&mut context, storage_slot, opt_notes, options);
}

#[test(should_fail)]
fn mismatched_address() {
let mut context = setup();

let note = MockNote::new(1).storage_slot(storage_slot).build(); // We're not setting the right contract address

let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL];
opt_notes[0] = Option::some(note);

let mut options = NoteGetterOptions::new();
let _ = constrain_get_notes_internal(&mut context, storage_slot, opt_notes, options);
}

#[test(should_fail)]
fn mismatched_storage_slot() {
let mut context = setup();

let note = MockNote::new(1).storage_slot(storage_slot + 1).build(); // We're not setting the right storage slot

let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL];
opt_notes[0] = Option::some(note);

let mut options = NoteGetterOptions::new();
let _ = constrain_get_notes_internal(&mut context, storage_slot, opt_notes, options);
}

#[test(should_fail)]
fn mismatched_selector() {
let mut context = setup();

let value = 10;
let note = build_valid_note(value);

let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL];
opt_notes[0] = Option::some(note);

let mut options = NoteGetterOptions::new();
options = options.select(
PropertySelector { index: 0, offset: 0, length: 32 },
value + 1,
Option::some(Comparator.EQ)
);

let _ = constrain_get_notes_internal(&mut context, storage_slot, opt_notes, options);
}

#[test(should_fail)]
fn mismatched_desc_sort_order() {
let mut context = setup();

let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL];
// Notes in ascending order
opt_notes[0] = Option::some(build_valid_note(1));
opt_notes[1] = Option::some(build_valid_note(2));

let mut options = NoteGetterOptions::new();
options = options.sort(
PropertySelector { index: 0, offset: 0, length: 32 },
SortOrder.DESC
);
let _ = constrain_get_notes_internal(&mut context, storage_slot, opt_notes, options);
}

#[test(should_fail)]
fn mismatched_asc_sort_order() {
let mut context = setup();

let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL];
// Notes in descending order
opt_notes[0] = Option::some(build_valid_note(2));
opt_notes[1] = Option::some(build_valid_note(1));

let mut options = NoteGetterOptions::new();
options = options.sort(
PropertySelector { index: 0, offset: 0, length: 32 },
SortOrder.ASC
);
let _ = constrain_get_notes_internal(&mut context, storage_slot, opt_notes, options);
}

#[test]
fn sparse_notes_array() {
let mut context = setup();

let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL];
opt_notes[1] = Option::some(build_valid_note(0));
opt_notes[2] = Option::some(build_valid_note(1));
opt_notes[3] = Option::some(build_valid_note(2));
opt_notes[5] = Option::some(build_valid_note(3));
opt_notes[8] = Option::some(build_valid_note(4));
opt_notes[13] = Option::some(build_valid_note(5));
opt_notes[21] = Option::some(build_valid_note(6));

let options = NoteGetterOptions::new();
let returned = constrain_get_notes_internal(&mut context, storage_slot, opt_notes, options);

for i in 0..7 {
assert(returned[i].unwrap().value == i as Field);
}
}
11 changes: 3 additions & 8 deletions aztec/src/state_vars/shared_mutable/shared_mutable.nr
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ mod test {
shared_mutable::SharedMutable, scheduled_value_change::ScheduledValueChange,
scheduled_delay_change::ScheduledDelayChange
},
test::helpers::context_builder::ContextBuilder,
oracle::get_public_data_witness::PublicDataWitness
};

Expand All @@ -194,20 +195,14 @@ mod test {

fn setup() -> (SharedMutable<Field, TEST_INITIAL_DELAY, &mut PublicContext>, Field) {
let block_number = 40;
let context = create_context(block_number);
let mut context = ContextBuilder::new().block_number(block_number).public();

let storage_slot = 57;
let state_var = SharedMutable::new(context, storage_slot);
let state_var = SharedMutable::new(&mut context, storage_slot);

(state_var, block_number)
}

fn create_context(block_number: Field) -> &mut PublicContext {
let mut public_context = PublicContext::empty();
let _ = OracleMock::mock("avmOpcodeBlockNumber").returns(block_number);
&mut public_context
}

fn mock_value_change_read(
state_var: SharedMutable<Field, TEST_INITIAL_DELAY, &mut PublicContext>,
pre: Field,
Expand Down
2 changes: 2 additions & 0 deletions aztec/src/test.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod helpers;
mod mocks;
1 change: 1 addition & 0 deletions aztec/src/test/helpers.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod context_builder;
52 changes: 52 additions & 0 deletions aztec/src/test/helpers/context_builder.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::context::{PrivateContext, PublicContext};
use dep::protocol_types::address::AztecAddress;
use dep::std::test::OracleMock;

struct ContextBuilder {
block_number: Option<Field>,
contract_address: Option<AztecAddress>,
}

impl ContextBuilder {
fn new() -> Self {
Self { block_number: Option::none(), contract_address: Option::none() }
}

fn block_number(&mut self, block_number: Field) -> &mut Self {
self.block_number = Option::some(block_number);
self
}

fn contract_address(&mut self, contract_address: AztecAddress) -> &mut Self {
self.contract_address = Option::some(contract_address);
self
}

fn private(&mut self) -> PrivateContext {
let mut context = PrivateContext::empty();

if self.block_number.is_some() {
context.inputs.historical_header.global_variables.block_number = self.block_number.unwrap_unchecked();
}

if self.contract_address.is_some() {
context.inputs.call_context.storage_contract_address = self.contract_address.unwrap_unchecked();
}

context
}

fn public(&mut self) -> PublicContext {
let mut context = PublicContext::empty();

if self.block_number.is_some() {
let _ = OracleMock::mock("avmOpcodeBlockNumber").returns(self.block_number.unwrap());
}

if self.contract_address.is_some() {
let _ = OracleMock::mock("avmOpcodeAddress").returns(self.contract_address.unwrap());
}

context
}
}
1 change: 1 addition & 0 deletions aztec/src/test/mocks.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod mock_note;
119 changes: 119 additions & 0 deletions aztec/src/test/mocks/mock_note.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use crate::{context::PrivateContext, note::{note_header::NoteHeader, note_interface::NoteInterface}};

use dep::protocol_types::{address::AztecAddress, grumpkin_point::GrumpkinPoint};

global MOCK_NOTE_LENGTH = 1;
// MOCK_NOTE_LENGTH * 32 + 32(storage_slot as bytes) + 32(note_type_id as bytes)
global MOCK_NOTE_BYTES_LENGTH: Field = 1 * 32 + 64;

struct MockNote {
header: NoteHeader,
value: Field,
}

impl NoteInterface<MOCK_NOTE_LENGTH, MOCK_NOTE_BYTES_LENGTH> for MockNote {
fn serialize_content(self) -> [Field; MOCK_NOTE_LENGTH] {
[self.value]
}

fn deserialize_content(fields: [Field; MOCK_NOTE_LENGTH]) -> Self {
Self {
value: fields[0],
header: NoteHeader::empty(),
}
}

fn compute_note_content_hash(self) -> Field {
0
}

fn get_header(self) -> NoteHeader {
self.header
}

fn set_header(&mut self, header: NoteHeader) -> () {
self.header = header;
}

fn get_note_type_id() -> Field {
0
}

fn compute_nullifier(self, _context: &mut PrivateContext) -> Field {
0
}

fn compute_nullifier_without_context(self) -> Field {
0
}

fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) {
assert(
false, "MockNote does not support broadcast."
);
}

fn to_be_bytes(self, storage_slot: Field) -> [u8; MOCK_NOTE_BYTES_LENGTH] {
let serialized_note = self.serialize_content();

let mut buffer: [u8; MOCK_NOTE_BYTES_LENGTH] = [0; MOCK_NOTE_BYTES_LENGTH];

let storage_slot_bytes = storage_slot.to_be_bytes(32);
let note_type_id_bytes = MockNote::get_note_type_id().to_be_bytes(32);

for i in 0..32 {
buffer[i] = storage_slot_bytes[i];
buffer[32 + i] = note_type_id_bytes[i];
}

for i in 0..serialized_note.len() {
let bytes = serialized_note[i].to_be_bytes(32);
for j in 0..32 {
buffer[64 + i * 32 + j] = bytes[j];
}
}
buffer
}
}

struct MockNoteBuilder {
value: Field,
contract_address: Option<AztecAddress>,
storage_slot: Option<Field>,
}

impl MockNoteBuilder {
fn new(value: Field) -> Self {
MockNoteBuilder { value, contract_address: Option::none(), storage_slot: Option::none() }
}

fn contract_address(&mut self, contract_address: AztecAddress) -> &mut Self {
self.contract_address = Option::some(contract_address);
self
}

fn storage_slot(&mut self, storage_slot: Field) -> &mut Self {
self.storage_slot = Option::some(storage_slot);
self
}

fn build(self) -> MockNote {
let mut header = NoteHeader::empty();

if self.contract_address.is_some() {
header.contract_address = self.contract_address.unwrap();
}

if self.storage_slot.is_some() {
header.storage_slot = self.storage_slot.unwrap();
}

MockNote { value: self.value, header }
}
}

impl MockNote {
pub fn new(value: Field) -> MockNoteBuilder {
MockNoteBuilder::new(value)
}
}
Loading

0 comments on commit 0d2a0b8

Please sign in to comment.