Skip to content

Commit

Permalink
more docs
Browse files Browse the repository at this point in the history
  • Loading branch information
brweisz committed Sep 5, 2024
1 parent 41ff8fd commit 614ffa8
Showing 1 changed file with 31 additions and 7 deletions.
38 changes: 31 additions & 7 deletions docs/opcodes/memoryOperations.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
### Memory Operations
There are 2 memory operations: MemoryInit and MemoryOp (read or write). This opcode exists to handle the case where we read or write values from arrays with an unknown position in circuit-building time.
There are 2 memory operations: MemoryInit and MemoryOp (read or write). This opcode exists to handle the case where we read or write values from arrays with an unknown position in circuit-building time. For example, the Noir program...

```rust
fn main(array: [Field; 5]) -> pub Field {
array[3]
}
```
...resolves to AssertZero opcodes, since the input Witness are w0, w1, w2, w3 & w4, the output witness is w5, and the code just makes an AssertZero to ensure that w3 is equal to w5. Instead, when the Noir code looks like this...

```rust
fn main(array: [Field; 5], index: Field) -> pub Field {
array[index]
}
```
...then we're making a random access into an array in a position unknown at the time the circuit is being built. This requires the concept of Memory blocks (or memory arrays).


#### MemoryInit
It represents the creation of an array in memory. What does this mean for a prover? Well, it depends on the prover. This opcode will be translated into a set of constraints that will be used to generate the Plonky2 circuit. It has 2 fields:
Expand All @@ -13,9 +28,9 @@ It represents either a memory read operation or a memory write operation.

##### Memory Read
The opcode has the following fields:
* block_id: the memory block index where we're reading from.
* index: witness that holds the value of the index where we want to read. For example, assume we have a witness w_0 and we want to read on index w_0. In this case is simpler to think of it as a variable and that we want to read from ```array[w_0]```.
* value: witness where the value of the memory read will be stored. Assume we have a value = w_1. To complete the example, this would be like doing ```w_1 = array[w_0]```.
* block_id: the index of the memory block we're reading from.
* index: witness that holds the value of the index where we want to read from. It's like ```array[index]```.
* value: witness where the value of the memory read will be stored. It's like ```value = array[index]```.

To implement this we used the Plonky2 RandomAccessMemory gate through the CircuitBuilder's ```random_access()``` method.

Expand All @@ -25,8 +40,17 @@ The opcode has the following fields:
* index: witness that holds the value of the index we want to write into.
* value: witness that holds the value we want to write. Assume we have a value = w_1.

This would be equivalent as doing ```array[w_0] = w_1```.
This would be equivalent as doing ```array[index] = value```.

Now, this operation is a bit tricky since we don't know the position we're writing while building the circuit. During the circuit construction, we have arrays of targets representing our memory blocks, and these targets have unchangeable values during circuit execution. We need to create a static circuit, but any slot's value could change. Therefore, we need to create an entirely new array and constrain its values to be the same as the previous ones, except for the position we're changing. As you can imagine, this operation is rather expensive.

To visualize this: imagine we need to write position 2 with some value v. Then we'll need to create a new set of targets and attach them to the corresponding value.

Previous memory block: | t0 | t1 | t2 | t3 | t4 |
↓ ↓ ↓ ↓
New memory block: | t0' | t1' | t2' | t3' | t4' |
New value: v

Now, this operation is a bit tricky since we don't know the values we're writing while building the circuit. During the circuit construction, we have arrays of targets representing our memory blocks, and these targets have unchangeable values during circuit execution. We need to create a static circuit, but any slot's value could change. Therefore, we need to create an entirely new array and constrain its values to be the same as the previous ones, except for the position we're changing. As you can imagine, this operation is rather expensive.

We iterate over all the targets, using the CircuitBuilder's ```is_equal()``` method to figure out which position are we changing.
We iterate over all the targets, using the CircuitBuilder's ```is_equal()``` method to figure out which position are we changing. If the position doesn't match the index, we link it to the target in the previous version of the memory block on the same position. If the position matches the index, then we create a new target with the value we want to write and link it to the new memory array.

0 comments on commit 614ffa8

Please sign in to comment.