Source code for constraints.base

"""Base classes for constraint evaluation.

ConstraintContext provides a uniform interface for constraint evaluation that works
for both prover (returns arrays) and verifier (returns scalars). The same constraint
code can be used in both contexts thanks to galois broadcasting.

Example:
    def eval_constraint(ctx: ConstraintContext):
        a = ctx.col('a')
        b = ctx.col('b')
        return a * b - ctx.challenge('alpha')

    # Works for prover (arrays)
    prover_result = eval_constraint(ProverConstraintContext(prover_data))

    # Works for verifier (scalars)
    verifier_result = eval_constraint(VerifierConstraintContext(verifier_data))
"""

from abc import ABC, abstractmethod

import numpy as np

from primitives.field import FF, FF3, GOLDILOCKS_PRIME
from protocol.data import ProverData, VerifierData

# Type aliases for clarity
[docs] FF3Poly = FF3 # Array of extension field elements
[docs] FFPoly = FF # Array of base field elements
[docs] def compress_2col( busid: int, col1: FF3, col2: FF3, alpha: FF3, gamma: FF3, n: int | None = None ) -> FF3: """Compress 2-column expression: ((col2*α + col1)*α + busid) + γ.""" if n is None: busid_val = FF3(busid % GOLDILOCKS_PRIME) else: busid_val = FF3(np.full(n, busid % GOLDILOCKS_PRIME, dtype=np.uint64)) return (col2 * alpha + col1) * alpha + busid_val + gamma
[docs] class ConstraintContext(ABC): """Uniform interface for constraint evaluation - works for prover and verifier.""" @abstractmethod
[docs] def col(self, name: str, index: int = 0) -> FF3Poly | FF3: """Get column at current row. Args: name: Column name index: Column index for multi-column polynomials (default 0) Returns: Prover: array of values at all domain points Verifier: scalar evaluation at xi """ pass
@abstractmethod
[docs] def next_col(self, name: str, index: int = 0) -> FF3Poly | FF3: """Get column at next row (offset +1). Args: name: Column name index: Column index for multi-column polynomials (default 0) Returns: Prover: array shifted by -1 (circular) Verifier: evaluation at xi * omega """ pass
@abstractmethod
[docs] def prev_col(self, name: str, index: int = 0) -> FF3Poly | FF3: """Get column at previous row (offset -1). Args: name: Column name index: Column index for multi-column polynomials (default 0) Returns: Prover: array shifted by +1 (circular) Verifier: evaluation at xi * omega^(-1) """ pass
@abstractmethod
[docs] def const(self, name: str, index: int = 0) -> FF3Poly | FF3: """Get constant polynomial at current row (converted to extension field). Args: name: Constant name (e.g., '__L1__' for Lagrange polynomial) index: Column index for AIRs with multiple constants sharing a name Returns: Prover: array of constant values (as FF3 for arithmetic compatibility) Verifier: scalar evaluation at xi """ pass
@abstractmethod
[docs] def next_const(self, name: str, index: int = 0) -> FF3Poly | FF3: """Get constant polynomial at next row (offset +1). Args: name: Constant name index: Column index for AIRs with multiple constants sharing a name Returns: Prover: array shifted by -1 (circular) Verifier: evaluation at xi * omega """ pass
@abstractmethod
[docs] def prev_const(self, name: str, index: int = 0) -> FF3Poly | FF3: """Get constant polynomial at previous row (offset -1). Args: name: Constant name index: Column index for AIRs with multiple constants sharing a name Returns: Prover: array shifted by +1 (circular) Verifier: evaluation at xi * omega^(-1) """ pass
@abstractmethod
[docs] def challenge(self, name: str) -> FF3: """Get Fiat-Shamir challenge (always scalar). Args: name: Challenge name (e.g., 'std_alpha') Returns: Scalar challenge value """ pass
@abstractmethod
[docs] def airgroup_value(self, index: int) -> FF3: """Get airgroup value (accumulated result across AIR instances). Args: index: Airgroup value index Returns: Scalar airgroup value (FF3) """ pass
[docs] class ProverConstraintContext(ConstraintContext): """Prover implementation - returns polynomial arrays. The prover evaluates constraints at all domain points simultaneously, producing arrays of constraint evaluations. """ def __init__(self, data: ProverData) -> None: self._data = data
[docs] def col(self, name: str, index: int = 0) -> FF3Poly: key = (name, index) return self._data.columns[key]
[docs] def next_col(self, name: str, index: int = 0) -> FF3Poly: # On extended domain, row offset is multiplied by extend factor extend = self._data.extend return np.roll(self.col(name, index), -extend)
[docs] def prev_col(self, name: str, index: int = 0) -> FF3Poly: # On extended domain, row offset is multiplied by extend factor extend = self._data.extend return np.roll(self.col(name, index), extend)
[docs] def const(self, name: str, index: int = 0) -> FF3Poly: # Convert base field constant to extension field for arithmetic compatibility return FF3(np.asarray(self._data.constants[(name, index)], dtype=np.uint64))
[docs] def next_const(self, name: str, index: int = 0) -> FF3Poly: # On extended domain, row offset is multiplied by extend factor extend = self._data.extend return np.roll(self.const(name, index), -extend)
[docs] def prev_const(self, name: str, index: int = 0) -> FF3Poly: # On extended domain, row offset is multiplied by extend factor extend = self._data.extend return np.roll(self.const(name, index), extend)
[docs] def challenge(self, name: str) -> FF3: return self._data.challenges[name]
[docs] def airgroup_value(self, index: int) -> FF3: return self._data.airgroup_values.get(index, FF3(0))
[docs] class VerifierConstraintContext(ConstraintContext): """Verifier implementation - returns scalar evaluations. The verifier evaluates constraints at a single random point xi, checking that the constraint polynomial evaluates to zero. """ def __init__(self, data: VerifierData) -> None: self._data = data
[docs] def col(self, name: str, index: int = 0) -> FF3: # offset=0 means evaluation at xi return self._data.evals[(name, index, 0)]
[docs] def next_col(self, name: str, index: int = 0) -> FF3: # offset=1 means evaluation at xi * omega return self._data.evals[(name, index, 1)]
[docs] def prev_col(self, name: str, index: int = 0) -> FF3: # offset=-1 means evaluation at xi * omega^(-1) return self._data.evals[(name, index, -1)]
[docs] def const(self, name: str, index: int = 0) -> FF3: return self._data.evals[(name, index, 0)]
[docs] def next_const(self, name: str, index: int = 0) -> FF3: return self._data.evals[(name, index, 1)]
[docs] def prev_const(self, name: str, index: int = 0) -> FF3: return self._data.evals[(name, index, -1)]
[docs] def challenge(self, name: str) -> FF3: return self._data.challenges[name]
[docs] def airgroup_value(self, index: int) -> FF3: return self._data.airgroup_values.get(index, FF3(0))
[docs] class ConstraintModule(ABC): """Per-AIR constraint evaluation. Used by both prover and verifier. Each AIR (Algebraic Intermediate Representation) has its own constraint module that defines how constraints are evaluated. The same module works for both prover and verifier contexts. """ @abstractmethod
[docs] def constraint_polynomial(self, ctx: ConstraintContext) -> FF3Poly | FF3: """Evaluate all constraints combined into single polynomial. Args: ctx: ConstraintContext providing access to columns, constants, challenges Returns: Prover: array of constraint evaluations at all domain points Verifier: single constraint evaluation at xi """ pass
def _combine_constraints(self, constraints: list[FF3], vc: FF3) -> FF3: """Combine constraint list using standard accumulation pattern. Computes: ((constraints[0] * vc + constraints[1]) * vc + ...) + constraints[-1] """ acc = constraints[0] * vc for i in range(1, len(constraints) - 1): acc = (acc + constraints[i]) * vc acc = acc + constraints[-1] return acc