Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions prover-ray/wiop/compilers/global/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,13 +508,11 @@ func (gv *Verifier) Check(rt wiop.Runtime) error {
// --- Recombine quotient shares: Q(r) = Σ_k r^{kn} · Q_k(r) ---
qr := field.ElemZero()
rPowKN := field.ElemOne() // r^{kn}, starts at r^0 = 1
for k, claim := range bkt.quotientClaims {
_ = k
for _, claim := range bkt.quotientClaims {
qk := rt.GetCellValue(claim) // Q_k(r)
qr = qr.Add(rPowKN.Mul(qk))
// Advance: r^{(k+1)n} = r^{kn} · r^n
rPowN := computeAnnihilator(r, n) // r^n − 1 + 1 = r^n ... just compute r^n
rPowN = rPowN.Add(field.ElemOne())
rPowN := expFieldElem(r, n)
rPowKN = rPowKN.Mul(rPowN)
}

Expand Down
73 changes: 73 additions & 0 deletions prover-ray/wiop/compilers/global/global_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package global_test

import (
"os"
"os/exec"
"path/filepath"
goruntime "runtime"
"strings"
"testing"

"github.com/consensys/linea-monorepo/prover-ray/wiop"
Expand Down Expand Up @@ -41,3 +46,71 @@ func TestCompile_Soundness(t *testing.T) {
})
}
}

func Test_zigTest(t *testing.T) {
zigTest(t)
}

func zigTest(t *testing.T) {
t.Helper()
zigPath, zigErr := exec.LookPath("zig")

repoRoot := repoRootFromTest(t)
outputDir := filepath.Join(repoRoot, "wiop", "zigverifiers")
zigStatics := filepath.Join(repoRoot, "wiop", "zigstatics", "koalabear_field.zig")
require.NoError(t, os.MkdirAll(outputDir, 0o755))

for _, build := range wioptest.VanishingScenarios() {
sc := build()
t.Run(sc.Name, func(t *testing.T) {
global.Compile(sc.Sys)

rt := wiop.NewRuntime(sc.Sys)
sc.AssignHonest(&rt)
require.NoError(t, wioptest.RunAndVerify(&rt),
"Go verifier must accept before generating the equivalent Zig verifier")

src, err := findGlobalVerifier(t, sc.Sys).GenerateZig(rt)
require.NoError(t, err)

zigFile := filepath.Join(outputDir, zigVerifierFileName(sc.Name))
require.NoError(t, os.WriteFile(zigFile, src, 0o600))
if zigErr != nil {
return
}

//nolint:gosec // Test executes the local Zig compiler on generated verifier source.
cmd := exec.Command(zigPath, "test", "--dep", "koalabear", "-Mroot="+zigFile, "-Mkoalabear="+zigStatics)
cmd.Dir = repoRoot
out, err := cmd.CombinedOutput()
require.NoError(t, err, "zig test failed:\n%s", out)
})
}
if zigErr != nil {
t.Skip("zig binary is not installed; generated Zig files were written but not compiled")
}
}

func findGlobalVerifier(t *testing.T, sys *wiop.System) *global.Verifier {
t.Helper()
for _, r := range sys.Rounds {
for _, action := range r.VerifierActions {
if verifier, ok := action.(*global.Verifier); ok {
return verifier
}
}
}
require.FailNow(t, "compiled system has no global verifier action")
return nil
}

func repoRootFromTest(t *testing.T) string {
t.Helper()
_, file, _, ok := goruntime.Caller(0)
require.True(t, ok, "runtime.Caller should locate the test file")
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
}

func zigVerifierFileName(scenarioName string) string {
return "global_verifier_" + strings.ToLower(scenarioName) + ".zig"
}
298 changes: 298 additions & 0 deletions prover-ray/wiop/compilers/global/zig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
package global

import (
"fmt"
"strings"

"github.com/consensys/linea-monorepo/prover-ray/maths/koalabear/field"
"github.com/consensys/linea-monorepo/prover-ray/wiop"
)

// GenerateZig returns standalone Zig source that mirrors [Verifier.Check] for
// this verifier and runtime snapshot.
//
// The generated program embeds only verifier-visible scalar data from rt:
// random coins, witness evaluation claim cells, and quotient claim cells. It
// deliberately does not mirror wiop.Runtime in Zig.
func (gv *Verifier) GenerateZig(rt wiop.Runtime) ([]byte, error) {
if gv == nil {
return nil, fmt.Errorf("wiop/compilers: cannot generate Zig for a nil global verifier")
}
gen := newZigVerifierGenerator(gv, rt)
return gen.generate()
}

type zigVerifierGenerator struct {
gv *Verifier
rt wiop.Runtime
witnessNames map[colViewKey]string
out zigWriter
}

func newZigVerifierGenerator(gv *Verifier, rt wiop.Runtime) *zigVerifierGenerator {
witnessNames := make(map[colViewKey]string, len(gv.witnessViews))
for i, cv := range gv.witnessViews {
key := colViewKey{id: cv.Column.Context.ID, shift: cv.ShiftingOffset}
witnessNames[key] = fmt.Sprintf("witness_eval_%d", i)
}
return &zigVerifierGenerator{
gv: gv,
rt: rt,
witnessNames: witnessNames,
}
}

func (g *zigVerifierGenerator) generate() ([]byte, error) {
w := &g.out
w.Line("// Code generated by wiop/compilers/global.Verifier.GenerateZig. DO NOT EDIT.")
w.Line("// Compile with: zig test --dep koalabear -Mroot=<this-file> -Mkoalabear=wiop/zigstatics/koalabear_field.zig")
w.Line("const std = @import(\"std\");")
w.Line("const koalabear = @import(\"koalabear\");")
w.Blank()
w.Line("const ModuleSize: usize = %d;", g.gv.n)
w.Line("const Ext = koalabear.Ext;")
w.Line("const Gen = koalabear.Gen;")
w.Line("const f = koalabear.f;")
w.Blank()

g.writeScalarSnapshot()
if err := g.writeExpressionEvaluators(); err != nil {
return nil, err
}
g.writeCheckFunction()
g.writeVerifyFunction()
g.writeTest()

return []byte(w.String()), nil
}

func (g *zigVerifierGenerator) writeScalarSnapshot() {
w := &g.out
w.Line("const eval_coin = %s;", zigGenLiteral(g.rt.GetCoinValue(g.gv.evalCoin)))
w.Line("const merge_coin = %s;", zigGenLiteral(g.rt.GetCoinValue(g.gv.mergeCoin)))

for i, claim := range g.gv.witnessClaims {
w.Line("const witness_eval_%d = %s;", i, zigGenLiteral(g.rt.GetCellValue(claim)))
}

for i, bkt := range g.gv.buckets {
for k, claim := range bkt.quotientClaims {
w.Line("const quotient_claim_b%d_s%d = %s;", i, k, zigGenLiteral(g.rt.GetCellValue(claim)))
}
for j, v := range bkt.vanishings {
w.Line("const cancelled_b%d_v%d = [_]i64{%s};", i, j, zigIntList(v.CancelledPositions))
}
}
w.Blank()
}

func (g *zigVerifierGenerator) writeExpressionEvaluators() error {
w := &g.out
for i, bkt := range g.gv.buckets {
for j, v := range bkt.vanishings {
expr, err := g.zigExpr(v.Expression)
if err != nil {
return fmt.Errorf("wiop/compilers: generate Zig expression for bucket %d vanishing %d: %w", i, j, err)
}
w.Line("fn evalVanishing_b%d_v%d() Gen {", i, j)
w.In()
w.Line("return %s;", expr)
w.Out()
w.Line("}")
w.Blank()
}
}
return nil
}

func (g *zigVerifierGenerator) writeCheckFunction() {
w := &g.out
w.Line("pub fn check() !void {")
w.In()
w.Line("const r = eval_coin;")
w.Line("const coin_ext = merge_coin.asExt();")
w.Line("const annihilator = koalabear.computeAnnihilator(r, ModuleSize);")
w.Blank()

for i, bkt := range g.gv.buckets {
g.writeBucketCheck(i, bkt)
if i != len(g.gv.buckets)-1 {
w.Blank()
}
}

w.Out()
w.Line("}")
w.Blank()
}

func (g *zigVerifierGenerator) writeBucketCheck(i int, bkt verifierBucket) {
w := &g.out
w.Line("{")
w.In()
w.Line("// Bucket %d, quotient ratio %d.", i, bkt.ratio)
w.Line("var qr = Gen.zero();")
w.Line("var r_pow_kn = Gen.one();")
w.Line("const r_pow_n = koalabear.expFieldElem(r, ModuleSize);")
for k := range bkt.quotientClaims {
w.Line("qr = qr.add(r_pow_kn.mul(quotient_claim_b%d_s%d));", i, k)
w.Line("r_pow_kn = r_pow_kn.mul(r_pow_n);")
}
w.Blank()
w.Line("var pagg = Gen.zero();")
w.Line("var coin_pow = Ext.one();")
for j := range bkt.vanishings {
w.Line("{")
w.In()
w.Line("const pr = evalVanishing_b%d_v%d();", i, j)
w.Line("const cr = koalabear.evalCancellationAtPoint(ModuleSize, r, &cancelled_b%d_v%d);", i, j)
w.Line("const p_times_c = pr.mul(cr);")
w.Line("const term = if (p_times_c.isBase())")
w.In()
w.Line("coin_pow.mulByField(p_times_c.asBase())")
w.Out()
w.Line("else")
w.In()
w.Line("coin_pow.mul(p_times_c.asExt());")
w.Out()
w.Line("pagg = pagg.add(Gen.fromExt(term));")
w.Line("coin_pow = coin_pow.mul(coin_ext);")
w.Out()
w.Line("}")
}
w.Blank()
w.Line("const lhs = pagg;")
w.Line("const rhs = annihilator.mul(qr);")
w.Line("if (!lhs.sub(rhs).isZero()) return error.GlobalQuotientCheckFailed;")
w.Out()
w.Line("}")
}

func (g *zigVerifierGenerator) writeVerifyFunction() {
w := &g.out
w.Line("pub fn verify() bool {")
w.In()
w.Line("check() catch return false;")
w.Line("return true;")
w.Out()
w.Line("}")
w.Blank()
}

func (g *zigVerifierGenerator) writeTest() {
w := &g.out
w.Line("test \"generated global verifier accepts runtime snapshot\" {")
w.In()
w.Line("try check();")
w.Line("try std.testing.expect(verify());")
w.Out()
w.Line("}")
}

func (g *zigVerifierGenerator) zigExpr(expr wiop.Expression) (string, error) {
switch e := expr.(type) {
case *wiop.ColumnView:
key := colViewKey{id: e.Column.Context.ID, shift: e.ShiftingOffset}
name, ok := g.witnessNames[key]
if !ok {
return "", fmt.Errorf("missing witness claim for column %q shift %d", e.Column.Context.Path(), e.ShiftingOffset)
}
return name, nil
case *wiop.ArithmeticOperation:
operands := make([]string, len(e.Operands))
for i, child := range e.Operands {
childExpr, err := g.zigExpr(child)
if err != nil {
return "", err
}
operands[i] = childExpr
}
return zigArithmeticExpr(e.Operator, operands)
case *wiop.Constant:
return fmt.Sprintf("Gen.base(%s)", zigFieldLiteral(e.Value)), nil
case *wiop.CoinField:
return zigGenLiteral(g.rt.GetCoinValue(e)), nil
case *wiop.Cell:
return zigGenLiteral(g.rt.GetCellValue(e)), nil
default:
return "", fmt.Errorf("unsupported expression type %T", expr)
}
}

func zigArithmeticExpr(op wiop.ArithmeticOperator, operands []string) (string, error) {
switch op {
case wiop.ArithmeticOperatorAdd:
return fmt.Sprintf("%s.add(%s)", operands[0], operands[1]), nil
case wiop.ArithmeticOperatorSub:
return fmt.Sprintf("%s.sub(%s)", operands[0], operands[1]), nil
case wiop.ArithmeticOperatorMul:
return fmt.Sprintf("%s.mul(%s)", operands[0], operands[1]), nil
case wiop.ArithmeticOperatorDiv:
return fmt.Sprintf("%s.div(%s)", operands[0], operands[1]), nil
case wiop.ArithmeticOperatorDouble:
return fmt.Sprintf("%s.add(%s)", operands[0], operands[0]), nil
case wiop.ArithmeticOperatorSquare:
return fmt.Sprintf("%s.square()", operands[0]), nil
case wiop.ArithmeticOperatorNegate:
return fmt.Sprintf("%s.neg()", operands[0]), nil
case wiop.ArithmeticOperatorInverse:
return fmt.Sprintf("%s.inverse()", operands[0]), nil
default:
return "", fmt.Errorf("unsupported arithmetic operator %s", op)
}
}

func zigGenLiteral(v field.Gen) string {
if v.IsBase() {
return fmt.Sprintf("Gen.base(%s)", zigFieldLiteral(v.AsBase()))
}
ext := v.AsExt()
c0, c1, c2, c3 := field.ExtToUint64s(&ext)
return fmt.Sprintf(
"Gen.fromExt(Ext.init(f(%d), f(%d), f(%d), f(%d)))",
c0, c1, c2, c3,
)
}

func zigFieldLiteral(v field.Element) string {
return fmt.Sprintf("f(%d)", v.Uint64())
}

func zigIntList(values []int) string {
if len(values) == 0 {
return ""
}
if len(values) == 1 {
return fmt.Sprintf("%d", values[0])
}
parts := make([]string, len(values))
for i, value := range values {
parts[i] = fmt.Sprintf("%d", value)
}
return " " + strings.Join(parts, ", ") + " "
}

type zigWriter struct {
buf strings.Builder
indent int
}

func (w *zigWriter) Line(format string, args ...any) {
fmt.Fprintf(&w.buf, "%s%s\n", strings.Repeat(" ", w.indent), fmt.Sprintf(format, args...))
}

func (w *zigWriter) Blank() {
w.buf.WriteByte('\n')
}

func (w *zigWriter) In() {
w.indent++
}

func (w *zigWriter) Out() {
w.indent--
}

func (w *zigWriter) String() string {
return w.buf.String()
}
Loading
Loading