by Blum at al. (1994)

Sources:

Read-only memory

The problem they tried to solve is to force an untrusted memory to prove it worked correctly, with very low space requirements of the verifier.

The verifier maintains two sets, which can be compressed and updated incrementally via permutation-invariant fingerprinting (see Permutation Check via Product Check):

  • Read Set: The tuples returned by the prover
  • Write Set: written to memory

Then, the verifier behaves as follows:

  • At the beginning of the execution, they write for each address the tuple (or some other initial value)
  • Each read returning is followed by a write of
  • At the end of the execution, the verifier reads all addresses

If and only if the prover is honest, the read and write multi-sets will be equal.

Application: Indexed Lookup

Source: Unlocking the lookup singularity with Lasso

Suppose we have:

  • A multi-linear extension of some table (i.e., read-only memory)
  • A multi-linear extension of addresses
  • A multi-linear extension of values

We want to show that:

where is the function that maps a field element to its bit representation.

We can show this is the case by simulating the offline memory verifier in a SNARK.

Specifically, the prover commits to two polynomials, and , containing the counts returned by the offline memory prover for the “normal” and final read operations.

The memory operations were performed correctly if where:

where is also a multi-linear polynomial:

The two multi-sets can be shown to be equal using the Permutation Check via Product Check! In particular, the grand product can be computed using a layered arithmetic circuit of depth and proven using GKR.

PIL Sketch

namespace main;
  // Address might be given (indexed lookup)
  // or has to be provided as a witness.
  col fixed address;
  col witness value;
  col witness time_stamp;
 
  // Read value
  bus_receive(1, (address, value, time_stamp));
  // Write value
  bus_send(1, (address, value, time_stamp + 1));
 
namespace table;
  // Often, this does not need to be committed
  // but corresponds to a polynomial that can be
  // evaluated cheaply by the verifier, e.g.:
  // f(x_0, ..., x_{n - 1}) =
  //   x_0 + ... + 2^{n - 1} * x^{n - 1}
  col fixed address;
  col fixed value;
  // The number of times each element is read, i.e.,
  // the final time steps.
  col witness multiplicities;
 
  // Initialize memory: Write all values with
  // time step 0
  bus_send(1, (address, value, 0));
 
  // Finalize memory: Read all values one last time
  bus_receive(1, (address, value, multiplicity));

Comparison to LogUp (see LogUp & cq):

  • The multiplicity with which elements are sent to the bus (the first argument of bus_send and bus_receive) is always 1.
  • As a consequence, the bus argument does not need to support multiplicities and can be simply a Permutation Check via Product Check. This is cheaper than the fractional sum-check needed in LogUp.
  • On the other hand, LogUp commits to a strict subset of columns: It does not need the time_step column. Also, in the case of non-indexed lookups (simply stating that main::value is a subset of table::value), the address columns are not needed.

Read-write memory

The original paper actually describes a read-write memory which is slightly more complex:

  • As described above, all memory cells are initialized with some value at time step
  • For each memory write of value to address at time :
    • The prover provides and
    • The verifier asserts that
    • The tuple is added to the read set
    • The tuple is added to the write set
  • A memory read works the same, but with