-
Notifications
You must be signed in to change notification settings - Fork 311
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
12 changed files
with
883 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
noir-projects/noir-contracts/contracts/spam_contract/Nargo.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
68
noir-projects/noir-contracts/contracts/spam_contract/src/main.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
2 changes: 2 additions & 0 deletions
2
noir-projects/noir-contracts/contracts/spam_contract/src/types.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
mod balance_set; | ||
mod token_note; |
136 changes: 136 additions & 0 deletions
136
noir-projects/noir-contracts/contracts/spam_contract/src/types/balance_set.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
135 changes: 135 additions & 0 deletions
135
noir-projects/noir-contracts/contracts/spam_contract/src/types/token_note.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.