Repository: openvm-org/openvm
Pull request #1457: specs: circuit architecture, memory, continuations
Author: login: Golovanov399
Created: 2025-03-15T03:53:17Z | Merged: 2025-03-18T08:43:54Z

Write the specs for the circuit and memory


Diff:
diff --git a/crates/vm/src/arch/integration_api.rs b/crates/vm/src/arch/integration_api.rs
index 710d2d1c03..745f8b58cc 100644
--- a/crates/vm/src/arch/integration_api.rs
+++ b/crates/vm/src/arch/integration_api.rs
@@ -64,7 +64,7 @@ pub trait VmAdapterChip<F> {
         Self::ReadRecord,
     )>;
 
-    /// Given instruction and the data to write, perform memory writes and return the `(record, timestamp_delta)`
+    /// Given instruction and the data to write, perform memory writes and return the `(record, next_timestamp)`
     /// of the full adapter record for this instruction. This is guaranteed to be called after `preprocess`.
     fn postprocess(
         &mut self,
diff --git a/crates/vm/src/system/memory/offline_checker/bus.rs b/crates/vm/src/system/memory/offline_checker/bus.rs
index 85d92a7f2d..d15f5798ea 100644
--- a/crates/vm/src/system/memory/offline_checker/bus.rs
+++ b/crates/vm/src/system/memory/offline_checker/bus.rs
@@ -87,7 +87,6 @@ impl<T: FieldAlgebra> MemoryBusInteraction<T> {
     where
         AB: InteractionBuilder<Expr = T>,
     {
-        // Chain 1 at the end to ensure that different length interactions are always distinct.
         let fields = iter::empty()
             .chain(iter::once(self.address.address_space))
             .chain(iter::once(self.address.pointer))
diff --git a/docs/specs/circuit.md b/docs/specs/circuit.md
index a2068d923a..4238c7c27b 100644
--- a/docs/specs/circuit.md
+++ b/docs/specs/circuit.md
@@ -3,210 +3,80 @@
 We build our virtual machines in a STARK proving system with a multi-matrix commitment scheme and shared verifier
 randomness between AIR matrices to enable permutation arguments such as log-up.
 
-In the following, we will refer to a circuit as a collection of AIR matrices (also referred to as chips) of possibly
-different heights, which may communicate with one another over buses using a log-up permutation argument. We refer to
-messages sent to such a bus as [interactions](https://github.com/openvm-org/stark-backend/tree/main/crates/stark-backend/src/interaction).
+In the following, we will refer to a circuit as a collection of chips that communicate with one another over buses using a LogUp permutation argument. We refer to messages sent to such a bus as [interactions](https://github.com/openvm-org/stark-backend/blob/main/docs/interactions.md). Every _chip_ is an entity responsible for a certain operation (or set of operations), and it has an AIR to check the correctness of its execution.
 
-Our framework is modular and allows the creation of custom VM circuits to support different instruction sets that follow
-our overall ISA framework.
+> [!NOTE]
+> A bus itself doesn't have any logic. It is just an index, and all related functionality is purely on the backend side.
+
+Usually we have _bridges_, which are basically APIs for buses. Using a bridge is preferred over communicating with a bus directly since bridges may enforce some special type of communication (for example, sending messages in pairs or communicating with different buses at once, thus synchronizing these communications).
+
+Our framework is modular and allows the creation of custom VM circuits to support different instruction sets that follow our overall ISA framework.
 
 ## Motivation
 
-We want to make the VM modular, so that adding new instructions and chips involves minimal to no changes to any
-centralized chip (commonly the CPU chip). We also want to avoid increasing the columns/interactions/buses of the CPU
-when we add new instructions/chips.
+We want to make the VM modular so that adding new instructions and chips is completely isolated from the existing components.
 
 ## Design
 
 The following must exist in any VM circuit:
 
-- The program chip
-- The core chip
-- A set of memory-related chips (different depending on the persistence type)
-- A program bus
-- An execution bus
-- A memory bus
-
-Notably, there is no CPU chip where the full transcript of executed instructions is materialized in a single trace
-matrix. The transcript of memory accesses is also not materialized in a single trace matrix. We discuss reasons for
-these choices below.
-
-### Program Chip
-
-We follow the Harvard architecture where the program code (ROM) is stored separately from memory. The program chip's
-trace matrix simply consists of the program code, one instruction per row, as a cached trace, together with interactions
-on the PROGRAM_BUS.
-
-A cached trace is used so that the commitment to the program code is the proof system trace commitment. This commitment
-could be changed to a flat hash, likely with worse performance.
-
-### Our no-CPU design
-
-The main motivation is that the existence of a CPU forces the existence of a trace matrix with rows growing with the
-total number of clock cycles of the program execution. We claim that the no-CPU design gives the minimum lower bound on
-the number of required trace cells added per opcode execution.
-
-Traditionally, the CPU is in charge of reading/writing from memory and forwarding that information to the appropriate
-chip. We have switched to a model where each chip directly accesses memory itself. Traditionally this is also
-inefficient because the CPU uses physical general purpose registers for instruction execution, whereas in our
-architecture, registers are emulated as memory in a dedicated address space.
-
-Each chip has IO columns `(timestamp, pc, instruction)` where `instruction` is `(opcode, operands)`.
-The chip receives `(pc, instruction)` on the PROGRAM_BUS to ensure it is reading the correct line of the program code.
-There is a maximum length to `operands` defined by the PROGRAM_BUS, but each chip can receive only a subset of the
-operands (setting the rest to zero) without paying the cost for the unused operands.
-
-**Note:** each chip receives an _offset_ on construction, and this offset basically means "where does the class of
-operations which this chip supports start". For example, if a `FieldArithmeticChip` has offset `0x100`, then its `SUB`
-operation would be encoded with opcode `0x100 + 1` and not just `1`.
-See [ISA spec](./ISA.md) for
-details.
-
-Each chip receives `(timestamp, pc)` on EXECUTION_BUS and "after"
-executing an instruction, sends `(new_timestamp, new_pc)` on the same bus (here `new_pc` is `pc + 1` most of the time,
-but not always).
-The chip is in charge of constraining that `new_timestamp` is consistent with `timestamp`. In
-particular, `new_timestamp` must be (almost always strictly) greater than `timestamp`, but not by a lot (so that the
-timestamps do not overflow the field characteristic).
-The bus enforces that each timestamp transition corresponds to a particular instruction being executed.
-
-There is an `ExecutionBridge` for more convenient communicating with these two buses.
-
-The chip must constrain that `opcode` is one of the opcodes the chip itself owns. The chip then constrains the rest of
-the validity of the opcode execution, according to the opcode spec.
-
-There is also another very simple "connector" chip with a 2 row trace that sends out `(1, 0)` on EXECUTION_BUS and
-receives `(final_timestamp, final_pc)` on EXECUTION_BUS. These four values are public values of the program. With
-continuations, the start and end timestamp/pc will need to be constrained with respect to the pre/post-states.
-
-The vm design is so that the core chip is just one of such chips. The instructions directed at managing the execution
-flow are passed to the core chip. Such design is modular enough in the sense that it allows us to treat the control flow
-instructions similarly to most of the other opcodes. Also, instead of having the execution protocol in a single matrix
-and duplicate the instructions with the chips that actually execute them, we have each step of execution only generating
-one new row in the machine chip (and maybe more lines in other primitive chips that it uses for execution).
-
-### Offline Memory
-
-In the no-CPU design, each chip receives the opcode instruction directly, and memory access (read or write) is
-constrained by the chip itself.
-
-The VM supports a read/write memory, which is constrained via offline memory checking. We use the offline memory
-checking argument of [BEGKN92](https://www.cs.ubc.ca/~will/papers/memcheck.pdf).
-
-Any offline memory checking aims to have a transcript consisting of `(a, v, t)` with address `a`, value `v`,
-timestamp `t` (in our ISA `a = (address_space, address)` but we omit this distinction here for brevity). The timestamp
-here is a single field element. As far as we know, the timestamp **must** be global and match what is used by the
-`EXECUTION_BUS` to ensure that the temporal sequencing of memory accesses matches the temporal sequencing of instruction
-execution.
-
-<!--
-[JPW] Lasso uses a per-address timestamp (renamed counter) but in a different setting. We did not see a way to use this argument because it did not allow constraining instruction execution matched memory access ordering.
--->
-
-Memory aims to support two operations: read and write.
+- **Range checker chip** and **range checker bus**. Every time an AIR needs to constrain that some expression is less than some power of two, it communicates with the range checker bus using the range checker chip. The range checker chip keeps track of all accesses to later balance out the interactions.
+- **Program chip** and **program bus**. The program chip's trace matrix simply consists of the program code, one instruction per row, as a cached trace. A cached trace is used so that the commitment to the program code is the proof system trace commitment. Every time an instruction executor (to be defined later) executes an instruction, it sends this instruction, along with the `pc`, to the program bus via the program chip. The program chip keeps track of all accesses to later balance out the interactions.
+- **Connector chip**. If we only had the above interactions with the execution bus, then the initial execution state would have only been sent and the final one would have only been received. The connector chip publishes the initial and final states and balances this out (in particular, its trace is a matrix with two rows -- because it has a preprocessed trace).
+- **Phantom chip**. We call an instruction _phantom_ if it doesn't mutate execution state (and, of course, the state of the memory). Phantom instructions are sent to the phantom chip.
+- A set of memory-related chips and a bus (different depending on the persistence type -- see [Memory](./memory.md)),
+- **Execution bus**. Every time an instruction executor executes an instruction, it sends the execution state before the instruction to the execution bus (with multiplicity $1$) and receives the execution state after the instruction from it. It has a convenient **execution bridge** that provides functions which do these two interactions at once.
 
-- A read of `(a, v)` at time `t` means that if we look through the transcript focusing on only entries with address `a`,
-  the entry with timestamp immediately preceding `t` must have value also equal `v`.
+Notably, there is no CPU chip where the full transcript of executed instructions is materialized in a single trace matrix. The transcript of memory accesses is also not materialized in a single trace matrix.
 
-- A write of `(a, v)` at time `t` means a new entry `(a, v, t)` must be introduced to the transcript.
+## Program execution
 
-The main distinction of [BEGKN92] is that the transcript does not need to be materialized explicitly in a single AIR
-matrix. The particular entries of the transcript are materialized on a per-access basis (in whatever chip needs it).
-This materialization is avoided by using the MEMORY_BUS and chip constraints to constrain the correctness of the
-transcript:
+When the program is being run, in the simple scenario, the following happens at the very highest level:
+- There is an _execution state_, which consists of two numbers: _timestamp_ and _program counter_ corresponding to the instruction that is currently being executed.
+- While not finished:
+  - The next instruction is drawn,
+  - It is passed to the _instruction executor_ (which is a special kind of chip, we define it later) responsible for executing this instruction,
+  - This instruction executor returns the new execution state (and maybe reports that the execution is finished). The timestamp and program counter change accordingly.
 
-We have an offline checking MEMORY_BUS, where message fields consist of `(a, v, t)`. The bus then has two sets (send vs
-receive) that must be equal at the end. To match the literature, let send (resp. receive) correspond to Write (resp.
-Read) sets. Any memory access in a chip must add one entry into each set and constrain a relation between them:
+There are limitations to how many interactions/trace rows/etc. we can have in total; see [soundness criteria](https://github.com/openvm-org/stark-backend/blob/main/docs/Soundness_of_Interactions_via_LogUp.pdf). If executing the full program would lead us to overflowing these limits, the program needs to be executed in several segments. Then the process is slightly different:
+- After executing an instruction, we may decide (based on `SegmentationStrategy`, which looks at the traces) to _segment_ our execution at this point. In this case, the execution will be split into several segments.
+- The timestamp resets on segmentation.
+- Each segment is going to be proven separately. Of course, this means that adjacent segments need to agree on some things (mainly memory state). See [Continuations](./continuations.md) for full details.
 
-- A read of $(a, v)$ at time $t$ must add $(a, v, t_{prev})$ to Read set and $(a, v, t)$ to Write set. It must constrain
-  $t_{prev} < t$.
-- A write of $(a, v)$ at time $t$ must add $(a,v_{prev},t_{prev})$ to Read set and $(a,v,t)$ to Write set, where $v_
-  {prev}$ is the previous value before the write. It must constrain $t_{prev} < t$.
+## Instruction executors
 
-To balance the Read and Write sets, an additional chip must ensure that every accessed address has an initial $(a, v_
-{init}, 0)$ added to the Write set, and $(a, v_{final}, t_{last})$ added to Read set.
+The chips that get to execute instructions are _instruction executors_. While not required, most instruction executor chips are implemented in two parts:
+- **Adapter:** communicates with the program and execution buses. Also communicates with memory to read inputs and write output from/to the required locations.
+- **Core:** performs chip's intended logic on the raw data. Is mostly isolated and doesn't have to bother about the other parts of the circuit, although it can if it wants, for example, to talk to the range checker.
 
-<!--
-For the offline checking to be sound, it must be constrained that the list of accessed addresses in the initial Write
-list are all **unique**. Uniqueness of the initial address list implies, together with the bus argument, that the final
-address list is also unique (vice versa, uniqueness of final set implies uniqueness of initial set). The key observation
-of [OLB24](https://eprint.iacr.org/2024/979.pdf) is that only _uniqueness_ is necessary and not sorted-ness of the
-address list. The traditional approach prior to OLB24 to enforce uniqueness is to enforce the list of addresses is
-sorted, which uses logup lookups for range checks necessary to constrain `IsLessThan`. OLB24 shows that one can
-implement an AIR with in-circuit randomness to constrain that all entries in a trace column are unique (with an
-extension to conditional uniqueness).
--->
+This modularity helps to separate the functionalities, reduce space for error, and also reuse the same adapters for various chips with similar instruction signatures.
+Note that technically these are parts of the same chip and therefore generate one trace, although both adapter and core have AIRs to deal with different parts of the trace.
+As already mentioned, it is not required that an instruction executor must be implemented in two parts:
+the entire chip with the combined functionality of the adapter and core can be implemented all at once.
 
-The initial and final memory accesses are constrained different when the VM has continuations.
-See [Continuations](./continuations.md) for full details. In summary, because the initial and final memory states are
-committed to in a **trie**, the uniqueness of the addresses is constrained by the trie, so the arguments of the previous
-paragraph are not used.
-
-### Memory Model With Variable Word Size
-
-In traditional machine memory models, memory is stored as a sequence of cells (typically bytes), chunked into words by a
-fixed **word size**.
-This word size then governs all memory load/store operations.
-This model was governed by the constraints of physical hardware, and
-we now discuss why it is unnecessary in the STARK architecture.
-
-For efficient vectorization of memory accesses, we allow each
-chip to perform "batch" read/writes of $\{(a + i, v_i, t)\}_{i \in [0,w)}$ where $w$ is any multiple of a
-fixed `WORD_BLOCK_SIZE` (set to `4` in practice).
-
-The main idea is that in the offline checking memory argument [above](#offline-memory), the MEMORY_BUS can hold $(a, v,
-t)$ where the length of $v$ is variable. The difference in word sizes only needs to be resolved when there is a sequence
-of read+write or write+read involving different word sizes.
-
-We introduce chips `AccessAdapterChip<N>` that can:
-
-- read $(a, v_0 || ... || v_{N-1}, t)$ and write $(a, v_0 || ... || v_{N/2 - 1}, t)$ and $(a + N/2, v_{N/2} || ... || v_{N-1}, t)$
-- read $(a, v_0 || ... || v_{N/2 - 1}, t_0)$ and $(a + N / 2, v_{N/2} || ... || v_{N-1}, t_1)$ and write $(a, v_0 || .. || v_{N-1}, max(t_0, t_1))$
-
-where we allow `N` to be different powers of two.
-
-The values of $a, v_i$ that appear in the trace of the access adapter chip are generated on-demand based on the needs of the
-runtime memory access. In other words, the converter inserts additional writes into the MEMORY_BUS when needed in order
-to link up accesses of different word sizes.
-
-### Timestamp Range Assertions
+> [!IMPORTANT]
+> It is a responsibility of the instruction executor (more specifically, the adapter) to update the execution state. It is also its responsibility to constrain that the timestamp increases. If any of these is not done correctly, the proof of correctness will fail to be generated.
 
-The execution and memory buses both contain a field for **timestamp** as a way to chronologically order instruction execution and the VM state accesses therein.
-The timestamp is a global variable shared across all chips within a single VM circuit that is monotonically increasing across VM execution.
-For the offline memory checking argument described above to work, we require that the `timestamp` always remains in the range `[0, 2^timestamp_max_bits)`, where
-`timestamp_max_bits` is a configurable parameter of the VM, but we require `timestamp_max_bits <= 29` when the proof system base field is 31 bits.
-In order for the VM circuit to maintain this invariant, we **require** that each VM chip that interacts with the execution and memory buses must satisfy the following condition:
+To prevent [timestamp overflow](./memory.md#time-goes-forward), we **require** that each instruction executor chip must satisfy the following condition:
 
 > [!IMPORTANT]
 > In the AIR, the amount that the timestamp is constrained to increase during execution of a single instruction is at most `num_interactions * num_rows_per_execution`.
 
 Here `num_interactions` is the number of interactions in the AIR: this is a static property of the AIR.
-The number of interactions does not depend on the number of trace rows, and it doesn't depend on whether messages are actually sent or not. In general, the trace for the execution of a single instruction can
+The number of AIR interactions does not depend on the number of trace rows, and it doesn't depend on whether messages are actually sent or not. In general, the trace for the execution of a single instruction can
 use multiple rows in the chip's trace matrix: this is represented by `num_rows_per_execution`. In summary
 we bound the integer amount that the timestamp should increment in a single instruction execution based on the number of interactions and the number of rows in the trace.
 
-Let us explain how this condition aids in the timestamp range bound.
-As part of the LogUp soundness checks, the verifier always checks the inequality:
-```
-sum_i height[i] * num_interactions[i] < p
-```
-where the sum is over all AIRs in the STARK proof, `height[i]` is the trace height of AIR `i`, and `num_interactions[i]` is the number of interactions in AIR `i`. The sum is taken over the integers and checked to be less than the modulus `p` of the base field.
+We check that all VM chips contained in the OpenVM system and the standard VM extensions satisfy this condition in the section [below](#inspection-of-vm-chip-timestamp-increments).
+
+## What to take into account when adding a new chip
+
+- [Ensure memory consistency](./memory.md#what-to-take-into-account-when-adding-a-new-chip)
+- Do not forget to constrain that `is_valid` is boolean in your core AIR.
+- If your chip generates some number of trace rows, and this number is not a power of two, the trace is padded with all-zero rows. It should correspond to a legitimate operation, most likely `is_valid = 0` though. For example, if your AIR asserts that the value in the first column is 1 less than the value in the second column, you cannot just write `builder.assert_eq(local.x + 1, local.y)`, because this is not the case for the padding rows.
+
+### Inspection of VM Chip Timestamp Increments
 
-Given the condition on instruction execution above, we deduce from this inequality the inequality
-```
-sum_{instruction_execution} timestamp_delta < p - 1
-```
-where the sum is over all instruction executions and `timestamp_delta` is the integer amount the timestamp increased in that execution. The change from `p` to `p - 1` is because the sum of AIRs includes the Connector AIR, which does not increment timestamp and has trace height 2 and at least one interaction (in fact it has 5).
-Within a VM circuit, the timestamp is always initialized to `1` and then advanced only during
-instruction execution. Thus from our discussion above, we deduce that the `end_timestamp`,
-defined as the timestamp the Connector Chip receives to signal either execution should be suspended
-or terminated, does not overflow the base field. Thus all intermediate timestamps also do not overflow.
-Given this, to constrain that all timestamps throughout the course of instruction execution are in
-the range `[0, 2^timestamp_max_bits)`, it suffices to range check the `end_timestamp`.
-This is done by the Connector AIR.
-
-#### Inspection of VM Chip Timestamp Increments
 Below we perform a survey on all VM chips contained in the OpenVM system and the standard VM extensions
 to justify that they all satisfy the condition on timestamp increments.
 
@@ -215,7 +85,7 @@ is constrained via the `ExecutionBridge` as the difference between the timestamp
 interactions on the execution bus for the "from" and "to" states. In most AIRs, the `timestamp_delta`
 is a constant which is computed by starting at `0` and incrementing by `1` on each memory access.
 The memory access constraint is done via the `MemoryBridge` interface.
-Any use of `read` or `write` via `MemoryBridge` uses `4` interactions: 2 on memory bus, 2 for range checks.
+Any use of `read` or `write` via `MemoryBridge` uses 4 interactions: 2 on memory bus, 2 for range checks.
 Therefore for chips where instruction execution uses only 1 row of the trace and timestamp increments
 once per memory access as above, we actually have that `timestamp_delta <= num_interactions / 4`.
 This includes all chips that use the integration API and `VmChipWrapper`.
diff --git a/docs/specs/memory.md b/docs/specs/memory.md
index 22170d1d62..446253c8a1 100644
--- a/docs/specs/memory.md
+++ b/docs/specs/memory.md
@@ -1,34 +1,252 @@
-# Overview
+# Memory overview
 
-Chips in the VM need to perform memory read and write operations. The goal of the memory offline checking is to ensure that memory consistency across all chips. Every memory operation consists of operation type (Read or Write), address (address_space and pointer), data, and timestamp. All memory operations across all chips should happen at distinct timestamps between 1 and 2^29. We assume that memory is initialized at timestamp 0. For simplicity, we assume that all memory operations are enabled (there is a way to disable them in the implementation).
+- [Basic performed interactions](#basic-performed-interactions)
+- [Access adapters](#access-adapters)
+- [Boundary chips](#boundary-chips)
+- [Soundness proof](#soundness-proof)
+  - [Time goes forward](#time-goes-forward)
+  - [Memory consistency](#memory-consistency)
+- [Volatile and persistent memory](#volatile-and-persistent-memory)
+  - [Volatile Memory: `VolatileBoundaryChip`](#volatile-memory-volatileboundarychip)
+  - [Persistent Memory: `PersistentBoundaryChip`](#persistent-memory-persistentboundarychip)
+- [Implementation details](#implementation-details)
+- [What to take into account when adding a new chip](#what-to-take-into-account-when-adding-a-new-chip)
 
-We call an address accessed when it's initialized, finalized, read from, or written to. An address is initialized at timestamp 0 and is finalized at the same timestamp it was last read from or written to (or 0 if there were no operations involving it).
+---
 
-To verify memory consistency, we use MEMORY_BUS to post information about all memory accesses, done through interactions.
+Chips in the VM need to perform memory read and write operations. The goal of memory checking is to ensure memory consistency across all chips. Every memory operation consists of an operation type (Read or Write), address (`address_space` and `pointer`), data, and timestamp. All memory operations across all chips should happen at distinct timestamps between $1$ and $2^{29}$. We assume that memory is initialized at timestamp $0$. For simplicity, we assume that all memory operations are enabled (there is a way to disable them in the implementation).
 
-- To verify Read (`address`, `data`, `new_timestamp`) operations, we need to know `prev_timestamp`, the previous timestamp the address was accessed. We enforce that `prev_timestamp < new_timestamp`, and do the following interactions on MEMORY_BUS:
-  - Send (`address`, `data`, `prev_timestamp`)
-  - Receive (`address`, `data`, `new_timestamp`)
-- To verify Write (`address, new_data, new_timestamp`) operations, we need to know `prev_timestamp` and `prev_data`, the previous timestamp the address was accessed and the data stored at the address at that time. We enforce that `prev_timestamp` < `new_timestamp`, and do the following interactions on MEMORY_BUS:
-  - Send (`address`, `prev_data`, `prev_timestamp`)
-  - Receive (`address`, `new_data`, `new_timestamp`)
+Our memory is split into **online** and **offline** memory. Online memory provides actual data and generates access records during program execution, while offline memory uses those records to verify memory consistency. Below we describe how offline memory works. First, we look at the interactions on the memory bus and why they are sound in our ecosystem. Next, we discuss two different memory models, namely, **volatile** and **persistent** memory.
 
-To initialize and finalize memory, we need a Memory Interface chip. For every `address` used in the segment, suppose it's initialized with `initial_data`, `final_data` is stored at the address at the end of the segment, and `final_timestamp` is the timestamp of the last operation involving it in the segment. Then, the interface chip does the following interactions on MEMORY_BUS: - Send (`address`, `initial_data`, 0) - Receive (`address`, `final_data`, `final_timestamp`)
+We use the offline memory checking argument of [BEGKN92](https://www.cs.ubc.ca/~will/papers/memcheck.pdf).
 
-Note that all interactions use multiplicity 1. Crucially, the Memory Interface does exactly one such Send and Receive for every `address` used in the segment. In particular, the AIR enforces that all addresses those interactions are done on are distinct.
+## Basic performed interactions
 
-# Soundness proof
+We say an address is _accessed_ when it's initialized, finalized, read from, or written to. An address is initialized at timestamp $0$ and is finalized at the same timestamp it was last read from or written to (or $0$ if there were no operations involving it).
+
+To verify memory consistency, we use MEMORY_BUS to post information about all memory accesses through interactions. More specifically, we use a **memory bridge** that **both** checks the timestamp inequality and performs the necessary interactions:
+
+- To verify Read (`address`, `data`, `new_timestamp`) operations, we need to know `prev_timestamp`, the previous timestamp when the address was accessed. We enforce that `prev_timestamp < new_timestamp`, and perform the following interactions on MEMORY_BUS:
+  - Receive (`address`, `data`, `prev_timestamp`),
+  - Send (`address`, `data`, `new_timestamp`).
+- To verify Write (`address`, `new_data`, `new_timestamp`) operations, we need to know `prev_timestamp` and `prev_data`, the previous timestamp when the address was accessed and the data stored at the address at that time. We enforce that `prev_timestamp < new_timestamp`, and perform the following interactions on MEMORY_BUS:
+  - Receive (`address`, `prev_data`, `prev_timestamp`),
+  - Send (`address`, `new_data`, `new_timestamp`).
+
+Here is how it's done, for example, for the write operations:
+
+```rust
+/// The max degree of constraints is:
+/// eval_timestamps: deg(enabled) + max(1, deg(self.timestamp))
+/// eval_bulk_access: refer to private function MemoryOfflineChecker::eval_bulk_access
+impl<T: FieldAlgebra, V: Copy + Into<T>, const N: usize> MemoryWriteOperation<'_, T, V, N> {
+    /// Evaluate constraints and send/receive interactions. `enabled` must be boolean.
+    pub fn eval<AB>(self, builder: &mut AB, enabled: impl Into<AB::Expr>)
+    where
+        AB: InteractionBuilder<Var = V, Expr = T>,
+    {
+        let enabled = enabled.into();
+        self.offline_checker.eval_timestamps(
+            builder,
+            self.timestamp.clone(),
+            &self.aux.base,
+            enabled.clone(),
+        );
+
+        self.offline_checker.eval_bulk_access(
+            builder,
+            self.address,
+            &self.data,
+            &self.aux.prev_data.map(Into::into),
+            self.timestamp,
+            self.aux.base.prev_timestamp,
+            enabled,
+        );
+    }
+}
+```
+
+Every instruction executor does this via the **memory controller** API. The memory controller communicates with the memory bus via the **access adapter inventory**, which consists of **access adapters**.
+
+## Access adapters
+
+Consider an example:
+
+- We write `[1, 2, 3, 4]` to `[0, 4)` of address space `2`.
+- We write `[5, 6, 7, 8]` to `[4, 8)` of address space `2`.
+- We read `[3, 4, 5, 6]` from `[2, 6)` of address space `2`.
+
+Using only interactions above, we won't be able to balance out the `[/* address */ 2, /* data */ 2, 3, 4, 5, 6, /* timestamp */3]` interaction. Access adapters are used to fix this.
+
+Formally speaking, our memory model is as follows:
+
+- Call a set of power-of-two subsegments of $[0,2^{\text{max bits}})$ _nice_. More specifically, a set $S$ of disjoint subsets of $[0,2^{\text{max bits}})$ is nice if the following holds:
+  - Every element of $S$ consists of consecutive integers (or _is a subsegment_),
+  - Every element of $S$ has a power-of-two size (hence nonempty).
+- At any point of time, for each valid address space, we store a nice set of _memory segments_, and for each of them we remember the actual values there.
+- In our memory model, the chips are allowed to perform reads and writes from/to power-of-two subsegments of memory (more specifically, subsegments with length that is a power of two not exceeding 32).
+- When we want to read from/write to a subsegment, **we transform the nice set for the corresponding address space,** so that our set contains the queried subsegment, using the following operations:
+  - _Create_ a new aligned subsegment of length `CHUNK` (which is a power of two). It must not overlap with any other subsegment.
+  - _Split_ an existing subsegment by replacing it with its halves.
+  - _Merge_ two adjacent subsegments of equal length into one.
+- It is easy to see that the set remains nice after each operation.
+
+The _split_ and _merge_ operations for a subsegment of length `N` are handled by `AccessAdapterAir<N>` (and we only have 5 of them, hence up to 32). More specifically:
+- We have a `timestamp` associated with each subsegment from our nice set -- basically the timestamp of last read/write of any part in this subsegment.
+- Of course, merging two halves creates a subsegment with the timestamp being the maximal of two timestamps for the halves.
+- Splitting, however, just makes all child timestamps equal to the former timestamp of the segment being split.
+- All these timestamp conditions are checked in the `AccessAdapterAir`.
+- When merging two segments `[l, r)` and `[m, r)`, the `AccessAdapterAir` sends to **memory bus** the information about `[l, r)` and receives the information about `[l, m)` and about `[m, r)`, all with multiplicity 1. Splitting does the same, but with multiplicity -1 (or, in other words, receives about `[l, r)` and sends about `[l, m)` and `[m, r)`).
+- The information about `[l, r)` sent to the bus is, in this order:
+  - address space,
+  - `l`,
+  - the values at `[l, r)`,
+  - the timestamp.
+
+In the example above, if, say, `CHUNK = 1`, then the following interactions are performed:
+- Write `[1, 2, 3, 4]` to `[0, 4)` of address space `2`.
+  - We _create_ subsegments `[0, 1)`, `[1, 2)`, `[2, 3)`, `[3, 4)` at time `0`:
+    - ← `[(2, 0), (0), 0]`.
+    - ← `[(2, 1), (0), 0]`.
+    - ← `[(2, 2), (0), 0]`.
+    - ← `[(2, 3), (0), 0]`.
+  - We _merge_ the first pair together and also the second pair together:
+    - → `[(2, 0), (0), 0]`, → `[(2, 1), (0), 0]`, ← `[(2, 0), (0, 0), 0]`.
+    - → `[(2, 2), (0), 0]`, → `[(2, 3), (0), 0]`, ← `[(2, 2), (0, 0), 0]`.
+  - We _merge_ the two segments we have into one:
+    - → `[(2, 0), (0, 0), 0]`, → `[(2, 2), (0, 0), 0]`, ← `[(2, 0), (0, 0, 0, 0), 0]`.
+  - _Basic interaction_: we send the old data and receive the new data.
+    - → `[(2, 0), (0, 0, 0, 0), 0]`.
+    - ← `[(2, 0), (1, 2, 3, 4), 1]`.
+- Write `[5, 6, 7, 8]` to `[4, 8)` of address space `2`.
+  - We do the same steps as above:
+    - ← `[(2, 4), (0), 0]`.
+    - ← `[(2, 5), (0), 0]`.
+    - ← `[(2, 6), (0), 0]`.
+    - ← `[(2, 7), (0), 0]`.
+    - → `[(2, 4), (0), 0]`, → `[(2, 5), (0), 0]`, ← `[(2, 4), (0, 0), 0]`.
+    - → `[(2, 6), (0), 0]`, → `[(2, 7), (0), 0]`, ← `[(2, 6), (0, 0), 0]`.
+    - → `[(2, 4), (0, 0), 0]`, → `[(2, 6), (0, 0), 0]`, ← `[(2, 4), (0, 0, 0, 0), 0]`.
+  - _Basic interaction_: we send the old data and receive the new data. Notice the new timestamp.
+    - → `[(2, 4), (0, 0, 0, 0), 0]`.
+    - ← `[(2, 4), (5, 6, 7, 8), 2]`.
+- Read `[3, 4, 5, 6]` from `[2, 6)` of address space `2`.
+  - Now we have subsegments `[0, 4)` and `[4, 8)` in our nice set. We need to split each of them in two, and then merge the middle two parts.
+  First we _split_ the left. Notice that the timestamps of the new parts are the same as the timestamp of the original segment:
+    - → `[(2, 0), (1, 2, 3, 4), 1]`, ← `[(2, 0), (1, 2), 1]`, ← `[(2, 2), (3, 4), 1]`.
+  - Then we _split_ the right:
+    - → `[(2, 4), (5, 6, 7, 8), 2]`, ← `[(2, 4), (5, 6), 2]`, ← `[(2, 6), (7, 8), 2]`.
+  - Finally, we _merge_ the middle two segments, making the new timestamp the maximum of the previous two:
+    - → `[(2, 2), (3, 4), 1]`, → `[(2, 6), (7, 8), 2]`, ← `[(2, 2), (3, 4, 5, 6), 2]`.
+  - _Basic interaction_: we send the old data and receive it again, but with the new timestamp.
+    - → `[(2, 2), (3, 4, 5, 6), 2]`.
+    - ← `[(2, 2), (3, 4, 5, 6), 3]`.
+
+One can see that the splits and merges mostly balance each other out -- more formally, each subsegment in the nice set, be it born or annihilated by split or merge, corresponds to a receive (→) interaction on birth and a send (←) interaction on death.
+
+## Boundary chips
+
+However, the only unaccounted for interactions are the initial birth of a segment and the final memory state. We use **boundary chips** to handle them.
+
+In **volatile** memory, we use `VolatileBoundaryChip` to handle the initial memory state. It does the following:
+- The chip assumes that all initial memory is filled with zeroes.
+- However, **initial memory is unconstrained** -- so the result of such program's execution is going to be something along the lines of "there is some way to fill the initial memory that this program finishes correctly".
+
+In **persistent** memory, we use `PersistentBoundaryChip` to handle the final memory state. It does the following:
+- There is some initial memory specified in the program.
+- The chip _commits_ to the initial memory -- see below.
+
+Both boundary chips perform, for every subsegment ever existed in our nice set, a receive interaction on birth and a send interaction on death.
+
+## Soundness proof
 
 Assume that the MEMORY_BUS interactions and the constraints mentioned above are satisfied.
 
-Fix any address `address` that is used in the segment. To prove memory consistency, it's enough to prove all memory operations on `address` are consistent. Let's look at all interactions done on MEMORY_BUS involving `address`.
+### Time goes forward
+
+In the connector chip, we constrain that the final timestamp is less than $2^{29}$. It is [guaranteed](https://github.com/openvm-org/stark-backend/blob/main/docs/interactions.md) that the total number of interaction messages is less than $p$. In our current circuit set, all chips increase timestamp [less than they do interactions](./circuit.md#inspection-of-vm-chip-timestamp-increments), which guarantees that the final timestamp cannot overflow: its actual (not mod $p$) value is less than $2^{29}$. Given that, our check that `timestamp - prev_timestamp - 1 < 2^29` guarantees that `prev_timestamp < timestamp` everywhere we check it.
+
+### Memory consistency
+
+To show that memory is consistent, consider any interactions with segments containing some `address`. They can be done by:
+
+- Boundary chip: one send with the initial timestamp and some value at this address, one receive with the final timestamp and some value at this address. The fact that there is only one send/receive is guaranteed by the fact that we ensure in the boundary chip that all the addresses are distinct.
+- Access adapter: each row corresponds to a merge or a split, and therefore for each row, if its split/merged segment contains `address`, we have exactly one send and exactly one receive corresponding to this row. They have the same value at this address, and the send timestamp is at least the receive timestamp -- and strictly greater unless these operations are within one memory access.
+- Instruction executors via memory bridge: one receive with the previous timestamp and some value at this address, one send with the new timestamp and some value at this address. We also have `prev_timestamp < timestamp`.
+
+To prove memory consistency, we need to show that, if we only consider basic interactions and boundary interactions, then accessing an address always gives us something that has the last value observed at this address (in a broad sense: for example, this includes "writing to `[3, 5)` generates a record where previous data at `4` is the last value written to or read from `4`, be it from `[4, 5)`, `[2, 10)` or any other segment containing `4`"). In other words, for every receive interaction with timestamp $t$ and value $v$ corresponding to this address, the value $v$ is the value from any _basic_ (not split/merge) send interaction with the greatest existing timestamp $t'$ less than $t$.
+
+For every row that gets handled by a memory bridge, draw a directed edge from the previous timestamp to the new timestamp with capacity $1$. We know that in the obtained network, where the source and the sink are defined by the boundary interactions, all edges go left to right, and the maximal flow is $1$ (because there is only one edge from the source, therefore the minimal cut is $1$). Since all edges go left to right, there are no circulation in this network. Hence, all edges represent exactly one path from the source to the sink. Therefore, for every vertex (which is a timestamp), the value for the edge going to this vertex (the last receive interaction) equals the value for the edge going from this vertex (the considered send instruction).
+
+> [!NOTE]
+> Technically, it is possible to add artificial rows in the access adapter AIRs that do nothing: for example, the one corresponding to splitting a segment `[7, 11)` into two and the one merging it back, all at the same timestamp and with the same data completely unrelated to the actual execution. However, this cannot be abused by memory accesses: informally, because this kind of happens at the same time and nobody gets to read these values, and formally, because even if a flow has a circulation, it still can be decomposed into paths and a circulation, and we only consider the path in the argument above.
+
+## Volatile and persistent memory
+
+There are two memory interfaces: **volatile** and **persistent**. From the memory POV, the difference is:
+- In volatile memory, both initial and final memory are unconstrained.
+- In persistent memory, we commit to the initial memory and to the final memory. These commitments are used for establishing memory consistency between segments.
+
+### Volatile Memory: `VolatileBoundaryChip`
+
+- **Purpose:**
+  In volatile memory, we assume that the initial memory is filled with zeros and that it is unconstrained. The `VolatileBoundaryChip` is used mainly for bookkeeping: it tracks the starting state of memory without enforcing a cryptographic commitment. This means that when a program begins execution, the chip simply "accepts" the initial (zeroed) memory without additional checks.
+
+- **Key Points:**
+  - **No Commitment:**
+    The chip does not compute a Merkle tree root for the initial memory state. Instead, any memory value that gets revealed later comes directly from the execution trace.
+  - **Distinctness Check:**
+    It ensures that the addresses used are distinct by enforcing a sort order across boundary records.
+
+For instance, this is how distinctness is enforced:
+
+```rust
+// Assert local addr < next addr when next.is_valid
+// This ensures the addresses in non-padding rows are all sorted
+let lt_io = IsLtArrayIo {
+    x: [local.addr_space, local.pointer].map(Into::into),
+    y: [next.addr_space, next.pointer].map(Into::into),
+    out: AB::Expr::ONE,
+    count: next.is_valid.into(),
+};
+// N.B.: this will do range checks (but not other constraints) on the last row if the first row has is_valid = 1 due to wraparound
+self.addr_lt_air
+    .eval(builder, (lt_io, (&local.addr_lt_aux).into()));
+```
+
+### Persistent Memory: `PersistentBoundaryChip`
+
+- **Purpose:**
+  When operating in persistent memory mode, the final state of memory must be verifiable. The `PersistentBoundaryChip` commits not only to the final memory state but also to the initial state provided by the program. These commitments become part of the public values used later in proof aggregation, see [Continuations](./continuations.md).
+
+- **Key Points:**
+  - **Commitments:**
+    The chip takes the initial memory (which is provided as part of the program) and computes a commitment over it—typically by incorporating it into a Merkle tree. Later, when the segment finishes, the chip produces a final commitment (Merkle root) that reflects every change made during execution.
+  - **Field: Expand Direction:**
+    Each memory chunk is tagged with an `expand_direction` field:
+    - `expand_direction = 1` indicates a boundary row representing the initial memory state.
+    - `expand_direction = -1` indicates a boundary row representing the final memory state.
+    - `expand_direction = 0` marks rows that are not relevant (for example, intermediary rows produced by splits/merges that cancel out).
+  - **Multiple Buses:**
+    To enforce these commitments, the PersistentBoundaryChip interacts with three buses:
+    - **Merkle Bus:**
+      For initial memory rows, it sends a record (for example, with a tag `0`), and for final memory rows, it receives a record (with a tag `1`). This allows the Merkle chip to build and later verify a full commitment over the memory.
+    - **Compression Bus:**
+      It sends the values and hash arrays for each chunk. The multiplicity of these interactions is determined by the square of the `expand_direction` (so that both initial and final rows are treated uniformly in the compression process).
+    - **Memory Bus:**
+      It performs the basic send/receive interactions that balance out each memory operation, similar to the ones described in the “Basic performed interactions” section.
+
+The uniqueness of the addresses is achieved by the interactions on the merkle bus. The other chip that does interactions there is the **merkle chip**, which build the merkle tree, and to balance everything out, the boundary chip's interactions must correspond to the leaves of the merkle tree, thus, all distinct.
 
-Suppose the list of operations involving `address` _sorted_ by `timestamp` is `ops_i` for `0 <= i < k`. As discussed above, for every operation `i`, we do one Receive, `r_i`, and one Send, `s_i`, on the MEMORY*BUS. Since the constraint `r_i.timestamp < s_i.timestamp` is enforced, the only way for the MEMORY_BUS interactions to balance out is through the interactions involving `address` done by the Memory Interface. This can be seen by noticing that none of the Receive interactions `r_i` can match `s*{k-1}`as it has the highest timestamp. In fact this implies that the Memory Interface has to do exactly one Receive involving`address` with the final timestamp and data, and, similarly, one Send with the initial timestamp (0) and data. Note that only one such Send and Receive are made as we enforce all addresses in Memory Interface are distinct.
+## Implementation details
 
-Using a similar technique, by induction, we can show that `s_i.timestamp = r_{i+1}.timestamp` and `s_i.data = r_{i+1}.data` for all `0 < i < k - 1`. Since `(s_i.address, s_i.data, s_i.timestamp) = (ops_i.address, ops_i.data, ops_i.timestamp)` for all operations and `s_i.data = r_i.data` for Read operations, this proves memory consistency for `address`.
+In this model, there is no central memory/offline checker AIR. Every chip is responsible for doing the necessary interactions discussed above for its memory operations. For this, every chip's AIR must have some auxiliary columns for every memory operation. The auxiliary columns include `prev_timestamp` and, for Write operations, `prev_data`.
 
-# Implementation details
+When we use Volatile Memory as the Memory Interface (PersistentBoundaryAir in the implementation), we do not, at the AIR level, constrain the initial memory. This means that if the first operation on an address is a Read, the corresponding data can be anything -- it's up to the program to read from addresses that have been written to.
 
-In this model, there is no central memory/offline checker AIR. Every chip is responsible for doing the necessary interactions discussed above for its memory operations. To do this, every chip's AIR to have some auxiliary columns for every memory operation. The auxiliary columns include `prev_timestamp` and, for Write operations, `prev_data`.
+## What to take into account when adding a new chip
 
-When we use Volatile Memory as the Memory Interface (PersistentBoundaryAir in the implementation), we do not, on the AIR level, constrain the initial memory. This means that if the first operation on an address is a Read, the corresponding data can be anything -- it's on the program to read from addresses that have been written to. Separately, the PeristentBoundaryAir enforces that all addresses are distinct by enforcing sorting, but there are other more efficient ways to do this.
+Key points:
+- For all memory accesses, use memory bridge. It will ensure that all memory interactions are of a certain kind, which we already established soundness for.
+- Do not increase the timestamp more than necessary -- otherwise the timestamp may overflow. Ideally, the timestamp should be increased by 1 for every memory access and never otherwise.
+- In general, do not communicate with system buses directly.


Reviews:
user: login: jonathanpwang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T20:39:45Z
user: login: jonathanpwang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T07:59:51Z
user: login: jonathanpwang
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T08:43:44Z

Review comments:
diff_hunk: @@ -53,228 +53,219 @@ The chips that get to execute instructions are _instruction executors_. These ch
 This modularity helps to separate the functionalities, reduce space for error and also reuse the same adapters for various chips with similar instruction signatures.
 Note that technically these are parts of the same chip and therefore generate one trace, although both adapter and core have AIRs to deal with different parts of the trace.
 
-> [!NOTE]
+> [!IMPORTANT]
 > It is a burden of the instruction executor (more specifically, the adapter) to update the execution state. It is also its burden to constrain that the time increases. If any of these is not done correctly, the proof of correctness will fail to be generated.
 
----
----
----
----
----
-
-### Our no-CPU design
-
-The main motivation is that the existence of a CPU forces the existence of a trace matrix with rows growing with the total number of clock cycles of the program execution. We claim that the no-CPU design gives the minimum lower bound on the number of required trace cells added per opcode execution.
-
-Traditionally, the CPU is in charge of reading/writing from memory and forwarding that information to the appropriate chip. We have switched to a model where each chip directly accesses memory itself. Traditionally this is also inefficient because the CPU uses physical general purpose registers for instruction execution, whereas in our architecture, registers are emulated as memory in a dedicated address space.
-
-Each chip has IO columns `(timestamp, pc, instruction)` where `instruction` is `(opcode, operands)`.
-The chip receives `(pc, instruction)` on the PROGRAM_BUS to ensure it is reading the correct line of the program code.
-There is a maximum length to `operands` defined by the PROGRAM_BUS, but each chip can receive only a subset of the
-operands (setting the rest to zero) without paying the cost for the unused operands.
-
-**Note:** each chip receives an _offset_ on construction, and this offset basically means "where does the class of
-operations which this chip supports start". For example, if a `FieldArithmeticChip` has offset `0x100`, then its `SUB`
-operation would be encoded with opcode `0x100 + 1` and not just `1`.
-See [ISA spec](./ISA.md) for
-details.
-
-Each chip receives `(timestamp, pc)` on EXECUTION_BUS and "after"
-executing an instruction, sends `(new_timestamp, new_pc)` on the same bus (here `new_pc` is `pc + 1` most of the time,
-but not always).
-The chip is in charge of constraining that `new_timestamp` is consistent with `timestamp`. In
-particular, `new_timestamp` must be (almost always strictly) greater than `timestamp`, but not by a lot (so that the
-timestamps do not overflow the field characteristic).
-The bus enforces that each timestamp transition corresponds to a particular instruction being executed.
-
-There is an `ExecutionBridge` for more convenient communicating with these two buses.
-
-The chip must constrain that `opcode` is one of the opcodes the chip itself owns. The chip then constrains the rest of
-the validity of the opcode execution, according to the opcode spec.
-
-There is also another very simple "connector" chip with a 2 row trace that sends out `(1, 0)` on EXECUTION_BUS and
-receives `(final_timestamp, final_pc)` on EXECUTION_BUS. These four values are public values of the program. With
-continuations, the start and end timestamp/pc will need to be constrained with respect to the pre/post-states.
-
-The vm design is so that the core chip is just one of such chips. The instructions directed at managing the execution
-flow are passed to the core chip. Such design is modular enough in the sense that it allows us to treat the control flow
-instructions similarly to most of the other opcodes. Also, instead of having the execution protocol in a single matrix
-and duplicate the instructions with the chips that actually execute them, we have each step of execution only generating
-one new row in the machine chip (and maybe more lines in other primitive chips that it uses for execution).
-
-### Offline Memory
-
-In the no-CPU design, each chip receives the opcode instruction directly, and memory access (read or write) is
-constrained by the chip itself.
-
-The VM supports a read/write memory, which is constrained via offline memory checking. We use the offline memory
-checking argument of [BEGKN92](https://www.cs.ubc.ca/~will/papers/memcheck.pdf).
-
-Any offline memory checking aims to have a transcript consisting of `(a, v, t)` with address `a`, value `v`,
-timestamp `t` (in our ISA `a = (address_space, address)` but we omit this distinction here for brevity). The timestamp
-here is a single field element. As far as we know, the timestamp **must** be global and match what is used by the
-`EXECUTION_BUS` to ensure that the temporal sequencing of memory accesses matches the temporal sequencing of instruction
-execution.
-
-<!--
-[JPW] Lasso uses a per-address timestamp (renamed counter) but in a different setting. We did not see a way to use this argument because it did not allow constraining instruction execution matched memory access ordering.
--->
-
-Memory aims to support two operations: read and write.
-
-- A read of `(a, v)` at time `t` means that if we look through the transcript focusing on only entries with address `a`,
-  the entry with timestamp immediately preceding `t` must have value also equal `v`.
-
-- A write of `(a, v)` at time `t` means a new entry `(a, v, t)` must be introduced to the transcript.
-
-The main distinction of [BEGKN92] is that the transcript does not need to be materialized explicitly in a single AIR
-matrix. The particular entries of the transcript are materialized on a per-access basis (in whatever chip needs it).
-This materialization is avoided by using the MEMORY_BUS and chip constraints to constrain the correctness of the
-transcript:
-
-We have an offline checking MEMORY_BUS, where message fields consist of `(a, v, t)`. The bus then has two sets (send vs
-receive) that must be equal at the end. To match the literature, let send (resp. receive) correspond to Write (resp.
-Read) sets. Any memory access in a chip must add one entry into each set and constrain a relation between them:
-
-- A read of $(a, v)$ at time $t$ must add $(a, v, t_{prev})$ to Read set and $(a, v, t)$ to Write set. It must constrain
-  $t_{prev} < t$.
-- A write of $(a, v)$ at time $t$ must add $(a,v_{prev},t_{prev})$ to Read set and $(a,v,t)$ to Write set, where $v_
-  {prev}$ is the previous value before the write. It must constrain $t_{prev} < t$.
-
-To balance the Read and Write sets, an additional chip must ensure that every accessed address has an initial $(a, v_
-{init}, 0)$ added to the Write set, and $(a, v_{final}, t_{last})$ added to Read set.
-
-<!--
-For the offline checking to be sound, it must be constrained that the list of accessed addresses in the initial Write
-list are all **unique**. Uniqueness of the initial address list implies, together with the bus argument, that the final
-address list is also unique (vice versa, uniqueness of final set implies uniqueness of initial set). The key observation
-of [OLB24](https://eprint.iacr.org/2024/979.pdf) is that only _uniqueness_ is necessary and not sorted-ness of the
-address list. The traditional approach prior to OLB24 to enforce uniqueness is to enforce the list of addresses is
-sorted, which uses logup lookups for range checks necessary to constrain `IsLessThan`. OLB24 shows that one can
-implement an AIR with in-circuit randomness to constrain that all entries in a trace column are unique (with an
-extension to conditional uniqueness).
--->
+## Adapter-core interface
+
+The adapter-core interface is defined by the `VmAdapterInterface` trait.
+
+```rust
+/// The interface between primitive AIR and machine adapter AIR.
+pub trait VmAdapterInterface<T> {
+    /// The memory read data that should be exposed for downstream use
+    type Reads;
+    /// The memory write data that are expected to be provided by the integrator
+    type Writes;
+    /// The parts of the instruction that should be exposed to the integrator.
+    /// This will typically include `is_valid`, which indicates whether the trace row
+    /// is being used and `opcode` to indicate which opcode is being executed if the
+    /// VmChip supports multiple opcodes.
+    type ProcessedInstruction;
+}
+```
 
-The initial and final memory accesses are constrained different when the VM has continuations.
-See [Continuations](./continuations.md) for full details. In summary, because the initial and final memory states are
-committed to in a **trie**, the uniqueness of the addresses is constrained by the trie, so the arguments of the previous
-paragraph are not used.
+Here, `Reads` is some type that describes the memory reads required for the chip, and `Writes` is similar but about writes. The `VmAdapterChip` defines what the adapter needs to be able to do:
+
+```rust
path: docs/specs/circuit.md
user: login: jonathanpwang
body: this kind of section with more specific code-level usage should go in `docs/crate` or in a README.md next to `integration_api.rs`

The "specs" markdown should be kept at a level above the code specifics, except in cases where you really need the code for precision. 

Since the integration API is only one of several ways to achieve what is laid out in our circuit architecture, I feel that it falls into the "crate docs" category and not part of the architecture spec.
created_at: 2025-03-15T20:39:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,210 +3,67 @@
 We build our virtual machines in a STARK proving system with a multi-matrix commitment scheme and shared verifier
 randomness between AIR matrices to enable permutation arguments such as log-up.
 
-In the following, we will refer to a circuit as a collection of AIR matrices (also referred to as chips) of possibly
-different heights, which may communicate with one another over buses using a log-up permutation argument. We refer to
-messages sent to such a bus as [interactions](https://github.com/openvm-org/stark-backend/tree/main/crates/stark-backend/src/interaction).
+In the following, we will refer to a circuit as a collection of chips that communicate with one another over buses using a LogUp permutation argument. We refer to messages sent to such a bus as [interactions](https://github.com/openvm-org/stark-backend/blob/main/docs/interactions.md). Every _chip_ is an entity responsible for a certain operation (or set of operations), and it has an AIR to check the correctness of its execution.
path: docs/specs/circuit.md
user: login: jonathanpwang
body: technically, in the backend we used "interaction" to refer to the "for every row, send ..." constraint/rule. But we do also colloquially use interaction to kind of refer to the message. I couldn't find a good way to really distinguish this, and here when you say refer to mesage**s** -> interaction, it is technically still correct in a sense. so no change required
created_at: 2025-03-18T07:59:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-15T04:00:04Z
body: <!--Benchmark Results-->
| group | app.proof_time_ms | app.cycles | app.cells_used | leaf.proof_time_ms | leaf.cycles | leaf.cells_used |
| -- | -- | -- | -- | -- | -- | -- |
| [verify_fibair](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/verify_fibair-a5cc2aab7039287e6d6d5933bdf6ffe5ebe087e3.md) |<span style='color: green'>(-40 [-3.1%])</span> 1,245 |  334,023 |  17,901,531 |- | - | - |
| [fibonacci_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/fibonacci-a5cc2aab7039287e6d6d5933bdf6ffe5ebe087e3.md) |<span style='color: red'>(+63 [+2.3%])</span> 2,754 |  1,500,096 |  51,485,167 |- | - | - |
| [regex_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/regex-a5cc2aab7039287e6d6d5933bdf6ffe5ebe087e3.md) | 7,760 |  4,140,164 |  167,389,450 |- | - | - |
| [ecrecover_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/ecrecover-a5cc2aab7039287e6d6d5933bdf6ffe5ebe087e3.md) |<span style='color: red'>(+10 [+0.7%])</span> 1,454 |  295,181 |  15,586,346 |- | - | - |
| [pairing](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/pairing-a5cc2aab7039287e6d6d5933bdf6ffe5ebe087e3.md) |<span style='color: red'>(+96 [+2.0%])</span> 4,784 |  1,711,640 |  92,585,975 |- | - | - |


Commit: https://github.com/openvm-org/openvm/commit/a5cc2aab7039287e6d6d5933bdf6ffe5ebe087e3

[Benchmark Workflow](https://github.com/openvm-org/openvm/actions/runs/13869237666)

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-15T19:40:48Z
body: <!--Benchmark Results-->
| group | app.proof_time_ms | app.cycles | app.cells_used | leaf.proof_time_ms | leaf.cycles | leaf.cells_used |
| -- | -- | -- | -- | -- | -- | -- |
| [verify_fibair](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/verify_fibair-336c5bd7d993f4e89474fb001720e2504ba75804.md) |<span style='color: green'>(-59 [-4.6%])</span> 1,226 |  333,970 |  17,900,895 |- | - | - |
| [fibonacci_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/fibonacci-336c5bd7d993f4e89474fb001720e2504ba75804.md) |<span style='color: red'>(+82 [+3.0%])</span> 2,773 |  1,500,096 |  51,485,167 |- | - | - |
| [regex_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/regex-336c5bd7d993f4e89474fb001720e2504ba75804.md) |<span style='color: green'>(-44 [-0.6%])</span> 7,715 |  4,140,164 |  167,389,450 |- | - | - |
| [ecrecover_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/ecrecover-336c5bd7d993f4e89474fb001720e2504ba75804.md) |<span style='color: red'>(+34 [+2.4%])</span> 1,478 |  295,181 |  15,586,346 |- | - | - |
| [pairing](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/pairing-336c5bd7d993f4e89474fb001720e2504ba75804.md) |<span style='color: red'>(+44 [+0.9%])</span> 4,732 |  1,711,640 |  92,585,975 |- | - | - |


Commit: https://github.com/openvm-org/openvm/commit/336c5bd7d993f4e89474fb001720e2504ba75804

[Benchmark Workflow](https://github.com/openvm-org/openvm/actions/runs/13876004348)

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-17T21:36:55Z
body: <!--Benchmark Results-->
| group | app.proof_time_ms | app.cycles | app.cells_used | leaf.proof_time_ms | leaf.cycles | leaf.cells_used |
| -- | -- | -- | -- | -- | -- | -- |
| [verify_fibair](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/verify_fibair-88680e885521ac6f426e76efd7efd4b296c16ae7.md) | 1,244 |  334,023 |  17,901,531 |- | - | - |
| [fibonacci_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/fibonacci-88680e885521ac6f426e76efd7efd4b296c16ae7.md) |<span style='color: red'>(+57 [+2.1%])</span> 2,726 |  1,500,096 |  51,485,167 |- | - | - |
| [regex_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/regex-88680e885521ac6f426e76efd7efd4b296c16ae7.md) |<span style='color: green'>(-342 [-4.3%])</span> 7,696 |  4,140,164 |  167,389,450 |- | - | - |
| [ecrecover_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/ecrecover-88680e885521ac6f426e76efd7efd4b296c16ae7.md) |<span style='color: red'>(+85 [+6.0%])</span> 1,496 |  295,181 |  15,586,346 |- | - | - |
| [pairing](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/pairing-88680e885521ac6f426e76efd7efd4b296c16ae7.md) |<span style='color: red'>(+31 [+0.7%])</span> 4,758 |  1,711,640 |  92,585,975 |- | - | - |


Commit: https://github.com/openvm-org/openvm/commit/88680e885521ac6f426e76efd7efd4b296c16ae7

[Benchmark Workflow](https://github.com/openvm-org/openvm/actions/runs/13910228933)

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-18T08:49:04Z
body: <!--Benchmark Results-->
| group | app.proof_time_ms | app.cycles | app.cells_used | leaf.proof_time_ms | leaf.cycles | leaf.cells_used |
| -- | -- | -- | -- | -- | -- | -- |
| [verify_fibair](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/verify_fibair-4be954c7c04657f94a42728faad850f0677eddee.md) |<span style='color: red'>(+2 [+0.2%])</span> 1,241 |  334,023 |  17,901,531 |- | - | - |
| [fibonacci_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/fibonacci-4be954c7c04657f94a42728faad850f0677eddee.md) |<span style='color: red'>(+55 [+2.1%])</span> 2,735 |  1,500,096 |  51,485,167 |- | - | - |
| [regex_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/regex-4be954c7c04657f94a42728faad850f0677eddee.md) |<span style='color: green'>(-213 [-2.7%])</span> 7,757 |  4,140,164 |  167,389,450 |- | - | - |
| [ecrecover_program](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/ecrecover-4be954c7c04657f94a42728faad850f0677eddee.md) |<span style='color: red'>(+51 [+3.6%])</span> 1,476 |  295,181 |  15,586,346 |- | - | - |
| [pairing](https://github.com/openvm-org/openvm/blob/benchmark-results/benchmarks-pr/1457/pairing-4be954c7c04657f94a42728faad850f0677eddee.md) |<span style='color: red'>(+76 [+1.6%])</span> 4,777 |  1,711,640 |  92,585,975 |- | - | - |


Commit: https://github.com/openvm-org/openvm/commit/4be954c7c04657f94a42728faad850f0677eddee

[Benchmark Workflow](https://github.com/openvm-org/openvm/actions/runs/13918883912)

author_association: NONE