Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Slow updates experimentation #2732

Merged
merged 36 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8d106cd
feat: initial impl
LHerskind Oct 6, 2023
05143a2
chore: minor fixes
LHerskind Oct 9, 2023
cbb0eda
feat: initial dirty slow_inclusion map
LHerskind Oct 9, 2023
e91d1c8
chore: fixed size map
LHerskind Oct 10, 2023
4b7a516
chore: fix tsconfig.json
LHerskind Oct 10, 2023
8416488
chore: purge slow_struct
LHerskind Oct 11, 2023
a2ddcef
feat: generic size. Simple full size sparse tree
LHerskind Oct 11, 2023
9e3888a
chore: cleanup
LHerskind Oct 13, 2023
9bc5965
chore: update cli_docs_sandbox_test
LHerskind Oct 13, 2023
e1f8d4a
chore: update comment
LHerskind Oct 13, 2023
374b9b6
chore: minor cleanup
LHerskind Oct 16, 2023
22a8c63
chore: update pedersen 💀
LHerskind Oct 31, 2023
ac32b0e
feat: add oracle for slow updates (#2878)
LHerskind Oct 31, 2023
0590e9a
chore: remove allo
LHerskind Oct 31, 2023
7a8f0e7
feat: initial usdc-like token
LHerskind Nov 1, 2023
664ce41
chore: artifact inclusion
LHerskind Nov 1, 2023
4a5ab6a
test: add blacklist tests
LHerskind Nov 2, 2023
d0cf908
chore: add to ci
LHerskind Nov 2, 2023
402acf1
feat: use slow updates for almost all access control in token
LHerskind Nov 2, 2023
76dc6ff
chore: 🧹
LHerskind Nov 2, 2023
2a6eb04
chore: fix import
LHerskind Nov 3, 2023
c7d44be
chore: add extra accounts if needed
LHerskind Nov 3, 2023
252f170
chore: add slow-tree test to ci
LHerskind Nov 3, 2023
1011f97
chore: recompile noir
LHerskind Nov 3, 2023
8d52ec4
chore: noir updates
LHerskind Nov 3, 2023
a85bb1a
chore: fix imports
LHerskind Nov 3, 2023
1bf0d99
chore: addressing minor comments
LHerskind Nov 7, 2023
2d61536
chore: add constraints to e2e_slow_tree.test.ts
LHerskind Nov 13, 2023
8faad6d
refactor: use capsules and capsule dispenser instead.
LHerskind Nov 13, 2023
23bd7f4
chore: formatting
LHerskind Nov 13, 2023
9a46997
chore: merge fixing
LHerskind Nov 13, 2023
b73201c
fix: merge
LHerskind Nov 13, 2023
ce11537
chore: linter
LHerskind Nov 14, 2023
343b027
chore: formatting
LHerskind Nov 19, 2023
bcbb362
chore: address comments
LHerskind Nov 19, 2023
401ab5d
chore: compiler version nargo toml
LHerskind Nov 20, 2023
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
26 changes: 26 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,28 @@ jobs:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_token_contract.test.ts

e2e-blacklist-token-contract:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_blacklist_token_contract.test.ts

e2e-slow-tree:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_slow_tree.test.ts

e2e-sandbox-example:
docker:
- image: aztecprotocol/alpine-build-image
Expand Down Expand Up @@ -1292,6 +1314,8 @@ workflows:
- e2e-deploy-contract: *e2e_test
- e2e-lending-contract: *e2e_test
- e2e-token-contract: *e2e_test
- e2e-blacklist-token-contract: *e2e_test
- e2e-slow-tree: *e2e_test
- e2e-sandbox-example: *e2e_test
- e2e-block-building: *e2e_test
- e2e-nested-contract: *e2e_test
Expand Down Expand Up @@ -1326,6 +1350,8 @@ workflows:
- e2e-deploy-contract
- e2e-lending-contract
- e2e-token-contract
- e2e-blacklist-token-contract
- e2e-slow-tree
- e2e-sandbox-example
- e2e-block-building
- e2e-nested-contract
Expand Down
8 changes: 8 additions & 0 deletions yarn-project/acir-simulator/src/acvm/oracle/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ export class Oracle {
return witness.map(toACVMField);
}

async popCapsule(): Promise<ACVMField[]> {
const capsule = await this.typedOracle.popCapsule();
if (!capsule) {
throw new Error(`No capsules available`);
}
return capsule.map(toACVMField);
}

async getNotes(
[storageSlot]: ACVMField[],
[numSelects]: ACVMField[],
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export abstract class TypedOracle {
throw new Error('Not available.');
}

popCapsule(): Promise<Fr[]> {
throw new Error('Not available.');
}

getNotes(
_storageSlot: Fr,
_numSelects: number,
Expand Down
7 changes: 7 additions & 0 deletions yarn-project/acir-simulator/src/client/db_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ export interface DBOracle extends CommitmentsDB {
*/
getAuthWitness(messageHash: Fr): Promise<Fr[]>;

/**
* Retrieve a capsule from the capsule dispenser.
* @returns A promise that resolves to an array of field elements representing the capsule.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @returns A promise that resolves to an array of field elements representing the capsule.
* @returns A promise that resolves to an array of field elements representing the capsule.
* @remarks A capsule is a "blob" of data that is passed to the contract through an oracle.

* @remarks A capsule is a "blob" of data that is passed to the contract through an oracle.
*/
popCapsule(): Promise<Fr[]>;

/**
* Retrieve the secret key associated with a specific public key.
* The function only allows access to the secret keys of the transaction creator,
Expand Down
12 changes: 11 additions & 1 deletion yarn-project/acir-simulator/src/client/view_data_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,17 @@ export class ViewDataOracle extends TypedOracle {
}

/**
* Gets some notes for a storage slot.
* Pops a capsule from the capsule dispenser
* @returns The capsule values
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @returns The capsule values
* @returns The capsule values
* @remarks A capsule is a "blob" of data that is passed to the contract through an oracle.

* @remarks A capsule is a "blob" of data that is passed to the contract through an oracle.
*/
public popCapsule(): Promise<Fr[]> {
return this.db.popCapsule();
}

/**
* Gets some notes for a contract address and storage slot.
* Returns a flattened array containing filtered notes.
*
* @remarks
* Check for pending notes with matching slot.
Expand Down
8 changes: 8 additions & 0 deletions yarn-project/aztec-nr/slow-updates-tree/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "slow_updates_tree"
authors = ["aztec-labs"]
compiler_version = ">=0.18.0"
type = "lib"

[dependencies]
aztec = { path = "../aztec" }
1 change: 1 addition & 0 deletions yarn-project/aztec-nr/slow-updates-tree/src/lib.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod slow_map;
276 changes: 276 additions & 0 deletions yarn-project/aztec-nr/slow-updates-tree/src/slow_map.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
use dep::aztec::context::{PrivateContext, PublicContext, Context};
use dep::aztec::types::type_serialization::TypeSerializationInterface;
use dep::aztec::oracle::storage::{storage_read, storage_write};

use dep::std::hash::pedersen_hash;
use dep::std::merkle::compute_merkle_root;
use dep::std::option::Option;

// The epoch length is just a random number for now.
global EPOCH_LENGTH: u120 = 100;

fn compute_next_change(time: Field) -> Field {
(((time as u120 / EPOCH_LENGTH + 1) * EPOCH_LENGTH)) as Field
LHerskind marked this conversation as resolved.
Show resolved Hide resolved
}

// A leaf in the tree.
struct Leaf {
next_change: Field,
before: Field,
after: Field,
}

fn serialize_leaf(leaf: Leaf) -> [Field; 3] {
[leaf.next_change, leaf.before, leaf.after]
}

fn deserialize_leaf(serialized: [Field; 3]) -> Leaf {
Leaf {
next_change: serialized[0],
before: serialized[1],
after: serialized[2],
}
}

impl Leaf {
fn serialize(self: Self) -> [Field; 3] {
serialize_leaf(self)
}
fn deserialize(serialized: [Field; 3]) -> Self {
deserialize_leaf(serialized)
}
}

// Subset of the MembershipProof that is needed for the slow update.
struct SlowUpdateInner<N> {
value: Field, // Value only really used for the private flow though :thinking:
sibling_path: [Field; N],
}

// The slow update proof. Containing two merkle paths
// One for the before and one for the after trees.
// M = 2 * N + 4
struct SlowUpdateProof<N, M> {
index: Field,
new_value: Field,
before: SlowUpdateInner<N>,
after: SlowUpdateInner<N>,
}

pub fn deserialize_slow_update_proof<N, M>(serialized: [Field; M]) -> SlowUpdateProof<N, M> {
SlowUpdateProof::deserialize(serialized)
}

impl<N, M> SlowUpdateProof<N, M> {
pub fn serialize(self: Self) -> [Field; M] {
let mut serialized = [0; M];
serialized[0] = self.index;
serialized[1] = self.new_value;
serialized[2] = self.before.value;
serialized[3 + N] = self.after.value;

for i in 0..N {
serialized[3 + i] = self.before.sibling_path[i];
serialized[4 + N + i] = self.after.sibling_path[i];
}
serialized
}

pub fn deserialize(serialized: [Field; M]) -> Self {
let mut before_sibling_path = [0; N];
let mut after_sibling_path = [0; N];

for i in 0..N {
before_sibling_path[i] = serialized[3 + i];
after_sibling_path[i] = serialized[4 + N + i];
}

Self {
index: serialized[0],
new_value: serialized[1],
before: SlowUpdateInner {
value: serialized[2],
sibling_path: before_sibling_path,
},
after: SlowUpdateInner {
value: serialized[3 + N],
sibling_path: after_sibling_path,
},
}
}
}

// The simple slow map which stores a sparse tree
struct SlowMap<N,M> {
context: Context,
storage_slot: Field
}

impl<N,M> SlowMap<N,M> {
pub fn new(
context: Context,
storage_slot: Field
) -> Self {
assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1.");
LHerskind marked this conversation as resolved.
Show resolved Hide resolved
Self {
context,
storage_slot,
}
}

pub fn read_root(self: Self) -> Leaf {
storage_read(self.storage_slot, deserialize_leaf)
}

// Beware that the initial root could include much state that is not shown by the public storage!
pub fn initialize(self: Self, initial_root: Field) {
let mut root_object = self.read_root();
assert(root_object.next_change == 0, "cannot initialize twice");
root_object = Leaf {
next_change: 0xffffffffffffffffffffffffffffff,
before: initial_root,
after: initial_root,
};
let fields = root_object.serialize();
storage_write(self.storage_slot, fields);
}

// Reads the "CURRENT" value of the root
pub fn current_root(self: Self) -> Field {
let time = self.context.public.unwrap().timestamp() as u120;
LHerskind marked this conversation as resolved.
Show resolved Hide resolved
let root_object = self.read_root();
if time <= root_object.next_change as u120 {
root_object.before
} else {
root_object.after
}
}

pub fn read_leaf_at(self: Self, key: Field) -> Leaf {
let derived_storage_slot = pedersen_hash([self.storage_slot, key]);
storage_read(derived_storage_slot, deserialize_leaf)
}

// Reads the "CURRENT" value of the leaf
pub fn read_at(self: Self, key: Field) -> Field {
let time = self.context.public.unwrap().timestamp() as u120;
let leaf = self.read_leaf_at(key);
if time <= leaf.next_change as u120 {
leaf.before
} else {
leaf.after
}
}

// Will update values in the "AFTER" tree
// - updates the leaf and root to follow current values, moving from after to before if
// needed.
// - checks that the provided merkle paths match state values
// - update the leaf and compute the net root
// Should only be used when updates from public are desired, since the hashing will be
// costly since done by sequencer.
pub fn update_at(self: Self, p: SlowUpdateProof<N, M>) {
// The calling function should ensure that the index is within the tree.
// This must be done separately to ensure we are not constraining too tight here.

let time = self.context.public.unwrap().timestamp() as u120;
let next_change = compute_next_change(time as Field);

let mut root = self.read_root();
let mut leaf = self.read_leaf_at(p.index);

// Move leaf if needed
if time > leaf.next_change as u120 {
leaf.before = leaf.after;
}

// Move root if needed
if time > root.next_change as u120 {
root.before = root.after;
}

// Ensures that when before is active, it is not altered by this update
assert(
root.before == compute_merkle_root(leaf.before, p.index, p.before.sibling_path),
"Before root don't match"
);

// Ensures that the provided sibling path is valid for the CURRENT "after" tree.
// Without this check, someone could provide a sibling path for a different tree
// and update the entire "after" tree at once, causing it to be out of sync with leaf storage.
assert(
root.after == compute_merkle_root(leaf.after, p.index, p.after.sibling_path),
Copy link
Contributor

Choose a reason for hiding this comment

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

I am missing something here. The value of root here is the old not-updated one loaded from storage and it would not contain the update we are just doing so why should it match?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, the root is the old, but so is the leaf and you are then providing the matching sibling path.

Say that you don't do this check. I could pass in whatever sibling path I want and change the entire "after" tree.

You do it to make sure that you are actually updating a value in the current "after" tree, and not one you pulled out of your ass.

In essence

  • checking "before" to make sure that it is updated if needed.
    • since the root, leaf are already constrained, you are pretty limited in what you can do for the sibling path.
  • checking "after" to make sure that the part you provide is correct
    • this is to do as before, and limit you to only the correct sibling path
  • computing the new "after" to include your state change.

Does it makes sense?

"After root don't match"
);

// Update the leaf
leaf.after = p.new_value;
leaf.next_change = next_change;

// Update the after root
root.after = compute_merkle_root(leaf.after, p.index, p.after.sibling_path);
root.next_change = next_change;

self.update_unsafe(p.index, leaf, root);
}

// A variation of `update_at` that skips the merkle-membership checks.
// To be used by a contract which has already checked the merkle-membership.
// This allows us to check the merkle-memberships in private and then update
// in public, limiting the cost of the update.
pub fn update_unsafe_at(self: Self, index: Field, leaf_value: Field, new_root: Field) {
// User must ensure that the checks from update_at is performed for safety
let time = self.context.public.unwrap().timestamp() as u120;
let next_change = compute_next_change(time as Field);

let mut root = self.read_root();
let mut leaf = self.read_leaf_at(index);

// Move leaf if needed
if time > leaf.next_change as u120 {
leaf.before = leaf.after;
}

// Move root if needed
if time > root.next_change as u120 {
root.before = root.after;
}

// Update the leaf
leaf.after = leaf_value;
leaf.next_change = next_change;

// Update the root
root.after = new_root;
root.next_change = next_change;

self.update_unsafe(index, leaf, root);
}

// Updates the value in the in storage with no checks.
fn update_unsafe(self: Self, index: Field, leaf: Leaf, root: Leaf) {
let derived_storage_slot = pedersen_hash([self.storage_slot, index]);
let fields = leaf.serialize();
storage_write(derived_storage_slot, fields);

let fields = root.serialize();
storage_write(self.storage_slot, fields);
}
}

/*pub fn compute_merkle_root<N>(leaf: Field, index: Field, hash_path: [Field; N]) -> Field {
let n = hash_path.len();
let index_bits = index.to_le_bits(n as u32);
let mut current = leaf;
for i in 0..n {
let path_bit = index_bits[i] as bool;
let (hash_left, hash_right) = if path_bit {
(hash_path[i], current)
} else {
(current, hash_path[i])
};
current = pedersen_hash([hash_left, hash_right]);
};
current
}
*/
Loading