Skip to content

Commit

Permalink
feat: Slow updates experimentation (#2732)
Browse files Browse the repository at this point in the history
Fixes #2801. (Also fixes #3214)

Experimentation with slow updates in Noir. 

- Adds a slow update structure
  - Sparse tree to support membership and non-memberships
- Wraps this structure in a contract to make interaction easier
- Adds an oracle to make it practical to pass the >200 params.
- Implement token contract with blacklist and access control in the slow
tree.
  • Loading branch information
LHerskind committed Nov 20, 2023
1 parent 0dc2d58 commit 193e6c8
Show file tree
Hide file tree
Showing 37 changed files with 3,208 additions and 6 deletions.
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.
* @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
* @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
}

// 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.");
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;
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),
"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

0 comments on commit 193e6c8

Please sign in to comment.