Longfellow ZK

repository·main·Indexed 23 days ago

https://github.com/google/longfellow-zk

A C++ library and Rust-based reference implementation enabling zero-knowledge protocols for legacy identity standards including ISO MDOC, JWT, and W3 Verifiable Credentials. It features a prover and verifier system implementing 'Anonymous Credentials from ECDSA', utilizing the LFC2 circuit format and Ligero polynomial commitment scheme.

Tokens
39.5K
Snippets
63
Records
206
Agent score
80%

What's inside Longfellow ZK

  1. Overview of the Longfellow ZK Rust implementation

    main

    The Rust implementation is a next-generation version of the Longfellow Zero-Knowledge (ZK) proof system. It provides several improvements over the previous C++ implementation:

    • Circuit Organization: Circuits are organized into units with cleanly defined interfaces, leveraging the Rust type system for better structure and auditability.
    • Stringent Checks: Circuits (especially mdoc-zk) perform paranoid input checks to maximize local auditability.
    • Improved Compiler: A new compiler generates more efficient circuits; for the full mdoc-zk application, circuit size decreased by ~10% despite increased checks.
    • Memory Efficiency: The Rust prover is highly memory-efficient, producing full mdoc-zk proofs using less than 100MB of memory.
    • LFC2 Circuit Format: A new, compact circuit format called LFC2 is used. mdoc-zk LFC2 circuits consume approximately 1MB (pre-compression), compared to ~100MB for the older LFC1 format.
    • Backward Compatibility: The implementation is 100% backward compatible with the C++ version, meaning it can read LFC1 circuits and produce bit-for-bit identical proofs.
  2. Understand the Longfellow ZK Reference Implementation

    main

    The Longfellow ZK Reference Implementation is an authoritative Rust-based specification of the Longfellow Zero-Knowledge Proof System (prover and verifier). It implements the protocol from 'Anonymous Credentials from ECDSA' and formally specifies the proof wire format, Fiat-Shamir transcript derivation, and the verification algorithm.

    Important Usage Notes:

    • Performance: This implementation is designed for mathematical correctness and clarity, not performance. It is slow and not intended for benchmarking. The prover uses an $O(n \log n)$ algorithm and Lagrange polynomial interpolation is computed in $O(n^2)$ time via explicit evaluation matrices.
    • Security: Zero-knowledge blinding requires a trusted source of random bits. The implementation provides a deterministic pseudo-random source for testing purposes to allow comparison against other implementations. Do not use this deterministic source in any real application.
  3. Understand Longfellow ZK benchmark performance and constraints

    main

    Longfellow ZK benchmarks are conducted using single-threaded execution to ensure compatibility with deployment environments and to prevent excessive battery consumption on user devices.

    Performance is measured across three primary primitives:

    1. FFT (Fast Fourier Transform): Measures FFT time over various fields. This is a potential bottleneck because Longfellow uses the Ligero proof system. Fields tested include Fp2 (quadratic extension over P256), Fp128, Fp64, and Fp64_2 (quadratic extension of Fp64).
    2. SHA: Measures the time required to prove knowledge of a pre-image of size up to $N$ blocks for a 256-bit string in zero-knowledge.
    3. ECDSA: Measures the time to prove possession of a signature (r,s) on a message e under a public key (x,y).
  4. How the Longfellow ZK protocol works

    main

    The Longfellow ZK protocol is a zero-knowledge system composed of two main components: an encrypted sumcheck prover and a commitment scheme.

    Protocol Workflow

    1. Commitment: The prover commits to all witnesses (private inputs and the one-time pad elements).
    2. Encrypted Sumcheck: The prover runs an encrypted sumcheck prover on the witness values to produce an encrypted proof, which is sent to the verifier.
    3. Constraint Generation: Both parties use the public inputs and the encrypted proof to generate a sequence of linear and quadratic constraints.
    4. Proof Generation: The prover generates a proof (using the commitment scheme and witnesses) that the constraints from step 3 are satisfied.
    5. Verification: The verifier uses the proof and the constraints to verify the system.

    Key Terminology

    • Public inputs: Inputs to the circuit known to both parties.
    • Private inputs: Inputs known only to the prover.
    • Inputs: The combined set of public and private inputs (ordered: public first, then private).
    • Witnesses: The combined set of private inputs and the one-time pad elements (ordered: private first, then one-time pad).

    Longfellow Profile

    An 'opinionated' profile is defined by combining:

    • The Longfellow sumcheck protocol.
    • The Ligero commitment protocol.
    • A Fiat-Shamir instantiation using SHA-256 as the hash function H.
  5. Understand the Ligero ZK Commitment procedure

    main

    The Ligero commitment is the first step of the proof procedure. The Prover commits to a witness vector W (padded with zeros to an even multiple of WR) by constructing a Merkle tree. The leaves of this tree are hashes of columns from a tableau matrix T[][] of size NROW x NCOL.

    Tableau Matrix Structure

    The matrix T is constructed row-by-row using an extend procedure:

    • Row ILDT (0): A random row for the low-degree test.
    • Row IDOT (1): A random row for the linear test, where the subarray from NREQ to NREQ + WR - 1 sums to 0.
    • Row IQD (2): A random row for the quadratic test, where the subarray from NREQ to NREQ + WR - 1 is zero.
    • Rows IW (3 to IQ): Padded witness rows. Each row is formed by extend([RANDOM[NREQ], W_segment], BLOCK, NCOL), where W_segment is a portion of the witness vector.
    • Rows IQ to NROW: Padded quadratic rows (Qx, Qy, Qz) containing random elements and the quadratic constraint elements required for verification.

    Optimization: Subfield Witnesses

    If the finite field contains a subfield and all witness elements in a row belong to it, the randomness for that row can also be chosen from the subfield. This allows the extend method to produce polynomial evaluations that require less space when serialized. The prover can convey an index SF to the verifier to indicate how many witnesses belong to the subfield.

    def commit(W[], lqc[]) {
        T[NROW][NCOL] = [0];   // 2d array initialized with 0
    
        layout_zk_rows(T);
        layout_witness_rows(T, W);
        layout_quadratic_rows(T, W, lqc);
    
        MerkleTree M;
        FOR DBLOCK <= j < NCOL DO
          M.set_leaf(j - BLOCK,
              hash( T[0][j] || T[1][j] || .. || T[NROW][j]) );
    
        return M.build_tree();
    }
  6. The EQ[] array and bindeq() function

    main

    The EQ_{n}[i, j] array is a special 2D array used to represent identity constraints: it is 1 if i = j and i < n, and 0 otherwise.

    In sumcheck protocols, bindv(EQ_{n}, X) can be computed efficiently in linear time without storing the full array. For $n = 2^l$ where $l$ is the number of challenges, the bindeq function can be implemented recursively.

    Note on Padding: If $m eq n$, bindv(EQ_{m}, X)[i] and bindv(EQ_{n}, X)[i] agree for $0 ext{ ≤} i < m$, but bindv(EQ_{n}, X)[i] may be non-zero for $i ext{ ≥} m$, whereas bindv(EQ_{m}, X)[i] is zero. This distinction is critical when performing subsequent bindings.

    def bindeq(
            field: FiniteField,
            challenges: list[FiniteRingElement],
            ) -> list[FiniteRingElement]:
        log_n = len(challenges)
        if log_n == 0:
            return [field.one()]
        n = 2 ** log_n
        b = [field.zero() for _ in range(n)]
        a = bindeq(field, challenges[1:])
        for i in range(n // 2):
            b[2 * i] = (field.one() - challenges[0]) * a[i]
            b[2 * i + 1] = challenges[0] * a[i]
        return b
  7. Transform circuit and wires into a padded proof

    main

    The prover uses the sumcheck protocol to certify that wires at each layer of a circuit are correctly calculated from the preceding layer. For a layer $j$, the prover must demonstrate that for every output wire index $g$, the following equations hold:

    1. $V[j][g] = \sum_{l, r} Q[j][g, l, r] V[j + 1][l] V[j + 1][r]$ (Correct calculation)
    2. $0 = \sum_{l, r} Z[j][g, l, r] V[j + 1][l] V[j + 1][r]$ (In-circuit assertions)

    These are combined into a single equation using random verifier challenges: claim = SUM_{l, r} QUAD[j][l, r] V[j + 1][l] V[j + 1][r].

    At each layer, the protocol starts with two claims representing linear combinations of output wire values, bound by verifier challenges $G[0]$ and $G[1]$. Through successive rounds, the function inside the summation is reduced in dimensionality, and the claim values are updated until the function becomes a constant. The final claim values are encrypted with a one-time pad before being sent to the verifier.

    Challenge Generation Note:

    • Before the first round, MAX_BINDINGS = 40 challenges are generated and discarded to reserve space for future protocol extensions.
    • For the initial output wire binding, MAX_BINDINGS = 40 challenges are generated, and the remainder are discarded.
    • For all subsequent layers, challenges for binding output wires are generated one at a time without extra unused challenges.
    def sumcheck_circuit(
            field: FiniteField,
            circuit: Circuit,
            wires: list[list[FiniteRingElement]],
            pad: list[LayerPad[FiniteRingElement]],
            transcript: Transcript) -> list[LayerProof]:
        for _ in range(MAX_BINDINGS):
            # Discard initial challenges. These are reserved for possible
            # future use.
            _ = transcript.generate_field(field)
        challenges = [
            transcript.generate_field(field)
            for _ in range(MAX_BINDINGS)
        ]
        G = (
            challenges[:circuit.log_num_outputs],
            challenges[:circuit.log_num_outputs],
        )
        proof: list[LayerProof] = []
        for j, layer in enumerate(circuit.layers):
            alpha = transcript.generate_field(field)
    
            # Form the combined quad, QZ = Q + beta * Z, to handle
            # in-circuit assertions.
            beta = transcript.generate_field(field)
            QZ = layer.quad + beta * layer.Z
    
            # QZ is three-dimensional, QZ[g, l, r].
            QUAD = QZ.bindv(G[0]) + alpha * QZ.bindv(G[1])
            # Having bound g, QUAD is now effectively two-dimensional,
            # QUAD[l, r].
            QUAD = QUAD.drop_dimension()
    
            layer_proof, G = sumcheck_layer(
                field, 
                QUAD, 
                wires[j + 1], 
                layer.log_num_input_wires, 
                pad[j], 
                transcript
            )
            proof.append(layer_proof)
        return proof
  8. Represent linear and quadratic constraints for Ligero

    main

    To use Ligero, constraints must be formatted as follows:

    Linear Constraints

    Linear constraints are represented as an array of triples (w, c, k).

    • w: The index of the witness.
    • c: The index of the constraint (row of matrix A).
    • k: The constant factor.

    Example: For the constraint W[2] + 2W[3] = 3, the triples are (2, 0, 1) and (3, 0, 2), and the vector b[0] is 3.

    Quadratic Constraints

    Quadratic constraints are represented as an array lqc[] of triples (x, y, z).

    • Each triple represents the constraint W[x] * W[y] = W[z].

    In the tableau, these are handled by adding three extra rows (Qx, Qy, Qz) where Qx[i] = W[x], Qy[i] = W[y], and Qz[i] = W[x] * W[y]. The prover then adds linear constraints to ensure these copied values are consistent with the original witnesses.

  9. Understand the Longfellow ZK system architecture

    main

    The Longfellow ZK scheme is a succinct non-interactive zero-knowledge (ZK) argument system. It allows a Prover to convince a Verifier that for a given input x and an arithmetic circuit C, there exists a private witness w such that C(x,w) = 0 without revealing w.

    The system is composed of two primary components:

    1. Ligero scheme: Provides a cryptographic commitment scheme that supports efficient ZK arguments, enabling the proving of linear and quadratic constraints on the committed witness.
    2. Public-coin interactive protocol (IP): An IP used to produce an argument that C(x,w)=0.

    Workflow:

    • The Prover commits to the witness w and a pad used to commit the IP transcript.
    • The Prover runs the IP with the Verifier to produce a commitment to the IP transcript.
    • The Prover uses the Ligero proof system to prove that the transcript in the commitment induces the IP verifier to accept.
  10. Layered circuit architecture and Quad representation

    main

    Longfellow ZK circuits are structured as a series of layers to verify properties via zero-valued outputs.

    • Layers: A circuit has NL layers. Layer j computes wires V[j] from wires V[j + 1]. V[0] is the output layer, and V[NL] is the input layer.
    • Wires: A wire is an element V[j][w]. A check is successful if V[0][w] = 0 for all w.
    • Quad Representation: The computation for layer j is defined by a 3D array of quads Q[j][g, l, r]. The relationship between layers is: V[j][g] = ∑_{l, r} Q[j][g, l, r] V[j + 1][l] V[j + 1][r]
    • Wire Naming: An auxiliary vector LV[j] specifies that V[j][w] = 0 for all w ≥ 2^{LV[j]}.
  11. Fiat-Shamir transform and Random Oracle best practices

    main

    The Fiat-Shamir transform converts an interactive protocol into a non-interactive (single-message) protocol by generating verifier challenges using a random oracle (typically a hash function) applied to the concatenation of all prover messages.

    Best practices for implementing the Random Oracle in Longfellow ZK:

    1. Complexity: The random oracle should have a higher circuit depth and require more gates to compute than the circuit $C$ being proved.
    2. Input Size: The size of the messages used as input to the oracle to generate challenges should be larger than the size of circuit $C$.
    3. Uniqueness: Each query to the random oracle must be uniquely mappable to a protocol transcript. To achieve this, incorporate the type and length of each message into the query string.