Skip to content

Commit

Permalink
test: add sync suite (#8550)
Browse files Browse the repository at this point in the history
Fixes #8536 by introducing an initial setup for testing synching. 

Adds a new "spam" contract that uses a large amount of data, such that
we can see the differences between "noop" transactions and more complex
things.

Adds a new `l1StartTime` option to the setup such that we can do an
instant "warp" to make the execution of past stored blocks much simpler.

While the intention was to have a lot of blocks to see synching over a
longer period, building that with non-trivial transactions will take
until I retire.
![Aging Matt Damon
GIF](https://media4.giphy.com/media/GrUhLU9q3nyRG/giphy.gif)

The test is **NOT** set to run in CI explicitly as it is a "measure"
test more than anything else, and without there being a target it is a
bit weird to restrict it 🤷
  • Loading branch information
LHerskind authored Sep 16, 2024
1 parent 979f267 commit ce0a9db
Show file tree
Hide file tree
Showing 12 changed files with 883 additions and 1 deletion.
1 change: 1 addition & 0 deletions noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ members = [
"contracts/schnorr_hardcoded_account_contract",
"contracts/schnorr_single_key_account_contract",
"contracts/stateful_test_contract",
"contracts/spam_contract",
"contracts/test_contract",
"contracts/test_log_contract",
"contracts/token_contract",
Expand Down
10 changes: 10 additions & 0 deletions noir-projects/noir-contracts/contracts/spam_contract/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "spam_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
value_note = { path = "../../../aztec-nr/value-note" }
token_portal_content_hash_lib = { path = "../token_portal_content_hash_lib" }
68 changes: 68 additions & 0 deletions noir-projects/noir-contracts/contracts/spam_contract/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
mod types;

// A contract used for testing a random hodgepodge of small features from simulator and end-to-end tests.
contract Spam {

use dep::aztec::{
prelude::{Map, AztecAddress, PublicMutable},
encrypted_logs::{encrypted_note_emission::encode_and_encrypt_note_with_keys_unconstrained},
keys::getters::get_current_public_keys,
protocol_types::{
hash::poseidon2_hash_with_separator,
constants::{
MAX_NOTE_HASHES_PER_CALL, MAX_NULLIFIERS_PER_CALL, GENERATOR_INDEX__NOTE_NULLIFIER,
MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_CALL
}
}
};

use crate::types::{token_note::TokenNote, balance_set::BalanceSet};

#[aztec(storage)]
struct Storage {
balances: Map<AztecAddress, BalanceSet<TokenNote>>,
public_balances: Map<Field, PublicMutable<U128>>,
}

#[aztec(private)]
fn spam(nullifier_seed: Field, nullifier_count: u32, call_public: bool) {
let caller = context.msg_sender();
let caller_keys = get_current_public_keys(&mut context, caller);
let amount = U128::from_integer(1);

for _ in 0..MAX_NOTE_HASHES_PER_CALL {
storage.balances.at(caller).add(caller_keys.npk_m, U128::from_integer(amount)).emit(
encode_and_encrypt_note_with_keys_unconstrained(&mut context, caller_keys.ovpk_m, caller_keys.ivpk_m, caller)
);
}

for i in 0..MAX_NULLIFIERS_PER_CALL {
if (i < nullifier_count) {
context.push_nullifier(
poseidon2_hash_with_separator(
[nullifier_seed, i as Field],
GENERATOR_INDEX__NOTE_NULLIFIER as Field
)
);
}
}

if (call_public) {
Spam::at(context.this_address()).public_spam(0, MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_CALL).enqueue(&mut context);
Spam::at(context.this_address()).public_spam(
MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_CALL,
MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX
).enqueue(&mut context);
}
}

#[aztec(public)]
#[aztec(internal)]
fn public_spam(start: u32, end: u32) {
let one = U128::from_integer(1);
for i in start..end {
let prev = storage.public_balances.at(i as Field).read();
storage.public_balances.at(i as Field).write(prev + one);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod balance_set;
mod token_note;
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// This file is copied from the token contract.
use dep::aztec::prelude::{NoteGetterOptions, NoteViewerOptions, NoteInterface, PrivateSet};
use dep::aztec::{
context::{PrivateContext, UnconstrainedContext},
protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL,
note::note_emission::OuterNoteEmission, keys::public_keys::NpkM
};
use crate::types::token_note::OwnedNote;

struct BalanceSet<T, Context> {
set: PrivateSet<T, Context>,
}

impl<T, Context> BalanceSet<T, Context> {
pub fn new(context: Context, storage_slot: Field) -> Self {
assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1.");
Self { set: PrivateSet::new(context, storage_slot) }
}
}

impl<T> BalanceSet<T, UnconstrainedContext> {
unconstrained pub fn balance_of<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(self: Self) -> U128 where T: NoteInterface<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN> + OwnedNote {
self.balance_of_with_offset(0)
}

unconstrained pub fn balance_of_with_offset<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(
self: Self,
offset: u32
) -> U128 where T: NoteInterface<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN> + OwnedNote {
let mut balance = U128::from_integer(0);
// docs:start:view_notes
let mut options = NoteViewerOptions::new();
let notes = self.set.view_notes(options.set_offset(offset));
// docs:end:view_notes
for i in 0..options.limit {
if i < notes.len() {
balance = balance + notes.get_unchecked(i).get_amount();
}
}
if (notes.len() == options.limit) {
balance = balance + self.balance_of_with_offset(offset + options.limit);
}

balance
}
}

impl<T> BalanceSet<T, &mut PrivateContext> {
pub fn add<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(
self: Self,
owner_npk_m: NpkM,
addend: U128
) -> OuterNoteEmission<T> where T: NoteInterface<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN> + OwnedNote + Eq {
if addend == U128::from_integer(0) {
OuterNoteEmission::new(Option::none())
} else {
// We fetch the nullifier public key hash from the registry / from our PXE
let mut addend_note = T::new(addend, owner_npk_m.hash());

// docs:start:insert
OuterNoteEmission::new(Option::some(self.set.insert(&mut addend_note)))
// docs:end:insert
}
}

pub fn sub<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(
self: Self,
owner_npk_m: NpkM,
amount: U128
) -> OuterNoteEmission<T> where T: NoteInterface<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN> + OwnedNote + Eq {
let subtracted = self.try_sub(amount, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL);

// try_sub may have substracted more or less than amount. We must ensure that we subtracted at least as much as
// we needed, and then create a new note for the owner for the change (if any).
assert(subtracted >= amount, "Balance too low");
self.add(owner_npk_m, subtracted - amount)
}

// Attempts to remove 'target_amount' from the owner's balance. try_sub returns how much was actually subtracted
// (i.e. the sum of the value of nullified notes), but this subtracted amount may be more or less than the target
// amount.
// This may seem odd, but is unfortunately unavoidable due to the number of notes available and their amounts being
// unknown. What try_sub does is a best-effort attempt to consume as few notes as possible that add up to more than
// `target_amount`.
// The `max_notes` parameter is used to fine-tune the number of constraints created by this function. The gate count
// scales relatively linearly with `max_notes`, but a lower `max_notes` parameter increases the likelihood of
// `try_sub` subtracting an amount smaller than `target_amount`.
pub fn try_sub<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(
self: Self,
target_amount: U128,
max_notes: u32
) -> U128 where T: NoteInterface<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN> + OwnedNote + Eq {
// We are using a preprocessor here (filter applied in an unconstrained context) instead of a filter because
// we do not need to prove correct execution of the preprocessor.
// Because the `min_sum` notes is not constrained, users could choose to e.g. not call it. However, all this
// might result in is simply higher DA costs due to more nullifiers being emitted. Since we don't care
// about proving optimal note usage, we can save these constraints and make the circuit smaller.
let options = NoteGetterOptions::with_preprocessor(preprocess_notes_min_sum, target_amount).set_limit(max_notes);
let notes = self.set.pop_notes(options);

let mut subtracted = U128::from_integer(0);
for i in 0..options.limit {
if i < notes.len() {
let note = notes.get_unchecked(i);
subtracted = subtracted + note.get_amount();
}
}

subtracted
}
}

// Computes the partial sum of the notes array, stopping once 'min_sum' is reached. This can be used to minimize the
// number of notes read that add to some value, e.g. when transferring some amount of tokens.
// The preprocessor (a filter applied in an unconstrained context) does not check if total sum is larger or equal to
// 'min_sum' - all it does is remove extra notes if it does reach that value.
// Note that proper usage of this preprocessor requires for notes to be sorted in descending order.
pub fn preprocess_notes_min_sum<T, T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(
notes: [Option<T>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL],
min_sum: U128
) -> [Option<T>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL] where T: NoteInterface<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN> + OwnedNote {
let mut selected = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL];
let mut sum = U128::from_integer(0);
for i in 0..notes.len() {
// Because we process notes in retrieved order, notes need to be sorted in descending amount order for this
// filter to be useful. Consider a 'min_sum' of 4, and a set of notes with amounts [3, 2, 1, 1, 1, 1, 1]. If
// sorted in descending order, the filter will only choose the notes with values 3 and 2, but if sorted in
// ascending order it will choose 4 notes of value 1.
if notes[i].is_some() & sum < min_sum {
let note = notes[i].unwrap_unchecked();
selected[i] = Option::some(note);
sum = sum.add(note.get_amount());
}
}
selected
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use dep::aztec::{
generators::{Ga1 as G_amt, Ga2 as G_npk, Ga3 as G_rnd, G_slot},
prelude::{NoteHeader, NoteInterface, PrivateContext},
protocol_types::{
constants::GENERATOR_INDEX__NOTE_NULLIFIER, point::{Point, POINT_LENGTH},
hash::poseidon2_hash_with_separator, traits::Serialize
},
note::utils::compute_note_hash_for_nullify, oracle::unsafe_rand::unsafe_rand,
keys::getters::get_nsk_app
};
use dep::std::{embedded_curve_ops::multi_scalar_mul, hash::from_field_unsafe};

trait OwnedNote {
fn new(amount: U128, owner_npk_m_hash: Field) -> Self;
fn get_amount(self) -> U128;
}

global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header.
global TOKEN_NOTE_BYTES_LEN: Field = 3 * 32 + 64;

// docs:start:TokenNote
#[aztec(note)]
struct TokenNote {
// The amount of tokens in the note
amount: U128,
// The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent.
npk_m_hash: Field,
// Randomness of the note to hide its contents
randomness: Field,
}
// docs:end:TokenNote

impl NoteInterface<TOKEN_NOTE_LEN, TOKEN_NOTE_BYTES_LEN> for TokenNote {
// docs:start:nullifier
fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field {
let secret = context.request_nsk_app(self.npk_m_hash);
poseidon2_hash_with_separator(
[
note_hash_for_nullify,
secret
],
GENERATOR_INDEX__NOTE_NULLIFIER as Field
)
}
// docs:end:nullifier

fn compute_nullifier_without_context(self) -> Field {
let note_hash_for_nullify = compute_note_hash_for_nullify(self);
let secret = get_nsk_app(self.npk_m_hash);
poseidon2_hash_with_separator(
[note_hash_for_nullify, secret],
GENERATOR_INDEX__NOTE_NULLIFIER
)
}

// docs:start:compute_note_hiding_point
fn compute_note_hiding_point(self) -> Point {
// We use the unsafe version because the multi_scalar_mul will constrain the scalars.
let amount_scalar = from_field_unsafe(self.amount.to_integer());
let npk_m_hash_scalar = from_field_unsafe(self.npk_m_hash);
let randomness_scalar = from_field_unsafe(self.randomness);
let slot_scalar = from_field_unsafe(self.header.storage_slot);
// We compute the note hiding point as:
// `G_amt * amount + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot`
// instead of using pedersen or poseidon2 because it allows us to privately add and subtract from amount
// in public by leveraging homomorphism.
multi_scalar_mul(
[G_amt, G_npk, G_rnd, G_slot],
[amount_scalar, npk_m_hash_scalar, randomness_scalar, slot_scalar]
)
}
// docs:end:compute_note_hiding_point
}

impl TokenNote {
// TODO: Merge this func with `compute_note_hiding_point`. I (benesjan) didn't do it in the initial PR to not have
// to modify macros and all the related funcs in it.
fn to_note_hiding_point(self) -> TokenNoteHidingPoint {
TokenNoteHidingPoint::new(self.compute_note_hiding_point())
}
}

struct TokenNoteHidingPoint {
inner: Point
}

impl TokenNoteHidingPoint {
fn new(point: Point) -> Self {
Self { inner: point }
}

fn add_amount(&mut self, amount: U128) {
self.inner = multi_scalar_mul([G_amt], [from_field_unsafe(amount.to_integer())]) + self.inner;
}

fn add_npk_m_hash(&mut self, npk_m_hash: Field) {
self.inner = multi_scalar_mul([G_npk], [from_field_unsafe(npk_m_hash)]) + self.inner;
}

fn add_randomness(&mut self, randomness: Field) {
self.inner = multi_scalar_mul([G_rnd], [from_field_unsafe(randomness)]) + self.inner;
}

fn add_slot(&mut self, slot: Field) {
self.inner = multi_scalar_mul([G_slot], [from_field_unsafe(slot)]) + self.inner;
}

fn finalize(self) -> Field {
self.inner.x
}
}

impl Serialize<POINT_LENGTH> for TokenNoteHidingPoint {
fn serialize(self) -> [Field; POINT_LENGTH] {
self.inner.serialize()
}
}

impl Eq for TokenNote {
fn eq(self, other: Self) -> bool {
(self.amount == other.amount)
& (self.npk_m_hash == other.npk_m_hash)
& (self.randomness == other.randomness)
}
}

impl OwnedNote for TokenNote {
fn new(amount: U128, owner_npk_m_hash: Field) -> Self {
Self { amount, npk_m_hash: owner_npk_m_hash, randomness: unsafe_rand(), header: NoteHeader::empty() }
}

fn get_amount(self) -> U128 {
self.amount
}
}
Loading

0 comments on commit ce0a9db

Please sign in to comment.