From db298a9081e1bd0947531fead45bdf4287fce786 Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Sat, 27 Jun 2026 16:29:40 +0200 Subject: [PATCH 1/8] feat: add webgpu backend --- .gitignore | 7 +- .../webgpu/groth16/bls12-377/prove.go | 207 +++ .../webgpu/groth16/bls12-377/provingkey.go | 76 + .../webgpu/groth16/bls12-377/serialize.go | 211 +++ .../webgpu/groth16/bls12-381/prove.go | 207 +++ .../webgpu/groth16/bls12-381/provingkey.go | 76 + .../webgpu/groth16/bls12-381/serialize.go | 211 +++ .../accelerated/webgpu/groth16/bn254/prove.go | 207 +++ .../webgpu/groth16/bn254/provingkey.go | 76 + .../webgpu/groth16/bn254/serialize.go | 211 +++ backend/accelerated/webgpu/groth16/doc.go | 19 + backend/accelerated/webgpu/groth16/groth16.go | 80 + .../webgpu/groth16/internal/bridge/bridge.go | 82 + .../groth16/internal/common/filter_indices.go | 41 + .../internal/wasmruntime/native/main.go | 46 + .../internal/wasmruntime/webgpu/main.go | 48 + .../webgpu/internal/bridge/bridge_js.go | 128 ++ .../webgpu/internal/bridge/groth16_bridge.js | 271 +++ .../webgpu/internal/wasmruntime/runtime.go | 468 ++++++ .../webgpu/plonk/bls12-377/caching.go | 530 ++++++ .../webgpu/plonk/bls12-377/prove.go | 1304 ++++++++++++++ .../webgpu/plonk/bls12-377/provingkey.go | 72 + .../webgpu/plonk/bls12-377/serialize.go | 264 +++ .../webgpu/plonk/bls12-381/caching.go | 530 ++++++ .../webgpu/plonk/bls12-381/prove.go | 1304 ++++++++++++++ .../webgpu/plonk/bls12-381/provingkey.go | 72 + .../webgpu/plonk/bls12-381/serialize.go | 264 +++ .../accelerated/webgpu/plonk/bn254/caching.go | 530 ++++++ .../accelerated/webgpu/plonk/bn254/prove.go | 1304 ++++++++++++++ .../webgpu/plonk/bn254/provingkey.go | 72 + .../webgpu/plonk/bn254/serialize.go | 265 +++ backend/accelerated/webgpu/plonk/doc.go | 8 + .../webgpu/plonk/internal/bridge/bridge.go | 149 ++ .../plonk/internal/wasmruntime/native/main.go | 52 + .../plonk/internal/wasmruntime/webgpu/main.go | 55 + backend/accelerated/webgpu/plonk/plonk.go | 100 ++ .../webgpu/shaders/common/g1_core.wgsl | 259 +++ .../shaders/common/g1_msm_bindings.wgsl | 40 + .../webgpu/shaders/common/g1_msm_jac.wgsl | 94 ++ .../shaders/common/g1_ops_bindings.wgsl | 11 + .../webgpu/shaders/common/g1_ops_main.wgsl | 10 + .../shaders/common/g2_msm_bindings.wgsl | 40 + .../webgpu/shaders/common/g2_msm_jac.wgsl | 128 ++ .../shaders/common/g2_ops_bindings.wgsl | 19 + .../webgpu/shaders/common/g2_ops_main.wgsl | 10 + .../shaders/curves/bls12_377/fp_arith.wgsl | 444 +++++ .../shaders/curves/bls12_377/fr_arith.wgsl | 492 ++++++ .../shaders/curves/bls12_377/fr_ntt.wgsl | 356 ++++ .../curves/bls12_377/fr_plonk_quotient.wgsl | 228 +++ .../shaders/curves/bls12_377/fr_vector.wgsl | 391 +++++ .../shaders/curves/bls12_377/g1_io.wgsl | 35 + .../shaders/curves/bls12_377/g2_arith.wgsl | 349 ++++ .../shaders/curves/bls12_377/g2_io.wgsl | 47 + .../shaders/curves/bls12_381/fp_arith.wgsl | 444 +++++ .../shaders/curves/bls12_381/fr_arith.wgsl | 492 ++++++ .../shaders/curves/bls12_381/fr_ntt.wgsl | 356 ++++ .../curves/bls12_381/fr_plonk_quotient.wgsl | 228 +++ .../shaders/curves/bls12_381/fr_vector.wgsl | 391 +++++ .../shaders/curves/bls12_381/g1_io.wgsl | 35 + .../shaders/curves/bls12_381/g2_arith.wgsl | 325 ++++ .../shaders/curves/bls12_381/g2_io.wgsl | 47 + .../webgpu/shaders/curves/bn254/fp_arith.wgsl | 525 ++++++ .../webgpu/shaders/curves/bn254/fr_arith.wgsl | 492 ++++++ .../webgpu/shaders/curves/bn254/fr_ntt.wgsl | 356 ++++ .../curves/bn254/fr_plonk_quotient.wgsl | 228 +++ .../shaders/curves/bn254/fr_vector.wgsl | 391 +++++ .../webgpu/shaders/curves/bn254/g1_io.wgsl | 50 + .../webgpu/shaders/curves/bn254/g2_arith.wgsl | 346 ++++ .../webgpu/shaders/curves/bn254/g2_io.wgsl | 47 + backend/accelerated/webgpu/web/.npmrc | 1 + .../accelerated/webgpu/web/eslint.config.js | 53 + backend/accelerated/webgpu/web/index.ts | 1 + .../accelerated/webgpu/web/package-lock.json | 1493 +++++++++++++++++ backend/accelerated/webgpu/web/package.json | 50 + .../webgpu/web/scripts/bundle-shaders.mjs | 78 + .../webgpu/web/src/curvegpu/api.ts | 721 ++++++++ .../webgpu/web/src/curvegpu/browser_utils.ts | 103 ++ .../webgpu/web/src/curvegpu/buffer_pool.ts | 94 ++ .../webgpu/web/src/curvegpu/context.ts | 110 ++ .../webgpu/web/src/curvegpu/convert.ts | 57 + .../webgpu/web/src/curvegpu/curves.ts | 341 ++++ .../webgpu/web/src/curvegpu/encoding.ts | 33 + .../webgpu/web/src/curvegpu/errors.ts | 41 + .../webgpu/web/src/curvegpu/field_module.ts | 213 +++ .../webgpu/web/src/curvegpu/g1_module.ts | 330 ++++ .../webgpu/web/src/curvegpu/g2_module.ts | 400 +++++ .../webgpu/web/src/curvegpu/g2_msm_module.ts | 225 +++ .../webgpu/web/src/curvegpu/groth16_module.ts | 330 ++++ .../web/src/curvegpu/groth16_webgpu_bridge.ts | 252 +++ .../webgpu/web/src/curvegpu/kernels.ts | 21 + .../web/src/curvegpu/msm_bench_sources.ts | 176 ++ .../web/src/curvegpu/msm_gpu_runtime.ts | 219 +++ .../webgpu/web/src/curvegpu/msm_module.ts | 208 +++ .../webgpu/web/src/curvegpu/msm_pippenger.ts | 264 +++ .../webgpu/web/src/curvegpu/msm_shared.ts | 204 +++ .../webgpu/web/src/curvegpu/ntt_module.ts | 633 +++++++ .../web/src/curvegpu/pipeline_registry.ts | 138 ++ .../webgpu/web/src/curvegpu/plonk_module.ts | 327 ++++ .../web/src/curvegpu/plonk_quotient_module.ts | 650 +++++++ .../web/src/curvegpu/plonk_webgpu_bridge.ts | 487 ++++++ .../webgpu/web/src/curvegpu/runtime_common.ts | 233 +++ .../webgpu/web/src/curvegpu/shaders.ts | 67 + .../webgpu/web/src/curvegpu/types.ts | 76 + backend/accelerated/webgpu/web/src/index.ts | 148 ++ backend/accelerated/webgpu/web/tsconfig.json | 18 + 105 files changed, 26047 insertions(+), 1 deletion(-) create mode 100644 backend/accelerated/webgpu/groth16/bls12-377/prove.go create mode 100644 backend/accelerated/webgpu/groth16/bls12-377/provingkey.go create mode 100644 backend/accelerated/webgpu/groth16/bls12-377/serialize.go create mode 100644 backend/accelerated/webgpu/groth16/bls12-381/prove.go create mode 100644 backend/accelerated/webgpu/groth16/bls12-381/provingkey.go create mode 100644 backend/accelerated/webgpu/groth16/bls12-381/serialize.go create mode 100644 backend/accelerated/webgpu/groth16/bn254/prove.go create mode 100644 backend/accelerated/webgpu/groth16/bn254/provingkey.go create mode 100644 backend/accelerated/webgpu/groth16/bn254/serialize.go create mode 100644 backend/accelerated/webgpu/groth16/doc.go create mode 100644 backend/accelerated/webgpu/groth16/groth16.go create mode 100644 backend/accelerated/webgpu/groth16/internal/bridge/bridge.go create mode 100644 backend/accelerated/webgpu/groth16/internal/common/filter_indices.go create mode 100644 backend/accelerated/webgpu/groth16/internal/wasmruntime/native/main.go create mode 100644 backend/accelerated/webgpu/groth16/internal/wasmruntime/webgpu/main.go create mode 100644 backend/accelerated/webgpu/internal/bridge/bridge_js.go create mode 100644 backend/accelerated/webgpu/internal/bridge/groth16_bridge.js create mode 100644 backend/accelerated/webgpu/internal/wasmruntime/runtime.go create mode 100644 backend/accelerated/webgpu/plonk/bls12-377/caching.go create mode 100644 backend/accelerated/webgpu/plonk/bls12-377/prove.go create mode 100644 backend/accelerated/webgpu/plonk/bls12-377/provingkey.go create mode 100644 backend/accelerated/webgpu/plonk/bls12-377/serialize.go create mode 100644 backend/accelerated/webgpu/plonk/bls12-381/caching.go create mode 100644 backend/accelerated/webgpu/plonk/bls12-381/prove.go create mode 100644 backend/accelerated/webgpu/plonk/bls12-381/provingkey.go create mode 100644 backend/accelerated/webgpu/plonk/bls12-381/serialize.go create mode 100644 backend/accelerated/webgpu/plonk/bn254/caching.go create mode 100644 backend/accelerated/webgpu/plonk/bn254/prove.go create mode 100644 backend/accelerated/webgpu/plonk/bn254/provingkey.go create mode 100644 backend/accelerated/webgpu/plonk/bn254/serialize.go create mode 100644 backend/accelerated/webgpu/plonk/doc.go create mode 100644 backend/accelerated/webgpu/plonk/internal/bridge/bridge.go create mode 100644 backend/accelerated/webgpu/plonk/internal/wasmruntime/native/main.go create mode 100644 backend/accelerated/webgpu/plonk/internal/wasmruntime/webgpu/main.go create mode 100644 backend/accelerated/webgpu/plonk/plonk.go create mode 100644 backend/accelerated/webgpu/shaders/common/g1_core.wgsl create mode 100644 backend/accelerated/webgpu/shaders/common/g1_msm_bindings.wgsl create mode 100644 backend/accelerated/webgpu/shaders/common/g1_msm_jac.wgsl create mode 100644 backend/accelerated/webgpu/shaders/common/g1_ops_bindings.wgsl create mode 100644 backend/accelerated/webgpu/shaders/common/g1_ops_main.wgsl create mode 100644 backend/accelerated/webgpu/shaders/common/g2_msm_bindings.wgsl create mode 100644 backend/accelerated/webgpu/shaders/common/g2_msm_jac.wgsl create mode 100644 backend/accelerated/webgpu/shaders/common/g2_ops_bindings.wgsl create mode 100644 backend/accelerated/webgpu/shaders/common/g2_ops_main.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_377/fp_arith.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_377/fr_arith.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_377/fr_ntt.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_377/fr_plonk_quotient.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_377/fr_vector.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_377/g1_io.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_377/g2_arith.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_377/g2_io.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_381/fp_arith.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_381/fr_arith.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_381/fr_ntt.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_381/fr_plonk_quotient.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_381/fr_vector.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_381/g1_io.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_381/g2_arith.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bls12_381/g2_io.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bn254/fp_arith.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bn254/fr_arith.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bn254/fr_ntt.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bn254/fr_plonk_quotient.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bn254/fr_vector.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bn254/g1_io.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bn254/g2_arith.wgsl create mode 100644 backend/accelerated/webgpu/shaders/curves/bn254/g2_io.wgsl create mode 100644 backend/accelerated/webgpu/web/.npmrc create mode 100644 backend/accelerated/webgpu/web/eslint.config.js create mode 100644 backend/accelerated/webgpu/web/index.ts create mode 100644 backend/accelerated/webgpu/web/package-lock.json create mode 100644 backend/accelerated/webgpu/web/package.json create mode 100644 backend/accelerated/webgpu/web/scripts/bundle-shaders.mjs create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/api.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/browser_utils.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/buffer_pool.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/context.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/convert.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/curves.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/encoding.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/errors.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/field_module.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/g1_module.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/g2_module.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/g2_msm_module.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/groth16_module.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/groth16_webgpu_bridge.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/kernels.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/msm_bench_sources.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/msm_gpu_runtime.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/msm_module.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/msm_pippenger.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/msm_shared.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/ntt_module.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/pipeline_registry.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/plonk_module.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/plonk_quotient_module.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/plonk_webgpu_bridge.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/runtime_common.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/shaders.ts create mode 100644 backend/accelerated/webgpu/web/src/curvegpu/types.ts create mode 100644 backend/accelerated/webgpu/web/src/index.ts create mode 100644 backend/accelerated/webgpu/web/tsconfig.json diff --git a/.gitignore b/.gitignore index 90c63b8a62..7ccbfc09f3 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,9 @@ go.work.sum # AI settings .claude/ -examples/gbotrel/** \ No newline at end of file +examples/gbotrel/** + +# WebGPU accelerated backend build outputs. +backend/accelerated/webgpu/web/node_modules/ +backend/accelerated/webgpu/web/dist/ +backend/accelerated/webgpu/web/src/curvegpu/shader_bundle.generated.ts diff --git a/backend/accelerated/webgpu/groth16/bls12-377/prove.go b/backend/accelerated/webgpu/groth16/bls12-377/prove.go new file mode 100644 index 0000000000..07f083ada0 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/bls12-377/prove.go @@ -0,0 +1,207 @@ +//go:build js && wasm + +package bls12377 + +import ( + "fmt" + "math/big" + "strconv" + + "github.com/consensys/gnark-crypto/ecc" + bls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/hash_to_field" + "github.com/consensys/gnark/backend" + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/bridge" + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/common" + native "github.com/consensys/gnark/backend/groth16/bls12-377" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + cs "github.com/consensys/gnark/constraint/bls12-377" + "github.com/consensys/gnark/constraint/solver" + fcs "github.com/consensys/gnark/frontend/cs" +) + +func Prove(r1cs *cs.R1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (*native.Proof, error) { + opt, err := backend.NewProverConfig(opts...) + if err != nil { + return nil, fmt.Errorf("new prover config: %w", err) + } + if opt.HashToFieldFn == nil { + opt.HashToFieldFn = hash_to_field.New([]byte(constraint.CommitmentDst)) + } + + commitmentInfo := r1cs.CommitmentInfo.(constraint.Groth16Commitments) + + if err := pk.Prepare(); err != nil { + return nil, err + } + pk.scratchMu.Lock() + defer pk.scratchMu.Unlock() + + proof := &native.Proof{ + Commitments: make([]bls12377.G1Affine, len(commitmentInfo)), + } + privateCommittedValues := make([][]fr.Element, len(commitmentInfo)) + solverOpts := opt.SolverOpts[:len(opt.SolverOpts):len(opt.SolverOpts)] + bsb22ID := solver.GetHintID(fcs.Bsb22CommitmentComputePlaceholder) + solverOpts = append(solverOpts, solver.OverrideHint(bsb22ID, func(_ *big.Int, in []*big.Int, out []*big.Int) error { + i := int(in[0].Int64()) + if i < 0 || i >= len(commitmentInfo) { + return fmt.Errorf("webgpu groth16 bls12_377: invalid commitment index %d", i) + } + in = in[1:] + hashedCount := len(commitmentInfo[i].PublicAndCommitmentCommitted) + if len(in) < hashedCount { + return fmt.Errorf("webgpu groth16 bls12_377: commitment hint %d has %d inputs, expected at least %d", i, len(in), hashedCount) + } + hashed := in[:hashedCount] + committed := in[hashedCount:] + + privateCommittedValues[i] = make([]fr.Element, len(committed)) + for j, inJ := range committed { + privateCommittedValues[i][j].SetBigInt(inJ) + } + + scalars := packFrVectorRegularLEInto(nil, privateCommittedValues[i]) + commitmentPacked, err := bridge.Bridge.MSMG1(pk.handle, "commitmentBasis"+strconv.Itoa(i), scalars) + if err != nil { + return fmt.Errorf("webgpu groth16 bls12_377: commitment %d MSM: %w", i, err) + } + if proof.Commitments[i], err = decodeG1AffineFromPacked(commitmentPacked, nil); err != nil { + return fmt.Errorf("webgpu groth16 bls12_377: commitment %d decode: %w", i, err) + } + + if _, err := opt.HashToFieldFn.Write(constraint.SerializeCommitment(proof.Commitments[i].Marshal(), hashed, (fr.Bits-1)/8+1)); err != nil { + return err + } + hashBts := opt.HashToFieldFn.Sum(nil) + opt.HashToFieldFn.Reset() + nbBuf := fr.Bytes + if opt.HashToFieldFn.Size() < fr.Bytes { + nbBuf = opt.HashToFieldFn.Size() + } + var res fr.Element + res.SetBytes(hashBts[:nbBuf]) + res.BigInt(out[0]) + return nil + })) + + _solution, err := r1cs.Solve(fullWitness, solverOpts...) + if err != nil { + return nil, err + } + solution := _solution.(*cs.R1CSSolution) + wireValues := []fr.Element(solution.W) + domainSize := int(pk.Domain.Cardinality) + + if len(commitmentInfo) > 0 { + poks := make([]bls12377.G1Affine, len(commitmentInfo)) + for i := range commitmentInfo { + if privateCommittedValues[i] == nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: commitment hint %d was not evaluated", i) + } + scalars := packFrVectorRegularLEInto(nil, privateCommittedValues[i]) + pokPacked, err := bridge.Bridge.MSMG1(pk.handle, "commitmentBasisExpSigma"+strconv.Itoa(i), scalars) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: commitment %d pok MSM: %w", i, err) + } + if poks[i], err = decodeG1AffineFromPacked(pokPacked, nil); err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: commitment %d pok decode: %w", i, err) + } + } + commitmentsSerialized := make([]byte, fr.Bytes*len(commitmentInfo)) + for i := range commitmentInfo { + copy(commitmentsSerialized[fr.Bytes*i:], wireValues[commitmentInfo[i].CommitmentIndex].Marshal()) + } + challenge, err := fr.Hash(commitmentsSerialized, []byte("G16-BSB22"), 1) + if err != nil { + return nil, err + } + if _, err = proof.CommitmentPok.Fold(poks, challenge[0], ecc.MultiExpConfig{NbTasks: 1}); err != nil { + return nil, err + } + } + + pk.scratch0 = packFrVectorMontLEPaddedInto(pk.scratch0, solution.A, domainSize) + pk.scratch1 = packFrVectorMontLEPaddedInto(pk.scratch1, solution.B, domainSize) + pk.scratch2 = packFrVectorMontLEPaddedInto(pk.scratch2, solution.C, domainSize) + zPacked, err := bridge.Bridge.ComputeHZMSMG1(pk.handle, pk.scratch0, pk.scratch1, pk.scratch2) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: quotient H + msm G1.Z: %w", err) + } + publicVariables := r1cs.GetNbPublicVariables() + + pk.scratch0, _ = packFrVectorFilteredInto(pk.scratch0, wireValues, pk.g1AIndices, len(pk.InfinityA)) + pk.scratch1, _ = packFrVectorFilteredInto(pk.scratch1, wireValues, pk.g1BIndices, len(pk.InfinityB)) + pk.scratch2 = packFrVectorRegularLEFilteredOutInto(pk.scratch2, wireValues[publicVariables:], publicVariables, common.CommitmentWireIndexesToRemove(commitmentInfo)) + batchMSM, err := bridge.Bridge.MSMBatch(pk.handle, pk.scratch0, pk.scratch1, pk.scratch2) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: batched MSMs: %w", err) + } + arBaseAff, err := decodeG1AffineFromPacked(batchMSM.G1ABytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: msm G1.A: %w", err) + } + bs1BaseAff, err := decodeG1AffineFromPacked(batchMSM.G1BBytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: msm G1.B: %w", err) + } + kBaseAff, err := decodeG1AffineFromPacked(batchMSM.G1KBytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: msm G1.K: %w", err) + } + zBaseAff, err := decodeG1AffineFromPacked(zPacked, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: msm G1.Z: %w", err) + } + bsBaseAff, err := decodeG2AffineFromPacked(batchMSM.G2BBytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_377: msm G2.B: %w", err) + } + + var r, s big.Int + var _r, _s, _kr fr.Element + if _, err := _r.SetRandom(); err != nil { + return nil, err + } + if _, err := _s.SetRandom(); err != nil { + return nil, err + } + _kr.Mul(&_r, &_s).Neg(&_kr) + _r.BigInt(&r) + _s.BigInt(&s) + + deltas := bls12377.BatchScalarMultiplicationG1(&pk.G1.Delta, []fr.Element{_r, _s, _kr}) + + var ar, bs1, krs, krs2, tmp bls12377.G1Jac + ar.FromAffine(&arBaseAff) + ar.AddMixed(&pk.G1.Alpha) + ar.AddMixed(&deltas[0]) + + bs1.FromAffine(&bs1BaseAff) + bs1.AddMixed(&pk.G1.Beta) + bs1.AddMixed(&deltas[1]) + + krs.FromAffine(&kBaseAff) + krs2.FromAffine(&zBaseAff) + krs.AddAssign(&krs2) + krs.AddMixed(&deltas[2]) + + tmp.ScalarMultiplication(&ar, &s) + krs.AddAssign(&tmp) + tmp.ScalarMultiplication(&bs1, &r) + krs.AddAssign(&tmp) + + var bs, deltaS bls12377.G2Jac + bs.FromAffine(&bsBaseAff) + deltaS.FromAffine(&pk.G2.Delta) + deltaS.ScalarMultiplication(&deltaS, &s) + bs.AddAssign(&deltaS) + bs.AddMixed(&pk.G2.Beta) + + proof.Ar.FromJacobian(&ar) + proof.Krs.FromJacobian(&krs) + proof.Bs.FromJacobian(&bs) + return proof, nil +} diff --git a/backend/accelerated/webgpu/groth16/bls12-377/provingkey.go b/backend/accelerated/webgpu/groth16/bls12-377/provingkey.go new file mode 100644 index 0000000000..e85d70ed66 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/bls12-377/provingkey.go @@ -0,0 +1,76 @@ +//go:build js && wasm + +package bls12377 + +import ( + "strconv" + "sync" + + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/bridge" + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/common" + native "github.com/consensys/gnark/backend/groth16/bls12-377" +) + +// ProvingKey wraps gnark's native BLS12-377 Groth16 proving key with +// browser-side cached MSM bases. +type ProvingKey struct { + native.ProvingKey + prepareMu sync.Mutex + scratchMu sync.Mutex + handle string + quotientWarmed bool + g1AIndices []int + g1BIndices []int + scratch0 []byte + scratch1 []byte + scratch2 []byte +} + +func (pk *ProvingKey) Prepare() error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + if pk.handle != "" && pk.quotientWarmed { + return nil + } + if err := bridge.Bridge.Init("bls12_377"); err != nil { + return err + } + + if pk.handle == "" { + payload := bridge.JSObject() + payload.Set("g1A", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.A))) + payload.Set("g1ACount", len(pk.G1.A)) + payload.Set("g1B", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.B))) + payload.Set("g1BCount", len(pk.G1.B)) + payload.Set("g1K", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.K))) + payload.Set("g1KCount", len(pk.G1.K)) + payload.Set("g1Z", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.Z))) + payload.Set("g1ZCount", len(pk.G1.Z)) + payload.Set("g2B", bridge.JSUint8Array(packG2AffineJacobianBatch(pk.G2.B))) + payload.Set("g2BCount", len(pk.G2.B)) + payload.Set("commitmentCount", len(pk.CommitmentKeys)) + for i := range pk.CommitmentKeys { + suffix := strconv.Itoa(i) + payload.Set("commitmentBasis"+suffix, bridge.JSUint8Array(packG1AffineJacobianBatch(pk.CommitmentKeys[i].Basis))) + payload.Set("commitmentBasis"+suffix+"Count", len(pk.CommitmentKeys[i].Basis)) + payload.Set("commitmentBasisExpSigma"+suffix, bridge.JSUint8Array(packG1AffineJacobianBatch(pk.CommitmentKeys[i].BasisExpSigma))) + payload.Set("commitmentBasisExpSigma"+suffix+"Count", len(pk.CommitmentKeys[i].BasisExpSigma)) + } + + handle, err := bridge.Bridge.PrepareKey("bls12_377", payload) + if err != nil { + return err + } + pk.handle = handle + pk.g1AIndices = common.ComputeKeptIndices(pk.InfinityA) + pk.g1BIndices = common.ComputeKeptIndices(pk.InfinityB) + } + if !pk.quotientWarmed { + if err := bridge.Bridge.PrewarmQuotientDomain("bls12_377", int(pk.Domain.Cardinality)); err != nil { + return err + } + pk.quotientWarmed = true + } + return nil +} diff --git a/backend/accelerated/webgpu/groth16/bls12-377/serialize.go b/backend/accelerated/webgpu/groth16/bls12-377/serialize.go new file mode 100644 index 0000000000..ef28191055 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/bls12-377/serialize.go @@ -0,0 +1,211 @@ +//go:build js && wasm + +package bls12377 + +import ( + "encoding/binary" + "fmt" + + bls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fp" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" +) + +const ( + frBytes = 32 + g1CoordinateBytes = 48 + g1PointBytes = 144 + g2ComponentBytes = 48 + g2PointBytes = 288 +) + +func packFrVectorRegularLEInto(dst []byte, values []fr.Element) []byte { + required := len(values) * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + for i := range values { + base := i * frBytes + writeFrRegularLE(dst[base:base+frBytes], &values[i]) + } + return dst +} + +func packFrVectorRegularLEFilteredOutInto(dst []byte, values []fr.Element, firstIndex int, remove []int) []byte { + if len(remove) == 0 { + return packFrVectorRegularLEInto(dst, values) + } + removeSet := make(map[int]struct{}, len(remove)) + for _, idx := range remove { + removeSet[idx] = struct{}{} + } + count := 0 + for i := range values { + if _, ok := removeSet[firstIndex+i]; !ok { + count++ + } + } + required := count * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + offset := 0 + for i := range values { + if _, ok := removeSet[firstIndex+i]; ok { + continue + } + writeFrRegularLE(dst[offset:offset+frBytes], &values[i]) + offset += frBytes + } + return dst +} + +func packFrVectorMontLEPaddedInto(dst []byte, values []fr.Element, size int) []byte { + required := size * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + clear(dst) + } + for i := range values { + base := i * frBytes + writeFrMontLE(dst[base:base+frBytes], &values[i]) + } + return dst +} + +func packFrVectorFilteredInto(dst []byte, values []fr.Element, keptPrefixIndices []int, prefixLen int) ([]byte, int) { + limit := prefixLen + if limit > len(values) { + limit = len(values) + } + count := len(values) - limit + for _, idx := range keptPrefixIndices { + if idx >= limit { + break + } + count++ + } + required := count * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + offset := 0 + for _, idx := range keptPrefixIndices { + if idx >= limit { + break + } + writeFrRegularLE(dst[offset:offset+frBytes], &values[idx]) + offset += frBytes + } + for i := limit; i < len(values); i++ { + writeFrRegularLE(dst[offset:offset+frBytes], &values[i]) + offset += frBytes + } + return dst, count +} + +func writeFrRegularLE(dst []byte, value *fr.Element) { + be := value.Bytes() + for i := 0; i < frBytes; i++ { + dst[i] = be[frBytes-1-i] + } +} + +func writeFrMontLE(dst []byte, value *fr.Element) { + for i, word := range [4]uint64(*value) { + binary.LittleEndian.PutUint64(dst[i*8:(i+1)*8], word) + } +} + +func packG1AffineJacobianBatch(points []bls12377.G1Affine) []byte { + out := make([]byte, len(points)*g1PointBytes) + one := fpOneMontLE() + for i := range points { + if points[i].IsInfinity() { + continue + } + base := i * g1PointBytes + writeFPMontLE(out[base:base+g1CoordinateBytes], &points[i].X) + writeFPMontLE(out[base+g1CoordinateBytes:base+2*g1CoordinateBytes], &points[i].Y) + copy(out[base+2*g1CoordinateBytes:base+3*g1CoordinateBytes], one) + } + return out +} + +func packG2AffineJacobianBatch(points []bls12377.G2Affine) []byte { + out := make([]byte, len(points)*g2PointBytes) + one := fpOneMontLE() + for i := range points { + if points[i].IsInfinity() { + continue + } + base := i * g2PointBytes + writeFPMontLE(out[base:base+g2ComponentBytes], &points[i].X.A0) + writeFPMontLE(out[base+g2ComponentBytes:base+2*g2ComponentBytes], &points[i].X.A1) + writeFPMontLE(out[base+2*g2ComponentBytes:base+3*g2ComponentBytes], &points[i].Y.A0) + writeFPMontLE(out[base+3*g2ComponentBytes:base+4*g2ComponentBytes], &points[i].Y.A1) + copy(out[base+4*g2ComponentBytes:base+5*g2ComponentBytes], one) + } + return out +} + +func decodeG1AffineFromPacked(packed []byte, err error) (bls12377.G1Affine, error) { + if err != nil { + return bls12377.G1Affine{}, err + } + if len(packed) != 2*g1CoordinateBytes { + return bls12377.G1Affine{}, fmt.Errorf("webgpu groth16 bls12_377: expected %d G1 bytes, got %d", 2*g1CoordinateBytes, len(packed)) + } + return bls12377.G1Affine{ + X: readFPMontLE(packed[:g1CoordinateBytes]), + Y: readFPMontLE(packed[g1CoordinateBytes:]), + }, nil +} + +func decodeG2AffineFromPacked(packed []byte, err error) (bls12377.G2Affine, error) { + if err != nil { + return bls12377.G2Affine{}, err + } + if len(packed) != 4*g2ComponentBytes { + return bls12377.G2Affine{}, fmt.Errorf("webgpu groth16 bls12_377: expected %d G2 bytes, got %d", 4*g2ComponentBytes, len(packed)) + } + var out bls12377.G2Affine + out.X.A0 = readFPMontLE(packed[0*g2ComponentBytes : 1*g2ComponentBytes]) + out.X.A1 = readFPMontLE(packed[1*g2ComponentBytes : 2*g2ComponentBytes]) + out.Y.A0 = readFPMontLE(packed[2*g2ComponentBytes : 3*g2ComponentBytes]) + out.Y.A1 = readFPMontLE(packed[3*g2ComponentBytes : 4*g2ComponentBytes]) + return out, nil +} + +// WebGPU point buffers use raw Montgomery little-endian coordinates. The +// fp.LittleEndian helpers convert to/from regular representation, so they are +// not equivalent here. +func readFPMontLE(src []byte) fp.Element { + var words [6]uint64 + for i := range words { + words[i] = binary.LittleEndian.Uint64(src[i*8 : (i+1)*8]) + } + return fp.Element(words) +} + +func writeFPMontLE(dst []byte, value *fp.Element) { + for i, word := range [6]uint64(*value) { + binary.LittleEndian.PutUint64(dst[i*8:(i+1)*8], word) + } +} + +func fpOneMontLE() []byte { + out := make([]byte, g1CoordinateBytes) + var one fp.Element + one.SetOne() + writeFPMontLE(out, &one) + return out +} diff --git a/backend/accelerated/webgpu/groth16/bls12-381/prove.go b/backend/accelerated/webgpu/groth16/bls12-381/prove.go new file mode 100644 index 0000000000..93bf41b007 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/bls12-381/prove.go @@ -0,0 +1,207 @@ +//go:build js && wasm + +package bls12381 + +import ( + "fmt" + "math/big" + "strconv" + + "github.com/consensys/gnark-crypto/ecc" + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/hash_to_field" + "github.com/consensys/gnark/backend" + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/bridge" + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/common" + native "github.com/consensys/gnark/backend/groth16/bls12-381" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + cs "github.com/consensys/gnark/constraint/bls12-381" + "github.com/consensys/gnark/constraint/solver" + fcs "github.com/consensys/gnark/frontend/cs" +) + +func Prove(r1cs *cs.R1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (*native.Proof, error) { + opt, err := backend.NewProverConfig(opts...) + if err != nil { + return nil, fmt.Errorf("new prover config: %w", err) + } + if opt.HashToFieldFn == nil { + opt.HashToFieldFn = hash_to_field.New([]byte(constraint.CommitmentDst)) + } + + commitmentInfo := r1cs.CommitmentInfo.(constraint.Groth16Commitments) + + if err := pk.Prepare(); err != nil { + return nil, err + } + pk.scratchMu.Lock() + defer pk.scratchMu.Unlock() + + proof := &native.Proof{ + Commitments: make([]bls12381.G1Affine, len(commitmentInfo)), + } + privateCommittedValues := make([][]fr.Element, len(commitmentInfo)) + solverOpts := opt.SolverOpts[:len(opt.SolverOpts):len(opt.SolverOpts)] + bsb22ID := solver.GetHintID(fcs.Bsb22CommitmentComputePlaceholder) + solverOpts = append(solverOpts, solver.OverrideHint(bsb22ID, func(_ *big.Int, in []*big.Int, out []*big.Int) error { + i := int(in[0].Int64()) + if i < 0 || i >= len(commitmentInfo) { + return fmt.Errorf("webgpu groth16 bls12_381: invalid commitment index %d", i) + } + in = in[1:] + hashedCount := len(commitmentInfo[i].PublicAndCommitmentCommitted) + if len(in) < hashedCount { + return fmt.Errorf("webgpu groth16 bls12_381: commitment hint %d has %d inputs, expected at least %d", i, len(in), hashedCount) + } + hashed := in[:hashedCount] + committed := in[hashedCount:] + + privateCommittedValues[i] = make([]fr.Element, len(committed)) + for j, inJ := range committed { + privateCommittedValues[i][j].SetBigInt(inJ) + } + + scalars := packFrVectorRegularLEInto(nil, privateCommittedValues[i]) + commitmentPacked, err := bridge.Bridge.MSMG1(pk.handle, "commitmentBasis"+strconv.Itoa(i), scalars) + if err != nil { + return fmt.Errorf("webgpu groth16 bls12_381: commitment %d MSM: %w", i, err) + } + if proof.Commitments[i], err = decodeG1AffineFromPacked(commitmentPacked, nil); err != nil { + return fmt.Errorf("webgpu groth16 bls12_381: commitment %d decode: %w", i, err) + } + + if _, err := opt.HashToFieldFn.Write(constraint.SerializeCommitment(proof.Commitments[i].Marshal(), hashed, (fr.Bits-1)/8+1)); err != nil { + return err + } + hashBts := opt.HashToFieldFn.Sum(nil) + opt.HashToFieldFn.Reset() + nbBuf := fr.Bytes + if opt.HashToFieldFn.Size() < fr.Bytes { + nbBuf = opt.HashToFieldFn.Size() + } + var res fr.Element + res.SetBytes(hashBts[:nbBuf]) + res.BigInt(out[0]) + return nil + })) + + _solution, err := r1cs.Solve(fullWitness, solverOpts...) + if err != nil { + return nil, err + } + solution := _solution.(*cs.R1CSSolution) + wireValues := []fr.Element(solution.W) + domainSize := int(pk.Domain.Cardinality) + + if len(commitmentInfo) > 0 { + poks := make([]bls12381.G1Affine, len(commitmentInfo)) + for i := range commitmentInfo { + if privateCommittedValues[i] == nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: commitment hint %d was not evaluated", i) + } + scalars := packFrVectorRegularLEInto(nil, privateCommittedValues[i]) + pokPacked, err := bridge.Bridge.MSMG1(pk.handle, "commitmentBasisExpSigma"+strconv.Itoa(i), scalars) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: commitment %d pok MSM: %w", i, err) + } + if poks[i], err = decodeG1AffineFromPacked(pokPacked, nil); err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: commitment %d pok decode: %w", i, err) + } + } + commitmentsSerialized := make([]byte, fr.Bytes*len(commitmentInfo)) + for i := range commitmentInfo { + copy(commitmentsSerialized[fr.Bytes*i:], wireValues[commitmentInfo[i].CommitmentIndex].Marshal()) + } + challenge, err := fr.Hash(commitmentsSerialized, []byte("G16-BSB22"), 1) + if err != nil { + return nil, err + } + if _, err = proof.CommitmentPok.Fold(poks, challenge[0], ecc.MultiExpConfig{NbTasks: 1}); err != nil { + return nil, err + } + } + + pk.scratch0 = packFrVectorMontLEPaddedInto(pk.scratch0, solution.A, domainSize) + pk.scratch1 = packFrVectorMontLEPaddedInto(pk.scratch1, solution.B, domainSize) + pk.scratch2 = packFrVectorMontLEPaddedInto(pk.scratch2, solution.C, domainSize) + zPacked, err := bridge.Bridge.ComputeHZMSMG1(pk.handle, pk.scratch0, pk.scratch1, pk.scratch2) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: quotient H + msm G1.Z: %w", err) + } + publicVariables := r1cs.GetNbPublicVariables() + + pk.scratch0, _ = packFrVectorFilteredInto(pk.scratch0, wireValues, pk.g1AIndices, len(pk.InfinityA)) + pk.scratch1, _ = packFrVectorFilteredInto(pk.scratch1, wireValues, pk.g1BIndices, len(pk.InfinityB)) + pk.scratch2 = packFrVectorRegularLEFilteredOutInto(pk.scratch2, wireValues[publicVariables:], publicVariables, common.CommitmentWireIndexesToRemove(commitmentInfo)) + batchMSM, err := bridge.Bridge.MSMBatch(pk.handle, pk.scratch0, pk.scratch1, pk.scratch2) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: batched MSMs: %w", err) + } + arBaseAff, err := decodeG1AffineFromPacked(batchMSM.G1ABytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: msm G1.A: %w", err) + } + bs1BaseAff, err := decodeG1AffineFromPacked(batchMSM.G1BBytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: msm G1.B: %w", err) + } + kBaseAff, err := decodeG1AffineFromPacked(batchMSM.G1KBytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: msm G1.K: %w", err) + } + zBaseAff, err := decodeG1AffineFromPacked(zPacked, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: msm G1.Z: %w", err) + } + bsBaseAff, err := decodeG2AffineFromPacked(batchMSM.G2BBytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bls12_381: msm G2.B: %w", err) + } + + var r, s big.Int + var _r, _s, _kr fr.Element + if _, err := _r.SetRandom(); err != nil { + return nil, err + } + if _, err := _s.SetRandom(); err != nil { + return nil, err + } + _kr.Mul(&_r, &_s).Neg(&_kr) + _r.BigInt(&r) + _s.BigInt(&s) + + deltas := bls12381.BatchScalarMultiplicationG1(&pk.G1.Delta, []fr.Element{_r, _s, _kr}) + + var ar, bs1, krs, krs2, tmp bls12381.G1Jac + ar.FromAffine(&arBaseAff) + ar.AddMixed(&pk.G1.Alpha) + ar.AddMixed(&deltas[0]) + + bs1.FromAffine(&bs1BaseAff) + bs1.AddMixed(&pk.G1.Beta) + bs1.AddMixed(&deltas[1]) + + krs.FromAffine(&kBaseAff) + krs2.FromAffine(&zBaseAff) + krs.AddAssign(&krs2) + krs.AddMixed(&deltas[2]) + + tmp.ScalarMultiplication(&ar, &s) + krs.AddAssign(&tmp) + tmp.ScalarMultiplication(&bs1, &r) + krs.AddAssign(&tmp) + + var bs, deltaS bls12381.G2Jac + bs.FromAffine(&bsBaseAff) + deltaS.FromAffine(&pk.G2.Delta) + deltaS.ScalarMultiplication(&deltaS, &s) + bs.AddAssign(&deltaS) + bs.AddMixed(&pk.G2.Beta) + + proof.Ar.FromJacobian(&ar) + proof.Krs.FromJacobian(&krs) + proof.Bs.FromJacobian(&bs) + return proof, nil +} diff --git a/backend/accelerated/webgpu/groth16/bls12-381/provingkey.go b/backend/accelerated/webgpu/groth16/bls12-381/provingkey.go new file mode 100644 index 0000000000..4faa0155d7 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/bls12-381/provingkey.go @@ -0,0 +1,76 @@ +//go:build js && wasm + +package bls12381 + +import ( + "strconv" + "sync" + + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/bridge" + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/common" + native "github.com/consensys/gnark/backend/groth16/bls12-381" +) + +// ProvingKey wraps gnark's native BLS12-381 Groth16 proving key with +// browser-side cached MSM bases. +type ProvingKey struct { + native.ProvingKey + prepareMu sync.Mutex + scratchMu sync.Mutex + handle string + quotientWarmed bool + g1AIndices []int + g1BIndices []int + scratch0 []byte + scratch1 []byte + scratch2 []byte +} + +func (pk *ProvingKey) Prepare() error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + if pk.handle != "" && pk.quotientWarmed { + return nil + } + if err := bridge.Bridge.Init("bls12_381"); err != nil { + return err + } + + if pk.handle == "" { + payload := bridge.JSObject() + payload.Set("g1A", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.A))) + payload.Set("g1ACount", len(pk.G1.A)) + payload.Set("g1B", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.B))) + payload.Set("g1BCount", len(pk.G1.B)) + payload.Set("g1K", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.K))) + payload.Set("g1KCount", len(pk.G1.K)) + payload.Set("g1Z", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.Z))) + payload.Set("g1ZCount", len(pk.G1.Z)) + payload.Set("g2B", bridge.JSUint8Array(packG2AffineJacobianBatch(pk.G2.B))) + payload.Set("g2BCount", len(pk.G2.B)) + payload.Set("commitmentCount", len(pk.CommitmentKeys)) + for i := range pk.CommitmentKeys { + suffix := strconv.Itoa(i) + payload.Set("commitmentBasis"+suffix, bridge.JSUint8Array(packG1AffineJacobianBatch(pk.CommitmentKeys[i].Basis))) + payload.Set("commitmentBasis"+suffix+"Count", len(pk.CommitmentKeys[i].Basis)) + payload.Set("commitmentBasisExpSigma"+suffix, bridge.JSUint8Array(packG1AffineJacobianBatch(pk.CommitmentKeys[i].BasisExpSigma))) + payload.Set("commitmentBasisExpSigma"+suffix+"Count", len(pk.CommitmentKeys[i].BasisExpSigma)) + } + + handle, err := bridge.Bridge.PrepareKey("bls12_381", payload) + if err != nil { + return err + } + pk.handle = handle + pk.g1AIndices = common.ComputeKeptIndices(pk.InfinityA) + pk.g1BIndices = common.ComputeKeptIndices(pk.InfinityB) + } + if !pk.quotientWarmed { + if err := bridge.Bridge.PrewarmQuotientDomain("bls12_381", int(pk.Domain.Cardinality)); err != nil { + return err + } + pk.quotientWarmed = true + } + return nil +} diff --git a/backend/accelerated/webgpu/groth16/bls12-381/serialize.go b/backend/accelerated/webgpu/groth16/bls12-381/serialize.go new file mode 100644 index 0000000000..af37fa22af --- /dev/null +++ b/backend/accelerated/webgpu/groth16/bls12-381/serialize.go @@ -0,0 +1,211 @@ +//go:build js && wasm + +package bls12381 + +import ( + "encoding/binary" + "fmt" + + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" +) + +const ( + frBytes = 32 + g1CoordinateBytes = 48 + g1PointBytes = 144 + g2ComponentBytes = 48 + g2PointBytes = 288 +) + +func packFrVectorRegularLEInto(dst []byte, values []fr.Element) []byte { + required := len(values) * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + for i := range values { + base := i * frBytes + writeFrRegularLE(dst[base:base+frBytes], &values[i]) + } + return dst +} + +func packFrVectorRegularLEFilteredOutInto(dst []byte, values []fr.Element, firstIndex int, remove []int) []byte { + if len(remove) == 0 { + return packFrVectorRegularLEInto(dst, values) + } + removeSet := make(map[int]struct{}, len(remove)) + for _, idx := range remove { + removeSet[idx] = struct{}{} + } + count := 0 + for i := range values { + if _, ok := removeSet[firstIndex+i]; !ok { + count++ + } + } + required := count * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + offset := 0 + for i := range values { + if _, ok := removeSet[firstIndex+i]; ok { + continue + } + writeFrRegularLE(dst[offset:offset+frBytes], &values[i]) + offset += frBytes + } + return dst +} + +func packFrVectorMontLEPaddedInto(dst []byte, values []fr.Element, size int) []byte { + required := size * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + clear(dst) + } + for i := range values { + base := i * frBytes + writeFrMontLE(dst[base:base+frBytes], &values[i]) + } + return dst +} + +func packFrVectorFilteredInto(dst []byte, values []fr.Element, keptPrefixIndices []int, prefixLen int) ([]byte, int) { + limit := prefixLen + if limit > len(values) { + limit = len(values) + } + count := len(values) - limit + for _, idx := range keptPrefixIndices { + if idx >= limit { + break + } + count++ + } + required := count * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + offset := 0 + for _, idx := range keptPrefixIndices { + if idx >= limit { + break + } + writeFrRegularLE(dst[offset:offset+frBytes], &values[idx]) + offset += frBytes + } + for i := limit; i < len(values); i++ { + writeFrRegularLE(dst[offset:offset+frBytes], &values[i]) + offset += frBytes + } + return dst, count +} + +func writeFrRegularLE(dst []byte, value *fr.Element) { + be := value.Bytes() + for i := 0; i < frBytes; i++ { + dst[i] = be[frBytes-1-i] + } +} + +func writeFrMontLE(dst []byte, value *fr.Element) { + for i, word := range [4]uint64(*value) { + binary.LittleEndian.PutUint64(dst[i*8:(i+1)*8], word) + } +} + +func packG1AffineJacobianBatch(points []bls12381.G1Affine) []byte { + out := make([]byte, len(points)*g1PointBytes) + one := fpOneMontLE() + for i := range points { + if points[i].IsInfinity() { + continue + } + base := i * g1PointBytes + writeFPMontLE(out[base:base+g1CoordinateBytes], &points[i].X) + writeFPMontLE(out[base+g1CoordinateBytes:base+2*g1CoordinateBytes], &points[i].Y) + copy(out[base+2*g1CoordinateBytes:base+3*g1CoordinateBytes], one) + } + return out +} + +func packG2AffineJacobianBatch(points []bls12381.G2Affine) []byte { + out := make([]byte, len(points)*g2PointBytes) + one := fpOneMontLE() + for i := range points { + if points[i].IsInfinity() { + continue + } + base := i * g2PointBytes + writeFPMontLE(out[base:base+g2ComponentBytes], &points[i].X.A0) + writeFPMontLE(out[base+g2ComponentBytes:base+2*g2ComponentBytes], &points[i].X.A1) + writeFPMontLE(out[base+2*g2ComponentBytes:base+3*g2ComponentBytes], &points[i].Y.A0) + writeFPMontLE(out[base+3*g2ComponentBytes:base+4*g2ComponentBytes], &points[i].Y.A1) + copy(out[base+4*g2ComponentBytes:base+5*g2ComponentBytes], one) + } + return out +} + +func decodeG1AffineFromPacked(packed []byte, err error) (bls12381.G1Affine, error) { + if err != nil { + return bls12381.G1Affine{}, err + } + if len(packed) != 2*g1CoordinateBytes { + return bls12381.G1Affine{}, fmt.Errorf("webgpu groth16 bls12_381: expected %d G1 bytes, got %d", 2*g1CoordinateBytes, len(packed)) + } + return bls12381.G1Affine{ + X: readFPMontLE(packed[:g1CoordinateBytes]), + Y: readFPMontLE(packed[g1CoordinateBytes:]), + }, nil +} + +func decodeG2AffineFromPacked(packed []byte, err error) (bls12381.G2Affine, error) { + if err != nil { + return bls12381.G2Affine{}, err + } + if len(packed) != 4*g2ComponentBytes { + return bls12381.G2Affine{}, fmt.Errorf("webgpu groth16 bls12_381: expected %d G2 bytes, got %d", 4*g2ComponentBytes, len(packed)) + } + var out bls12381.G2Affine + out.X.A0 = readFPMontLE(packed[0*g2ComponentBytes : 1*g2ComponentBytes]) + out.X.A1 = readFPMontLE(packed[1*g2ComponentBytes : 2*g2ComponentBytes]) + out.Y.A0 = readFPMontLE(packed[2*g2ComponentBytes : 3*g2ComponentBytes]) + out.Y.A1 = readFPMontLE(packed[3*g2ComponentBytes : 4*g2ComponentBytes]) + return out, nil +} + +// WebGPU point buffers use raw Montgomery little-endian coordinates. The +// fp.LittleEndian helpers convert to/from regular representation, so they are +// not equivalent here. +func readFPMontLE(src []byte) fp.Element { + var words [6]uint64 + for i := range words { + words[i] = binary.LittleEndian.Uint64(src[i*8 : (i+1)*8]) + } + return fp.Element(words) +} + +func writeFPMontLE(dst []byte, value *fp.Element) { + for i, word := range [6]uint64(*value) { + binary.LittleEndian.PutUint64(dst[i*8:(i+1)*8], word) + } +} + +func fpOneMontLE() []byte { + out := make([]byte, g1CoordinateBytes) + var one fp.Element + one.SetOne() + writeFPMontLE(out, &one) + return out +} diff --git a/backend/accelerated/webgpu/groth16/bn254/prove.go b/backend/accelerated/webgpu/groth16/bn254/prove.go new file mode 100644 index 0000000000..3a86445464 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/bn254/prove.go @@ -0,0 +1,207 @@ +//go:build js && wasm + +package bn254 + +import ( + "fmt" + "math/big" + "strconv" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/hash_to_field" + "github.com/consensys/gnark/backend" + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/bridge" + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/common" + native "github.com/consensys/gnark/backend/groth16/bn254" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + cs "github.com/consensys/gnark/constraint/bn254" + "github.com/consensys/gnark/constraint/solver" + fcs "github.com/consensys/gnark/frontend/cs" +) + +func Prove(r1cs *cs.R1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (*native.Proof, error) { + opt, err := backend.NewProverConfig(opts...) + if err != nil { + return nil, fmt.Errorf("new prover config: %w", err) + } + if opt.HashToFieldFn == nil { + opt.HashToFieldFn = hash_to_field.New([]byte(constraint.CommitmentDst)) + } + + commitmentInfo := r1cs.CommitmentInfo.(constraint.Groth16Commitments) + + if err := pk.Prepare(); err != nil { + return nil, err + } + pk.scratchMu.Lock() + defer pk.scratchMu.Unlock() + + proof := &native.Proof{ + Commitments: make([]bn254.G1Affine, len(commitmentInfo)), + } + privateCommittedValues := make([][]fr.Element, len(commitmentInfo)) + solverOpts := opt.SolverOpts[:len(opt.SolverOpts):len(opt.SolverOpts)] + bsb22ID := solver.GetHintID(fcs.Bsb22CommitmentComputePlaceholder) + solverOpts = append(solverOpts, solver.OverrideHint(bsb22ID, func(_ *big.Int, in []*big.Int, out []*big.Int) error { + i := int(in[0].Int64()) + if i < 0 || i >= len(commitmentInfo) { + return fmt.Errorf("webgpu groth16 bn254: invalid commitment index %d", i) + } + in = in[1:] + hashedCount := len(commitmentInfo[i].PublicAndCommitmentCommitted) + if len(in) < hashedCount { + return fmt.Errorf("webgpu groth16 bn254: commitment hint %d has %d inputs, expected at least %d", i, len(in), hashedCount) + } + hashed := in[:hashedCount] + committed := in[hashedCount:] + + privateCommittedValues[i] = make([]fr.Element, len(committed)) + for j, inJ := range committed { + privateCommittedValues[i][j].SetBigInt(inJ) + } + + scalars := packFrVectorRegularLEInto(nil, privateCommittedValues[i]) + commitmentPacked, err := bridge.Bridge.MSMG1(pk.handle, "commitmentBasis"+strconv.Itoa(i), scalars) + if err != nil { + return fmt.Errorf("webgpu groth16 bn254: commitment %d MSM: %w", i, err) + } + if proof.Commitments[i], err = decodeG1AffineFromPacked(commitmentPacked, nil); err != nil { + return fmt.Errorf("webgpu groth16 bn254: commitment %d decode: %w", i, err) + } + + if _, err := opt.HashToFieldFn.Write(constraint.SerializeCommitment(proof.Commitments[i].Marshal(), hashed, (fr.Bits-1)/8+1)); err != nil { + return err + } + hashBts := opt.HashToFieldFn.Sum(nil) + opt.HashToFieldFn.Reset() + nbBuf := fr.Bytes + if opt.HashToFieldFn.Size() < fr.Bytes { + nbBuf = opt.HashToFieldFn.Size() + } + var res fr.Element + res.SetBytes(hashBts[:nbBuf]) + res.BigInt(out[0]) + return nil + })) + + _solution, err := r1cs.Solve(fullWitness, solverOpts...) + if err != nil { + return nil, err + } + solution := _solution.(*cs.R1CSSolution) + wireValues := []fr.Element(solution.W) + domainSize := int(pk.Domain.Cardinality) + + if len(commitmentInfo) > 0 { + poks := make([]bn254.G1Affine, len(commitmentInfo)) + for i := range commitmentInfo { + if privateCommittedValues[i] == nil { + return nil, fmt.Errorf("webgpu groth16 bn254: commitment hint %d was not evaluated", i) + } + scalars := packFrVectorRegularLEInto(nil, privateCommittedValues[i]) + pokPacked, err := bridge.Bridge.MSMG1(pk.handle, "commitmentBasisExpSigma"+strconv.Itoa(i), scalars) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bn254: commitment %d pok MSM: %w", i, err) + } + if poks[i], err = decodeG1AffineFromPacked(pokPacked, nil); err != nil { + return nil, fmt.Errorf("webgpu groth16 bn254: commitment %d pok decode: %w", i, err) + } + } + commitmentsSerialized := make([]byte, fr.Bytes*len(commitmentInfo)) + for i := range commitmentInfo { + copy(commitmentsSerialized[fr.Bytes*i:], wireValues[commitmentInfo[i].CommitmentIndex].Marshal()) + } + challenge, err := fr.Hash(commitmentsSerialized, []byte("G16-BSB22"), 1) + if err != nil { + return nil, err + } + if _, err = proof.CommitmentPok.Fold(poks, challenge[0], ecc.MultiExpConfig{NbTasks: 1}); err != nil { + return nil, err + } + } + + pk.scratch0 = packFrVectorMontLEPaddedInto(pk.scratch0, solution.A, domainSize) + pk.scratch1 = packFrVectorMontLEPaddedInto(pk.scratch1, solution.B, domainSize) + pk.scratch2 = packFrVectorMontLEPaddedInto(pk.scratch2, solution.C, domainSize) + zPacked, err := bridge.Bridge.ComputeHZMSMG1(pk.handle, pk.scratch0, pk.scratch1, pk.scratch2) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bn254: quotient H + msm G1.Z: %w", err) + } + publicVariables := r1cs.GetNbPublicVariables() + + pk.scratch0, _ = packFrVectorFilteredInto(pk.scratch0, wireValues, pk.g1AIndices, len(pk.InfinityA)) + pk.scratch1, _ = packFrVectorFilteredInto(pk.scratch1, wireValues, pk.g1BIndices, len(pk.InfinityB)) + pk.scratch2 = packFrVectorRegularLEFilteredOutInto(pk.scratch2, wireValues[publicVariables:], publicVariables, common.CommitmentWireIndexesToRemove(commitmentInfo)) + batchMSM, err := bridge.Bridge.MSMBatch(pk.handle, pk.scratch0, pk.scratch1, pk.scratch2) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bn254: batched MSMs: %w", err) + } + arBaseAff, err := decodeG1AffineFromPacked(batchMSM.G1ABytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bn254: msm G1.A: %w", err) + } + bs1BaseAff, err := decodeG1AffineFromPacked(batchMSM.G1BBytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bn254: msm G1.B: %w", err) + } + kBaseAff, err := decodeG1AffineFromPacked(batchMSM.G1KBytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bn254: msm G1.K: %w", err) + } + zBaseAff, err := decodeG1AffineFromPacked(zPacked, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bn254: msm G1.Z: %w", err) + } + bsBaseAff, err := decodeG2AffineFromPacked(batchMSM.G2BBytes, nil) + if err != nil { + return nil, fmt.Errorf("webgpu groth16 bn254: msm G2.B: %w", err) + } + + var r, s big.Int + var _r, _s, _kr fr.Element + if _, err := _r.SetRandom(); err != nil { + return nil, err + } + if _, err := _s.SetRandom(); err != nil { + return nil, err + } + _kr.Mul(&_r, &_s).Neg(&_kr) + _r.BigInt(&r) + _s.BigInt(&s) + + deltas := bn254.BatchScalarMultiplicationG1(&pk.G1.Delta, []fr.Element{_r, _s, _kr}) + + var ar, bs1, krs, krs2, tmp bn254.G1Jac + ar.FromAffine(&arBaseAff) + ar.AddMixed(&pk.G1.Alpha) + ar.AddMixed(&deltas[0]) + + bs1.FromAffine(&bs1BaseAff) + bs1.AddMixed(&pk.G1.Beta) + bs1.AddMixed(&deltas[1]) + + krs.FromAffine(&kBaseAff) + krs2.FromAffine(&zBaseAff) + krs.AddAssign(&krs2) + krs.AddMixed(&deltas[2]) + + tmp.ScalarMultiplication(&ar, &s) + krs.AddAssign(&tmp) + tmp.ScalarMultiplication(&bs1, &r) + krs.AddAssign(&tmp) + + var bs, deltaS bn254.G2Jac + bs.FromAffine(&bsBaseAff) + deltaS.FromAffine(&pk.G2.Delta) + deltaS.ScalarMultiplication(&deltaS, &s) + bs.AddAssign(&deltaS) + bs.AddMixed(&pk.G2.Beta) + + proof.Ar.FromJacobian(&ar) + proof.Krs.FromJacobian(&krs) + proof.Bs.FromJacobian(&bs) + return proof, nil +} diff --git a/backend/accelerated/webgpu/groth16/bn254/provingkey.go b/backend/accelerated/webgpu/groth16/bn254/provingkey.go new file mode 100644 index 0000000000..01928f01f8 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/bn254/provingkey.go @@ -0,0 +1,76 @@ +//go:build js && wasm + +package bn254 + +import ( + "strconv" + "sync" + + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/bridge" + "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/internal/common" + native "github.com/consensys/gnark/backend/groth16/bn254" +) + +// ProvingKey wraps gnark's native BN254 Groth16 proving key with +// browser-side cached MSM bases. +type ProvingKey struct { + native.ProvingKey + prepareMu sync.Mutex + scratchMu sync.Mutex + handle string + quotientWarmed bool + g1AIndices []int + g1BIndices []int + scratch0 []byte + scratch1 []byte + scratch2 []byte +} + +func (pk *ProvingKey) Prepare() error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + if pk.handle != "" && pk.quotientWarmed { + return nil + } + if err := bridge.Bridge.Init("bn254"); err != nil { + return err + } + + if pk.handle == "" { + payload := bridge.JSObject() + payload.Set("g1A", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.A))) + payload.Set("g1ACount", len(pk.G1.A)) + payload.Set("g1B", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.B))) + payload.Set("g1BCount", len(pk.G1.B)) + payload.Set("g1K", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.K))) + payload.Set("g1KCount", len(pk.G1.K)) + payload.Set("g1Z", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.G1.Z))) + payload.Set("g1ZCount", len(pk.G1.Z)) + payload.Set("g2B", bridge.JSUint8Array(packG2AffineJacobianBatch(pk.G2.B))) + payload.Set("g2BCount", len(pk.G2.B)) + payload.Set("commitmentCount", len(pk.CommitmentKeys)) + for i := range pk.CommitmentKeys { + suffix := strconv.Itoa(i) + payload.Set("commitmentBasis"+suffix, bridge.JSUint8Array(packG1AffineJacobianBatch(pk.CommitmentKeys[i].Basis))) + payload.Set("commitmentBasis"+suffix+"Count", len(pk.CommitmentKeys[i].Basis)) + payload.Set("commitmentBasisExpSigma"+suffix, bridge.JSUint8Array(packG1AffineJacobianBatch(pk.CommitmentKeys[i].BasisExpSigma))) + payload.Set("commitmentBasisExpSigma"+suffix+"Count", len(pk.CommitmentKeys[i].BasisExpSigma)) + } + + handle, err := bridge.Bridge.PrepareKey("bn254", payload) + if err != nil { + return err + } + pk.handle = handle + pk.g1AIndices = common.ComputeKeptIndices(pk.InfinityA) + pk.g1BIndices = common.ComputeKeptIndices(pk.InfinityB) + } + if !pk.quotientWarmed { + if err := bridge.Bridge.PrewarmQuotientDomain("bn254", int(pk.Domain.Cardinality)); err != nil { + return err + } + pk.quotientWarmed = true + } + return nil +} diff --git a/backend/accelerated/webgpu/groth16/bn254/serialize.go b/backend/accelerated/webgpu/groth16/bn254/serialize.go new file mode 100644 index 0000000000..679c0406a3 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/bn254/serialize.go @@ -0,0 +1,211 @@ +//go:build js && wasm + +package bn254 + +import ( + "encoding/binary" + "fmt" + + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark-crypto/ecc/bn254/fp" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +const ( + frBytes = 32 + g1CoordinateBytes = 32 + g1PointBytes = 96 + g2ComponentBytes = 32 + g2PointBytes = 192 +) + +func packFrVectorRegularLEInto(dst []byte, values []fr.Element) []byte { + required := len(values) * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + for i := range values { + base := i * frBytes + writeFrRegularLE(dst[base:base+frBytes], &values[i]) + } + return dst +} + +func packFrVectorRegularLEFilteredOutInto(dst []byte, values []fr.Element, firstIndex int, remove []int) []byte { + if len(remove) == 0 { + return packFrVectorRegularLEInto(dst, values) + } + removeSet := make(map[int]struct{}, len(remove)) + for _, idx := range remove { + removeSet[idx] = struct{}{} + } + count := 0 + for i := range values { + if _, ok := removeSet[firstIndex+i]; !ok { + count++ + } + } + required := count * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + offset := 0 + for i := range values { + if _, ok := removeSet[firstIndex+i]; ok { + continue + } + writeFrRegularLE(dst[offset:offset+frBytes], &values[i]) + offset += frBytes + } + return dst +} + +func packFrVectorMontLEPaddedInto(dst []byte, values []fr.Element, size int) []byte { + required := size * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + clear(dst) + } + for i := range values { + base := i * frBytes + writeFrMontLE(dst[base:base+frBytes], &values[i]) + } + return dst +} + +func packFrVectorFilteredInto(dst []byte, values []fr.Element, keptPrefixIndices []int, prefixLen int) ([]byte, int) { + limit := prefixLen + if limit > len(values) { + limit = len(values) + } + count := len(values) - limit + for _, idx := range keptPrefixIndices { + if idx >= limit { + break + } + count++ + } + required := count * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + offset := 0 + for _, idx := range keptPrefixIndices { + if idx >= limit { + break + } + writeFrRegularLE(dst[offset:offset+frBytes], &values[idx]) + offset += frBytes + } + for i := limit; i < len(values); i++ { + writeFrRegularLE(dst[offset:offset+frBytes], &values[i]) + offset += frBytes + } + return dst, count +} + +func writeFrRegularLE(dst []byte, value *fr.Element) { + be := value.Bytes() + for i := 0; i < frBytes; i++ { + dst[i] = be[frBytes-1-i] + } +} + +func writeFrMontLE(dst []byte, value *fr.Element) { + for i, word := range [4]uint64(*value) { + binary.LittleEndian.PutUint64(dst[i*8:(i+1)*8], word) + } +} + +func packG1AffineJacobianBatch(points []bn254.G1Affine) []byte { + out := make([]byte, len(points)*g1PointBytes) + one := fpOneMontLE() + for i := range points { + if points[i].IsInfinity() { + continue + } + base := i * g1PointBytes + writeFPMontLE(out[base:base+g1CoordinateBytes], &points[i].X) + writeFPMontLE(out[base+g1CoordinateBytes:base+2*g1CoordinateBytes], &points[i].Y) + copy(out[base+2*g1CoordinateBytes:base+3*g1CoordinateBytes], one) + } + return out +} + +func packG2AffineJacobianBatch(points []bn254.G2Affine) []byte { + out := make([]byte, len(points)*g2PointBytes) + one := fpOneMontLE() + for i := range points { + if points[i].IsInfinity() { + continue + } + base := i * g2PointBytes + writeFPMontLE(out[base:base+g2ComponentBytes], &points[i].X.A0) + writeFPMontLE(out[base+g2ComponentBytes:base+2*g2ComponentBytes], &points[i].X.A1) + writeFPMontLE(out[base+2*g2ComponentBytes:base+3*g2ComponentBytes], &points[i].Y.A0) + writeFPMontLE(out[base+3*g2ComponentBytes:base+4*g2ComponentBytes], &points[i].Y.A1) + copy(out[base+4*g2ComponentBytes:base+5*g2ComponentBytes], one) + } + return out +} + +func decodeG1AffineFromPacked(packed []byte, err error) (bn254.G1Affine, error) { + if err != nil { + return bn254.G1Affine{}, err + } + if len(packed) != 2*g1CoordinateBytes { + return bn254.G1Affine{}, fmt.Errorf("webgpu groth16 bn254: expected %d G1 bytes, got %d", 2*g1CoordinateBytes, len(packed)) + } + return bn254.G1Affine{ + X: readFPMontLE(packed[:g1CoordinateBytes]), + Y: readFPMontLE(packed[g1CoordinateBytes:]), + }, nil +} + +func decodeG2AffineFromPacked(packed []byte, err error) (bn254.G2Affine, error) { + if err != nil { + return bn254.G2Affine{}, err + } + if len(packed) != 4*g2ComponentBytes { + return bn254.G2Affine{}, fmt.Errorf("webgpu groth16 bn254: expected %d G2 bytes, got %d", 4*g2ComponentBytes, len(packed)) + } + var out bn254.G2Affine + out.X.A0 = readFPMontLE(packed[0*g2ComponentBytes : 1*g2ComponentBytes]) + out.X.A1 = readFPMontLE(packed[1*g2ComponentBytes : 2*g2ComponentBytes]) + out.Y.A0 = readFPMontLE(packed[2*g2ComponentBytes : 3*g2ComponentBytes]) + out.Y.A1 = readFPMontLE(packed[3*g2ComponentBytes : 4*g2ComponentBytes]) + return out, nil +} + +// WebGPU point buffers use raw Montgomery little-endian coordinates. The +// fp.LittleEndian helpers convert to/from regular representation, so they are +// not equivalent here. +func readFPMontLE(src []byte) fp.Element { + var words [4]uint64 + for i := range words { + words[i] = binary.LittleEndian.Uint64(src[i*8 : (i+1)*8]) + } + return fp.Element(words) +} + +func writeFPMontLE(dst []byte, value *fp.Element) { + for i, word := range [4]uint64(*value) { + binary.LittleEndian.PutUint64(dst[i*8:(i+1)*8], word) + } +} + +func fpOneMontLE() []byte { + out := make([]byte, g1CoordinateBytes) + var one fp.Element + one.SetOne() + writeFPMontLE(out, &one) + return out +} diff --git a/backend/accelerated/webgpu/groth16/doc.go b/backend/accelerated/webgpu/groth16/doc.go new file mode 100644 index 0000000000..d73299734b --- /dev/null +++ b/backend/accelerated/webgpu/groth16/doc.go @@ -0,0 +1,19 @@ +//go:build js && wasm + +// Package groth16 provides an experimental browser/WebGPU-accelerated Groth16 +// prover surface for wasm targets. +// +// Scope of the current implementation: +// - circuit compilation, setup, witness assignment, and solver stay native +// (no WebGPU offload) +// - Groth16 heavy MSMs are offloaded through a JS bridge to the browser +// WebGPU runtime in this repository +// - BSB22 commitment hint, commitment MSM, and PoK MSM work is wired through +// the same WebGPU bridge +// +// Curve-specific proving code lives in the bn254, bls12-377, and bls12-381 +// subpackages, while this package keeps the curve-switching facade. Host +// applications are expected to install the WebGPU bridge from the TS package +// before invoking Prove so the wasm code can call into the browser runtime +// through `syscall/js`. +package groth16 diff --git a/backend/accelerated/webgpu/groth16/groth16.go b/backend/accelerated/webgpu/groth16/groth16.go new file mode 100644 index 0000000000..46757891b7 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/groth16.go @@ -0,0 +1,80 @@ +//go:build js && wasm + +package groth16 + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/backend" + webgpu_bls12377 "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/bls12-377" + webgpu_bls12381 "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/bls12-381" + webgpu_bn254 "github.com/consensys/gnark/backend/accelerated/webgpu/groth16/bn254" + "github.com/consensys/gnark/backend/groth16" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + csbls12377 "github.com/consensys/gnark/constraint/bls12-377" + csbls12381 "github.com/consensys/gnark/constraint/bls12-381" + csbn254 "github.com/consensys/gnark/constraint/bn254" +) + +// Prove runs the Groth16 prover with browser/WebGPU acceleration on supported +// wasm targets. +// +// The current implementation accelerates the heavy quotient-H and MSM stages +// while leaving witness solving in Go. BSB22 commitments are handled by +// replacing gnark's commitment hint during solving and offloading the +// commitment/PoK MSMs to WebGPU. +func Prove(r1cs constraint.ConstraintSystem, pk groth16.ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (groth16.Proof, error) { + switch _r1cs := r1cs.(type) { + case *csbn254.R1CS: + _tpk, ok := pk.(*webgpu_bn254.ProvingKey) + if !ok { + return nil, fmt.Errorf("webgpu groth16: expected *webgpu_bn254.ProvingKey, got %T", pk) + } + return webgpu_bn254.Prove(_r1cs, _tpk, fullWitness, opts...) + case *csbls12377.R1CS: + _tpk, ok := pk.(*webgpu_bls12377.ProvingKey) + if !ok { + return nil, fmt.Errorf("webgpu groth16: expected *webgpu_bls12377.ProvingKey, got %T", pk) + } + return webgpu_bls12377.Prove(_r1cs, _tpk, fullWitness, opts...) + case *csbls12381.R1CS: + _tpk, ok := pk.(*webgpu_bls12381.ProvingKey) + if !ok { + return nil, fmt.Errorf("webgpu groth16: expected *webgpu_bls12381.ProvingKey, got %T", pk) + } + return webgpu_bls12381.Prove(_r1cs, _tpk, fullWitness, opts...) + default: + return nil, fmt.Errorf("webgpu groth16: unsupported constraint system %T", r1cs) + } +} + +// NewProvingKey returns an empty proving key wrapper for supported curves. +func NewProvingKey(curveID ecc.ID) groth16.ProvingKey { + switch curveID { + case ecc.BN254: + return &webgpu_bn254.ProvingKey{} + case ecc.BLS12_377: + return &webgpu_bls12377.ProvingKey{} + case ecc.BLS12_381: + return &webgpu_bls12381.ProvingKey{} + default: + panic("webgpu groth16: unsupported curve") + } +} + +// Prepare initializes browser-side MSM caches for a deserialized proving key so +// the first proof does not include one-time bridge setup cost. +func Prepare(pk groth16.ProvingKey) error { + switch typed := pk.(type) { + case *webgpu_bn254.ProvingKey: + return typed.Prepare() + case *webgpu_bls12377.ProvingKey: + return typed.Prepare() + case *webgpu_bls12381.ProvingKey: + return typed.Prepare() + default: + return fmt.Errorf("webgpu groth16: unsupported proving key type %T", pk) + } +} diff --git a/backend/accelerated/webgpu/groth16/internal/bridge/bridge.go b/backend/accelerated/webgpu/groth16/internal/bridge/bridge.go new file mode 100644 index 0000000000..bdfda7ca5d --- /dev/null +++ b/backend/accelerated/webgpu/groth16/internal/bridge/bridge.go @@ -0,0 +1,82 @@ +//go:build js && wasm + +package bridge + +import ( + "syscall/js" + + webgpubridge "github.com/consensys/gnark/backend/accelerated/webgpu/internal/bridge" +) + +var Bridge = Groth16Client{Client: webgpubridge.NewClient("gnarkGroth16WebGPU", "webgpu groth16")} + +type Groth16Client struct { + webgpubridge.Client +} + +type MSMBatchResult struct { + G1ABytes []byte + G1BBytes []byte + G1KBytes []byte + G2BBytes []byte +} + +func JSUint8Array(src []byte) js.Value { + return webgpubridge.JSUint8Array(src) +} + +func JSObject() js.Value { + return webgpubridge.JSObject() +} + +func (c Groth16Client) MSMG1(handle, vectorName string, scalarsPacked []byte) ([]byte, error) { + value, err := c.CallPromise("msmG1", handle, vectorName, webgpubridge.JSUint8Array(scalarsPacked)) + if err != nil { + return nil, err + } + return webgpubridge.GoBytes(c.ErrorPrefix, value) +} + +func (c Groth16Client) MSMBatch(handle string, g1A, g1B, g1K []byte) (MSMBatchResult, error) { + payload := webgpubridge.JSObject() + payload.Set("g1A", webgpubridge.JSUint8Array(g1A)) + payload.Set("g1B", webgpubridge.JSUint8Array(g1B)) + payload.Set("g1K", webgpubridge.JSUint8Array(g1K)) + value, err := c.CallPromise("msmBatch", handle, payload) + if err != nil { + return MSMBatchResult{}, err + } + result := MSMBatchResult{} + if result.G1ABytes, err = webgpubridge.GoBytes(c.ErrorPrefix, value.Get("g1A")); err != nil { + return MSMBatchResult{}, err + } + if result.G1BBytes, err = webgpubridge.GoBytes(c.ErrorPrefix, value.Get("g1B")); err != nil { + return MSMBatchResult{}, err + } + if result.G1KBytes, err = webgpubridge.GoBytes(c.ErrorPrefix, value.Get("g1K")); err != nil { + return MSMBatchResult{}, err + } + if result.G2BBytes, err = webgpubridge.GoBytes(c.ErrorPrefix, value.Get("g2B")); err != nil { + return MSMBatchResult{}, err + } + return result, nil +} + +func (c Groth16Client) ComputeHZMSMG1(handle string, aPacked, bPacked, cPacked []byte) ([]byte, error) { + value, err := c.CallPromise( + "computeHZMSMG1", + handle, + webgpubridge.JSUint8Array(aPacked), + webgpubridge.JSUint8Array(bPacked), + webgpubridge.JSUint8Array(cPacked), + ) + if err != nil { + return nil, err + } + return webgpubridge.GoBytes(c.ErrorPrefix, value) +} + +func (c Groth16Client) PrewarmQuotientDomain(curve string, domainSize int) error { + _, err := c.CallPromise("prewarmQuotientDomain", curve, domainSize) + return err +} diff --git a/backend/accelerated/webgpu/groth16/internal/common/filter_indices.go b/backend/accelerated/webgpu/groth16/internal/common/filter_indices.go new file mode 100644 index 0000000000..2469a6a318 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/internal/common/filter_indices.go @@ -0,0 +1,41 @@ +//go:build js && wasm + +package common + +import "github.com/consensys/gnark/constraint" + +func ComputeKeptIndices(infinity []bool) []int { + if len(infinity) == 0 { + return nil + } + count := 0 + for _, isInfinity := range infinity { + if !isInfinity { + count++ + } + } + indices := make([]int, 0, count) + for i, isInfinity := range infinity { + if !isInfinity { + indices = append(indices, i) + } + } + return indices +} + +func CommitmentWireIndexesToRemove(commitmentInfo constraint.Groth16Commitments) []int { + if len(commitmentInfo) == 0 { + return nil + } + count := len(commitmentInfo) + privateCommitted := commitmentInfo.GetPrivateCommitted() + for _, indexes := range privateCommitted { + count += len(indexes) + } + out := make([]int, 0, count) + for _, indexes := range privateCommitted { + out = append(out, indexes...) + } + out = append(out, commitmentInfo.CommitmentIndexes()...) + return out +} diff --git a/backend/accelerated/webgpu/groth16/internal/wasmruntime/native/main.go b/backend/accelerated/webgpu/groth16/internal/wasmruntime/native/main.go new file mode 100644 index 0000000000..e2de0150f1 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/internal/wasmruntime/native/main.go @@ -0,0 +1,46 @@ +//go:build js && wasm + +package main + +import ( + "bytes" + "fmt" + + "github.com/consensys/gnark/backend/accelerated/webgpu/internal/wasmruntime" + gnarkgroth16 "github.com/consensys/gnark/backend/groth16" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" +) + +func main() { + if err := wasmruntime.Install(wasmruntime.Config[gnarkgroth16.ProvingKey, gnarkgroth16.VerifyingKey, gnarkgroth16.Proof]{ + GlobalName: "gnarkGroth16RuntimeNative", + CSFactory: gnarkgroth16.NewCS, + PKFactory: gnarkgroth16.NewProvingKey, + VKFactory: gnarkgroth16.NewVerifyingKey, + ProofFactory: gnarkgroth16.NewProof, + ReadProvingKey: func(pk gnarkgroth16.ProvingKey, format string, data []byte) error { + switch format { + case "serialized": + if _, err := pk.ReadFrom(bytes.NewReader(data)); err != nil { + return fmt.Errorf("read pk: %w", err) + } + case "dump": + if err := pk.ReadDump(bytes.NewReader(data)); err != nil { + return fmt.Errorf("read pk dump: %w", err) + } + default: + return fmt.Errorf("unsupported proving key format %q", format) + } + return nil + }, + Prove: func(ccs constraint.ConstraintSystem, pk gnarkgroth16.ProvingKey, fullWitness witness.Witness) (gnarkgroth16.Proof, error) { + return gnarkgroth16.Prove(ccs, pk, fullWitness) + }, + Verify: func(proof gnarkgroth16.Proof, vk gnarkgroth16.VerifyingKey, publicWitness witness.Witness) error { + return gnarkgroth16.Verify(proof, vk, publicWitness) + }, + }); err != nil { + panic(err) + } +} diff --git a/backend/accelerated/webgpu/groth16/internal/wasmruntime/webgpu/main.go b/backend/accelerated/webgpu/groth16/internal/wasmruntime/webgpu/main.go new file mode 100644 index 0000000000..6933fb9691 --- /dev/null +++ b/backend/accelerated/webgpu/groth16/internal/wasmruntime/webgpu/main.go @@ -0,0 +1,48 @@ +//go:build js && wasm + +package main + +import ( + "bytes" + "fmt" + + webgpugroth16 "github.com/consensys/gnark/backend/accelerated/webgpu/groth16" + "github.com/consensys/gnark/backend/accelerated/webgpu/internal/wasmruntime" + gnarkgroth16 "github.com/consensys/gnark/backend/groth16" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" +) + +func main() { + if err := wasmruntime.Install(wasmruntime.Config[gnarkgroth16.ProvingKey, gnarkgroth16.VerifyingKey, gnarkgroth16.Proof]{ + GlobalName: "gnarkGroth16RuntimeWebGPU", + CSFactory: gnarkgroth16.NewCS, + PKFactory: webgpugroth16.NewProvingKey, + VKFactory: gnarkgroth16.NewVerifyingKey, + ProofFactory: gnarkgroth16.NewProof, + ReadProvingKey: func(pk gnarkgroth16.ProvingKey, format string, data []byte) error { + switch format { + case "serialized": + if _, err := pk.ReadFrom(bytes.NewReader(data)); err != nil { + return fmt.Errorf("read pk: %w", err) + } + case "dump": + if err := pk.ReadDump(bytes.NewReader(data)); err != nil { + return fmt.Errorf("read pk dump: %w", err) + } + default: + return fmt.Errorf("unsupported proving key format %q", format) + } + return nil + }, + Prepare: webgpugroth16.Prepare, + Prove: func(ccs constraint.ConstraintSystem, pk gnarkgroth16.ProvingKey, fullWitness witness.Witness) (gnarkgroth16.Proof, error) { + return webgpugroth16.Prove(ccs, pk, fullWitness) + }, + Verify: func(proof gnarkgroth16.Proof, vk gnarkgroth16.VerifyingKey, publicWitness witness.Witness) error { + return gnarkgroth16.Verify(proof, vk, publicWitness) + }, + }); err != nil { + panic(err) + } +} diff --git a/backend/accelerated/webgpu/internal/bridge/bridge_js.go b/backend/accelerated/webgpu/internal/bridge/bridge_js.go new file mode 100644 index 0000000000..8d970c46e3 --- /dev/null +++ b/backend/accelerated/webgpu/internal/bridge/bridge_js.go @@ -0,0 +1,128 @@ +//go:build js && wasm + +package bridge + +import ( + "fmt" + "syscall/js" +) + +type Client struct { + GlobalName string + ErrorPrefix string +} + +func NewClient(globalName, errorPrefix string) Client { + return Client{GlobalName: globalName, ErrorPrefix: errorPrefix} +} + +func (c Client) getBridge() (js.Value, error) { + bridge := js.Global().Get(c.GlobalName) + if bridge.IsUndefined() || bridge.IsNull() { + return js.Undefined(), fmt.Errorf("%s: %s bridge not found on global object", c.ErrorPrefix, c.GlobalName) + } + return bridge, nil +} + +func (c Client) AwaitPromise(promise js.Value) (js.Value, error) { + if promise.IsUndefined() || promise.IsNull() { + return js.Undefined(), fmt.Errorf("%s: bridge returned empty promise", c.ErrorPrefix) + } + + type result struct { + value js.Value + err error + } + ch := make(chan result, 1) + + resolve := js.FuncOf(func(this js.Value, args []js.Value) any { + value := js.Undefined() + if len(args) > 0 { + value = args[0] + } + ch <- result{value: value} + return nil + }) + reject := js.FuncOf(func(this js.Value, args []js.Value) any { + var err error + if len(args) > 0 { + err = c.JSError(args[0]) + } else { + err = fmt.Errorf("%s: bridge promise rejected", c.ErrorPrefix) + } + ch <- result{err: err} + return nil + }) + defer resolve.Release() + defer reject.Release() + + promise.Call("then", resolve, reject) + out := <-ch + return out.value, out.err +} + +func (c Client) JSError(v js.Value) error { + if v.IsUndefined() || v.IsNull() { + return fmt.Errorf("%s: unknown JS error", c.ErrorPrefix) + } + if message := v.Get("message"); message.Type() == js.TypeString { + return fmt.Errorf("%s: %s", c.ErrorPrefix, message.String()) + } + return fmt.Errorf("%s: %s", c.ErrorPrefix, v.String()) +} + +func (c Client) CallPromise(method string, args ...any) (js.Value, error) { + bridge, err := c.getBridge() + if err != nil { + return js.Undefined(), err + } + fn := bridge.Get(method) + if fn.Type() != js.TypeFunction { + return js.Undefined(), fmt.Errorf("%s: bridge method %q is not available", c.ErrorPrefix, method) + } + return c.AwaitPromise(fn.Invoke(args...)) +} + +func JSUint8Array(src []byte) js.Value { + out := js.Global().Get("Uint8Array").New(len(src)) + if len(src) > 0 { + js.CopyBytesToJS(out, src) + } + return out +} + +func GoBytes(prefix string, src js.Value) ([]byte, error) { + if src.IsUndefined() || src.IsNull() { + return nil, fmt.Errorf("%s: expected Uint8Array result, got empty value", prefix) + } + n := src.Get("byteLength") + if n.Type() != js.TypeNumber { + return nil, fmt.Errorf("%s: JS result does not expose byteLength", prefix) + } + out := make([]byte, n.Int()) + if len(out) > 0 { + js.CopyBytesToGo(out, src) + } + return out, nil +} + +func JSObject() js.Value { + return js.Global().Get("Object").New() +} + +func (c Client) Init(curve string) error { + _, err := c.CallPromise("init", curve) + return err +} + +func (c Client) PrepareKey(curve string, payload js.Value) (string, error) { + value, err := c.CallPromise("prepareKey", curve, payload) + if err != nil { + return "", err + } + handle := value.Get("handle") + if handle.Type() != js.TypeString || handle.String() == "" { + return "", fmt.Errorf("%s: bridge returned invalid key handle", c.ErrorPrefix) + } + return handle.String(), nil +} diff --git a/backend/accelerated/webgpu/internal/bridge/groth16_bridge.js b/backend/accelerated/webgpu/internal/bridge/groth16_bridge.js new file mode 100644 index 0000000000..192eb7d0c0 --- /dev/null +++ b/backend/accelerated/webgpu/internal/bridge/groth16_bridge.js @@ -0,0 +1,271 @@ +import { createBLS12377, createBLS12381, createBN254, createCurveGPUContext } from "../../web/dist/index.js"; + +const CURVE_CONFIG = { + bn254: { + g1CoordinateBytes: 32, + g1PointBytes: 96, + g2ComponentBytes: 32, + g2PointBytes: 192, + }, + bls12_381: { + g1CoordinateBytes: 48, + g1PointBytes: 144, + g2ComponentBytes: 48, + g2PointBytes: 288, + }, + bls12_377: { + g1CoordinateBytes: 48, + g1PointBytes: 144, + g2ComponentBytes: 48, + g2PointBytes: 288, + }, +}; + +let contextPromise = null; +const modulePromises = new Map(); +const keyCache = new Map(); +let nextHandle = 1; + +function cloneBytes(bytes) { + return new Uint8Array(bytes); +} + +function createCurveForID(curve, context) { + switch (curve) { + case "bn254": + return createBN254(context); + case "bls12_381": + return createBLS12381(context); + case "bls12_377": + return createBLS12377(context); + default: + throw new Error(`unsupported curve ${curve}`); + } +} + +async function getContext() { + if (!contextPromise) { + contextPromise = createCurveGPUContext(); + } + return contextPromise; +} + +async function getCurveModule(curve) { + if (!modulePromises.has(curve)) { + modulePromises.set( + curve, + (async () => { + const context = await getContext(); + return createCurveForID(curve, context); + })(), + ); + } + return modulePromises.get(curve); +} + +function unpackG1JacobianPoint(curve, packedPoint) { + const coordinateBytes = CURVE_CONFIG[curve].g1CoordinateBytes; + return { + x: cloneBytes(packedPoint.slice(0, coordinateBytes)), + y: cloneBytes(packedPoint.slice(coordinateBytes, 2 * coordinateBytes)), + z: cloneBytes(packedPoint.slice(2 * coordinateBytes, 3 * coordinateBytes)), + }; +} + +function unpackG2JacobianPoint(curve, packedPoint) { + const componentBytes = CURVE_CONFIG[curve].g2ComponentBytes; + return { + x: { + c0: cloneBytes(packedPoint.slice(0, componentBytes)), + c1: cloneBytes(packedPoint.slice(componentBytes, 2 * componentBytes)), + }, + y: { + c0: cloneBytes(packedPoint.slice(2 * componentBytes, 3 * componentBytes)), + c1: cloneBytes(packedPoint.slice(3 * componentBytes, 4 * componentBytes)), + }, + z: { + c0: cloneBytes(packedPoint.slice(4 * componentBytes, 5 * componentBytes)), + c1: cloneBytes(packedPoint.slice(5 * componentBytes, 6 * componentBytes)), + }, + }; +} + +function getKey(handle) { + const entry = keyCache.get(handle); + if (!entry) { + throw new Error(`unknown Groth16 key handle ${handle}`); + } + return entry; +} + +async function init(curve) { + const [context] = await Promise.all([getContext(), getCurveModule(curve)]); + return { + curve, + adapter: { + vendor: context.diagnostics.vendor ?? "", + architecture: context.diagnostics.architecture ?? "", + description: context.diagnostics.description ?? "", + }, + }; +} + +async function prepareKey(curve, payload) { + await init(curve); + const handle = `${curve}:${nextHandle++}`; + const commitmentCount = Number(payload.commitmentCount ?? 0); + const entry = { + curve, + g1A: cloneBytes(payload.g1A), + g1ACount: Number(payload.g1ACount), + g1B: cloneBytes(payload.g1B), + g1BCount: Number(payload.g1BCount), + g1K: cloneBytes(payload.g1K), + g1KCount: Number(payload.g1KCount), + g1Z: cloneBytes(payload.g1Z), + g1ZCount: Number(payload.g1ZCount), + g2B: cloneBytes(payload.g2B), + g2BCount: Number(payload.g2BCount), + commitmentCount, + }; + for (let i = 0; i < commitmentCount; i++) { + const basisName = `commitmentBasis${i}`; + const basisExpSigmaName = `commitmentBasisExpSigma${i}`; + entry[basisName] = cloneBytes(payload[basisName]); + entry[`${basisName}Count`] = Number(payload[`${basisName}Count`]); + entry[basisExpSigmaName] = cloneBytes(payload[basisExpSigmaName]); + entry[`${basisExpSigmaName}Count`] = Number(payload[`${basisExpSigmaName}Count`]); + } + keyCache.set(handle, entry); + return { handle }; +} + +async function releaseKey(handle) { + keyCache.delete(handle); +} + +async function msmG1(handle, vectorName, scalarsPacked) { + const entry = getKey(handle); + const module = await getCurveModule(entry.curve); + const config = CURVE_CONFIG[entry.curve]; + const basesPacked = entry[vectorName]; + const count = entry[`${vectorName}Count`]; + if (!(basesPacked instanceof Uint8Array) || typeof count !== "number") { + throw new Error(`missing cached G1 vector ${vectorName}`); + } + const resultPacked = await module.g1msm.pippengerPackedJacobianBases(basesPacked, cloneBytes(scalarsPacked), { + count: 1, + termsPerInstance: count, + window: module.g1msm.bestWindow(count), + }); + const jacobian = unpackG1JacobianPoint(entry.curve, resultPacked.slice(0, config.g1PointBytes)); + const affine = await module.g1.jacobianToAffine(jacobian); + const out = new Uint8Array(2 * config.g1CoordinateBytes); + out.set(affine.x, 0); + out.set(affine.y, config.g1CoordinateBytes); + return out; +} + +async function msmG1Cached(entry, vectorName, scalarsPacked) { + const module = await getCurveModule(entry.curve); + const config = CURVE_CONFIG[entry.curve]; + const basesPacked = entry[vectorName]; + const count = entry[`${vectorName}Count`]; + if (!(basesPacked instanceof Uint8Array) || typeof count !== "number") { + throw new Error(`missing cached G1 vector ${vectorName}`); + } + const resultPacked = await module.g1msm.pippengerPackedJacobianBases(basesPacked, scalarsPacked, { + count: 1, + termsPerInstance: count, + window: module.g1msm.bestWindow(count), + }); + const jacobian = unpackG1JacobianPoint(entry.curve, resultPacked.slice(0, config.g1PointBytes)); + const affine = await module.g1.jacobianToAffine(jacobian); + const out = new Uint8Array(2 * config.g1CoordinateBytes); + out.set(affine.x, 0); + out.set(affine.y, config.g1CoordinateBytes); + return out; +} + +async function msmG2(handle, vectorName, scalarsPacked) { + const entry = getKey(handle); + const point = await msmG2Cached(entry, vectorName, cloneBytes(scalarsPacked)); + return point; +} + +async function msmG2Cached(entry, vectorName, scalarsPacked) { + const module = await getCurveModule(entry.curve); + const config = CURVE_CONFIG[entry.curve]; + const basesPacked = entry[vectorName]; + const count = entry[`${vectorName}Count`]; + if (!(basesPacked instanceof Uint8Array) || typeof count !== "number") { + throw new Error(`missing cached G2 vector ${vectorName}`); + } + const resultPacked = await module.g2msm.pippengerPackedJacobianBases(basesPacked, cloneBytes(scalarsPacked), { + count: 1, + termsPerInstance: count, + window: module.g2msm.bestWindow(count), + }); + const jacobian = unpackG2JacobianPoint(entry.curve, resultPacked.slice(0, config.g2PointBytes)); + const affine = await module.g2.jacobianToAffine(jacobian); + const out = new Uint8Array(4 * config.g2ComponentBytes); + out.set(affine.x.c0, 0); + out.set(affine.x.c1, config.g2ComponentBytes); + out.set(affine.y.c0, 2 * config.g2ComponentBytes); + out.set(affine.y.c1, 3 * config.g2ComponentBytes); + return out; +} + +async function msmBatch(handle, payload) { + const entry = getKey(handle); + const points = {}; + + if (payload.g1A) { + points.g1A = await msmG1Cached(entry, "g1A", payload.g1A); + } + if (payload.g1B) { + const g1BScalars = payload.g1B; + points.g1B = await msmG1Cached(entry, "g1B", g1BScalars); + points.g2B = await msmG2Cached(entry, "g2B", g1BScalars); + } + if (payload.g1K) { + points.g1K = await msmG1Cached(entry, "g1K", payload.g1K); + } + + return points; +} + +async function computeH(curve, aPacked, bPacked, cPacked) { + const module = await getCurveModule(curve); + return module.groth16.computeGroth16QuotientPackedRegular(cloneBytes(aPacked), cloneBytes(bPacked), cloneBytes(cPacked)); +} + +async function computeHZMSMG1(handle, aPacked, bPacked, cPacked) { + const entry = getKey(handle); + const module = await getCurveModule(entry.curve); + const quotient = await module.groth16.computeGroth16QuotientPackedMont( + cloneBytes(aPacked), + cloneBytes(bPacked), + cloneBytes(cPacked), + ); + const zCount = Number(entry.g1ZCount); + const scalars = quotient.subarray(0, zCount * 32); + return msmG1Cached(entry, "g1Z", scalars); +} + +async function prewarmQuotientDomain(curve, size) { + const module = await getCurveModule(curve); + await module.groth16.prewarmGroth16QuotientDomain(Number(size)); +} + +globalThis.gnarkGroth16WebGPU = { + init, + prepareKey, + releaseKey, + msmG1, + msmG2, + msmBatch, + computeH, + computeHZMSMG1, + prewarmQuotientDomain, +}; diff --git a/backend/accelerated/webgpu/internal/wasmruntime/runtime.go b/backend/accelerated/webgpu/internal/wasmruntime/runtime.go new file mode 100644 index 0000000000..8cd1d58c7a --- /dev/null +++ b/backend/accelerated/webgpu/internal/wasmruntime/runtime.go @@ -0,0 +1,468 @@ +//go:build js && wasm + +package wasmruntime + +import ( + "bytes" + "fmt" + "io" + "syscall/js" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" +) + +type ProveFunc[PK, Proof any] func(constraint.ConstraintSystem, PK, witness.Witness) (Proof, error) +type PrepareFunc[PK any] func(PK) error +type PrepareWithCSFunc[PK any] func(constraint.ConstraintSystem, PK) error +type VerifyFunc[VK, Proof any] func(Proof, VK, witness.Witness) error + +type Config[PK, VK, Proof any] struct { + GlobalName string + + SupportedCurves map[string]ecc.ID + CSFactory func(ecc.ID) constraint.ConstraintSystem + PKFactory func(ecc.ID) PK + VKFactory func(ecc.ID) VK + ProofFactory func(ecc.ID) Proof + + ReadProvingKey func(PK, string, []byte) error + Prepare PrepareFunc[PK] + PrepareWithCS PrepareWithCSFunc[PK] + Prove ProveFunc[PK, Proof] + Verify VerifyFunc[VK, Proof] +} + +type Runtime[PK, VK, Proof any] struct { + cfg Config[PK, VK, Proof] + next uint64 + funcs []js.Func + ccs map[string]ccsEntry + pks map[string]pkEntry[PK] + vks map[string]vkEntry[VK] + handles map[string]string +} + +type ccsEntry struct { + curve ecc.ID + value constraint.ConstraintSystem +} + +type pkEntry[PK any] struct { + curve ecc.ID + value PK + prepared bool +} + +type vkEntry[VK any] struct { + curve ecc.ID + value VK +} + +func Install[PK, VK, Proof any](cfg Config[PK, VK, Proof]) error { + if cfg.GlobalName == "" { + return fmt.Errorf("missing global name") + } + if cfg.CSFactory == nil { + return fmt.Errorf("missing constraint system factory") + } + if cfg.PKFactory == nil { + return fmt.Errorf("missing proving key factory") + } + if cfg.VKFactory == nil { + return fmt.Errorf("missing verification key factory") + } + if cfg.ProofFactory == nil { + return fmt.Errorf("missing proof factory") + } + if cfg.Prove == nil { + return fmt.Errorf("missing prove function") + } + if cfg.Verify == nil { + return fmt.Errorf("missing verify function") + } + + r := &Runtime[PK, VK, Proof]{ + cfg: cfg, + ccs: make(map[string]ccsEntry), + pks: make(map[string]pkEntry[PK]), + vks: make(map[string]vkEntry[VK]), + handles: make(map[string]string), + } + js.Global().Set(cfg.GlobalName, r.object()) + select {} +} + +func (r *Runtime[PK, VK, Proof]) object() js.Value { + obj := js.Global().Get("Object").New() + r.setMethod(obj, "readConstraintSystem", r.readConstraintSystem) + r.setMethod(obj, "readProvingKey", r.readProvingKey) + r.setMethod(obj, "readVerificationKey", r.readVerificationKey) + r.setMethod(obj, "prepareProvingKey", r.prepareProvingKey) + r.setMethod(obj, "prove", r.prove) + r.setMethod(obj, "verify", r.verify) + r.setMethod(obj, "release", r.release) + return obj +} + +func (r *Runtime[PK, VK, Proof]) setMethod(obj js.Value, name string, fn func([]js.Value) (js.Value, error)) { + callback := js.FuncOf(func(this js.Value, args []js.Value) any { + return promise(func() (js.Value, error) { + return fn(args) + }) + }) + r.funcs = append(r.funcs, callback) + obj.Set(name, callback) +} + +func promise(fn func() (js.Value, error)) js.Value { + executor := js.FuncOf(func(this js.Value, args []js.Value) any { + resolve := args[0] + reject := args[1] + go func() { + value, err := fn() + if err != nil { + reject.Invoke(js.Global().Get("Error").New(err.Error())) + return + } + resolve.Invoke(value) + }() + return nil + }) + p := js.Global().Get("Promise").New(executor) + executor.Release() + return p +} + +func (r *Runtime[PK, VK, Proof]) readConstraintSystem(args []js.Value) (js.Value, error) { + curveID, err := r.curveIDFromArg(args, 0) + if err != nil { + return js.Undefined(), err + } + data, err := bytesFromArg(args, 1) + if err != nil { + return js.Undefined(), err + } + ccs := r.cfg.CSFactory(curveID) + if _, err := ccs.ReadFrom(bytes.NewReader(data)); err != nil { + return js.Undefined(), fmt.Errorf("read ccs: %w", err) + } + handle := r.store("ccs") + r.ccs[handle] = ccsEntry{curve: curveID, value: ccs} + + out := js.Global().Get("Object").New() + out.Set("handle", handle) + out.Set("constraints", ccs.GetNbConstraints()) + return out, nil +} + +func (r *Runtime[PK, VK, Proof]) readProvingKey(args []js.Value) (js.Value, error) { + curveID, err := r.curveIDFromArg(args, 0) + if err != nil { + return js.Undefined(), err + } + data, err := bytesFromArg(args, 1) + if err != nil { + return js.Undefined(), err + } + format := "serialized" + if len(args) > 2 && args[2].Type() == js.TypeString { + format = args[2].String() + } + pk := r.cfg.PKFactory(curveID) + if r.cfg.ReadProvingKey != nil { + if err := r.cfg.ReadProvingKey(pk, format, data); err != nil { + return js.Undefined(), err + } + } else if format != "serialized" { + return js.Undefined(), fmt.Errorf("unsupported proving key format %q", format) + } else if err := readFromBytes(pk, "pk", data); err != nil { + return js.Undefined(), err + } + handle := r.store("pk") + r.pks[handle] = pkEntry[PK]{curve: curveID, value: pk} + return handleObject(handle), nil +} + +func (r *Runtime[PK, VK, Proof]) readVerificationKey(args []js.Value) (js.Value, error) { + curveID, err := r.curveIDFromArg(args, 0) + if err != nil { + return js.Undefined(), err + } + data, err := bytesFromArg(args, 1) + if err != nil { + return js.Undefined(), err + } + vk := r.cfg.VKFactory(curveID) + if err := readFromBytes(vk, "vk", data); err != nil { + return js.Undefined(), err + } + handle := r.store("vk") + r.vks[handle] = vkEntry[VK]{curve: curveID, value: vk} + return handleObject(handle), nil +} + +func (r *Runtime[PK, VK, Proof]) prepareProvingKey(args []js.Value) (js.Value, error) { + handle, pk, err := r.pkFromArg(args, 0) + if err != nil { + return js.Undefined(), err + } + var ccs *ccsEntry + if len(args) > 1 && args[1].Type() == js.TypeString { + entry, err := r.ccsFromArg(args, 1) + if err != nil { + return js.Undefined(), err + } + if entry.curve != pk.curve { + return js.Undefined(), fmt.Errorf("ccs and proving key curves do not match") + } + ccs = &entry + } + if err := r.ensurePrepared(handle, pk, ccs); err != nil { + return js.Undefined(), err + } + return js.Undefined(), nil +} + +func (r *Runtime[PK, VK, Proof]) prove(args []js.Value) (js.Value, error) { + ccs, err := r.ccsFromArg(args, 0) + if err != nil { + return js.Undefined(), err + } + pkHandle, pk, err := r.pkFromArg(args, 1) + if err != nil { + return js.Undefined(), err + } + if ccs.curve != pk.curve { + return js.Undefined(), fmt.Errorf("ccs and proving key curves do not match") + } + witnessBytes, err := bytesFromArg(args, 2) + if err != nil { + return js.Undefined(), err + } + fullWitness, err := readWitness(ccs.curve, witnessBytes) + if err != nil { + return js.Undefined(), fmt.Errorf("read witness: %w", err) + } + if err := r.ensurePrepared(pkHandle, pk, &ccs); err != nil { + return js.Undefined(), err + } + proof, err := r.cfg.Prove(ccs.value, pk.value, fullWitness) + if err != nil { + return js.Undefined(), fmt.Errorf("prove: %w", err) + } + proofBytes, err := writeToBytes(proof) + if err != nil { + return js.Undefined(), fmt.Errorf("serialize proof: %w", err) + } + return jsBytes(proofBytes), nil +} + +func (r *Runtime[PK, VK, Proof]) verify(args []js.Value) (js.Value, error) { + proofBytes, err := bytesFromArg(args, 0) + if err != nil { + return js.Undefined(), err + } + vk, err := r.vkFromArg(args, 1) + if err != nil { + return js.Undefined(), err + } + publicWitnessBytes, err := bytesFromArg(args, 2) + if err != nil { + return js.Undefined(), err + } + proof := r.cfg.ProofFactory(vk.curve) + if err := readFromBytes(proof, "proof", proofBytes); err != nil { + return js.Undefined(), err + } + publicWitness, err := readWitness(vk.curve, publicWitnessBytes) + if err != nil { + return js.Undefined(), fmt.Errorf("read public witness: %w", err) + } + if err := r.cfg.Verify(proof, vk.value, publicWitness); err != nil { + return js.ValueOf(false), nil + } + return js.ValueOf(true), nil +} + +func (r *Runtime[PK, VK, Proof]) release(args []js.Value) (js.Value, error) { + if len(args) < 1 || args[0].Type() != js.TypeString { + return js.Undefined(), fmt.Errorf("missing handle") + } + handle := args[0].String() + switch r.handles[handle] { + case "ccs": + delete(r.ccs, handle) + case "pk": + delete(r.pks, handle) + case "vk": + delete(r.vks, handle) + } + delete(r.handles, handle) + return js.Undefined(), nil +} + +func (r *Runtime[PK, VK, Proof]) ensurePrepared(handle string, pk pkEntry[PK], ccs *ccsEntry) error { + if pk.prepared { + return nil + } + if ccs != nil && r.cfg.PrepareWithCS != nil { + if err := r.cfg.PrepareWithCS(ccs.value, pk.value); err != nil { + return fmt.Errorf("prepare pk: %w", err) + } + pk.prepared = true + r.pks[handle] = pk + return nil + } + if r.cfg.Prepare != nil { + if err := r.cfg.Prepare(pk.value); err != nil { + return fmt.Errorf("prepare pk: %w", err) + } + } + if r.cfg.PrepareWithCS == nil { + pk.prepared = true + } + r.pks[handle] = pk + return nil +} + +func (r *Runtime[PK, VK, Proof]) ccsFromArg(args []js.Value, index int) (ccsEntry, error) { + handle, err := handleFromArg(args, index) + if err != nil { + return ccsEntry{}, err + } + entry, ok := r.ccs[handle] + if !ok { + return ccsEntry{}, fmt.Errorf("unknown ccs handle %q", handle) + } + return entry, nil +} + +func (r *Runtime[PK, VK, Proof]) pkFromArg(args []js.Value, index int) (string, pkEntry[PK], error) { + handle, err := handleFromArg(args, index) + if err != nil { + return "", pkEntry[PK]{}, err + } + entry, ok := r.pks[handle] + if !ok { + return "", pkEntry[PK]{}, fmt.Errorf("unknown proving key handle %q", handle) + } + return handle, entry, nil +} + +func (r *Runtime[PK, VK, Proof]) vkFromArg(args []js.Value, index int) (vkEntry[VK], error) { + handle, err := handleFromArg(args, index) + if err != nil { + return vkEntry[VK]{}, err + } + entry, ok := r.vks[handle] + if !ok { + return vkEntry[VK]{}, fmt.Errorf("unknown verification key handle %q", handle) + } + return entry, nil +} + +func (r *Runtime[PK, VK, Proof]) store(kind string) string { + r.next++ + handle := fmt.Sprintf("%s:%d", kind, r.next) + r.handles[handle] = kind + return handle +} + +func (r *Runtime[PK, VK, Proof]) curveIDFromArg(args []js.Value, index int) (ecc.ID, error) { + if len(args) <= index || args[index].Type() != js.TypeString { + return ecc.UNKNOWN, fmt.Errorf("missing curve") + } + name := args[index].String() + curves := r.cfg.SupportedCurves + if len(curves) == 0 { + curves = defaultSupportedCurves + } + curveID, ok := curves[name] + if !ok { + return ecc.UNKNOWN, fmt.Errorf("unsupported curve %q", name) + } + return curveID, nil +} + +var defaultSupportedCurves = map[string]ecc.ID{ + "bn254": ecc.BN254, + "bls12_377": ecc.BLS12_377, + "bls12_381": ecc.BLS12_381, +} + +func handleObject(handle string) js.Value { + out := js.Global().Get("Object").New() + out.Set("handle", handle) + return out +} + +func handleFromArg(args []js.Value, index int) (string, error) { + if len(args) <= index || args[index].Type() != js.TypeString { + return "", fmt.Errorf("missing handle") + } + return args[index].String(), nil +} + +func bytesFromArg(args []js.Value, index int) ([]byte, error) { + if len(args) <= index { + return nil, fmt.Errorf("missing bytes argument") + } + src := args[index] + n := src.Get("byteLength") + if n.Type() != js.TypeNumber { + return nil, fmt.Errorf("expected Uint8Array") + } + out := make([]byte, n.Int()) + if len(out) > 0 { + js.CopyBytesToGo(out, src) + } + return out, nil +} + +func jsBytes(src []byte) js.Value { + out := js.Global().Get("Uint8Array").New(len(src)) + if len(src) > 0 { + js.CopyBytesToJS(out, src) + } + return out +} + +func readWitness(curveID ecc.ID, data []byte) (witness.Witness, error) { + w, err := witness.New(curveID.ScalarField()) + if err != nil { + return nil, err + } + if _, err := w.ReadFrom(bytes.NewReader(data)); err != nil { + return nil, err + } + return w, nil +} + +func readFromBytes(value any, label string, data []byte) error { + reader, ok := value.(interface { + ReadFrom(io.Reader) (int64, error) + }) + if !ok { + return fmt.Errorf("%s does not support ReadFrom", label) + } + if _, err := reader.ReadFrom(bytes.NewReader(data)); err != nil { + return fmt.Errorf("read %s: %w", label, err) + } + return nil +} + +func writeToBytes(value any) ([]byte, error) { + writer, ok := value.(interface { + WriteTo(io.Writer) (int64, error) + }) + if !ok { + return nil, fmt.Errorf("value does not support WriteTo") + } + var buf bytes.Buffer + if _, err := writer.WriteTo(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/backend/accelerated/webgpu/plonk/bls12-377/caching.go b/backend/accelerated/webgpu/plonk/bls12-377/caching.go new file mode 100644 index 0000000000..58b0f66f52 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bls12-377/caching.go @@ -0,0 +1,530 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bls12377 + +import ( + "encoding/binary" + "errors" + "fmt" + "math/big" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/iop" + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" + native "github.com/consensys/gnark/backend/plonk/bls12-377" +) + +const ( + quotientBaseDynamicVectorCount = 5 + quotientBaseStaticVectorCount = 7 + quotientEvalScalarCount = 7 +) + +var quotientTransformCacheKey int +var quotientStaticMontCacheKey int +var quotientAuxMontCacheKey int +var cacheKeyMu sync.Mutex + +type staticNumeratorCache struct { + domain0Cardinality uint64 + domain1Cardinality uint64 + qcpCount int + canonical staticNumeratorPolys + cosets []staticNumeratorPolys + quotientAux quotientAuxCache + webgpuStaticMontKeys []int + webgpuStaticMontPopulated []bool + webgpuAuxMontKey int + webgpuAuxMontPopulated bool +} + +type staticNumeratorPolys struct { + ql, qr, qm, qo *iop.Polynomial + s1, s2, s3 *iop.Polynomial + qcp []*iop.Polynomial +} + +type quotientAuxCache struct { + twiddlesPacked []byte + scalingPacked []byte + denominatorsPacked []byte + cosets []fr.Element + cosetExpMinusOnes []fr.Element + lagrangeScales []fr.Element + cs, css fr.Element +} + +func nextCacheKey(counter *int) int { + cacheKeyMu.Lock() + defer cacheKeyMu.Unlock() + + *counter = *counter + 1 + if *counter <= 0 { + *counter = 1 + } + return *counter +} + +func (pk *ProvingKey) ensureStaticNumeratorCache(trace *native.Trace, domain0, domain1 *fft.Domain) error { + qcpCount := len(trace.Qcp) + if pk.staticNumeratorCache != nil && + pk.staticNumeratorCache.domain0Cardinality == domain0.Cardinality && + pk.staticNumeratorCache.domain1Cardinality == domain1.Cardinality && + pk.staticNumeratorCache.qcpCount == qcpCount { + if len(pk.staticNumeratorCache.webgpuStaticMontKeys) != len(pk.staticNumeratorCache.cosets) { + pk.staticNumeratorCache.webgpuStaticMontKeys = make([]int, len(pk.staticNumeratorCache.cosets)) + for i := range pk.staticNumeratorCache.webgpuStaticMontKeys { + pk.staticNumeratorCache.webgpuStaticMontKeys[i] = nextCacheKey("ientStaticMontCacheKey) + } + pk.staticNumeratorCache.webgpuStaticMontPopulated = make([]bool, len(pk.staticNumeratorCache.cosets)) + } + if len(pk.staticNumeratorCache.webgpuStaticMontPopulated) != len(pk.staticNumeratorCache.cosets) { + pk.staticNumeratorCache.webgpuStaticMontPopulated = make([]bool, len(pk.staticNumeratorCache.cosets)) + } + if err := pk.staticNumeratorCache.ensureQuotientAuxCache(domain0, domain1); err != nil { + return err + } + pk.staticNumeratorCache.canonical.applyToTrace(trace) + return nil + } + + canonical := cloneStaticNumeratorPolys(trace) + if err := canonicalizePolynomialsRegularWithWebGPU(canonical.polynomials(), int(domain0.Cardinality)); err != nil { + return err + } + + rho := int(domain1.Cardinality / domain0.Cardinality) + cosets := make([]staticNumeratorPolys, rho) + webgpuStaticMontKeys := make([]int, rho) + for i := range webgpuStaticMontKeys { + webgpuStaticMontKeys[i] = nextCacheKey("ientStaticMontCacheKey) + } + + cosetTable, err := domain0.CosetTable() + if err != nil { + return err + } + scalingVector := cosetTable + working := canonical.clone() + for i := 0; i < rho; i++ { + if i == 1 { + w := domain1.Generator + scalingVector = make([]fr.Element, domain0.Cardinality) + fft.BuildExpTable(w, scalingVector) + } + + if err := transformPolynomialsToCoset(working.polynomials(), domain0, scalingVector); err != nil { + return err + } + cosets[i] = working.clone() + } + quotientAux, err := buildQuotientAuxCache(domain0, domain1) + if err != nil { + return err + } + + pk.staticNumeratorCache = &staticNumeratorCache{ + domain0Cardinality: domain0.Cardinality, + domain1Cardinality: domain1.Cardinality, + qcpCount: qcpCount, + canonical: canonical, + cosets: cosets, + quotientAux: quotientAux, + webgpuStaticMontKeys: webgpuStaticMontKeys, + webgpuStaticMontPopulated: make([]bool, rho), + webgpuAuxMontKey: nextCacheKey("ientAuxMontCacheKey), + } + pk.staticNumeratorCache.canonical.applyToTrace(trace) + return nil +} + +func (c *staticNumeratorCache) ensureQuotientAuxCache(domain0, domain1 *fft.Domain) error { + n := int(domain0.Cardinality) + rho := int(domain1.Cardinality / domain0.Cardinality) + if c.webgpuAuxMontKey <= 0 { + c.webgpuAuxMontKey = nextCacheKey("ientAuxMontCacheKey) + c.webgpuAuxMontPopulated = false + } + if c.quotientAux.valid(n, rho) { + return nil + } + aux, err := buildQuotientAuxCache(domain0, domain1) + if err != nil { + return err + } + c.quotientAux = aux + c.webgpuAuxMontPopulated = false + return nil +} + +func (a quotientAuxCache) valid(n, rho int) bool { + vectorBytes := n * frBytes + return rho > 0 && + len(a.twiddlesPacked) == vectorBytes && + len(a.scalingPacked) == rho*vectorBytes && + len(a.denominatorsPacked) == rho*vectorBytes && + len(a.cosets) == rho && + len(a.cosetExpMinusOnes) == rho && + len(a.lagrangeScales) == rho +} + +func buildQuotientAuxCache(domain0, domain1 *fft.Domain) (quotientAuxCache, error) { + n := int(domain0.Cardinality) + rho := int(domain1.Cardinality / domain0.Cardinality) + if n <= 0 || rho <= 0 { + return quotientAuxCache{}, fmt.Errorf("webgpu plonk bls12_377: invalid quotient auxiliary domain n=%d rho=%d", n, rho) + } + + twiddles0 := make([]fr.Element, n) + if n == 1 { + twiddles0[0].SetOne() + } else { + twiddles, err := domain0.Twiddles() + if err != nil { + return quotientAuxCache{}, err + } + copy(twiddles0, twiddles[0]) + w := twiddles0[1] + for i := len(twiddles[0]); i < len(twiddles0); i++ { + twiddles0[i].Mul(&twiddles0[i-1], &w) + } + } + + cosetTable, err := domain0.CosetTable() + if err != nil { + return quotientAuxCache{}, err + } + + vectorBytes := n * frBytes + aux := quotientAuxCache{ + twiddlesPacked: packFrVectorRegularLEInto(nil, twiddles0), + scalingPacked: make([]byte, rho*vectorBytes), + denominatorsPacked: make([]byte, rho*vectorBytes), + cosets: make([]fr.Element, rho), + cosetExpMinusOnes: make([]fr.Element, rho), + lagrangeScales: make([]fr.Element, rho), + } + aux.cs.Set(&domain1.FrMultiplicativeGen) + aux.css.Square(&aux.cs) + + shifters := make([]fr.Element, rho) + shifters[0].Set(&domain1.FrMultiplicativeGen) + for i := 1; i < rho; i++ { + shifters[i].Set(&domain1.Generator) + } + + denominators := make([]fr.Element, n) + bufBatchInvert := make([]fr.Element, n) + scalingVector := make([]fr.Element, n) + var coset, cosetExpMinusOne, one fr.Element + coset.SetOne() + one.SetOne() + bn := big.NewInt(int64(domain0.Cardinality)) + for i := 0; i < rho; i++ { + coset.Mul(&coset, &shifters[i]) + aux.cosets[i].Set(&coset) + cosetExpMinusOne.Exp(coset, bn).Sub(&cosetExpMinusOne, &one) + aux.cosetExpMinusOnes[i].Set(&cosetExpMinusOne) + aux.lagrangeScales[i].Mul(&cosetExpMinusOne, &domain0.CardinalityInv) + + for j := 0; j < n; j++ { + denominators[j].Mul(&coset, &twiddles0[j]).Sub(&denominators[j], &one) + } + batchInvert(denominators, bufBatchInvert) + packFrVectorRegularLEInto(aux.denominatorsPacked[i*vectorBytes:(i+1)*vectorBytes], denominators) + + currentScalingVector := scalingVector + if i == 0 { + currentScalingVector = cosetTable + } else { + fft.BuildExpTable(coset, scalingVector) + } + packFrVectorRegularLEInto(aux.scalingPacked[i*vectorBytes:(i+1)*vectorBytes], currentScalingVector) + } + return aux, nil +} + +func cloneStaticNumeratorPolys(trace *native.Trace) staticNumeratorPolys { + res := staticNumeratorPolys{ + ql: trace.Ql.Clone(), + qr: trace.Qr.Clone(), + qm: trace.Qm.Clone(), + qo: trace.Qo.Clone(), + s1: trace.S1.Clone(), + s2: trace.S2.Clone(), + s3: trace.S3.Clone(), + qcp: make([]*iop.Polynomial, len(trace.Qcp)), + } + for i := range trace.Qcp { + res.qcp[i] = trace.Qcp[i].Clone() + } + return res +} + +func (p staticNumeratorPolys) clone() staticNumeratorPolys { + res := staticNumeratorPolys{ + ql: p.ql.Clone(), + qr: p.qr.Clone(), + qm: p.qm.Clone(), + qo: p.qo.Clone(), + s1: p.s1.Clone(), + s2: p.s2.Clone(), + s3: p.s3.Clone(), + qcp: make([]*iop.Polynomial, len(p.qcp)), + } + for i := range p.qcp { + res.qcp[i] = p.qcp[i].Clone() + } + return res +} + +func (p staticNumeratorPolys) polynomials() []*iop.Polynomial { + res := []*iop.Polynomial{p.ql, p.qr, p.qm, p.qo, p.s1, p.s2, p.s3} + res = append(res, p.qcp...) + return res +} + +func (p staticNumeratorPolys) applyToTrace(trace *native.Trace) { + trace.Ql = p.ql + trace.Qr = p.qr + trace.Qm = p.qm + trace.Qo = p.qo + trace.S1 = p.s1 + trace.S2 = p.s2 + trace.S3 = p.s3 + trace.Qcp = p.qcp +} + +func transformPolynomialsToCoset(polys []*iop.Polynomial, domain *fft.Domain, scalingVector []fr.Element) error { + // shift polynomials to be in the correct coset + if err := canonicalizePolynomialsRegularWithWebGPU(polys, int(domain.Cardinality)); err != nil { + return err + } + + // scale by shifter + for _, p := range polys { + cp := p.Coefficients() + for j := range cp { + cp[j].Mul(&cp[j], &scalingVector[j]) + } + } + return lagrangePolynomialsRegularWithWebGPU(polys, int(domain.Cardinality)) +} + +func (pk *ProvingKey) ensureStaticNumeratorCacheForTrace(trace *native.Trace, domain0, domain1 *fft.Domain) error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + return pk.ensureStaticNumeratorCache(trace, domain0, domain1) +} + +func (pk *ProvingKey) preloadQuotientStaticCaches(trace *native.Trace, domain0, domain1 *fft.Domain) error { + staticCache := pk.staticNumeratorCache + if staticCache == nil { + return errors.New("webgpu plonk bls12_377: missing static numerator cache") + } + n := int(domain0.Cardinality) + rho := int(domain1.Cardinality / domain0.Cardinality) + if len(staticCache.cosets) != rho { + return fmt.Errorf("webgpu plonk bls12_377: static numerator cache has %d cosets, expected %d", len(staticCache.cosets), rho) + } + quotientAux := staticCache.quotientAux + if !quotientAux.valid(n, rho) { + return errors.New("webgpu plonk bls12_377: invalid quotient auxiliary cache") + } + auxMontCacheKey := staticCache.webgpuAuxMontKey + if auxMontCacheKey <= 0 || int(uint32(auxMontCacheKey)) != auxMontCacheKey { + return fmt.Errorf("webgpu plonk bls12_377: invalid quotient auxiliary WebGPU cache key %d", auxMontCacheKey) + } + + commitmentCount := len(trace.Qcp) + staticVectorCount := quotientBaseStaticVectorCount + commitmentCount + vectorBytes := n * frBytes + staticPacked, err := packStaticNumeratorCosets(staticCache, rho, staticVectorCount, commitmentCount, n, vectorBytes) + if err != nil { + return err + } + + staticMontCacheKeysPacked, err := packStaticMontCacheKeys(staticCache, rho) + if err != nil { + return err + } + + if err := bridge.Bridge.PreloadQuotientStaticAndAux( + "bls12_377", + staticPacked, + staticMontCacheKeysPacked, + quotientAux.scalingPacked, + quotientAux.twiddlesPacked, + quotientAux.denominatorsPacked, + n, + staticVectorCount, + rho, + auxMontCacheKey, + ); err != nil { + return err + } + + for i := range staticCache.webgpuStaticMontPopulated { + staticCache.webgpuStaticMontPopulated[i] = true + } + staticCache.webgpuAuxMontPopulated = true + return nil +} + +type quotientStaticBridgeInputs struct { + staticCache *staticNumeratorCache + quotientAux quotientAuxCache + staticPacked []byte + staticMontCacheKeysPacked []byte + twiddlesPacked []byte + scalingPacked []byte + denominatorsPacked []byte + auxMontCacheKey int +} + +func (pk *ProvingKey) quotientStaticBridgeInputs(rho, staticVectorCount, commitmentCount, n, vectorBytes int) (quotientStaticBridgeInputs, error) { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + staticCache := pk.staticNumeratorCache + if staticCache == nil || len(staticCache.cosets) != rho { + return quotientStaticBridgeInputs{}, errors.New("missing static numerator cache") + } + quotientAux := staticCache.quotientAux + if !quotientAux.valid(n, rho) { + return quotientStaticBridgeInputs{}, errors.New("webgpu plonk bls12_377: invalid quotient auxiliary cache") + } + staticMontCacheKeysPacked, err := packStaticMontCacheKeys(staticCache, rho) + if err != nil { + return quotientStaticBridgeInputs{}, err + } + + reuseStaticMontCache := true + for i := 0; i < rho; i++ { + if !staticCache.webgpuStaticMontPopulated[i] { + reuseStaticMontCache = false + } + } + auxMontCacheKey := staticCache.webgpuAuxMontKey + if auxMontCacheKey <= 0 || int(uint32(auxMontCacheKey)) != auxMontCacheKey { + return quotientStaticBridgeInputs{}, fmt.Errorf("webgpu plonk bls12_377: invalid quotient auxiliary WebGPU cache key %d", auxMontCacheKey) + } + + var staticPacked []byte + if !reuseStaticMontCache { + staticPacked, err = packStaticNumeratorCosets(staticCache, rho, staticVectorCount, commitmentCount, n, vectorBytes) + if err != nil { + return quotientStaticBridgeInputs{}, err + } + } + + twiddlesPacked := quotientAux.twiddlesPacked + scalingPacked := quotientAux.scalingPacked + denominatorsPacked := quotientAux.denominatorsPacked + if staticCache.webgpuAuxMontPopulated { + twiddlesPacked = nil + scalingPacked = nil + denominatorsPacked = nil + } + + return quotientStaticBridgeInputs{ + staticCache: staticCache, + quotientAux: quotientAux, + staticPacked: staticPacked, + staticMontCacheKeysPacked: staticMontCacheKeysPacked, + twiddlesPacked: twiddlesPacked, + scalingPacked: scalingPacked, + denominatorsPacked: denominatorsPacked, + auxMontCacheKey: auxMontCacheKey, + }, nil +} + +func (pk *ProvingKey) markQuotientStaticBridgeInputsPopulated(staticCache *staticNumeratorCache) { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + for i := range staticCache.webgpuStaticMontPopulated { + staticCache.webgpuStaticMontPopulated[i] = true + } + staticCache.webgpuAuxMontPopulated = true +} + +func packStaticMontCacheKeys(staticCache *staticNumeratorCache, rho int) ([]byte, error) { + if len(staticCache.webgpuStaticMontKeys) != rho || len(staticCache.webgpuStaticMontPopulated) != rho { + return nil, errors.New("webgpu plonk bls12_377: invalid static numerator WebGPU cache metadata") + } + out := make([]byte, rho*4) + for i := 0; i < rho; i++ { + key := staticCache.webgpuStaticMontKeys[i] + if key <= 0 || int(uint32(key)) != key { + return nil, fmt.Errorf("webgpu plonk bls12_377: invalid static numerator WebGPU cache key %d", key) + } + binary.LittleEndian.PutUint32(out[i*4:(i+1)*4], uint32(key)) + } + return out, nil +} + +func packStaticNumeratorCosets(staticCache *staticNumeratorCache, rho, staticVectorCount, commitmentCount, n, vectorBytes int) ([]byte, error) { + staticPacked := make([]byte, rho*staticVectorCount*vectorBytes) + for i := 0; i < rho; i++ { + start := i * staticVectorCount * vectorBytes + if err := packStaticNumeratorPolys( + staticPacked[start:start+staticVectorCount*vectorBytes], + staticCache.cosets[i], + staticVectorCount, + commitmentCount, + n, + vectorBytes, + ); err != nil { + return nil, err + } + } + return staticPacked, nil +} + +func packStaticNumeratorPolys(dst []byte, polys staticNumeratorPolys, staticVectorCount, commitmentCount, n, vectorBytes int) error { + if len(polys.qcp) != commitmentCount { + return fmt.Errorf("webgpu plonk bls12_377: quotient evaluator expected %d qcp vectors, got %d", commitmentCount, len(polys.qcp)) + } + staticVectors := polys.polynomials() + if len(staticVectors) != staticVectorCount { + return fmt.Errorf("webgpu plonk bls12_377: quotient evaluator expected %d static vectors, got %d", staticVectorCount, len(staticVectors)) + } + for i, p := range staticVectors { + if p == nil { + return fmt.Errorf("webgpu plonk bls12_377: missing quotient static polynomial %d", i) + } + coeffs := p.Coefficients() + if len(coeffs) != n { + return fmt.Errorf("webgpu plonk bls12_377: quotient static polynomial %d has %d coefficients, expected %d", i, len(coeffs), n) + } + packFrVectorRegularLEInto(dst[i*vectorBytes:(i+1)*vectorBytes], coeffs) + } + return nil +} + +// batchInvert modifies in place vec, with vec[i]<-vec[i]^{-1}, using +// the Montgomery batch inversion trick. We don't use gnark-crypto's batchInvert +// because we want to use a buffer preallocated, to avoid wasting memory. +// /!\ it doesn't check that all vec's inputs or non zero, it is ensured by the size +// of the field /!\ +func batchInvert(vec, buf []fr.Element) { + // local function only, vec and buf are of the same size + copy(buf, vec) + for i := 1; i < len(vec); i++ { + vec[i].Mul(&vec[i], &vec[i-1]) + } + acc := vec[len(vec)-1] + acc.Inverse(&acc) + for i := len(vec) - 1; i > 0; i-- { + vec[i].Mul(&acc, &vec[i-1]) + acc.Mul(&acc, &buf[i]) + } + vec[0].Set(&acc) +} diff --git a/backend/accelerated/webgpu/plonk/bls12-377/prove.go b/backend/accelerated/webgpu/plonk/bls12-377/prove.go new file mode 100644 index 0000000000..43b7a612b2 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bls12-377/prove.go @@ -0,0 +1,1304 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bls12377 + +import ( + "errors" + "fmt" + "hash" + "math/big" + "math/bits" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/hash_to_field" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/iop" + "github.com/consensys/gnark-crypto/ecc/bls12-377/kzg" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark/backend" + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" + native "github.com/consensys/gnark/backend/plonk/bls12-377" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + cs "github.com/consensys/gnark/constraint/bls12-377" + "github.com/consensys/gnark/constraint/solver" + fcs "github.com/consensys/gnark/frontend/cs" +) + +const ( + id_L int = iota + id_R + id_O + id_Z + id_ZS + id_Ql + id_Qr + id_Qm + id_Qo + id_Qk + id_S1 + id_S2 + id_S3 + id_Qci // [ .. , Qc_i, Pi_i, ...] +) + +// blinding factors +const ( + id_Bl int = iota + id_Br + id_Bo + id_Bz + nb_blinding_polynomials +) + +// blinding orders (-1 to deactivate) +const ( + order_blinding_L = 1 + order_blinding_R = 1 + order_blinding_O = 1 + order_blinding_Z = 2 +) + +func Prove(spr *cs.SparseR1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (proof *native.Proof, err error) { + // parse the options + opt, err := backend.NewProverConfig(opts...) + if err != nil { + return nil, fmt.Errorf("get prover options: %w", err) + } + + if err := pk.Prepare(); err != nil { + return nil, fmt.Errorf("prepare proving key: %w", err) + } + + // init instance + instance, err := newInstance(spr, pk, fullWitness, &opt) + if err != nil { + return nil, fmt.Errorf("new instance: %w", err) + } + + if err := instance.initBlindingPolynomials(); err != nil { + return nil, fmt.Errorf("init blinding polynomials: %w", err) + } + if err := instance.solveConstraints(); err != nil { + return nil, fmt.Errorf("solve constraints: %w", err) + } + if err := instance.completeQk(); err != nil { + return nil, fmt.Errorf("complete qk: %w", err) + } + if err := instance.deriveGammaAndBeta(); err != nil { + return nil, fmt.Errorf("derive gamma and beta: %w", err) + } + if err := instance.buildRatioCopyConstraint(); err != nil { + return nil, fmt.Errorf("build ratio copy constraint: %w", err) + } + if err := instance.computeQuotient(); err != nil { + return nil, fmt.Errorf("compute quotient: %w", err) + } + if err := instance.openZ(); err != nil { + return nil, fmt.Errorf("open z: %w", err) + } + if err := instance.computeLinearizedPolynomial(); err != nil { + return nil, fmt.Errorf("compute linearized polynomial: %w", err) + } + if err := instance.batchOpening(); err != nil { + return nil, fmt.Errorf("batch opening: %w", err) + } + + return instance.proof, nil +} + +// represents a Prover instance +type instance struct { + pk *ProvingKey + proof *native.Proof + spr *cs.SparseR1CS + opt *backend.ProverConfig + + fs *fiatshamir.Transcript + kzgFoldingHash hash.Hash // for KZG folding + htfFunc hash.Hash // hash to field function + + // polynomials + x []*iop.Polynomial // x stores tracks the polynomial we need + bp []*iop.Polynomial // blinding polynomials + h *iop.Polynomial // h is the quotient polynomial + blindedZ []fr.Element // blindedZ is the blinded version of Z + quotientShardsRandomizers [2]fr.Element // random elements for blinding the shards of the quotient + + precomputedDenominators []fr.Element // stores the denominators of the Lagrange polynomials + linearizedPolynomial []fr.Element + linearizedPolynomialDigest kzg.Digest + + fullWitness witness.Witness + + // bsb22 commitment stuff + commitmentInfo constraint.PlonkCommitments + commitmentVal []fr.Element + cCommitments []*iop.Polynomial + + // challenges + gamma, beta, alpha, zeta fr.Element + + domain0, domain1 *fft.Domain + + trace *native.Trace +} + +func newInstance(spr *cs.SparseR1CS, pk *ProvingKey, fullWitness witness.Witness, opts *backend.ProverConfig) (*instance, error) { + if opts.HashToFieldFn == nil { + opts.HashToFieldFn = hash_to_field.New([]byte("BSB22-Plonk")) + } + s := instance{ + pk: pk, + proof: &native.Proof{}, + spr: spr, + opt: opts, + fullWitness: fullWitness, + bp: make([]*iop.Polynomial, nb_blinding_polynomials), + fs: fiatshamir.NewTranscript(opts.ChallengeHash, "gamma", "beta", "alpha", "zeta"), + kzgFoldingHash: opts.KZGFoldingHash, + htfFunc: opts.HashToFieldFn, + } + s.initBSB22Commitments() + s.x = make([]*iop.Polynomial, id_Qci+2*len(s.commitmentInfo)) + + // init fft domains + s.domain0, s.domain1 = domainsForSPR(spr) + + // sampling random numbers for blinding the quotient + if opts.StatisticalZK { + s.quotientShardsRandomizers[0].SetRandom() + s.quotientShardsRandomizers[1].SetRandom() + } + + // build trace + s.trace = native.NewTrace(spr, s.domain0) + if err := pk.ensureStaticNumeratorCacheForTrace(s.trace, s.domain0, s.domain1); err != nil { + return nil, err + } + + return &s, nil +} + +func domainsForSPR(spr *cs.SparseR1CS) (*fft.Domain, *fft.Domain) { + nbConstraints := spr.GetNbConstraints() + sizeSystem := uint64(nbConstraints + len(spr.Public)) // len(spr.Public) is for the placeholder constraints + domain0 := fft.NewDomain(sizeSystem) + + // h, the quotient polynomial is of degree 3(n+1)+2, so it's in a 3(n+2) dim vector space, + // the domain is the next power of 2 superior to 3(n+2). 4*domainNum is enough in all cases + // except when n<6. + var domain1 *fft.Domain + if sizeSystem < 6 { + domain1 = fft.NewDomain(8*sizeSystem, fft.WithoutPrecompute()) + } else { + domain1 = fft.NewDomain(4*sizeSystem, fft.WithoutPrecompute()) + } + return domain0, domain1 +} + +func (s *instance) initBlindingPolynomials() error { + s.bp[id_Bl] = getRandomPolynomial(order_blinding_L) + s.bp[id_Br] = getRandomPolynomial(order_blinding_R) + s.bp[id_Bo] = getRandomPolynomial(order_blinding_O) + s.bp[id_Bz] = getRandomPolynomial(order_blinding_Z) + return nil +} + +func (s *instance) initBSB22Commitments() { + s.commitmentInfo = s.spr.CommitmentInfo.(constraint.PlonkCommitments) + s.commitmentVal = make([]fr.Element, len(s.commitmentInfo)) // TODO @Tabaie get rid of this + s.cCommitments = make([]*iop.Polynomial, len(s.commitmentInfo)) + s.proof.Bsb22Commitments = make([]kzg.Digest, len(s.commitmentInfo)) + + // override the hint for the commitment constraints + bsb22ID := solver.GetHintID(fcs.Bsb22CommitmentComputePlaceholder) + s.opt.SolverOpts = append(s.opt.SolverOpts, solver.OverrideHint(bsb22ID, s.bsb22Hint)) +} + +// Computing and verifying Bsb22 multi-commits explained in https://hackmd.io/x8KsadW3RRyX7YTCFJIkHg +func (s *instance) bsb22Hint(_ *big.Int, ins, outs []*big.Int) error { + var err error + commDepth := int(ins[0].Int64()) + ins = ins[1:] + + res := &s.commitmentVal[commDepth] + + commitmentInfo := s.spr.CommitmentInfo.(constraint.PlonkCommitments)[commDepth] + committedValues := make([]fr.Element, s.domain0.Cardinality) + offset := s.spr.GetNbPublicVariables() + for i := range ins { + committedValues[offset+commitmentInfo.Committed[i]].SetBigInt(ins[i]) + } + if _, err = committedValues[offset+commitmentInfo.CommitmentIndex].SetRandom(); err != nil { // Commitment injection constraint has qcp = 0. Safe to use for blinding. + return err + } + if _, err = committedValues[offset+s.spr.GetNbConstraints()-1].SetRandom(); err != nil { // Last constraint has qcp = 0. Safe to use for blinding + return err + } + s.cCommitments[commDepth] = iop.NewPolynomial(&committedValues, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + if s.proof.Bsb22Commitments[commDepth], err = kzg.Commit(s.cCommitments[commDepth].Coefficients(), s.pk.KzgLagrange, 1); err != nil { + return err + } + + s.htfFunc.Write(s.proof.Bsb22Commitments[commDepth].Marshal()) + hashBts := s.htfFunc.Sum(nil) + s.htfFunc.Reset() + nbBuf := fr.Bytes + if s.htfFunc.Size() < fr.Bytes { + nbBuf = s.htfFunc.Size() + } + res.SetBytes(hashBts[:nbBuf]) // TODO @Tabaie use CommitmentIndex for this; create a new variable CommitmentConstraintIndex for other uses + res.BigInt(outs[0]) + + return nil +} + +// solveConstraints computes the evaluation of the polynomials L, R, O +// and sets x[id_L], x[id_R], x[id_O] in Lagrange form +func (s *instance) solveConstraints() error { + _solution, err := s.spr.Solve(s.fullWitness, s.opt.SolverOpts...) + if err != nil { + return err + } + solution := _solution.(*cs.SparseR1CSSolution) + evaluationLDomainSmall := []fr.Element(solution.L) + evaluationRDomainSmall := []fr.Element(solution.R) + evaluationODomainSmall := []fr.Element(solution.O) + s.x[id_L] = iop.NewPolynomial(&evaluationLDomainSmall, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + s.x[id_R] = iop.NewPolynomial(&evaluationRDomainSmall, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + s.x[id_O] = iop.NewPolynomial(&evaluationODomainSmall, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + + // commit to l, r, o and add blinding factors + if err := s.commitToLRO(); err != nil { + return err + } + return nil +} + +func (s *instance) completeQk() error { + qk := s.trace.Qk.Clone() + qkCoeffs := qk.Coefficients() + + wWitness, ok := s.fullWitness.Vector().(fr.Vector) + if !ok { + return witness.ErrInvalidWitness + } + + copy(qkCoeffs, wWitness[:len(s.spr.Public)]) + + for i := range s.commitmentInfo { + qkCoeffs[s.spr.GetNbPublicVariables()+s.commitmentInfo[i].CommitmentIndex] = s.commitmentVal[i] + } + + s.x[id_Qk] = qk + + return nil +} + +// commitToLRO commits to L, R, O polynomials using reduced-size MSMs. +// +// L, R, O live on a domain of size n = 2^k, but only offset = nbPublic + nbConstraints +// entries carry actual values. The rest are s0 = witness[0] (first public input). +// For R and O, the first nbPublic entries (placeholders) are also s0. +// +// Key identity: Σ_{i=0}^{n-1} KzgLagrange.G1[i] = [Σ L_i(τ)]₁ = [1]₁ = Kzg.G1[0] +// +// So we can rewrite the commitment as: +// +// [P] = Σ P[i]·G1_lag[i] +// = Σ (P[i]-s0)·G1_lag[i] + s0·Σ G1_lag[i] +// = MSM((P[i]-s0), G1_lag[i]) + s0·Kzg.G1[0] +// +// The (P[i]-s0) terms are zero in the padding region, so the MSM only needs +// the non-padding entries. For a 2.2M-constraint circuit on a 4M domain, +// this nearly halves each MSM. +func (s *instance) commitToLRO() error { + n := int(s.domain0.Cardinality) + nbPublic := len(s.spr.Public) + offset := nbPublic + s.spr.GetNbConstraints() + + // s0 = witness[0] = first public input + wWitness, ok := s.fullWitness.Vector().(fr.Vector) + if !ok { + return witness.ErrInvalidWitness + } + s0 := wWitness[0] + + // correctionPoint = s0 · [1]₁ = s0 · Kzg.G1[0] + var s0BigInt big.Int + s0.BigInt(&s0BigInt) + var correctionPoint curve.G1Affine + correctionPoint.ScalarMultiplication(&s.pk.Kzg.G1[0], &s0BigInt) + + // L: subtract s0, MSM on [0:offset], add correction + blinding, restore + coeffs := s.x[id_L].Coefficients() + for i := 0; i < offset; i++ { + coeffs[i].Sub(&coeffs[i], &s0) + } + var commit curve.G1Affine + commit, err := s.msmG1("kzgLagrange", 0, coeffs[:offset]) + if err != nil { + return err + } + for i := 0; i < offset; i++ { + coeffs[i].Add(&coeffs[i], &s0) + } + commit.Add(&commit, &correctionPoint) + cb := commitBlindingFactor(n, s.bp[id_Bl], s.pk.Kzg) + s.proof.LRO[0].Add(&commit, &cb) + + // R: subtract s0, MSM on [nbPublic:offset], add correction + blinding, restore + coeffs = s.x[id_R].Coefficients() + for i := nbPublic; i < offset; i++ { + coeffs[i].Sub(&coeffs[i], &s0) + } + commit, err = s.msmG1("kzgLagrange", nbPublic, coeffs[nbPublic:offset]) + if err != nil { + return err + } + for i := nbPublic; i < offset; i++ { + coeffs[i].Add(&coeffs[i], &s0) + } + commit.Add(&commit, &correctionPoint) + cb = commitBlindingFactor(n, s.bp[id_Br], s.pk.Kzg) + s.proof.LRO[1].Add(&commit, &cb) + + // O: same as R + coeffs = s.x[id_O].Coefficients() + for i := nbPublic; i < offset; i++ { + coeffs[i].Sub(&coeffs[i], &s0) + } + commit, err = s.msmG1("kzgLagrange", nbPublic, coeffs[nbPublic:offset]) + if err != nil { + return err + } + for i := nbPublic; i < offset; i++ { + coeffs[i].Add(&coeffs[i], &s0) + } + commit.Add(&commit, &correctionPoint) + cb = commitBlindingFactor(n, s.bp[id_Bo], s.pk.Kzg) + s.proof.LRO[2].Add(&commit, &cb) + + return nil +} + +func (s *instance) msmG1(vectorName string, start int, scalars []fr.Element) (curve.G1Affine, error) { + scalarsPacked := packFrVectorRegularLEInto(nil, scalars) + packed, err := bridge.Bridge.MSMG1Slice(s.pk.handle, vectorName, start, len(scalars), scalarsPacked) + return decodeG1AffineFromPacked(packed, err) +} + +func (s *instance) msmG1Batch(vectorName string, start int, scalarVectors ...[]fr.Element) ([]curve.G1Affine, error) { + if len(scalarVectors) == 0 { + return nil, errors.New("webgpu plonk bls12_377: empty MSM batch") + } + termCount := 0 + for _, scalars := range scalarVectors { + if len(scalars) > termCount { + termCount = len(scalars) + } + } + scalarsPacked, err := packFrVectorsRegularLEPaddedInto(nil, scalarVectors, termCount) + if err != nil { + return nil, err + } + packed, err := bridge.Bridge.MSMG1Batch(s.pk.handle, vectorName, start, termCount, len(scalarVectors), scalarsPacked) + return decodeG1AffineBatchFromPacked(packed, len(scalarVectors), err) +} + +// deriveGammaAndBeta (copy constraint) +func (s *instance) deriveGammaAndBeta() error { + wWitness, ok := s.fullWitness.Vector().(fr.Vector) + if !ok { + return witness.ErrInvalidWitness + } + + if err := bindPublicData(s.fs, "gamma", s.pk.Vk, wWitness[:len(s.spr.Public)]); err != nil { + return err + } + + gamma, err := deriveRandomness(s.fs, "gamma", &s.proof.LRO[0], &s.proof.LRO[1], &s.proof.LRO[2]) + if err != nil { + return err + } + + bbeta, err := s.fs.ComputeChallenge("beta") + if err != nil { + return err + } + s.gamma = gamma + s.beta.SetBytes(bbeta) + + return nil +} + +// commitToPolyAndBlinding computes the KZG commitment of a polynomial p +// in Lagrange form (large degree) +// and add the contribution of a blinding polynomial b (small degree) +// /!\ The polynomial p is supposed to be in Lagrange form. +func (s *instance) commitToPolyAndBlinding(p, b *iop.Polynomial) (commit curve.G1Affine, err error) { + + commit, err = s.msmG1("kzgLagrange", 0, p.Coefficients()) + + // we add in the blinding contribution + n := int(s.domain0.Cardinality) + cb := commitBlindingFactor(n, b, s.pk.Kzg) + commit.Add(&commit, &cb) + + return +} + +func (s *instance) deriveAlpha() (err error) { + alphaDeps := make([]*curve.G1Affine, len(s.proof.Bsb22Commitments)+1) + for i := range s.proof.Bsb22Commitments { + alphaDeps[i] = &s.proof.Bsb22Commitments[i] + } + alphaDeps[len(alphaDeps)-1] = &s.proof.Z + s.alpha, err = deriveRandomness(s.fs, "alpha", alphaDeps...) + return err +} + +func (s *instance) deriveZeta() (err error) { + s.zeta, err = deriveRandomness(s.fs, "zeta", &s.proof.H[0], &s.proof.H[1], &s.proof.H[2]) + return +} + +// computeQuotient computes H +func (s *instance) computeQuotient() (err error) { + s.x[id_Ql] = s.trace.Ql + s.x[id_Qr] = s.trace.Qr + s.x[id_Qm] = s.trace.Qm + s.x[id_Qo] = s.trace.Qo + s.x[id_S1] = s.trace.S1 + s.x[id_S2] = s.trace.S2 + s.x[id_S3] = s.trace.S3 + + for i := 0; i < len(s.commitmentInfo); i++ { + s.x[id_Qci+2*i] = s.trace.Qcp[i] + } + + n := s.domain0.Cardinality + lone := make([]fr.Element, n) + lone[0].SetOne() + + for i := 0; i < len(s.commitmentInfo); i++ { + s.x[id_Qci+2*i+1] = s.cCommitments[i] + } + + // derive alpha + if err = s.deriveAlpha(); err != nil { + return err + } + + // TODO complete waste of memory find another way to do that + identity := make([]fr.Element, n) + identity[1].Set(&s.beta) + + s.x[id_ZS] = s.x[id_Z].ShallowClone().Shift(1) + + numerator, err := s.computeNumerator() + if err != nil { + return err + } + + s.h, err = divideByZH(numerator, [2]*fft.Domain{s.domain0, s.domain1}) + if err != nil { + return err + } + + // commit to h + if err := s.commitToQuotient(s.h1(), s.h2(), s.h3()); err != nil { + return err + } + + if err := s.deriveZeta(); err != nil { + return err + } + + return nil +} + +func (s *instance) buildRatioCopyConstraint() (err error) { + // TODO @gbotrel having iop.BuildRatioCopyConstraint return something + // with capacity = len() + 4 would avoid extra alloc / copy during openZ + s.x[id_Z], err = iop.BuildRatioCopyConstraint( + []*iop.Polynomial{ + s.x[id_L], + s.x[id_R], + s.x[id_O], + }, + s.trace.S, + s.beta, + s.gamma, + iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}, + s.domain0, + ) + if err != nil { + return err + } + + // commit to the blinded version of z + s.proof.Z, err = s.commitToPolyAndBlinding(s.x[id_Z], s.bp[id_Bz]) + + return +} + +// open Z (blinded) at ωζ +func (s *instance) openZ() (err error) { + var zetaShifted fr.Element + zetaShifted.Mul(&s.zeta, &s.pk.Vk.Generator) + s.blindedZ = getBlindedCoefficients(s.x[id_Z], s.bp[id_Bz]) + // open z at zeta + s.proof.ZShiftedOpening, err = s.openKZG(s.blindedZ, zetaShifted) + if err != nil { + return err + } + return nil +} + +func (s *instance) openKZG(p []fr.Element, point fr.Element) (kzg.OpeningProof, error) { + if len(p) > len(s.pk.Kzg.G1) { + return kzg.OpeningProof{}, kzg.ErrInvalidPolynomialSize + } + + var proof kzg.OpeningProof + proof.ClaimedValue = evalKZGPolynomial(p, point) + + cp := make([]fr.Element, len(p)) + copy(cp, p) + h := dividePolyByXMinusA(cp, proof.ClaimedValue, point) + + hCommit, err := s.msmG1("kzg", 0, h) + if err != nil { + return kzg.OpeningProof{}, err + } + proof.H.Set(&hCommit) + + return proof, nil +} + +func (s *instance) h1() []fr.Element { + var h1 []fr.Element + if !s.opt.StatisticalZK { + h1 = s.h.Coefficients()[:s.domain0.Cardinality+2] + } else { + h1 = make([]fr.Element, s.domain0.Cardinality+3) + copy(h1, s.h.Coefficients()[:s.domain0.Cardinality+2]) + h1[s.domain0.Cardinality+2].Set(&s.quotientShardsRandomizers[0]) + } + return h1 +} + +func (s *instance) h2() []fr.Element { + var h2 []fr.Element + if !s.opt.StatisticalZK { + h2 = s.h.Coefficients()[s.domain0.Cardinality+2 : 2*(s.domain0.Cardinality+2)] + } else { + h2 = make([]fr.Element, s.domain0.Cardinality+3) + copy(h2, s.h.Coefficients()[s.domain0.Cardinality+2:2*(s.domain0.Cardinality+2)]) + h2[0].Sub(&h2[0], &s.quotientShardsRandomizers[0]) + h2[s.domain0.Cardinality+2].Set(&s.quotientShardsRandomizers[1]) + } + return h2 +} + +func (s *instance) h3() []fr.Element { + var h3 []fr.Element + if !s.opt.StatisticalZK { + h3 = s.h.Coefficients()[2*(s.domain0.Cardinality+2) : 3*(s.domain0.Cardinality+2)] + } else { + h3 = make([]fr.Element, s.domain0.Cardinality+2) + copy(h3, s.h.Coefficients()[2*(s.domain0.Cardinality+2):3*(s.domain0.Cardinality+2)]) + h3[0].Sub(&h3[0], &s.quotientShardsRandomizers[1]) + } + return h3 +} + +func (s *instance) computeLinearizedPolynomial() error { + qcpzeta := make([]fr.Element, len(s.commitmentInfo)) + for i := range s.commitmentInfo { + qcpzeta[i] = s.trace.Qcp[i].Evaluate(s.zeta) + } + + blzeta := evaluateBlinded(s.x[id_L], s.bp[id_Bl], s.zeta) + brzeta := evaluateBlinded(s.x[id_R], s.bp[id_Br], s.zeta) + bozeta := evaluateBlinded(s.x[id_O], s.bp[id_Bo], s.zeta) + bzuzeta := s.proof.ZShiftedOpening.ClaimedValue + + linearizedPolynomial, err := s.innerComputeLinearizedPoly( + blzeta, + brzeta, + bozeta, + s.alpha, + s.beta, + s.gamma, + s.zeta, + bzuzeta, + qcpzeta, + s.blindedZ, + coefficients(s.cCommitments), + s.pk, + ) + if err != nil { + return err + } + s.linearizedPolynomial = linearizedPolynomial + + s.linearizedPolynomialDigest, err = s.msmG1("kzg", 0, s.linearizedPolynomial) + return err +} + +func (s *instance) batchOpening() error { + polysQcp := coefficients(s.trace.Qcp) + polysToOpen := make([][]fr.Element, 6+len(polysQcp)) + copy(polysToOpen[6:], polysQcp) + + polysToOpen[0] = s.linearizedPolynomial + polysToOpen[1] = getBlindedCoefficients(s.x[id_L], s.bp[id_Bl]) + polysToOpen[2] = getBlindedCoefficients(s.x[id_R], s.bp[id_Br]) + polysToOpen[3] = getBlindedCoefficients(s.x[id_O], s.bp[id_Bo]) + polysToOpen[4] = s.trace.S1.Coefficients() + polysToOpen[5] = s.trace.S2.Coefficients() + + digestsToOpen := make([]curve.G1Affine, len(s.pk.Vk.Qcp)+6) + copy(digestsToOpen[6:], s.pk.Vk.Qcp) + + digestsToOpen[0] = s.linearizedPolynomialDigest + digestsToOpen[1] = s.proof.LRO[0] + digestsToOpen[2] = s.proof.LRO[1] + digestsToOpen[3] = s.proof.LRO[2] + digestsToOpen[4] = s.pk.Vk.S[0] + digestsToOpen[5] = s.pk.Vk.S[1] + + var err error + s.proof.BatchedProof, err = s.batchOpenSinglePoint( + polysToOpen, + digestsToOpen, + s.zeta, + s.kzgFoldingHash, + s.proof.ZShiftedOpening.ClaimedValue.Marshal(), + ) + return err +} + +func (s *instance) batchOpenSinglePoint(polynomials [][]fr.Element, digests []curve.G1Affine, point fr.Element, hf hash.Hash, dataTranscript ...[]byte) (kzg.BatchOpeningProof, error) { + nbDigests := len(digests) + if nbDigests != len(polynomials) { + return kzg.BatchOpeningProof{}, kzg.ErrInvalidNbDigests + } + if nbDigests == 0 { + return kzg.BatchOpeningProof{}, kzg.ErrZeroNbDigests + } + + largestPoly := -1 + for _, p := range polynomials { + if len(p) > len(s.pk.Kzg.G1) { + return kzg.BatchOpeningProof{}, kzg.ErrInvalidPolynomialSize + } + if len(p) > largestPoly { + largestPoly = len(p) + } + } + + var res kzg.BatchOpeningProof + res.ClaimedValues = make([]fr.Element, len(polynomials)) + for i := range polynomials { + res.ClaimedValues[i] = evalKZGPolynomial(polynomials[i], point) + } + + gamma, err := deriveKZGBatchGamma(point, digests, res.ClaimedValues, hf, dataTranscript...) + if err != nil { + return kzg.BatchOpeningProof{}, err + } + + var foldedEvaluations fr.Element + foldedEvaluations = res.ClaimedValues[nbDigests-1] + for i := nbDigests - 2; i >= 0; i-- { + foldedEvaluations.Mul(&foldedEvaluations, &gamma). + Add(&foldedEvaluations, &res.ClaimedValues[i]) + } + + foldedPolynomials := make([]fr.Element, largestPoly) + copy(foldedPolynomials, polynomials[0]) + + gammaPower := gamma + for i := 1; i < len(polynomials); i++ { + var term fr.Element + for j := range polynomials[i] { + term.Mul(&polynomials[i][j], &gammaPower) + foldedPolynomials[j].Add(&foldedPolynomials[j], &term) + } + gammaPower.Mul(&gammaPower, &gamma) + } + + h := dividePolyByXMinusA(foldedPolynomials, foldedEvaluations, point) + + hCommit, err := s.msmG1("kzg", 0, h) + if err != nil { + return kzg.BatchOpeningProof{}, err + } + res.H.Set(&hCommit) + + return res, nil +} + +// evaluate the full set of constraints, all polynomials in x are back in +// canonical regular form at the end +func (s *instance) computeNumerator() (*iop.Polynomial, error) { + // init vectors that are used multiple times throughout the computation + n := s.domain0.Cardinality + + rho := int(s.domain1.Cardinality / n) + + // init the result polynomial & buffer + cres := make([]fr.Element, s.domain1.Cardinality) + buf := make([]fr.Element, n) + + // pre-computed to compute the bit reverse index + // of the result polynomial + m := uint64(s.domain1.Cardinality) + mm := uint64(64 - bits.TrailingZeros64(m)) + + dynamicPolyIDs := []int{id_L, id_R, id_O, id_Z, id_Qk} + commitmentValuePolyIDs := make([]int, 0, len(s.commitmentInfo)) + for i := range s.commitmentInfo { + commitmentValuePolyIDs = append(commitmentValuePolyIDs, id_Qci+2*i+1) + } + quotientDynamicPolyIDs := append(append([]int(nil), dynamicPolyIDs...), commitmentValuePolyIDs...) + dynamicTransformCacheKey := nextCacheKey("ientTransformCacheKey) + + vectorBytes := int(n) * frBytes + commitmentCount := len(quotientDynamicPolyIDs) - quotientBaseDynamicVectorCount + if commitmentCount < 0 { + return nil, fmt.Errorf("webgpu plonk bls12_377: quotient evaluator expected at least %d dynamic vectors, got %d", quotientBaseDynamicVectorCount, len(quotientDynamicPolyIDs)) + } + staticVectorCount := quotientBaseStaticVectorCount + commitmentCount + staticInputs, err := s.pk.quotientStaticBridgeInputs(rho, staticVectorCount, commitmentCount, int(n), vectorBytes) + if err != nil { + return nil, err + } + quotientAux := staticInputs.quotientAux + + dynamicPacked := make([]byte, len(quotientDynamicPolyIDs)*vectorBytes) + for i, id := range quotientDynamicPolyIDs { + if id >= len(s.x) || s.x[id] == nil { + return nil, fmt.Errorf("webgpu plonk bls12_377: missing quotient dynamic polynomial %d", id) + } + coeffs := s.x[id].Coefficients() + if len(coeffs) != int(n) { + return nil, fmt.Errorf("webgpu plonk bls12_377: quotient dynamic polynomial %d has %d coefficients, expected %d", id, len(coeffs), n) + } + packFrVectorRegularLEInto(dynamicPacked[i*vectorBytes:(i+1)*vectorBytes], coeffs) + } + + blinds := [][]fr.Element{ + s.bp[id_Bl].Coefficients(), + s.bp[id_Br].Coefficients(), + s.bp[id_Bo].Coefficients(), + s.bp[id_Bz].Coefficients(), + } + blindCoeffCount := 0 + for _, blind := range blinds { + if len(blind) > blindCoeffCount { + blindCoeffCount = len(blind) + } + } + + blindBytes := len(blinds) * blindCoeffCount * frBytes + blindsPacked := make([]byte, rho*blindBytes) + scalarBytes := quotientEvalScalarCount * frBytes + scalarsPacked := make([]byte, rho*scalarBytes) + + for i := 0; i < rho; i++ { + coset := quotientAux.cosets[i] + cosetExpMinusOne := quotientAux.cosetExpMinusOnes[i] + blindStart := i * blindBytes + for blindIndex, blind := range blinds { + start := blindStart + blindIndex*blindCoeffCount*frBytes + acc := cosetExpMinusOne + for j := range blind { + var scaled fr.Element + scaled.Mul(&blind[j], &acc) + writeFrRegularLE(blindsPacked[start+j*frBytes:start+(j+1)*frBytes], &scaled) + acc.Mul(&acc, &coset) + } + } + + packFrVectorRegularLEInto(scalarsPacked[i*scalarBytes:(i+1)*scalarBytes], []fr.Element{ + coset, + quotientAux.lagrangeScales[i], + quotientAux.cs, + quotientAux.css, + s.beta, + s.gamma, + s.alpha, + }) + } + + outputPacked, err := bridge.Bridge.TransformAndEvaluateQuotientCosets( + "bls12_377", + dynamicPacked, + staticInputs.scalingPacked, + staticInputs.staticPacked, + staticInputs.staticMontCacheKeysPacked, + staticInputs.twiddlesPacked, + staticInputs.denominatorsPacked, + blindsPacked, + scalarsPacked, + int(n), + blindCoeffCount, + commitmentCount, + dynamicTransformCacheKey, + rho, + staticInputs.auxMontCacheKey, + ) + if err != nil { + return nil, err + } + if len(outputPacked) != rho*vectorBytes { + return nil, fmt.Errorf("webgpu plonk bls12_377: quotient all-coset evaluator returned %d bytes, expected %d", len(outputPacked), rho*vectorBytes) + } + s.pk.markQuotientStaticBridgeInputsPopulated(staticInputs.staticCache) + for i := 0; i < rho; i++ { + if err := unpackFrVectorRegularLEInto(buf, outputPacked[i*vectorBytes:(i+1)*vectorBytes]); err != nil { + return nil, err + } + for j := 0; j < int(n); j++ { + // we build the polynomial in bit reverse order + cres[bits.Reverse64(uint64(rho*j+i))>>mm] = buf[j] + } + } + + canonicalizeGroup := func(ids []int) error { + polys := make([]*iop.Polynomial, 0, len(ids)) + for _, id := range ids { + if id >= len(s.x) || id == id_ZS || s.x[id] == nil { + continue + } + polys = append(polys, s.x[id]) + } + if err := canonicalizePolynomialsRegularWithWebGPU(polys, int(s.domain0.Cardinality)); err != nil { + return err + } + return nil + } + + s.x[id_ZS] = nil + s.x[id_Qk] = nil + + if err := canonicalizeGroup(dynamicPolyIDs); err != nil { + return nil, err + } + if len(commitmentValuePolyIDs) > 0 { + if err := canonicalizeGroup(commitmentValuePolyIDs); err != nil { + return nil, err + } + } + + res := iop.NewPolynomial(&cres, iop.Form{Basis: iop.LagrangeCoset, Layout: iop.BitReverse}) + + return res, nil + +} + +func evaluateBlinded(p, bp *iop.Polynomial, zeta fr.Element) fr.Element { + // Get the size of the polynomial + n := big.NewInt(int64(p.Size())) + + var pEvaluatedAtZeta fr.Element + + // Evaluate the polynomial and blinded polynomial at zeta + pEvaluatedAtZeta = p.Evaluate(zeta) + bpEvaluatedAtZeta := bp.Evaluate(zeta) + + // Multiply the evaluated blinded polynomial by tempElement + var t fr.Element + one := fr.One() + t.Exp(zeta, n).Sub(&t, &one) + bpEvaluatedAtZeta.Mul(&bpEvaluatedAtZeta, &t) + + // Add the evaluated polynomial and the evaluated blinded polynomial + pEvaluatedAtZeta.Add(&pEvaluatedAtZeta, &bpEvaluatedAtZeta) + + // Return the result + return pEvaluatedAtZeta +} + +// /!\ modifies the size +func getBlindedCoefficients(p, bp *iop.Polynomial) []fr.Element { + cp := p.Coefficients() + cbp := bp.Coefficients() + cp = append(cp, cbp...) + for i := 0; i < len(cbp); i++ { + cp[i].Sub(&cp[i], &cbp[i]) + } + return cp +} + +// commits to a polynomial of the form b*(Xⁿ-1) where b is of small degree +func commitBlindingFactor(n int, b *iop.Polynomial, key kzg.ProvingKey) curve.G1Affine { + cp := b.Coefficients() + np := b.Size() + + var res curve.G1Affine + for i := 0; i < np; i++ { + var scalar big.Int + cp[i].BigInt(&scalar) + + var hi, lo curve.G1Affine + hi.ScalarMultiplication(&key.G1[n+i], &scalar) + lo.ScalarMultiplication(&key.G1[i], &scalar) + hi.Sub(&hi, &lo) + res.Add(&res, &hi) + } + return res +} + +func evalKZGPolynomial(p []fr.Element, point fr.Element) fr.Element { + var res fr.Element + for i := len(p) - 1; i >= 0; i-- { + res.Mul(&res, &point).Add(&res, &p[i]) + } + return res +} + +// dividePolyByXMinusA computes (f-f(a))/(x-a), reusing f for the result. +func dividePolyByXMinusA(f []fr.Element, fa, a fr.Element) []fr.Element { + if len(f) == 0 { + return []fr.Element{} + } + + f[0].Sub(&f[0], &fa) + + var t fr.Element + for i := len(f) - 2; i >= 0; i-- { + t.Mul(&f[i+1], &a) + f[i].Add(&f[i], &t) + } + + return f[1:] +} + +func deriveKZGBatchGamma(point fr.Element, digests []curve.G1Affine, claimedValues []fr.Element, hf hash.Hash, dataTranscript ...[]byte) (fr.Element, error) { + fs := fiatshamir.NewTranscript(hf, "gamma") + if err := fs.Bind("gamma", point.Marshal()); err != nil { + return fr.Element{}, err + } + for i := range digests { + if err := fs.Bind("gamma", digests[i].Marshal()); err != nil { + return fr.Element{}, err + } + } + for i := range claimedValues { + if err := fs.Bind("gamma", claimedValues[i].Marshal()); err != nil { + return fr.Element{}, err + } + } + for i := range dataTranscript { + if err := fs.Bind("gamma", dataTranscript[i]); err != nil { + return fr.Element{}, err + } + } + + gammaBytes, err := fs.ComputeChallenge("gamma") + if err != nil { + return fr.Element{}, err + } + var gamma fr.Element + gamma.SetBytes(gammaBytes) + return gamma, nil +} + +// return a random polynomial of degree n, if n==-1 cancel the blinding +func getRandomPolynomial(n int) *iop.Polynomial { + var a []fr.Element + if n == -1 { + a = make([]fr.Element, 1) + a[0].SetZero() + } else { + a = make([]fr.Element, n+1) + for i := 0; i <= n; i++ { + a[i].SetRandom() + } + } + res := iop.NewPolynomial(&a, iop.Form{ + Basis: iop.Canonical, Layout: iop.Regular}) + return res +} + +func coefficients(p []*iop.Polynomial) [][]fr.Element { + res := make([][]fr.Element, len(p)) + for i, pI := range p { + res[i] = pI.Coefficients() + } + return res +} + +func (s *instance) commitToQuotient(h1, h2, h3 []fr.Element) error { + commits, err := s.msmG1Batch("kzg", 0, h1, h2, h3) + if err != nil { + return err + } + copy(s.proof.H[:], commits) + return nil +} + +// divideByZH +// The input must be in LagrangeCoset. +// The result is in Canonical Regular. (in place using a) +func divideByZH(a *iop.Polynomial, domains [2]*fft.Domain) (*iop.Polynomial, error) { + smallDomain, bigDomain := domains[0], domains[1] + if smallDomain == nil || bigDomain == nil { + return nil, errors.New("invalid domain") + } + if smallDomain.Cardinality == 0 || bigDomain.Cardinality == 0 { + return nil, errors.New("invalid domain cardinality") + } + if bigDomain.Cardinality%smallDomain.Cardinality != 0 { + return nil, errors.New("invalid domain ratio") + } + + // check that the basis is LagrangeCoset + if a.Basis != iop.LagrangeCoset || a.Layout != iop.BitReverse { + return nil, errors.New("invalid form") + } + + // prepare the evaluations of x^n-1 on the big domain's coset + xnMinusOneInverseLagrangeCoset := evaluateXnMinusOneDomainBigCoset(domains) + rho := int(bigDomain.Cardinality / smallDomain.Cardinality) + + r := a.Coefficients() + n := uint64(len(r)) + nn := uint64(64 - bits.TrailingZeros64(n)) + + for i := range r { + iRev := bits.Reverse64(uint64(i)) >> nn + r[i].Mul(&r[i], &xnMinusOneInverseLagrangeCoset[int(iRev)%rho]) + } + + if err := canonicalizeQuotientFromCosetWithWebGPU(a); err != nil { + return nil, err + } + + return a, nil + +} + +// evaluateXnMinusOneDomainBigCoset evaluates Xᵐ-1 on DomainBig coset +func evaluateXnMinusOneDomainBigCoset(domains [2]*fft.Domain) []fr.Element { + + rho := domains[1].Cardinality / domains[0].Cardinality + + res := make([]fr.Element, rho) + + expo := big.NewInt(int64(domains[0].Cardinality)) + res[0].Exp(domains[1].FrMultiplicativeGen, expo) + + var t fr.Element + t.Exp(domains[1].Generator, expo) + + one := fr.One() + + for i := 1; i < int(rho); i++ { + res[i].Mul(&res[i-1], &t) + res[i-1].Sub(&res[i-1], &one) + } + res[len(res)-1].Sub(&res[len(res)-1], &one) + + res = fr.BatchInvert(res) + + return res +} + +// innerComputeLinearizedPoly computes the linearized polynomial in canonical basis. +// The purpose is to commit and open all in one ql, qr, qm, qo, qk. +// * lZeta, rZeta, oZeta are the evaluation of l, r, o at zeta +// * z is the permutation polynomial, zu is Z(μX), the shifted version of Z +// * pk is the proving key: the linearized polynomial is a linear combination of ql, qr, qm, qo, qk. +// +// The Linearized polynomial is: +// +// α²*L₁(ζ)*Z(X) +// + α*( (l(ζ)+β*s1(ζ)+γ)*(r(ζ)+β*s2(ζ)+γ)*(β*s3(X))*Z(μζ) - Z(X)*(l(ζ)+β*id1(ζ)+γ)*(r(ζ)+β*id2(ζ)+γ)*(o(ζ)+β*id3(ζ)+γ)) +// + l(ζ)*Ql(X) + l(ζ)r(ζ)*Qm(X) + r(ζ)*Qr(X) + o(ζ)*Qo(X) + Qk(X) + ∑ᵢQcp_(ζ)Pi_(X) +// - Z_{H}(ζ)*((H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) +// +// /!\ blindedZCanonical is modified +func (s *instance) innerComputeLinearizedPoly(lZeta, rZeta, oZeta, alpha, beta, gamma, zeta, zu fr.Element, qcpZeta, blindedZCanonical []fr.Element, pi2Canonical [][]fr.Element, pk *ProvingKey) ([]fr.Element, error) { + + // l(ζ)r(ζ) + var rl fr.Element + rl.Mul(&rZeta, &lZeta) + + // s1 = α*(l(ζ)+β*s1(β)+γ)*(r(ζ)+β*s2(β)+γ)*β*Z(μζ) + // s2 = -α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + // the linearised polynomial is + // α²*L₁(ζ)*Z(X) + + // s1*s3(X)+s2*Z(X) + l(ζ)*Ql(X) + + // l(ζ)r(ζ)*Qm(X) + r(ζ)*Qr(X) + o(ζ)*Qo(X) + Qk(X) + ∑ᵢQcp_(ζ)Pi_(X) - + // Z_{H}(ζ)*((H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) + var s1, s2 fr.Element + s1 = s.trace.S1.Evaluate(zeta) // s1(ζ) + s1.Mul(&s1, &beta).Add(&s1, &lZeta).Add(&s1, &gamma) // (l(ζ)+β*s1(ζ)+γ) + tmp := s.trace.S2.Evaluate(zeta) // s2(ζ) + tmp.Mul(&tmp, &beta).Add(&tmp, &rZeta).Add(&tmp, &gamma) // (r(ζ)+β*s2(ζ)+γ) + s1.Mul(&s1, &tmp).Mul(&s1, &zu).Mul(&s1, &beta).Mul(&s1, &alpha) // (l(ζ)+β*s1(ζ)+γ)*(r(ζ)+β*s2(ζ)+γ)*β*Z(μζ)*α + + var uzeta, uuzeta fr.Element + uzeta.Mul(&zeta, &pk.Vk.CosetShift) + uuzeta.Mul(&uzeta, &pk.Vk.CosetShift) + + s2.Mul(&beta, &zeta).Add(&s2, &lZeta).Add(&s2, &gamma) // (l(ζ)+β*ζ+γ) + tmp.Mul(&beta, &uzeta).Add(&tmp, &rZeta).Add(&tmp, &gamma) // (r(ζ)+β*u*ζ+γ) + s2.Mul(&s2, &tmp) // (l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ) + tmp.Mul(&beta, &uuzeta).Add(&tmp, &oZeta).Add(&tmp, &gamma) // (o(ζ)+β*u²*ζ+γ) + s2.Mul(&s2, &tmp) // (l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + s2.Neg(&s2).Mul(&s2, &alpha) + + // Z_h(ζ), ζⁿ⁺², L₁(ζ)*α²*Z + var zhZeta, zetaNPlusTwo, alphaSquareLagrangeZero, one, den, frNbElmt fr.Element + one.SetOne() + nbElmt := int64(s.domain0.Cardinality) + alphaSquareLagrangeZero.Set(&zeta).Exp(alphaSquareLagrangeZero, big.NewInt(nbElmt)) // ζⁿ + zetaNPlusTwo.Mul(&alphaSquareLagrangeZero, &zeta).Mul(&zetaNPlusTwo, &zeta) // ζⁿ⁺² + alphaSquareLagrangeZero.Sub(&alphaSquareLagrangeZero, &one) // ζⁿ - 1 + zhZeta.Set(&alphaSquareLagrangeZero) // Z_h(ζ) = ζⁿ - 1 + frNbElmt.SetUint64(uint64(nbElmt)) + den.Sub(&zeta, &one).Inverse(&den) // 1/(ζ-1) + alphaSquareLagrangeZero.Mul(&alphaSquareLagrangeZero, &den). // L₁ = (ζⁿ - 1)/(ζ-1) + Mul(&alphaSquareLagrangeZero, &alpha). + Mul(&alphaSquareLagrangeZero, &alpha). + Mul(&alphaSquareLagrangeZero, &s.domain0.CardinalityInv) // α²*L₁(ζ) + + s3canonical := s.trace.S3.Coefficients() + + if err := canonicalizePolynomialsRegularWithWebGPU([]*iop.Polynomial{s.trace.Qk}, int(s.domain0.Cardinality)); err != nil { + return nil, err + } + + // len(h1)=len(h2)=len(blindedZCanonical)=len(h3)+1 when Statistical ZK is activated + // len(h1)=len(h2)=len(h3)=len(blindedZCanonical)-1 when Statistical ZK is deactivated + h1 := s.h1() + h2 := s.h2() + h3 := s.h3() + + // at this stage we have + // s1 = α*(l(ζ)+β*s1(β)+γ)*(r(ζ)+β*s2(β)+γ)*β*Z(μζ) + // s2 = -α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + cql := s.trace.Ql.Coefficients() + cqr := s.trace.Qr.Coefficients() + cqm := s.trace.Qm.Coefficients() + cqo := s.trace.Qo.Coefficients() + cqk := s.trace.Qk.Coefficients() + + var t, t0, t1 fr.Element + + for i := range blindedZCanonical { + t.Mul(&blindedZCanonical[i], &s2) // -Z(X)*α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + if i < len(s3canonical) { + t0.Mul(&s3canonical[i], &s1) // α*(l(ζ)+β*s1(β)+γ)*(r(ζ)+β*s2(β)+γ)*β*Z(μζ)*β*s3(X) + t.Add(&t, &t0) + } + if i < len(cqm) { + t1.Mul(&cqm[i], &rl) // l(ζ)r(ζ)*Qm(X) + t.Add(&t, &t1) // linPol += l(ζ)r(ζ)*Qm(X) + t0.Mul(&cql[i], &lZeta) // l(ζ)Q_l(X) + t.Add(&t, &t0) // linPol += l(ζ)*Ql(X) + t0.Mul(&cqr[i], &rZeta) //r(ζ)*Qr(X) + t.Add(&t, &t0) // linPol += r(ζ)*Qr(X) + t0.Mul(&cqo[i], &oZeta) // o(ζ)*Qo(X) + t.Add(&t, &t0) // linPol += o(ζ)*Qo(X) + t.Add(&t, &cqk[i]) // linPol += Qk(X) + for j := range qcpZeta { // linPol += ∑ᵢQcp_(ζ)Pi_(X) + t0.Mul(&pi2Canonical[j][i], &qcpZeta[j]) + t.Add(&t, &t0) + } + } + + t0.Mul(&blindedZCanonical[i], &alphaSquareLagrangeZero) // α²L₁(ζ)Z(X) + blindedZCanonical[i].Add(&t, &t0) // linPol += α²L₁(ζ)Z(X) + + // if statistical zeroknowledge is deactivated, len(h1)=len(h2)=len(h3)=len(blindedZ)-1. + // Else len(h1)=len(h2)=len(blindedZCanonical)=len(h3)+1 + if i < len(h3) { + t.Mul(&h3[i], &zetaNPlusTwo). + Add(&t, &h2[i]). + Mul(&t, &zetaNPlusTwo). + Add(&t, &h1[i]). + Mul(&t, &zhZeta) + blindedZCanonical[i].Sub(&blindedZCanonical[i], &t) // linPol -= Z_h(ζ)*(H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) + } else if s.opt.StatisticalZK { + t.Mul(&h2[i], &zetaNPlusTwo). + Add(&t, &h1[i]). + Mul(&t, &zhZeta) + blindedZCanonical[i].Sub(&blindedZCanonical[i], &t) // linPol -= Z_h(ζ)*(H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) + } + } + + return blindedZCanonical, nil +} + +func bindPublicData(fs *fiatshamir.Transcript, challenge string, vk *native.VerifyingKey, publicInputs []fr.Element) error { + if err := fs.Bind(challenge, vk.S[0].Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.S[1].Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.S[2].Marshal()); err != nil { + return err + } + + if err := fs.Bind(challenge, vk.Ql.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qr.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qm.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qo.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qk.Marshal()); err != nil { + return err + } + for i := range vk.Qcp { + if err := fs.Bind(challenge, vk.Qcp[i].Marshal()); err != nil { + return err + } + } + + for i := 0; i < len(publicInputs); i++ { + if err := fs.Bind(challenge, publicInputs[i].Marshal()); err != nil { + return err + } + } + + return nil +} + +func deriveRandomness(fs *fiatshamir.Transcript, challenge string, points ...*curve.G1Affine) (fr.Element, error) { + var buf [curve.SizeOfG1AffineUncompressed]byte + var r fr.Element + + for _, p := range points { + buf = p.RawBytes() + if err := fs.Bind(challenge, buf[:]); err != nil { + return r, err + } + } + + b, err := fs.ComputeChallenge(challenge) + if err != nil { + return r, err + } + r.SetBytes(b) + return r, nil +} diff --git a/backend/accelerated/webgpu/plonk/bls12-377/provingkey.go b/backend/accelerated/webgpu/plonk/bls12-377/provingkey.go new file mode 100644 index 0000000000..9163a4e251 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bls12-377/provingkey.go @@ -0,0 +1,72 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bls12377 + +import ( + "sync" + + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" + native "github.com/consensys/gnark/backend/plonk/bls12-377" + cs "github.com/consensys/gnark/constraint/bls12-377" +) + +type ProvingKey struct { + native.ProvingKey + prepareMu sync.Mutex + handle string + staticNumeratorCache *staticNumeratorCache +} + +func (pk *ProvingKey) Prepare() error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + return pk.ensurePreparedLocked() +} + +func (pk *ProvingKey) ensurePreparedLocked() error { + if pk.handle != "" { + return nil + } + if err := bridge.Bridge.Init("bls12_377"); err != nil { + return err + } + payload := bridge.JSObject() + payload.Set("kzg", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.Kzg.G1))) + payload.Set("kzgCount", len(pk.Kzg.G1)) + payload.Set("kzgLagrange", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.KzgLagrange.G1))) + payload.Set("kzgLagrangeCount", len(pk.KzgLagrange.G1)) + handle, err := bridge.Bridge.PrepareKey("bls12_377", payload) + if err != nil { + return err + } + pk.handle = handle + return nil +} + +func (pk *ProvingKey) PrepareWithCS(spr *cs.SparseR1CS) error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + if err := pk.ensurePreparedLocked(); err != nil { + return err + } + domain0, domain1 := domainsForSPR(spr) + trace := native.NewTrace(spr, domain0) + if err := pk.ensureStaticNumeratorCache(trace, domain0, domain1); err != nil { + return err + } + if err := pk.preloadQuotientStaticCaches(trace, domain0, domain1); err != nil { + return err + } + if err := bridge.Bridge.PrewarmQuotientTransformDomain("bls12_377", int(domain0.Cardinality)); err != nil { + return err + } + if err := bridge.Bridge.PrewarmQuotientEvaluateKernel("bls12_377", len(trace.Qcp)); err != nil { + return err + } + return bridge.Bridge.PrewarmQuotientCanonicalizeDomain("bls12_377", int(domain1.Cardinality)) +} diff --git a/backend/accelerated/webgpu/plonk/bls12-377/serialize.go b/backend/accelerated/webgpu/plonk/bls12-377/serialize.go new file mode 100644 index 0000000000..3d6e88d4d5 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bls12-377/serialize.go @@ -0,0 +1,264 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bls12377 + +import ( + "encoding/binary" + "errors" + "fmt" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" + fp "github.com/consensys/gnark-crypto/ecc/bls12-377/fp" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/iop" + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" +) + +const ( + frBytes = fr.Bytes + g1CoordinateBytes = fp.Bytes + g1PointBytes = 3 * g1CoordinateBytes +) + +func packFrVectorRegularLEInto(dst []byte, values []fr.Element) []byte { + required := len(values) * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + for i := range values { + base := i * frBytes + writeFrRegularLE(dst[base:base+frBytes], &values[i]) + } + return dst +} + +func packFrVectorsRegularLEPaddedInto(dst []byte, vectors [][]fr.Element, elementCount int) ([]byte, error) { + if elementCount <= 0 { + return nil, errors.New("webgpu plonk bls12_377: empty MSM batch") + } + required := len(vectors) * elementCount * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + clear(dst) + } + for i, values := range vectors { + if len(values) > elementCount { + return nil, fmt.Errorf("webgpu plonk bls12_377: MSM batch vector %d has %d elements, expected at most %d", i, len(values), elementCount) + } + start := i * elementCount * frBytes + packFrVectorRegularLEInto(dst[start:start+len(values)*frBytes], values) + } + return dst, nil +} + +func writeFrRegularLE(dst []byte, value *fr.Element) { + be := value.Bytes() + for i := 0; i < frBytes; i++ { + dst[i] = be[frBytes-1-i] + } +} + +func readFrRegularLE(src []byte) (fr.Element, error) { + if len(src) != frBytes { + return fr.Element{}, fmt.Errorf("webgpu plonk bls12_377: expected %d Fr bytes, got %d", frBytes, len(src)) + } + var le [frBytes]byte + copy(le[:], src) + return fr.LittleEndian.Element(&le) +} + +func unpackFrVectorRegularLEInto(dst []fr.Element, src []byte) error { + if len(src) != len(dst)*frBytes { + return fmt.Errorf("webgpu plonk bls12_377: expected %d Fr vector bytes, got %d", len(dst)*frBytes, len(src)) + } + for i := range dst { + value, err := readFrRegularLE(src[i*frBytes : (i+1)*frBytes]) + if err != nil { + return err + } + dst[i] = value + } + return nil +} + +type canonicalizeGroupKey struct { + inputBitReversed bool + inverseCoset bool +} + +func canonicalizePolynomialsRegularWithWebGPU(polys []*iop.Polynomial, elementCount int) error { + n := elementCount + groups := make(map[canonicalizeGroupKey][]*iop.Polynomial) + for _, p := range polys { + if p == nil { + continue + } + if p.Basis == iop.Canonical { + p.ToRegular() + continue + } + coeffs := p.Coefficients() + if len(coeffs) != n { + return fmt.Errorf("webgpu plonk bls12_377: canonicalize polynomial has %d coefficients, expected %d", len(coeffs), n) + } + switch p.Basis { + case iop.Lagrange: + case iop.LagrangeCoset: + default: + return fmt.Errorf("webgpu plonk bls12_377: unsupported polynomial basis %d", p.Basis) + } + switch p.Layout { + case iop.Regular: + case iop.BitReverse: + default: + return fmt.Errorf("webgpu plonk bls12_377: unsupported polynomial layout %d", p.Layout) + } + key := canonicalizeGroupKey{ + inputBitReversed: p.Layout == iop.BitReverse, + inverseCoset: p.Basis == iop.LagrangeCoset, + } + groups[key] = append(groups[key], p) + } + + vectorBytes := n * frBytes + for key, group := range groups { + valuesPacked := make([]byte, len(group)*vectorBytes) + for i, p := range group { + packFrVectorRegularLEInto(valuesPacked[i*vectorBytes:(i+1)*vectorBytes], p.Coefficients()) + } + canonicalPacked, err := bridge.Bridge.CanonicalizeQuotientVectors("bls12_377", valuesPacked, len(group), n, key.inputBitReversed, key.inverseCoset) + if err != nil { + return err + } + if len(canonicalPacked) != len(valuesPacked) { + return fmt.Errorf("webgpu plonk bls12_377: quotient canonicalize returned %d bytes, expected %d", len(canonicalPacked), len(valuesPacked)) + } + for i, p := range group { + if err := unpackFrVectorRegularLEInto(p.Coefficients(), canonicalPacked[i*vectorBytes:(i+1)*vectorBytes]); err != nil { + return err + } + p.Basis = iop.Canonical + p.Layout = iop.Regular + } + } + return nil +} + +func canonicalizeQuotientFromCosetWithWebGPU(p *iop.Polynomial) error { + return canonicalizePolynomialsRegularWithWebGPU([]*iop.Polynomial{p}, len(p.Coefficients())) +} + +func lagrangePolynomialsRegularWithWebGPU(polys []*iop.Polynomial, elementCount int) error { + n := elementCount + filtered := make([]*iop.Polynomial, 0, len(polys)) + for _, p := range polys { + if p == nil { + continue + } + if p.Basis == iop.Lagrange { + p.ToRegular() + continue + } + if p.Basis != iop.Canonical || p.Layout != iop.Regular { + return fmt.Errorf("webgpu plonk bls12_377: expected canonical regular polynomial, got basis %d layout %d", p.Basis, p.Layout) + } + if len(p.Coefficients()) != n { + return fmt.Errorf("webgpu plonk bls12_377: lagrange polynomial has %d coefficients, expected %d", len(p.Coefficients()), n) + } + filtered = append(filtered, p) + } + if len(filtered) == 0 { + return nil + } + + vectorBytes := n * frBytes + valuesPacked := make([]byte, len(filtered)*vectorBytes) + for i, p := range filtered { + packFrVectorRegularLEInto(valuesPacked[i*vectorBytes:(i+1)*vectorBytes], p.Coefficients()) + } + lagrangePacked, err := bridge.Bridge.LagrangeQuotientVectors("bls12_377", valuesPacked, len(filtered), n) + if err != nil { + return err + } + if len(lagrangePacked) != len(valuesPacked) { + return fmt.Errorf("webgpu plonk bls12_377: quotient lagrange returned %d bytes, expected %d", len(lagrangePacked), len(valuesPacked)) + } + for i, p := range filtered { + if err := unpackFrVectorRegularLEInto(p.Coefficients(), lagrangePacked[i*vectorBytes:(i+1)*vectorBytes]); err != nil { + return err + } + p.Basis = iop.Lagrange + p.Layout = iop.Regular + } + return nil +} + +func packG1AffineJacobianBatch(points []curve.G1Affine) []byte { + out := make([]byte, len(points)*g1PointBytes) + for i := range points { + base := i * g1PointBytes + writeFPMontLE(out[base:base+g1CoordinateBytes], &points[i].X) + writeFPMontLE(out[base+g1CoordinateBytes:base+2*g1CoordinateBytes], &points[i].Y) + writeG1JacobianZOne(out[base+2*g1CoordinateBytes : base+3*g1CoordinateBytes]) + } + return out +} + +func decodeG1AffineFromPacked(packed []byte, err error) (curve.G1Affine, error) { + if err != nil { + return curve.G1Affine{}, err + } + if len(packed) != 2*g1CoordinateBytes { + return curve.G1Affine{}, fmt.Errorf("webgpu plonk bls12_377: expected %d G1 bytes, got %d", 2*g1CoordinateBytes, len(packed)) + } + return curve.G1Affine{ + X: readFPMontLE(packed[:g1CoordinateBytes]), + Y: readFPMontLE(packed[g1CoordinateBytes:]), + }, nil +} + +func decodeG1AffineBatchFromPacked(packed []byte, count int, err error) ([]curve.G1Affine, error) { + if err != nil { + return nil, err + } + expected := count * 2 * g1CoordinateBytes + if len(packed) != expected { + return nil, fmt.Errorf("webgpu plonk bls12_377: expected %d G1 batch bytes, got %d", expected, len(packed)) + } + res := make([]curve.G1Affine, count) + for i := range res { + start := i * 2 * g1CoordinateBytes + res[i] = curve.G1Affine{ + X: readFPMontLE(packed[start : start+g1CoordinateBytes]), + Y: readFPMontLE(packed[start+g1CoordinateBytes : start+2*g1CoordinateBytes]), + } + } + return res, nil +} + +func readFPMontLE(src []byte) fp.Element { + var z fp.Element + for i := range z { + z[i] = binary.LittleEndian.Uint64(src[i*8 : (i+1)*8]) + } + return z +} + +func writeFPMontLE(dst []byte, value *fp.Element) { + for i := range *value { + binary.LittleEndian.PutUint64(dst[i*8:(i+1)*8], (*value)[i]) + } +} + +func writeG1JacobianZOne(dst []byte) { + var one fp.Element + one.SetOne() + writeFPMontLE(dst, &one) +} diff --git a/backend/accelerated/webgpu/plonk/bls12-381/caching.go b/backend/accelerated/webgpu/plonk/bls12-381/caching.go new file mode 100644 index 0000000000..be35e178e6 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bls12-381/caching.go @@ -0,0 +1,530 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bls12381 + +import ( + "encoding/binary" + "errors" + "fmt" + "math/big" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/iop" + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" + native "github.com/consensys/gnark/backend/plonk/bls12-381" +) + +const ( + quotientBaseDynamicVectorCount = 5 + quotientBaseStaticVectorCount = 7 + quotientEvalScalarCount = 7 +) + +var quotientTransformCacheKey int +var quotientStaticMontCacheKey int +var quotientAuxMontCacheKey int +var cacheKeyMu sync.Mutex + +type staticNumeratorCache struct { + domain0Cardinality uint64 + domain1Cardinality uint64 + qcpCount int + canonical staticNumeratorPolys + cosets []staticNumeratorPolys + quotientAux quotientAuxCache + webgpuStaticMontKeys []int + webgpuStaticMontPopulated []bool + webgpuAuxMontKey int + webgpuAuxMontPopulated bool +} + +type staticNumeratorPolys struct { + ql, qr, qm, qo *iop.Polynomial + s1, s2, s3 *iop.Polynomial + qcp []*iop.Polynomial +} + +type quotientAuxCache struct { + twiddlesPacked []byte + scalingPacked []byte + denominatorsPacked []byte + cosets []fr.Element + cosetExpMinusOnes []fr.Element + lagrangeScales []fr.Element + cs, css fr.Element +} + +func nextCacheKey(counter *int) int { + cacheKeyMu.Lock() + defer cacheKeyMu.Unlock() + + *counter = *counter + 1 + if *counter <= 0 { + *counter = 1 + } + return *counter +} + +func (pk *ProvingKey) ensureStaticNumeratorCache(trace *native.Trace, domain0, domain1 *fft.Domain) error { + qcpCount := len(trace.Qcp) + if pk.staticNumeratorCache != nil && + pk.staticNumeratorCache.domain0Cardinality == domain0.Cardinality && + pk.staticNumeratorCache.domain1Cardinality == domain1.Cardinality && + pk.staticNumeratorCache.qcpCount == qcpCount { + if len(pk.staticNumeratorCache.webgpuStaticMontKeys) != len(pk.staticNumeratorCache.cosets) { + pk.staticNumeratorCache.webgpuStaticMontKeys = make([]int, len(pk.staticNumeratorCache.cosets)) + for i := range pk.staticNumeratorCache.webgpuStaticMontKeys { + pk.staticNumeratorCache.webgpuStaticMontKeys[i] = nextCacheKey("ientStaticMontCacheKey) + } + pk.staticNumeratorCache.webgpuStaticMontPopulated = make([]bool, len(pk.staticNumeratorCache.cosets)) + } + if len(pk.staticNumeratorCache.webgpuStaticMontPopulated) != len(pk.staticNumeratorCache.cosets) { + pk.staticNumeratorCache.webgpuStaticMontPopulated = make([]bool, len(pk.staticNumeratorCache.cosets)) + } + if err := pk.staticNumeratorCache.ensureQuotientAuxCache(domain0, domain1); err != nil { + return err + } + pk.staticNumeratorCache.canonical.applyToTrace(trace) + return nil + } + + canonical := cloneStaticNumeratorPolys(trace) + if err := canonicalizePolynomialsRegularWithWebGPU(canonical.polynomials(), int(domain0.Cardinality)); err != nil { + return err + } + + rho := int(domain1.Cardinality / domain0.Cardinality) + cosets := make([]staticNumeratorPolys, rho) + webgpuStaticMontKeys := make([]int, rho) + for i := range webgpuStaticMontKeys { + webgpuStaticMontKeys[i] = nextCacheKey("ientStaticMontCacheKey) + } + + cosetTable, err := domain0.CosetTable() + if err != nil { + return err + } + scalingVector := cosetTable + working := canonical.clone() + for i := 0; i < rho; i++ { + if i == 1 { + w := domain1.Generator + scalingVector = make([]fr.Element, domain0.Cardinality) + fft.BuildExpTable(w, scalingVector) + } + + if err := transformPolynomialsToCoset(working.polynomials(), domain0, scalingVector); err != nil { + return err + } + cosets[i] = working.clone() + } + quotientAux, err := buildQuotientAuxCache(domain0, domain1) + if err != nil { + return err + } + + pk.staticNumeratorCache = &staticNumeratorCache{ + domain0Cardinality: domain0.Cardinality, + domain1Cardinality: domain1.Cardinality, + qcpCount: qcpCount, + canonical: canonical, + cosets: cosets, + quotientAux: quotientAux, + webgpuStaticMontKeys: webgpuStaticMontKeys, + webgpuStaticMontPopulated: make([]bool, rho), + webgpuAuxMontKey: nextCacheKey("ientAuxMontCacheKey), + } + pk.staticNumeratorCache.canonical.applyToTrace(trace) + return nil +} + +func (c *staticNumeratorCache) ensureQuotientAuxCache(domain0, domain1 *fft.Domain) error { + n := int(domain0.Cardinality) + rho := int(domain1.Cardinality / domain0.Cardinality) + if c.webgpuAuxMontKey <= 0 { + c.webgpuAuxMontKey = nextCacheKey("ientAuxMontCacheKey) + c.webgpuAuxMontPopulated = false + } + if c.quotientAux.valid(n, rho) { + return nil + } + aux, err := buildQuotientAuxCache(domain0, domain1) + if err != nil { + return err + } + c.quotientAux = aux + c.webgpuAuxMontPopulated = false + return nil +} + +func (a quotientAuxCache) valid(n, rho int) bool { + vectorBytes := n * frBytes + return rho > 0 && + len(a.twiddlesPacked) == vectorBytes && + len(a.scalingPacked) == rho*vectorBytes && + len(a.denominatorsPacked) == rho*vectorBytes && + len(a.cosets) == rho && + len(a.cosetExpMinusOnes) == rho && + len(a.lagrangeScales) == rho +} + +func buildQuotientAuxCache(domain0, domain1 *fft.Domain) (quotientAuxCache, error) { + n := int(domain0.Cardinality) + rho := int(domain1.Cardinality / domain0.Cardinality) + if n <= 0 || rho <= 0 { + return quotientAuxCache{}, fmt.Errorf("webgpu plonk bls12_381: invalid quotient auxiliary domain n=%d rho=%d", n, rho) + } + + twiddles0 := make([]fr.Element, n) + if n == 1 { + twiddles0[0].SetOne() + } else { + twiddles, err := domain0.Twiddles() + if err != nil { + return quotientAuxCache{}, err + } + copy(twiddles0, twiddles[0]) + w := twiddles0[1] + for i := len(twiddles[0]); i < len(twiddles0); i++ { + twiddles0[i].Mul(&twiddles0[i-1], &w) + } + } + + cosetTable, err := domain0.CosetTable() + if err != nil { + return quotientAuxCache{}, err + } + + vectorBytes := n * frBytes + aux := quotientAuxCache{ + twiddlesPacked: packFrVectorRegularLEInto(nil, twiddles0), + scalingPacked: make([]byte, rho*vectorBytes), + denominatorsPacked: make([]byte, rho*vectorBytes), + cosets: make([]fr.Element, rho), + cosetExpMinusOnes: make([]fr.Element, rho), + lagrangeScales: make([]fr.Element, rho), + } + aux.cs.Set(&domain1.FrMultiplicativeGen) + aux.css.Square(&aux.cs) + + shifters := make([]fr.Element, rho) + shifters[0].Set(&domain1.FrMultiplicativeGen) + for i := 1; i < rho; i++ { + shifters[i].Set(&domain1.Generator) + } + + denominators := make([]fr.Element, n) + bufBatchInvert := make([]fr.Element, n) + scalingVector := make([]fr.Element, n) + var coset, cosetExpMinusOne, one fr.Element + coset.SetOne() + one.SetOne() + bn := big.NewInt(int64(domain0.Cardinality)) + for i := 0; i < rho; i++ { + coset.Mul(&coset, &shifters[i]) + aux.cosets[i].Set(&coset) + cosetExpMinusOne.Exp(coset, bn).Sub(&cosetExpMinusOne, &one) + aux.cosetExpMinusOnes[i].Set(&cosetExpMinusOne) + aux.lagrangeScales[i].Mul(&cosetExpMinusOne, &domain0.CardinalityInv) + + for j := 0; j < n; j++ { + denominators[j].Mul(&coset, &twiddles0[j]).Sub(&denominators[j], &one) + } + batchInvert(denominators, bufBatchInvert) + packFrVectorRegularLEInto(aux.denominatorsPacked[i*vectorBytes:(i+1)*vectorBytes], denominators) + + currentScalingVector := scalingVector + if i == 0 { + currentScalingVector = cosetTable + } else { + fft.BuildExpTable(coset, scalingVector) + } + packFrVectorRegularLEInto(aux.scalingPacked[i*vectorBytes:(i+1)*vectorBytes], currentScalingVector) + } + return aux, nil +} + +func cloneStaticNumeratorPolys(trace *native.Trace) staticNumeratorPolys { + res := staticNumeratorPolys{ + ql: trace.Ql.Clone(), + qr: trace.Qr.Clone(), + qm: trace.Qm.Clone(), + qo: trace.Qo.Clone(), + s1: trace.S1.Clone(), + s2: trace.S2.Clone(), + s3: trace.S3.Clone(), + qcp: make([]*iop.Polynomial, len(trace.Qcp)), + } + for i := range trace.Qcp { + res.qcp[i] = trace.Qcp[i].Clone() + } + return res +} + +func (p staticNumeratorPolys) clone() staticNumeratorPolys { + res := staticNumeratorPolys{ + ql: p.ql.Clone(), + qr: p.qr.Clone(), + qm: p.qm.Clone(), + qo: p.qo.Clone(), + s1: p.s1.Clone(), + s2: p.s2.Clone(), + s3: p.s3.Clone(), + qcp: make([]*iop.Polynomial, len(p.qcp)), + } + for i := range p.qcp { + res.qcp[i] = p.qcp[i].Clone() + } + return res +} + +func (p staticNumeratorPolys) polynomials() []*iop.Polynomial { + res := []*iop.Polynomial{p.ql, p.qr, p.qm, p.qo, p.s1, p.s2, p.s3} + res = append(res, p.qcp...) + return res +} + +func (p staticNumeratorPolys) applyToTrace(trace *native.Trace) { + trace.Ql = p.ql + trace.Qr = p.qr + trace.Qm = p.qm + trace.Qo = p.qo + trace.S1 = p.s1 + trace.S2 = p.s2 + trace.S3 = p.s3 + trace.Qcp = p.qcp +} + +func transformPolynomialsToCoset(polys []*iop.Polynomial, domain *fft.Domain, scalingVector []fr.Element) error { + // shift polynomials to be in the correct coset + if err := canonicalizePolynomialsRegularWithWebGPU(polys, int(domain.Cardinality)); err != nil { + return err + } + + // scale by shifter + for _, p := range polys { + cp := p.Coefficients() + for j := range cp { + cp[j].Mul(&cp[j], &scalingVector[j]) + } + } + return lagrangePolynomialsRegularWithWebGPU(polys, int(domain.Cardinality)) +} + +func (pk *ProvingKey) ensureStaticNumeratorCacheForTrace(trace *native.Trace, domain0, domain1 *fft.Domain) error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + return pk.ensureStaticNumeratorCache(trace, domain0, domain1) +} + +func (pk *ProvingKey) preloadQuotientStaticCaches(trace *native.Trace, domain0, domain1 *fft.Domain) error { + staticCache := pk.staticNumeratorCache + if staticCache == nil { + return errors.New("webgpu plonk bls12_381: missing static numerator cache") + } + n := int(domain0.Cardinality) + rho := int(domain1.Cardinality / domain0.Cardinality) + if len(staticCache.cosets) != rho { + return fmt.Errorf("webgpu plonk bls12_381: static numerator cache has %d cosets, expected %d", len(staticCache.cosets), rho) + } + quotientAux := staticCache.quotientAux + if !quotientAux.valid(n, rho) { + return errors.New("webgpu plonk bls12_381: invalid quotient auxiliary cache") + } + auxMontCacheKey := staticCache.webgpuAuxMontKey + if auxMontCacheKey <= 0 || int(uint32(auxMontCacheKey)) != auxMontCacheKey { + return fmt.Errorf("webgpu plonk bls12_381: invalid quotient auxiliary WebGPU cache key %d", auxMontCacheKey) + } + + commitmentCount := len(trace.Qcp) + staticVectorCount := quotientBaseStaticVectorCount + commitmentCount + vectorBytes := n * frBytes + staticPacked, err := packStaticNumeratorCosets(staticCache, rho, staticVectorCount, commitmentCount, n, vectorBytes) + if err != nil { + return err + } + + staticMontCacheKeysPacked, err := packStaticMontCacheKeys(staticCache, rho) + if err != nil { + return err + } + + if err := bridge.Bridge.PreloadQuotientStaticAndAux( + "bls12_381", + staticPacked, + staticMontCacheKeysPacked, + quotientAux.scalingPacked, + quotientAux.twiddlesPacked, + quotientAux.denominatorsPacked, + n, + staticVectorCount, + rho, + auxMontCacheKey, + ); err != nil { + return err + } + + for i := range staticCache.webgpuStaticMontPopulated { + staticCache.webgpuStaticMontPopulated[i] = true + } + staticCache.webgpuAuxMontPopulated = true + return nil +} + +type quotientStaticBridgeInputs struct { + staticCache *staticNumeratorCache + quotientAux quotientAuxCache + staticPacked []byte + staticMontCacheKeysPacked []byte + twiddlesPacked []byte + scalingPacked []byte + denominatorsPacked []byte + auxMontCacheKey int +} + +func (pk *ProvingKey) quotientStaticBridgeInputs(rho, staticVectorCount, commitmentCount, n, vectorBytes int) (quotientStaticBridgeInputs, error) { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + staticCache := pk.staticNumeratorCache + if staticCache == nil || len(staticCache.cosets) != rho { + return quotientStaticBridgeInputs{}, errors.New("missing static numerator cache") + } + quotientAux := staticCache.quotientAux + if !quotientAux.valid(n, rho) { + return quotientStaticBridgeInputs{}, errors.New("webgpu plonk bls12_381: invalid quotient auxiliary cache") + } + staticMontCacheKeysPacked, err := packStaticMontCacheKeys(staticCache, rho) + if err != nil { + return quotientStaticBridgeInputs{}, err + } + + reuseStaticMontCache := true + for i := 0; i < rho; i++ { + if !staticCache.webgpuStaticMontPopulated[i] { + reuseStaticMontCache = false + } + } + auxMontCacheKey := staticCache.webgpuAuxMontKey + if auxMontCacheKey <= 0 || int(uint32(auxMontCacheKey)) != auxMontCacheKey { + return quotientStaticBridgeInputs{}, fmt.Errorf("webgpu plonk bls12_381: invalid quotient auxiliary WebGPU cache key %d", auxMontCacheKey) + } + + var staticPacked []byte + if !reuseStaticMontCache { + staticPacked, err = packStaticNumeratorCosets(staticCache, rho, staticVectorCount, commitmentCount, n, vectorBytes) + if err != nil { + return quotientStaticBridgeInputs{}, err + } + } + + twiddlesPacked := quotientAux.twiddlesPacked + scalingPacked := quotientAux.scalingPacked + denominatorsPacked := quotientAux.denominatorsPacked + if staticCache.webgpuAuxMontPopulated { + twiddlesPacked = nil + scalingPacked = nil + denominatorsPacked = nil + } + + return quotientStaticBridgeInputs{ + staticCache: staticCache, + quotientAux: quotientAux, + staticPacked: staticPacked, + staticMontCacheKeysPacked: staticMontCacheKeysPacked, + twiddlesPacked: twiddlesPacked, + scalingPacked: scalingPacked, + denominatorsPacked: denominatorsPacked, + auxMontCacheKey: auxMontCacheKey, + }, nil +} + +func (pk *ProvingKey) markQuotientStaticBridgeInputsPopulated(staticCache *staticNumeratorCache) { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + for i := range staticCache.webgpuStaticMontPopulated { + staticCache.webgpuStaticMontPopulated[i] = true + } + staticCache.webgpuAuxMontPopulated = true +} + +func packStaticMontCacheKeys(staticCache *staticNumeratorCache, rho int) ([]byte, error) { + if len(staticCache.webgpuStaticMontKeys) != rho || len(staticCache.webgpuStaticMontPopulated) != rho { + return nil, errors.New("webgpu plonk bls12_381: invalid static numerator WebGPU cache metadata") + } + out := make([]byte, rho*4) + for i := 0; i < rho; i++ { + key := staticCache.webgpuStaticMontKeys[i] + if key <= 0 || int(uint32(key)) != key { + return nil, fmt.Errorf("webgpu plonk bls12_381: invalid static numerator WebGPU cache key %d", key) + } + binary.LittleEndian.PutUint32(out[i*4:(i+1)*4], uint32(key)) + } + return out, nil +} + +func packStaticNumeratorCosets(staticCache *staticNumeratorCache, rho, staticVectorCount, commitmentCount, n, vectorBytes int) ([]byte, error) { + staticPacked := make([]byte, rho*staticVectorCount*vectorBytes) + for i := 0; i < rho; i++ { + start := i * staticVectorCount * vectorBytes + if err := packStaticNumeratorPolys( + staticPacked[start:start+staticVectorCount*vectorBytes], + staticCache.cosets[i], + staticVectorCount, + commitmentCount, + n, + vectorBytes, + ); err != nil { + return nil, err + } + } + return staticPacked, nil +} + +func packStaticNumeratorPolys(dst []byte, polys staticNumeratorPolys, staticVectorCount, commitmentCount, n, vectorBytes int) error { + if len(polys.qcp) != commitmentCount { + return fmt.Errorf("webgpu plonk bls12_381: quotient evaluator expected %d qcp vectors, got %d", commitmentCount, len(polys.qcp)) + } + staticVectors := polys.polynomials() + if len(staticVectors) != staticVectorCount { + return fmt.Errorf("webgpu plonk bls12_381: quotient evaluator expected %d static vectors, got %d", staticVectorCount, len(staticVectors)) + } + for i, p := range staticVectors { + if p == nil { + return fmt.Errorf("webgpu plonk bls12_381: missing quotient static polynomial %d", i) + } + coeffs := p.Coefficients() + if len(coeffs) != n { + return fmt.Errorf("webgpu plonk bls12_381: quotient static polynomial %d has %d coefficients, expected %d", i, len(coeffs), n) + } + packFrVectorRegularLEInto(dst[i*vectorBytes:(i+1)*vectorBytes], coeffs) + } + return nil +} + +// batchInvert modifies in place vec, with vec[i]<-vec[i]^{-1}, using +// the Montgomery batch inversion trick. We don't use gnark-crypto's batchInvert +// because we want to use a buffer preallocated, to avoid wasting memory. +// /!\ it doesn't check that all vec's inputs or non zero, it is ensured by the size +// of the field /!\ +func batchInvert(vec, buf []fr.Element) { + // local function only, vec and buf are of the same size + copy(buf, vec) + for i := 1; i < len(vec); i++ { + vec[i].Mul(&vec[i], &vec[i-1]) + } + acc := vec[len(vec)-1] + acc.Inverse(&acc) + for i := len(vec) - 1; i > 0; i-- { + vec[i].Mul(&acc, &vec[i-1]) + acc.Mul(&acc, &buf[i]) + } + vec[0].Set(&acc) +} diff --git a/backend/accelerated/webgpu/plonk/bls12-381/prove.go b/backend/accelerated/webgpu/plonk/bls12-381/prove.go new file mode 100644 index 0000000000..8e65575f52 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bls12-381/prove.go @@ -0,0 +1,1304 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bls12381 + +import ( + "errors" + "fmt" + "hash" + "math/big" + "math/bits" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/hash_to_field" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/iop" + "github.com/consensys/gnark-crypto/ecc/bls12-381/kzg" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark/backend" + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" + native "github.com/consensys/gnark/backend/plonk/bls12-381" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + cs "github.com/consensys/gnark/constraint/bls12-381" + "github.com/consensys/gnark/constraint/solver" + fcs "github.com/consensys/gnark/frontend/cs" +) + +const ( + id_L int = iota + id_R + id_O + id_Z + id_ZS + id_Ql + id_Qr + id_Qm + id_Qo + id_Qk + id_S1 + id_S2 + id_S3 + id_Qci // [ .. , Qc_i, Pi_i, ...] +) + +// blinding factors +const ( + id_Bl int = iota + id_Br + id_Bo + id_Bz + nb_blinding_polynomials +) + +// blinding orders (-1 to deactivate) +const ( + order_blinding_L = 1 + order_blinding_R = 1 + order_blinding_O = 1 + order_blinding_Z = 2 +) + +func Prove(spr *cs.SparseR1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (proof *native.Proof, err error) { + // parse the options + opt, err := backend.NewProverConfig(opts...) + if err != nil { + return nil, fmt.Errorf("get prover options: %w", err) + } + + if err := pk.Prepare(); err != nil { + return nil, fmt.Errorf("prepare proving key: %w", err) + } + + // init instance + instance, err := newInstance(spr, pk, fullWitness, &opt) + if err != nil { + return nil, fmt.Errorf("new instance: %w", err) + } + + if err := instance.initBlindingPolynomials(); err != nil { + return nil, fmt.Errorf("init blinding polynomials: %w", err) + } + if err := instance.solveConstraints(); err != nil { + return nil, fmt.Errorf("solve constraints: %w", err) + } + if err := instance.completeQk(); err != nil { + return nil, fmt.Errorf("complete qk: %w", err) + } + if err := instance.deriveGammaAndBeta(); err != nil { + return nil, fmt.Errorf("derive gamma and beta: %w", err) + } + if err := instance.buildRatioCopyConstraint(); err != nil { + return nil, fmt.Errorf("build ratio copy constraint: %w", err) + } + if err := instance.computeQuotient(); err != nil { + return nil, fmt.Errorf("compute quotient: %w", err) + } + if err := instance.openZ(); err != nil { + return nil, fmt.Errorf("open z: %w", err) + } + if err := instance.computeLinearizedPolynomial(); err != nil { + return nil, fmt.Errorf("compute linearized polynomial: %w", err) + } + if err := instance.batchOpening(); err != nil { + return nil, fmt.Errorf("batch opening: %w", err) + } + + return instance.proof, nil +} + +// represents a Prover instance +type instance struct { + pk *ProvingKey + proof *native.Proof + spr *cs.SparseR1CS + opt *backend.ProverConfig + + fs *fiatshamir.Transcript + kzgFoldingHash hash.Hash // for KZG folding + htfFunc hash.Hash // hash to field function + + // polynomials + x []*iop.Polynomial // x stores tracks the polynomial we need + bp []*iop.Polynomial // blinding polynomials + h *iop.Polynomial // h is the quotient polynomial + blindedZ []fr.Element // blindedZ is the blinded version of Z + quotientShardsRandomizers [2]fr.Element // random elements for blinding the shards of the quotient + + precomputedDenominators []fr.Element // stores the denominators of the Lagrange polynomials + linearizedPolynomial []fr.Element + linearizedPolynomialDigest kzg.Digest + + fullWitness witness.Witness + + // bsb22 commitment stuff + commitmentInfo constraint.PlonkCommitments + commitmentVal []fr.Element + cCommitments []*iop.Polynomial + + // challenges + gamma, beta, alpha, zeta fr.Element + + domain0, domain1 *fft.Domain + + trace *native.Trace +} + +func newInstance(spr *cs.SparseR1CS, pk *ProvingKey, fullWitness witness.Witness, opts *backend.ProverConfig) (*instance, error) { + if opts.HashToFieldFn == nil { + opts.HashToFieldFn = hash_to_field.New([]byte("BSB22-Plonk")) + } + s := instance{ + pk: pk, + proof: &native.Proof{}, + spr: spr, + opt: opts, + fullWitness: fullWitness, + bp: make([]*iop.Polynomial, nb_blinding_polynomials), + fs: fiatshamir.NewTranscript(opts.ChallengeHash, "gamma", "beta", "alpha", "zeta"), + kzgFoldingHash: opts.KZGFoldingHash, + htfFunc: opts.HashToFieldFn, + } + s.initBSB22Commitments() + s.x = make([]*iop.Polynomial, id_Qci+2*len(s.commitmentInfo)) + + // init fft domains + s.domain0, s.domain1 = domainsForSPR(spr) + + // sampling random numbers for blinding the quotient + if opts.StatisticalZK { + s.quotientShardsRandomizers[0].SetRandom() + s.quotientShardsRandomizers[1].SetRandom() + } + + // build trace + s.trace = native.NewTrace(spr, s.domain0) + if err := pk.ensureStaticNumeratorCacheForTrace(s.trace, s.domain0, s.domain1); err != nil { + return nil, err + } + + return &s, nil +} + +func domainsForSPR(spr *cs.SparseR1CS) (*fft.Domain, *fft.Domain) { + nbConstraints := spr.GetNbConstraints() + sizeSystem := uint64(nbConstraints + len(spr.Public)) // len(spr.Public) is for the placeholder constraints + domain0 := fft.NewDomain(sizeSystem) + + // h, the quotient polynomial is of degree 3(n+1)+2, so it's in a 3(n+2) dim vector space, + // the domain is the next power of 2 superior to 3(n+2). 4*domainNum is enough in all cases + // except when n<6. + var domain1 *fft.Domain + if sizeSystem < 6 { + domain1 = fft.NewDomain(8*sizeSystem, fft.WithoutPrecompute()) + } else { + domain1 = fft.NewDomain(4*sizeSystem, fft.WithoutPrecompute()) + } + return domain0, domain1 +} + +func (s *instance) initBlindingPolynomials() error { + s.bp[id_Bl] = getRandomPolynomial(order_blinding_L) + s.bp[id_Br] = getRandomPolynomial(order_blinding_R) + s.bp[id_Bo] = getRandomPolynomial(order_blinding_O) + s.bp[id_Bz] = getRandomPolynomial(order_blinding_Z) + return nil +} + +func (s *instance) initBSB22Commitments() { + s.commitmentInfo = s.spr.CommitmentInfo.(constraint.PlonkCommitments) + s.commitmentVal = make([]fr.Element, len(s.commitmentInfo)) // TODO @Tabaie get rid of this + s.cCommitments = make([]*iop.Polynomial, len(s.commitmentInfo)) + s.proof.Bsb22Commitments = make([]kzg.Digest, len(s.commitmentInfo)) + + // override the hint for the commitment constraints + bsb22ID := solver.GetHintID(fcs.Bsb22CommitmentComputePlaceholder) + s.opt.SolverOpts = append(s.opt.SolverOpts, solver.OverrideHint(bsb22ID, s.bsb22Hint)) +} + +// Computing and verifying Bsb22 multi-commits explained in https://hackmd.io/x8KsadW3RRyX7YTCFJIkHg +func (s *instance) bsb22Hint(_ *big.Int, ins, outs []*big.Int) error { + var err error + commDepth := int(ins[0].Int64()) + ins = ins[1:] + + res := &s.commitmentVal[commDepth] + + commitmentInfo := s.spr.CommitmentInfo.(constraint.PlonkCommitments)[commDepth] + committedValues := make([]fr.Element, s.domain0.Cardinality) + offset := s.spr.GetNbPublicVariables() + for i := range ins { + committedValues[offset+commitmentInfo.Committed[i]].SetBigInt(ins[i]) + } + if _, err = committedValues[offset+commitmentInfo.CommitmentIndex].SetRandom(); err != nil { // Commitment injection constraint has qcp = 0. Safe to use for blinding. + return err + } + if _, err = committedValues[offset+s.spr.GetNbConstraints()-1].SetRandom(); err != nil { // Last constraint has qcp = 0. Safe to use for blinding + return err + } + s.cCommitments[commDepth] = iop.NewPolynomial(&committedValues, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + if s.proof.Bsb22Commitments[commDepth], err = kzg.Commit(s.cCommitments[commDepth].Coefficients(), s.pk.KzgLagrange, 1); err != nil { + return err + } + + s.htfFunc.Write(s.proof.Bsb22Commitments[commDepth].Marshal()) + hashBts := s.htfFunc.Sum(nil) + s.htfFunc.Reset() + nbBuf := fr.Bytes + if s.htfFunc.Size() < fr.Bytes { + nbBuf = s.htfFunc.Size() + } + res.SetBytes(hashBts[:nbBuf]) // TODO @Tabaie use CommitmentIndex for this; create a new variable CommitmentConstraintIndex for other uses + res.BigInt(outs[0]) + + return nil +} + +// solveConstraints computes the evaluation of the polynomials L, R, O +// and sets x[id_L], x[id_R], x[id_O] in Lagrange form +func (s *instance) solveConstraints() error { + _solution, err := s.spr.Solve(s.fullWitness, s.opt.SolverOpts...) + if err != nil { + return err + } + solution := _solution.(*cs.SparseR1CSSolution) + evaluationLDomainSmall := []fr.Element(solution.L) + evaluationRDomainSmall := []fr.Element(solution.R) + evaluationODomainSmall := []fr.Element(solution.O) + s.x[id_L] = iop.NewPolynomial(&evaluationLDomainSmall, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + s.x[id_R] = iop.NewPolynomial(&evaluationRDomainSmall, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + s.x[id_O] = iop.NewPolynomial(&evaluationODomainSmall, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + + // commit to l, r, o and add blinding factors + if err := s.commitToLRO(); err != nil { + return err + } + return nil +} + +func (s *instance) completeQk() error { + qk := s.trace.Qk.Clone() + qkCoeffs := qk.Coefficients() + + wWitness, ok := s.fullWitness.Vector().(fr.Vector) + if !ok { + return witness.ErrInvalidWitness + } + + copy(qkCoeffs, wWitness[:len(s.spr.Public)]) + + for i := range s.commitmentInfo { + qkCoeffs[s.spr.GetNbPublicVariables()+s.commitmentInfo[i].CommitmentIndex] = s.commitmentVal[i] + } + + s.x[id_Qk] = qk + + return nil +} + +// commitToLRO commits to L, R, O polynomials using reduced-size MSMs. +// +// L, R, O live on a domain of size n = 2^k, but only offset = nbPublic + nbConstraints +// entries carry actual values. The rest are s0 = witness[0] (first public input). +// For R and O, the first nbPublic entries (placeholders) are also s0. +// +// Key identity: Σ_{i=0}^{n-1} KzgLagrange.G1[i] = [Σ L_i(τ)]₁ = [1]₁ = Kzg.G1[0] +// +// So we can rewrite the commitment as: +// +// [P] = Σ P[i]·G1_lag[i] +// = Σ (P[i]-s0)·G1_lag[i] + s0·Σ G1_lag[i] +// = MSM((P[i]-s0), G1_lag[i]) + s0·Kzg.G1[0] +// +// The (P[i]-s0) terms are zero in the padding region, so the MSM only needs +// the non-padding entries. For a 2.2M-constraint circuit on a 4M domain, +// this nearly halves each MSM. +func (s *instance) commitToLRO() error { + n := int(s.domain0.Cardinality) + nbPublic := len(s.spr.Public) + offset := nbPublic + s.spr.GetNbConstraints() + + // s0 = witness[0] = first public input + wWitness, ok := s.fullWitness.Vector().(fr.Vector) + if !ok { + return witness.ErrInvalidWitness + } + s0 := wWitness[0] + + // correctionPoint = s0 · [1]₁ = s0 · Kzg.G1[0] + var s0BigInt big.Int + s0.BigInt(&s0BigInt) + var correctionPoint curve.G1Affine + correctionPoint.ScalarMultiplication(&s.pk.Kzg.G1[0], &s0BigInt) + + // L: subtract s0, MSM on [0:offset], add correction + blinding, restore + coeffs := s.x[id_L].Coefficients() + for i := 0; i < offset; i++ { + coeffs[i].Sub(&coeffs[i], &s0) + } + var commit curve.G1Affine + commit, err := s.msmG1("kzgLagrange", 0, coeffs[:offset]) + if err != nil { + return err + } + for i := 0; i < offset; i++ { + coeffs[i].Add(&coeffs[i], &s0) + } + commit.Add(&commit, &correctionPoint) + cb := commitBlindingFactor(n, s.bp[id_Bl], s.pk.Kzg) + s.proof.LRO[0].Add(&commit, &cb) + + // R: subtract s0, MSM on [nbPublic:offset], add correction + blinding, restore + coeffs = s.x[id_R].Coefficients() + for i := nbPublic; i < offset; i++ { + coeffs[i].Sub(&coeffs[i], &s0) + } + commit, err = s.msmG1("kzgLagrange", nbPublic, coeffs[nbPublic:offset]) + if err != nil { + return err + } + for i := nbPublic; i < offset; i++ { + coeffs[i].Add(&coeffs[i], &s0) + } + commit.Add(&commit, &correctionPoint) + cb = commitBlindingFactor(n, s.bp[id_Br], s.pk.Kzg) + s.proof.LRO[1].Add(&commit, &cb) + + // O: same as R + coeffs = s.x[id_O].Coefficients() + for i := nbPublic; i < offset; i++ { + coeffs[i].Sub(&coeffs[i], &s0) + } + commit, err = s.msmG1("kzgLagrange", nbPublic, coeffs[nbPublic:offset]) + if err != nil { + return err + } + for i := nbPublic; i < offset; i++ { + coeffs[i].Add(&coeffs[i], &s0) + } + commit.Add(&commit, &correctionPoint) + cb = commitBlindingFactor(n, s.bp[id_Bo], s.pk.Kzg) + s.proof.LRO[2].Add(&commit, &cb) + + return nil +} + +func (s *instance) msmG1(vectorName string, start int, scalars []fr.Element) (curve.G1Affine, error) { + scalarsPacked := packFrVectorRegularLEInto(nil, scalars) + packed, err := bridge.Bridge.MSMG1Slice(s.pk.handle, vectorName, start, len(scalars), scalarsPacked) + return decodeG1AffineFromPacked(packed, err) +} + +func (s *instance) msmG1Batch(vectorName string, start int, scalarVectors ...[]fr.Element) ([]curve.G1Affine, error) { + if len(scalarVectors) == 0 { + return nil, errors.New("webgpu plonk bls12_381: empty MSM batch") + } + termCount := 0 + for _, scalars := range scalarVectors { + if len(scalars) > termCount { + termCount = len(scalars) + } + } + scalarsPacked, err := packFrVectorsRegularLEPaddedInto(nil, scalarVectors, termCount) + if err != nil { + return nil, err + } + packed, err := bridge.Bridge.MSMG1Batch(s.pk.handle, vectorName, start, termCount, len(scalarVectors), scalarsPacked) + return decodeG1AffineBatchFromPacked(packed, len(scalarVectors), err) +} + +// deriveGammaAndBeta (copy constraint) +func (s *instance) deriveGammaAndBeta() error { + wWitness, ok := s.fullWitness.Vector().(fr.Vector) + if !ok { + return witness.ErrInvalidWitness + } + + if err := bindPublicData(s.fs, "gamma", s.pk.Vk, wWitness[:len(s.spr.Public)]); err != nil { + return err + } + + gamma, err := deriveRandomness(s.fs, "gamma", &s.proof.LRO[0], &s.proof.LRO[1], &s.proof.LRO[2]) + if err != nil { + return err + } + + bbeta, err := s.fs.ComputeChallenge("beta") + if err != nil { + return err + } + s.gamma = gamma + s.beta.SetBytes(bbeta) + + return nil +} + +// commitToPolyAndBlinding computes the KZG commitment of a polynomial p +// in Lagrange form (large degree) +// and add the contribution of a blinding polynomial b (small degree) +// /!\ The polynomial p is supposed to be in Lagrange form. +func (s *instance) commitToPolyAndBlinding(p, b *iop.Polynomial) (commit curve.G1Affine, err error) { + + commit, err = s.msmG1("kzgLagrange", 0, p.Coefficients()) + + // we add in the blinding contribution + n := int(s.domain0.Cardinality) + cb := commitBlindingFactor(n, b, s.pk.Kzg) + commit.Add(&commit, &cb) + + return +} + +func (s *instance) deriveAlpha() (err error) { + alphaDeps := make([]*curve.G1Affine, len(s.proof.Bsb22Commitments)+1) + for i := range s.proof.Bsb22Commitments { + alphaDeps[i] = &s.proof.Bsb22Commitments[i] + } + alphaDeps[len(alphaDeps)-1] = &s.proof.Z + s.alpha, err = deriveRandomness(s.fs, "alpha", alphaDeps...) + return err +} + +func (s *instance) deriveZeta() (err error) { + s.zeta, err = deriveRandomness(s.fs, "zeta", &s.proof.H[0], &s.proof.H[1], &s.proof.H[2]) + return +} + +// computeQuotient computes H +func (s *instance) computeQuotient() (err error) { + s.x[id_Ql] = s.trace.Ql + s.x[id_Qr] = s.trace.Qr + s.x[id_Qm] = s.trace.Qm + s.x[id_Qo] = s.trace.Qo + s.x[id_S1] = s.trace.S1 + s.x[id_S2] = s.trace.S2 + s.x[id_S3] = s.trace.S3 + + for i := 0; i < len(s.commitmentInfo); i++ { + s.x[id_Qci+2*i] = s.trace.Qcp[i] + } + + n := s.domain0.Cardinality + lone := make([]fr.Element, n) + lone[0].SetOne() + + for i := 0; i < len(s.commitmentInfo); i++ { + s.x[id_Qci+2*i+1] = s.cCommitments[i] + } + + // derive alpha + if err = s.deriveAlpha(); err != nil { + return err + } + + // TODO complete waste of memory find another way to do that + identity := make([]fr.Element, n) + identity[1].Set(&s.beta) + + s.x[id_ZS] = s.x[id_Z].ShallowClone().Shift(1) + + numerator, err := s.computeNumerator() + if err != nil { + return err + } + + s.h, err = divideByZH(numerator, [2]*fft.Domain{s.domain0, s.domain1}) + if err != nil { + return err + } + + // commit to h + if err := s.commitToQuotient(s.h1(), s.h2(), s.h3()); err != nil { + return err + } + + if err := s.deriveZeta(); err != nil { + return err + } + + return nil +} + +func (s *instance) buildRatioCopyConstraint() (err error) { + // TODO @gbotrel having iop.BuildRatioCopyConstraint return something + // with capacity = len() + 4 would avoid extra alloc / copy during openZ + s.x[id_Z], err = iop.BuildRatioCopyConstraint( + []*iop.Polynomial{ + s.x[id_L], + s.x[id_R], + s.x[id_O], + }, + s.trace.S, + s.beta, + s.gamma, + iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}, + s.domain0, + ) + if err != nil { + return err + } + + // commit to the blinded version of z + s.proof.Z, err = s.commitToPolyAndBlinding(s.x[id_Z], s.bp[id_Bz]) + + return +} + +// open Z (blinded) at ωζ +func (s *instance) openZ() (err error) { + var zetaShifted fr.Element + zetaShifted.Mul(&s.zeta, &s.pk.Vk.Generator) + s.blindedZ = getBlindedCoefficients(s.x[id_Z], s.bp[id_Bz]) + // open z at zeta + s.proof.ZShiftedOpening, err = s.openKZG(s.blindedZ, zetaShifted) + if err != nil { + return err + } + return nil +} + +func (s *instance) openKZG(p []fr.Element, point fr.Element) (kzg.OpeningProof, error) { + if len(p) > len(s.pk.Kzg.G1) { + return kzg.OpeningProof{}, kzg.ErrInvalidPolynomialSize + } + + var proof kzg.OpeningProof + proof.ClaimedValue = evalKZGPolynomial(p, point) + + cp := make([]fr.Element, len(p)) + copy(cp, p) + h := dividePolyByXMinusA(cp, proof.ClaimedValue, point) + + hCommit, err := s.msmG1("kzg", 0, h) + if err != nil { + return kzg.OpeningProof{}, err + } + proof.H.Set(&hCommit) + + return proof, nil +} + +func (s *instance) h1() []fr.Element { + var h1 []fr.Element + if !s.opt.StatisticalZK { + h1 = s.h.Coefficients()[:s.domain0.Cardinality+2] + } else { + h1 = make([]fr.Element, s.domain0.Cardinality+3) + copy(h1, s.h.Coefficients()[:s.domain0.Cardinality+2]) + h1[s.domain0.Cardinality+2].Set(&s.quotientShardsRandomizers[0]) + } + return h1 +} + +func (s *instance) h2() []fr.Element { + var h2 []fr.Element + if !s.opt.StatisticalZK { + h2 = s.h.Coefficients()[s.domain0.Cardinality+2 : 2*(s.domain0.Cardinality+2)] + } else { + h2 = make([]fr.Element, s.domain0.Cardinality+3) + copy(h2, s.h.Coefficients()[s.domain0.Cardinality+2:2*(s.domain0.Cardinality+2)]) + h2[0].Sub(&h2[0], &s.quotientShardsRandomizers[0]) + h2[s.domain0.Cardinality+2].Set(&s.quotientShardsRandomizers[1]) + } + return h2 +} + +func (s *instance) h3() []fr.Element { + var h3 []fr.Element + if !s.opt.StatisticalZK { + h3 = s.h.Coefficients()[2*(s.domain0.Cardinality+2) : 3*(s.domain0.Cardinality+2)] + } else { + h3 = make([]fr.Element, s.domain0.Cardinality+2) + copy(h3, s.h.Coefficients()[2*(s.domain0.Cardinality+2):3*(s.domain0.Cardinality+2)]) + h3[0].Sub(&h3[0], &s.quotientShardsRandomizers[1]) + } + return h3 +} + +func (s *instance) computeLinearizedPolynomial() error { + qcpzeta := make([]fr.Element, len(s.commitmentInfo)) + for i := range s.commitmentInfo { + qcpzeta[i] = s.trace.Qcp[i].Evaluate(s.zeta) + } + + blzeta := evaluateBlinded(s.x[id_L], s.bp[id_Bl], s.zeta) + brzeta := evaluateBlinded(s.x[id_R], s.bp[id_Br], s.zeta) + bozeta := evaluateBlinded(s.x[id_O], s.bp[id_Bo], s.zeta) + bzuzeta := s.proof.ZShiftedOpening.ClaimedValue + + linearizedPolynomial, err := s.innerComputeLinearizedPoly( + blzeta, + brzeta, + bozeta, + s.alpha, + s.beta, + s.gamma, + s.zeta, + bzuzeta, + qcpzeta, + s.blindedZ, + coefficients(s.cCommitments), + s.pk, + ) + if err != nil { + return err + } + s.linearizedPolynomial = linearizedPolynomial + + s.linearizedPolynomialDigest, err = s.msmG1("kzg", 0, s.linearizedPolynomial) + return err +} + +func (s *instance) batchOpening() error { + polysQcp := coefficients(s.trace.Qcp) + polysToOpen := make([][]fr.Element, 6+len(polysQcp)) + copy(polysToOpen[6:], polysQcp) + + polysToOpen[0] = s.linearizedPolynomial + polysToOpen[1] = getBlindedCoefficients(s.x[id_L], s.bp[id_Bl]) + polysToOpen[2] = getBlindedCoefficients(s.x[id_R], s.bp[id_Br]) + polysToOpen[3] = getBlindedCoefficients(s.x[id_O], s.bp[id_Bo]) + polysToOpen[4] = s.trace.S1.Coefficients() + polysToOpen[5] = s.trace.S2.Coefficients() + + digestsToOpen := make([]curve.G1Affine, len(s.pk.Vk.Qcp)+6) + copy(digestsToOpen[6:], s.pk.Vk.Qcp) + + digestsToOpen[0] = s.linearizedPolynomialDigest + digestsToOpen[1] = s.proof.LRO[0] + digestsToOpen[2] = s.proof.LRO[1] + digestsToOpen[3] = s.proof.LRO[2] + digestsToOpen[4] = s.pk.Vk.S[0] + digestsToOpen[5] = s.pk.Vk.S[1] + + var err error + s.proof.BatchedProof, err = s.batchOpenSinglePoint( + polysToOpen, + digestsToOpen, + s.zeta, + s.kzgFoldingHash, + s.proof.ZShiftedOpening.ClaimedValue.Marshal(), + ) + return err +} + +func (s *instance) batchOpenSinglePoint(polynomials [][]fr.Element, digests []curve.G1Affine, point fr.Element, hf hash.Hash, dataTranscript ...[]byte) (kzg.BatchOpeningProof, error) { + nbDigests := len(digests) + if nbDigests != len(polynomials) { + return kzg.BatchOpeningProof{}, kzg.ErrInvalidNbDigests + } + if nbDigests == 0 { + return kzg.BatchOpeningProof{}, kzg.ErrZeroNbDigests + } + + largestPoly := -1 + for _, p := range polynomials { + if len(p) > len(s.pk.Kzg.G1) { + return kzg.BatchOpeningProof{}, kzg.ErrInvalidPolynomialSize + } + if len(p) > largestPoly { + largestPoly = len(p) + } + } + + var res kzg.BatchOpeningProof + res.ClaimedValues = make([]fr.Element, len(polynomials)) + for i := range polynomials { + res.ClaimedValues[i] = evalKZGPolynomial(polynomials[i], point) + } + + gamma, err := deriveKZGBatchGamma(point, digests, res.ClaimedValues, hf, dataTranscript...) + if err != nil { + return kzg.BatchOpeningProof{}, err + } + + var foldedEvaluations fr.Element + foldedEvaluations = res.ClaimedValues[nbDigests-1] + for i := nbDigests - 2; i >= 0; i-- { + foldedEvaluations.Mul(&foldedEvaluations, &gamma). + Add(&foldedEvaluations, &res.ClaimedValues[i]) + } + + foldedPolynomials := make([]fr.Element, largestPoly) + copy(foldedPolynomials, polynomials[0]) + + gammaPower := gamma + for i := 1; i < len(polynomials); i++ { + var term fr.Element + for j := range polynomials[i] { + term.Mul(&polynomials[i][j], &gammaPower) + foldedPolynomials[j].Add(&foldedPolynomials[j], &term) + } + gammaPower.Mul(&gammaPower, &gamma) + } + + h := dividePolyByXMinusA(foldedPolynomials, foldedEvaluations, point) + + hCommit, err := s.msmG1("kzg", 0, h) + if err != nil { + return kzg.BatchOpeningProof{}, err + } + res.H.Set(&hCommit) + + return res, nil +} + +// evaluate the full set of constraints, all polynomials in x are back in +// canonical regular form at the end +func (s *instance) computeNumerator() (*iop.Polynomial, error) { + // init vectors that are used multiple times throughout the computation + n := s.domain0.Cardinality + + rho := int(s.domain1.Cardinality / n) + + // init the result polynomial & buffer + cres := make([]fr.Element, s.domain1.Cardinality) + buf := make([]fr.Element, n) + + // pre-computed to compute the bit reverse index + // of the result polynomial + m := uint64(s.domain1.Cardinality) + mm := uint64(64 - bits.TrailingZeros64(m)) + + dynamicPolyIDs := []int{id_L, id_R, id_O, id_Z, id_Qk} + commitmentValuePolyIDs := make([]int, 0, len(s.commitmentInfo)) + for i := range s.commitmentInfo { + commitmentValuePolyIDs = append(commitmentValuePolyIDs, id_Qci+2*i+1) + } + quotientDynamicPolyIDs := append(append([]int(nil), dynamicPolyIDs...), commitmentValuePolyIDs...) + dynamicTransformCacheKey := nextCacheKey("ientTransformCacheKey) + + vectorBytes := int(n) * frBytes + commitmentCount := len(quotientDynamicPolyIDs) - quotientBaseDynamicVectorCount + if commitmentCount < 0 { + return nil, fmt.Errorf("webgpu plonk bls12_381: quotient evaluator expected at least %d dynamic vectors, got %d", quotientBaseDynamicVectorCount, len(quotientDynamicPolyIDs)) + } + staticVectorCount := quotientBaseStaticVectorCount + commitmentCount + staticInputs, err := s.pk.quotientStaticBridgeInputs(rho, staticVectorCount, commitmentCount, int(n), vectorBytes) + if err != nil { + return nil, err + } + quotientAux := staticInputs.quotientAux + + dynamicPacked := make([]byte, len(quotientDynamicPolyIDs)*vectorBytes) + for i, id := range quotientDynamicPolyIDs { + if id >= len(s.x) || s.x[id] == nil { + return nil, fmt.Errorf("webgpu plonk bls12_381: missing quotient dynamic polynomial %d", id) + } + coeffs := s.x[id].Coefficients() + if len(coeffs) != int(n) { + return nil, fmt.Errorf("webgpu plonk bls12_381: quotient dynamic polynomial %d has %d coefficients, expected %d", id, len(coeffs), n) + } + packFrVectorRegularLEInto(dynamicPacked[i*vectorBytes:(i+1)*vectorBytes], coeffs) + } + + blinds := [][]fr.Element{ + s.bp[id_Bl].Coefficients(), + s.bp[id_Br].Coefficients(), + s.bp[id_Bo].Coefficients(), + s.bp[id_Bz].Coefficients(), + } + blindCoeffCount := 0 + for _, blind := range blinds { + if len(blind) > blindCoeffCount { + blindCoeffCount = len(blind) + } + } + + blindBytes := len(blinds) * blindCoeffCount * frBytes + blindsPacked := make([]byte, rho*blindBytes) + scalarBytes := quotientEvalScalarCount * frBytes + scalarsPacked := make([]byte, rho*scalarBytes) + + for i := 0; i < rho; i++ { + coset := quotientAux.cosets[i] + cosetExpMinusOne := quotientAux.cosetExpMinusOnes[i] + blindStart := i * blindBytes + for blindIndex, blind := range blinds { + start := blindStart + blindIndex*blindCoeffCount*frBytes + acc := cosetExpMinusOne + for j := range blind { + var scaled fr.Element + scaled.Mul(&blind[j], &acc) + writeFrRegularLE(blindsPacked[start+j*frBytes:start+(j+1)*frBytes], &scaled) + acc.Mul(&acc, &coset) + } + } + + packFrVectorRegularLEInto(scalarsPacked[i*scalarBytes:(i+1)*scalarBytes], []fr.Element{ + coset, + quotientAux.lagrangeScales[i], + quotientAux.cs, + quotientAux.css, + s.beta, + s.gamma, + s.alpha, + }) + } + + outputPacked, err := bridge.Bridge.TransformAndEvaluateQuotientCosets( + "bls12_381", + dynamicPacked, + staticInputs.scalingPacked, + staticInputs.staticPacked, + staticInputs.staticMontCacheKeysPacked, + staticInputs.twiddlesPacked, + staticInputs.denominatorsPacked, + blindsPacked, + scalarsPacked, + int(n), + blindCoeffCount, + commitmentCount, + dynamicTransformCacheKey, + rho, + staticInputs.auxMontCacheKey, + ) + if err != nil { + return nil, err + } + if len(outputPacked) != rho*vectorBytes { + return nil, fmt.Errorf("webgpu plonk bls12_381: quotient all-coset evaluator returned %d bytes, expected %d", len(outputPacked), rho*vectorBytes) + } + s.pk.markQuotientStaticBridgeInputsPopulated(staticInputs.staticCache) + for i := 0; i < rho; i++ { + if err := unpackFrVectorRegularLEInto(buf, outputPacked[i*vectorBytes:(i+1)*vectorBytes]); err != nil { + return nil, err + } + for j := 0; j < int(n); j++ { + // we build the polynomial in bit reverse order + cres[bits.Reverse64(uint64(rho*j+i))>>mm] = buf[j] + } + } + + canonicalizeGroup := func(ids []int) error { + polys := make([]*iop.Polynomial, 0, len(ids)) + for _, id := range ids { + if id >= len(s.x) || id == id_ZS || s.x[id] == nil { + continue + } + polys = append(polys, s.x[id]) + } + if err := canonicalizePolynomialsRegularWithWebGPU(polys, int(s.domain0.Cardinality)); err != nil { + return err + } + return nil + } + + s.x[id_ZS] = nil + s.x[id_Qk] = nil + + if err := canonicalizeGroup(dynamicPolyIDs); err != nil { + return nil, err + } + if len(commitmentValuePolyIDs) > 0 { + if err := canonicalizeGroup(commitmentValuePolyIDs); err != nil { + return nil, err + } + } + + res := iop.NewPolynomial(&cres, iop.Form{Basis: iop.LagrangeCoset, Layout: iop.BitReverse}) + + return res, nil + +} + +func evaluateBlinded(p, bp *iop.Polynomial, zeta fr.Element) fr.Element { + // Get the size of the polynomial + n := big.NewInt(int64(p.Size())) + + var pEvaluatedAtZeta fr.Element + + // Evaluate the polynomial and blinded polynomial at zeta + pEvaluatedAtZeta = p.Evaluate(zeta) + bpEvaluatedAtZeta := bp.Evaluate(zeta) + + // Multiply the evaluated blinded polynomial by tempElement + var t fr.Element + one := fr.One() + t.Exp(zeta, n).Sub(&t, &one) + bpEvaluatedAtZeta.Mul(&bpEvaluatedAtZeta, &t) + + // Add the evaluated polynomial and the evaluated blinded polynomial + pEvaluatedAtZeta.Add(&pEvaluatedAtZeta, &bpEvaluatedAtZeta) + + // Return the result + return pEvaluatedAtZeta +} + +// /!\ modifies the size +func getBlindedCoefficients(p, bp *iop.Polynomial) []fr.Element { + cp := p.Coefficients() + cbp := bp.Coefficients() + cp = append(cp, cbp...) + for i := 0; i < len(cbp); i++ { + cp[i].Sub(&cp[i], &cbp[i]) + } + return cp +} + +// commits to a polynomial of the form b*(Xⁿ-1) where b is of small degree +func commitBlindingFactor(n int, b *iop.Polynomial, key kzg.ProvingKey) curve.G1Affine { + cp := b.Coefficients() + np := b.Size() + + var res curve.G1Affine + for i := 0; i < np; i++ { + var scalar big.Int + cp[i].BigInt(&scalar) + + var hi, lo curve.G1Affine + hi.ScalarMultiplication(&key.G1[n+i], &scalar) + lo.ScalarMultiplication(&key.G1[i], &scalar) + hi.Sub(&hi, &lo) + res.Add(&res, &hi) + } + return res +} + +func evalKZGPolynomial(p []fr.Element, point fr.Element) fr.Element { + var res fr.Element + for i := len(p) - 1; i >= 0; i-- { + res.Mul(&res, &point).Add(&res, &p[i]) + } + return res +} + +// dividePolyByXMinusA computes (f-f(a))/(x-a), reusing f for the result. +func dividePolyByXMinusA(f []fr.Element, fa, a fr.Element) []fr.Element { + if len(f) == 0 { + return []fr.Element{} + } + + f[0].Sub(&f[0], &fa) + + var t fr.Element + for i := len(f) - 2; i >= 0; i-- { + t.Mul(&f[i+1], &a) + f[i].Add(&f[i], &t) + } + + return f[1:] +} + +func deriveKZGBatchGamma(point fr.Element, digests []curve.G1Affine, claimedValues []fr.Element, hf hash.Hash, dataTranscript ...[]byte) (fr.Element, error) { + fs := fiatshamir.NewTranscript(hf, "gamma") + if err := fs.Bind("gamma", point.Marshal()); err != nil { + return fr.Element{}, err + } + for i := range digests { + if err := fs.Bind("gamma", digests[i].Marshal()); err != nil { + return fr.Element{}, err + } + } + for i := range claimedValues { + if err := fs.Bind("gamma", claimedValues[i].Marshal()); err != nil { + return fr.Element{}, err + } + } + for i := range dataTranscript { + if err := fs.Bind("gamma", dataTranscript[i]); err != nil { + return fr.Element{}, err + } + } + + gammaBytes, err := fs.ComputeChallenge("gamma") + if err != nil { + return fr.Element{}, err + } + var gamma fr.Element + gamma.SetBytes(gammaBytes) + return gamma, nil +} + +// return a random polynomial of degree n, if n==-1 cancel the blinding +func getRandomPolynomial(n int) *iop.Polynomial { + var a []fr.Element + if n == -1 { + a = make([]fr.Element, 1) + a[0].SetZero() + } else { + a = make([]fr.Element, n+1) + for i := 0; i <= n; i++ { + a[i].SetRandom() + } + } + res := iop.NewPolynomial(&a, iop.Form{ + Basis: iop.Canonical, Layout: iop.Regular}) + return res +} + +func coefficients(p []*iop.Polynomial) [][]fr.Element { + res := make([][]fr.Element, len(p)) + for i, pI := range p { + res[i] = pI.Coefficients() + } + return res +} + +func (s *instance) commitToQuotient(h1, h2, h3 []fr.Element) error { + commits, err := s.msmG1Batch("kzg", 0, h1, h2, h3) + if err != nil { + return err + } + copy(s.proof.H[:], commits) + return nil +} + +// divideByZH +// The input must be in LagrangeCoset. +// The result is in Canonical Regular. (in place using a) +func divideByZH(a *iop.Polynomial, domains [2]*fft.Domain) (*iop.Polynomial, error) { + smallDomain, bigDomain := domains[0], domains[1] + if smallDomain == nil || bigDomain == nil { + return nil, errors.New("invalid domain") + } + if smallDomain.Cardinality == 0 || bigDomain.Cardinality == 0 { + return nil, errors.New("invalid domain cardinality") + } + if bigDomain.Cardinality%smallDomain.Cardinality != 0 { + return nil, errors.New("invalid domain ratio") + } + + // check that the basis is LagrangeCoset + if a.Basis != iop.LagrangeCoset || a.Layout != iop.BitReverse { + return nil, errors.New("invalid form") + } + + // prepare the evaluations of x^n-1 on the big domain's coset + xnMinusOneInverseLagrangeCoset := evaluateXnMinusOneDomainBigCoset(domains) + rho := int(bigDomain.Cardinality / smallDomain.Cardinality) + + r := a.Coefficients() + n := uint64(len(r)) + nn := uint64(64 - bits.TrailingZeros64(n)) + + for i := range r { + iRev := bits.Reverse64(uint64(i)) >> nn + r[i].Mul(&r[i], &xnMinusOneInverseLagrangeCoset[int(iRev)%rho]) + } + + if err := canonicalizeQuotientFromCosetWithWebGPU(a); err != nil { + return nil, err + } + + return a, nil + +} + +// evaluateXnMinusOneDomainBigCoset evaluates Xᵐ-1 on DomainBig coset +func evaluateXnMinusOneDomainBigCoset(domains [2]*fft.Domain) []fr.Element { + + rho := domains[1].Cardinality / domains[0].Cardinality + + res := make([]fr.Element, rho) + + expo := big.NewInt(int64(domains[0].Cardinality)) + res[0].Exp(domains[1].FrMultiplicativeGen, expo) + + var t fr.Element + t.Exp(domains[1].Generator, expo) + + one := fr.One() + + for i := 1; i < int(rho); i++ { + res[i].Mul(&res[i-1], &t) + res[i-1].Sub(&res[i-1], &one) + } + res[len(res)-1].Sub(&res[len(res)-1], &one) + + res = fr.BatchInvert(res) + + return res +} + +// innerComputeLinearizedPoly computes the linearized polynomial in canonical basis. +// The purpose is to commit and open all in one ql, qr, qm, qo, qk. +// * lZeta, rZeta, oZeta are the evaluation of l, r, o at zeta +// * z is the permutation polynomial, zu is Z(μX), the shifted version of Z +// * pk is the proving key: the linearized polynomial is a linear combination of ql, qr, qm, qo, qk. +// +// The Linearized polynomial is: +// +// α²*L₁(ζ)*Z(X) +// + α*( (l(ζ)+β*s1(ζ)+γ)*(r(ζ)+β*s2(ζ)+γ)*(β*s3(X))*Z(μζ) - Z(X)*(l(ζ)+β*id1(ζ)+γ)*(r(ζ)+β*id2(ζ)+γ)*(o(ζ)+β*id3(ζ)+γ)) +// + l(ζ)*Ql(X) + l(ζ)r(ζ)*Qm(X) + r(ζ)*Qr(X) + o(ζ)*Qo(X) + Qk(X) + ∑ᵢQcp_(ζ)Pi_(X) +// - Z_{H}(ζ)*((H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) +// +// /!\ blindedZCanonical is modified +func (s *instance) innerComputeLinearizedPoly(lZeta, rZeta, oZeta, alpha, beta, gamma, zeta, zu fr.Element, qcpZeta, blindedZCanonical []fr.Element, pi2Canonical [][]fr.Element, pk *ProvingKey) ([]fr.Element, error) { + + // l(ζ)r(ζ) + var rl fr.Element + rl.Mul(&rZeta, &lZeta) + + // s1 = α*(l(ζ)+β*s1(β)+γ)*(r(ζ)+β*s2(β)+γ)*β*Z(μζ) + // s2 = -α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + // the linearised polynomial is + // α²*L₁(ζ)*Z(X) + + // s1*s3(X)+s2*Z(X) + l(ζ)*Ql(X) + + // l(ζ)r(ζ)*Qm(X) + r(ζ)*Qr(X) + o(ζ)*Qo(X) + Qk(X) + ∑ᵢQcp_(ζ)Pi_(X) - + // Z_{H}(ζ)*((H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) + var s1, s2 fr.Element + s1 = s.trace.S1.Evaluate(zeta) // s1(ζ) + s1.Mul(&s1, &beta).Add(&s1, &lZeta).Add(&s1, &gamma) // (l(ζ)+β*s1(ζ)+γ) + tmp := s.trace.S2.Evaluate(zeta) // s2(ζ) + tmp.Mul(&tmp, &beta).Add(&tmp, &rZeta).Add(&tmp, &gamma) // (r(ζ)+β*s2(ζ)+γ) + s1.Mul(&s1, &tmp).Mul(&s1, &zu).Mul(&s1, &beta).Mul(&s1, &alpha) // (l(ζ)+β*s1(ζ)+γ)*(r(ζ)+β*s2(ζ)+γ)*β*Z(μζ)*α + + var uzeta, uuzeta fr.Element + uzeta.Mul(&zeta, &pk.Vk.CosetShift) + uuzeta.Mul(&uzeta, &pk.Vk.CosetShift) + + s2.Mul(&beta, &zeta).Add(&s2, &lZeta).Add(&s2, &gamma) // (l(ζ)+β*ζ+γ) + tmp.Mul(&beta, &uzeta).Add(&tmp, &rZeta).Add(&tmp, &gamma) // (r(ζ)+β*u*ζ+γ) + s2.Mul(&s2, &tmp) // (l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ) + tmp.Mul(&beta, &uuzeta).Add(&tmp, &oZeta).Add(&tmp, &gamma) // (o(ζ)+β*u²*ζ+γ) + s2.Mul(&s2, &tmp) // (l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + s2.Neg(&s2).Mul(&s2, &alpha) + + // Z_h(ζ), ζⁿ⁺², L₁(ζ)*α²*Z + var zhZeta, zetaNPlusTwo, alphaSquareLagrangeZero, one, den, frNbElmt fr.Element + one.SetOne() + nbElmt := int64(s.domain0.Cardinality) + alphaSquareLagrangeZero.Set(&zeta).Exp(alphaSquareLagrangeZero, big.NewInt(nbElmt)) // ζⁿ + zetaNPlusTwo.Mul(&alphaSquareLagrangeZero, &zeta).Mul(&zetaNPlusTwo, &zeta) // ζⁿ⁺² + alphaSquareLagrangeZero.Sub(&alphaSquareLagrangeZero, &one) // ζⁿ - 1 + zhZeta.Set(&alphaSquareLagrangeZero) // Z_h(ζ) = ζⁿ - 1 + frNbElmt.SetUint64(uint64(nbElmt)) + den.Sub(&zeta, &one).Inverse(&den) // 1/(ζ-1) + alphaSquareLagrangeZero.Mul(&alphaSquareLagrangeZero, &den). // L₁ = (ζⁿ - 1)/(ζ-1) + Mul(&alphaSquareLagrangeZero, &alpha). + Mul(&alphaSquareLagrangeZero, &alpha). + Mul(&alphaSquareLagrangeZero, &s.domain0.CardinalityInv) // α²*L₁(ζ) + + s3canonical := s.trace.S3.Coefficients() + + if err := canonicalizePolynomialsRegularWithWebGPU([]*iop.Polynomial{s.trace.Qk}, int(s.domain0.Cardinality)); err != nil { + return nil, err + } + + // len(h1)=len(h2)=len(blindedZCanonical)=len(h3)+1 when Statistical ZK is activated + // len(h1)=len(h2)=len(h3)=len(blindedZCanonical)-1 when Statistical ZK is deactivated + h1 := s.h1() + h2 := s.h2() + h3 := s.h3() + + // at this stage we have + // s1 = α*(l(ζ)+β*s1(β)+γ)*(r(ζ)+β*s2(β)+γ)*β*Z(μζ) + // s2 = -α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + cql := s.trace.Ql.Coefficients() + cqr := s.trace.Qr.Coefficients() + cqm := s.trace.Qm.Coefficients() + cqo := s.trace.Qo.Coefficients() + cqk := s.trace.Qk.Coefficients() + + var t, t0, t1 fr.Element + + for i := range blindedZCanonical { + t.Mul(&blindedZCanonical[i], &s2) // -Z(X)*α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + if i < len(s3canonical) { + t0.Mul(&s3canonical[i], &s1) // α*(l(ζ)+β*s1(β)+γ)*(r(ζ)+β*s2(β)+γ)*β*Z(μζ)*β*s3(X) + t.Add(&t, &t0) + } + if i < len(cqm) { + t1.Mul(&cqm[i], &rl) // l(ζ)r(ζ)*Qm(X) + t.Add(&t, &t1) // linPol += l(ζ)r(ζ)*Qm(X) + t0.Mul(&cql[i], &lZeta) // l(ζ)Q_l(X) + t.Add(&t, &t0) // linPol += l(ζ)*Ql(X) + t0.Mul(&cqr[i], &rZeta) //r(ζ)*Qr(X) + t.Add(&t, &t0) // linPol += r(ζ)*Qr(X) + t0.Mul(&cqo[i], &oZeta) // o(ζ)*Qo(X) + t.Add(&t, &t0) // linPol += o(ζ)*Qo(X) + t.Add(&t, &cqk[i]) // linPol += Qk(X) + for j := range qcpZeta { // linPol += ∑ᵢQcp_(ζ)Pi_(X) + t0.Mul(&pi2Canonical[j][i], &qcpZeta[j]) + t.Add(&t, &t0) + } + } + + t0.Mul(&blindedZCanonical[i], &alphaSquareLagrangeZero) // α²L₁(ζ)Z(X) + blindedZCanonical[i].Add(&t, &t0) // linPol += α²L₁(ζ)Z(X) + + // if statistical zeroknowledge is deactivated, len(h1)=len(h2)=len(h3)=len(blindedZ)-1. + // Else len(h1)=len(h2)=len(blindedZCanonical)=len(h3)+1 + if i < len(h3) { + t.Mul(&h3[i], &zetaNPlusTwo). + Add(&t, &h2[i]). + Mul(&t, &zetaNPlusTwo). + Add(&t, &h1[i]). + Mul(&t, &zhZeta) + blindedZCanonical[i].Sub(&blindedZCanonical[i], &t) // linPol -= Z_h(ζ)*(H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) + } else if s.opt.StatisticalZK { + t.Mul(&h2[i], &zetaNPlusTwo). + Add(&t, &h1[i]). + Mul(&t, &zhZeta) + blindedZCanonical[i].Sub(&blindedZCanonical[i], &t) // linPol -= Z_h(ζ)*(H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) + } + } + + return blindedZCanonical, nil +} + +func bindPublicData(fs *fiatshamir.Transcript, challenge string, vk *native.VerifyingKey, publicInputs []fr.Element) error { + if err := fs.Bind(challenge, vk.S[0].Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.S[1].Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.S[2].Marshal()); err != nil { + return err + } + + if err := fs.Bind(challenge, vk.Ql.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qr.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qm.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qo.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qk.Marshal()); err != nil { + return err + } + for i := range vk.Qcp { + if err := fs.Bind(challenge, vk.Qcp[i].Marshal()); err != nil { + return err + } + } + + for i := 0; i < len(publicInputs); i++ { + if err := fs.Bind(challenge, publicInputs[i].Marshal()); err != nil { + return err + } + } + + return nil +} + +func deriveRandomness(fs *fiatshamir.Transcript, challenge string, points ...*curve.G1Affine) (fr.Element, error) { + var buf [curve.SizeOfG1AffineUncompressed]byte + var r fr.Element + + for _, p := range points { + buf = p.RawBytes() + if err := fs.Bind(challenge, buf[:]); err != nil { + return r, err + } + } + + b, err := fs.ComputeChallenge(challenge) + if err != nil { + return r, err + } + r.SetBytes(b) + return r, nil +} diff --git a/backend/accelerated/webgpu/plonk/bls12-381/provingkey.go b/backend/accelerated/webgpu/plonk/bls12-381/provingkey.go new file mode 100644 index 0000000000..44f3a148b4 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bls12-381/provingkey.go @@ -0,0 +1,72 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bls12381 + +import ( + "sync" + + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" + native "github.com/consensys/gnark/backend/plonk/bls12-381" + cs "github.com/consensys/gnark/constraint/bls12-381" +) + +type ProvingKey struct { + native.ProvingKey + prepareMu sync.Mutex + handle string + staticNumeratorCache *staticNumeratorCache +} + +func (pk *ProvingKey) Prepare() error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + return pk.ensurePreparedLocked() +} + +func (pk *ProvingKey) ensurePreparedLocked() error { + if pk.handle != "" { + return nil + } + if err := bridge.Bridge.Init("bls12_381"); err != nil { + return err + } + payload := bridge.JSObject() + payload.Set("kzg", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.Kzg.G1))) + payload.Set("kzgCount", len(pk.Kzg.G1)) + payload.Set("kzgLagrange", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.KzgLagrange.G1))) + payload.Set("kzgLagrangeCount", len(pk.KzgLagrange.G1)) + handle, err := bridge.Bridge.PrepareKey("bls12_381", payload) + if err != nil { + return err + } + pk.handle = handle + return nil +} + +func (pk *ProvingKey) PrepareWithCS(spr *cs.SparseR1CS) error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + if err := pk.ensurePreparedLocked(); err != nil { + return err + } + domain0, domain1 := domainsForSPR(spr) + trace := native.NewTrace(spr, domain0) + if err := pk.ensureStaticNumeratorCache(trace, domain0, domain1); err != nil { + return err + } + if err := pk.preloadQuotientStaticCaches(trace, domain0, domain1); err != nil { + return err + } + if err := bridge.Bridge.PrewarmQuotientTransformDomain("bls12_381", int(domain0.Cardinality)); err != nil { + return err + } + if err := bridge.Bridge.PrewarmQuotientEvaluateKernel("bls12_381", len(trace.Qcp)); err != nil { + return err + } + return bridge.Bridge.PrewarmQuotientCanonicalizeDomain("bls12_381", int(domain1.Cardinality)) +} diff --git a/backend/accelerated/webgpu/plonk/bls12-381/serialize.go b/backend/accelerated/webgpu/plonk/bls12-381/serialize.go new file mode 100644 index 0000000000..771e5c5121 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bls12-381/serialize.go @@ -0,0 +1,264 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bls12381 + +import ( + "encoding/binary" + "errors" + "fmt" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" + fp "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/iop" + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" +) + +const ( + frBytes = fr.Bytes + g1CoordinateBytes = fp.Bytes + g1PointBytes = 3 * g1CoordinateBytes +) + +func packFrVectorRegularLEInto(dst []byte, values []fr.Element) []byte { + required := len(values) * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + for i := range values { + base := i * frBytes + writeFrRegularLE(dst[base:base+frBytes], &values[i]) + } + return dst +} + +func packFrVectorsRegularLEPaddedInto(dst []byte, vectors [][]fr.Element, elementCount int) ([]byte, error) { + if elementCount <= 0 { + return nil, errors.New("webgpu plonk bls12_381: empty MSM batch") + } + required := len(vectors) * elementCount * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + clear(dst) + } + for i, values := range vectors { + if len(values) > elementCount { + return nil, fmt.Errorf("webgpu plonk bls12_381: MSM batch vector %d has %d elements, expected at most %d", i, len(values), elementCount) + } + start := i * elementCount * frBytes + packFrVectorRegularLEInto(dst[start:start+len(values)*frBytes], values) + } + return dst, nil +} + +func writeFrRegularLE(dst []byte, value *fr.Element) { + be := value.Bytes() + for i := 0; i < frBytes; i++ { + dst[i] = be[frBytes-1-i] + } +} + +func readFrRegularLE(src []byte) (fr.Element, error) { + if len(src) != frBytes { + return fr.Element{}, fmt.Errorf("webgpu plonk bls12_381: expected %d Fr bytes, got %d", frBytes, len(src)) + } + var le [frBytes]byte + copy(le[:], src) + return fr.LittleEndian.Element(&le) +} + +func unpackFrVectorRegularLEInto(dst []fr.Element, src []byte) error { + if len(src) != len(dst)*frBytes { + return fmt.Errorf("webgpu plonk bls12_381: expected %d Fr vector bytes, got %d", len(dst)*frBytes, len(src)) + } + for i := range dst { + value, err := readFrRegularLE(src[i*frBytes : (i+1)*frBytes]) + if err != nil { + return err + } + dst[i] = value + } + return nil +} + +type canonicalizeGroupKey struct { + inputBitReversed bool + inverseCoset bool +} + +func canonicalizePolynomialsRegularWithWebGPU(polys []*iop.Polynomial, elementCount int) error { + n := elementCount + groups := make(map[canonicalizeGroupKey][]*iop.Polynomial) + for _, p := range polys { + if p == nil { + continue + } + if p.Basis == iop.Canonical { + p.ToRegular() + continue + } + coeffs := p.Coefficients() + if len(coeffs) != n { + return fmt.Errorf("webgpu plonk bls12_381: canonicalize polynomial has %d coefficients, expected %d", len(coeffs), n) + } + switch p.Basis { + case iop.Lagrange: + case iop.LagrangeCoset: + default: + return fmt.Errorf("webgpu plonk bls12_381: unsupported polynomial basis %d", p.Basis) + } + switch p.Layout { + case iop.Regular: + case iop.BitReverse: + default: + return fmt.Errorf("webgpu plonk bls12_381: unsupported polynomial layout %d", p.Layout) + } + key := canonicalizeGroupKey{ + inputBitReversed: p.Layout == iop.BitReverse, + inverseCoset: p.Basis == iop.LagrangeCoset, + } + groups[key] = append(groups[key], p) + } + + vectorBytes := n * frBytes + for key, group := range groups { + valuesPacked := make([]byte, len(group)*vectorBytes) + for i, p := range group { + packFrVectorRegularLEInto(valuesPacked[i*vectorBytes:(i+1)*vectorBytes], p.Coefficients()) + } + canonicalPacked, err := bridge.Bridge.CanonicalizeQuotientVectors("bls12_381", valuesPacked, len(group), n, key.inputBitReversed, key.inverseCoset) + if err != nil { + return err + } + if len(canonicalPacked) != len(valuesPacked) { + return fmt.Errorf("webgpu plonk bls12_381: quotient canonicalize returned %d bytes, expected %d", len(canonicalPacked), len(valuesPacked)) + } + for i, p := range group { + if err := unpackFrVectorRegularLEInto(p.Coefficients(), canonicalPacked[i*vectorBytes:(i+1)*vectorBytes]); err != nil { + return err + } + p.Basis = iop.Canonical + p.Layout = iop.Regular + } + } + return nil +} + +func canonicalizeQuotientFromCosetWithWebGPU(p *iop.Polynomial) error { + return canonicalizePolynomialsRegularWithWebGPU([]*iop.Polynomial{p}, len(p.Coefficients())) +} + +func lagrangePolynomialsRegularWithWebGPU(polys []*iop.Polynomial, elementCount int) error { + n := elementCount + filtered := make([]*iop.Polynomial, 0, len(polys)) + for _, p := range polys { + if p == nil { + continue + } + if p.Basis == iop.Lagrange { + p.ToRegular() + continue + } + if p.Basis != iop.Canonical || p.Layout != iop.Regular { + return fmt.Errorf("webgpu plonk bls12_381: expected canonical regular polynomial, got basis %d layout %d", p.Basis, p.Layout) + } + if len(p.Coefficients()) != n { + return fmt.Errorf("webgpu plonk bls12_381: lagrange polynomial has %d coefficients, expected %d", len(p.Coefficients()), n) + } + filtered = append(filtered, p) + } + if len(filtered) == 0 { + return nil + } + + vectorBytes := n * frBytes + valuesPacked := make([]byte, len(filtered)*vectorBytes) + for i, p := range filtered { + packFrVectorRegularLEInto(valuesPacked[i*vectorBytes:(i+1)*vectorBytes], p.Coefficients()) + } + lagrangePacked, err := bridge.Bridge.LagrangeQuotientVectors("bls12_381", valuesPacked, len(filtered), n) + if err != nil { + return err + } + if len(lagrangePacked) != len(valuesPacked) { + return fmt.Errorf("webgpu plonk bls12_381: quotient lagrange returned %d bytes, expected %d", len(lagrangePacked), len(valuesPacked)) + } + for i, p := range filtered { + if err := unpackFrVectorRegularLEInto(p.Coefficients(), lagrangePacked[i*vectorBytes:(i+1)*vectorBytes]); err != nil { + return err + } + p.Basis = iop.Lagrange + p.Layout = iop.Regular + } + return nil +} + +func packG1AffineJacobianBatch(points []curve.G1Affine) []byte { + out := make([]byte, len(points)*g1PointBytes) + for i := range points { + base := i * g1PointBytes + writeFPMontLE(out[base:base+g1CoordinateBytes], &points[i].X) + writeFPMontLE(out[base+g1CoordinateBytes:base+2*g1CoordinateBytes], &points[i].Y) + writeG1JacobianZOne(out[base+2*g1CoordinateBytes : base+3*g1CoordinateBytes]) + } + return out +} + +func decodeG1AffineFromPacked(packed []byte, err error) (curve.G1Affine, error) { + if err != nil { + return curve.G1Affine{}, err + } + if len(packed) != 2*g1CoordinateBytes { + return curve.G1Affine{}, fmt.Errorf("webgpu plonk bls12_381: expected %d G1 bytes, got %d", 2*g1CoordinateBytes, len(packed)) + } + return curve.G1Affine{ + X: readFPMontLE(packed[:g1CoordinateBytes]), + Y: readFPMontLE(packed[g1CoordinateBytes:]), + }, nil +} + +func decodeG1AffineBatchFromPacked(packed []byte, count int, err error) ([]curve.G1Affine, error) { + if err != nil { + return nil, err + } + expected := count * 2 * g1CoordinateBytes + if len(packed) != expected { + return nil, fmt.Errorf("webgpu plonk bls12_381: expected %d G1 batch bytes, got %d", expected, len(packed)) + } + res := make([]curve.G1Affine, count) + for i := range res { + start := i * 2 * g1CoordinateBytes + res[i] = curve.G1Affine{ + X: readFPMontLE(packed[start : start+g1CoordinateBytes]), + Y: readFPMontLE(packed[start+g1CoordinateBytes : start+2*g1CoordinateBytes]), + } + } + return res, nil +} + +func readFPMontLE(src []byte) fp.Element { + var z fp.Element + for i := range z { + z[i] = binary.LittleEndian.Uint64(src[i*8 : (i+1)*8]) + } + return z +} + +func writeFPMontLE(dst []byte, value *fp.Element) { + for i := range *value { + binary.LittleEndian.PutUint64(dst[i*8:(i+1)*8], (*value)[i]) + } +} + +func writeG1JacobianZOne(dst []byte) { + var one fp.Element + one.SetOne() + writeFPMontLE(dst, &one) +} diff --git a/backend/accelerated/webgpu/plonk/bn254/caching.go b/backend/accelerated/webgpu/plonk/bn254/caching.go new file mode 100644 index 0000000000..68bafd5291 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bn254/caching.go @@ -0,0 +1,530 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bn254 + +import ( + "encoding/binary" + "errors" + "fmt" + "math/big" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/iop" + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" + native "github.com/consensys/gnark/backend/plonk/bn254" +) + +const ( + quotientBaseDynamicVectorCount = 5 + quotientBaseStaticVectorCount = 7 + quotientEvalScalarCount = 7 +) + +var quotientTransformCacheKey int +var quotientStaticMontCacheKey int +var quotientAuxMontCacheKey int +var cacheKeyMu sync.Mutex + +type staticNumeratorCache struct { + domain0Cardinality uint64 + domain1Cardinality uint64 + qcpCount int + canonical staticNumeratorPolys + cosets []staticNumeratorPolys + quotientAux quotientAuxCache + webgpuStaticMontKeys []int + webgpuStaticMontPopulated []bool + webgpuAuxMontKey int + webgpuAuxMontPopulated bool +} + +type staticNumeratorPolys struct { + ql, qr, qm, qo *iop.Polynomial + s1, s2, s3 *iop.Polynomial + qcp []*iop.Polynomial +} + +type quotientAuxCache struct { + twiddlesPacked []byte + scalingPacked []byte + denominatorsPacked []byte + cosets []fr.Element + cosetExpMinusOnes []fr.Element + lagrangeScales []fr.Element + cs, css fr.Element +} + +func nextCacheKey(counter *int) int { + cacheKeyMu.Lock() + defer cacheKeyMu.Unlock() + + *counter = *counter + 1 + if *counter <= 0 { + *counter = 1 + } + return *counter +} + +func (pk *ProvingKey) ensureStaticNumeratorCache(trace *native.Trace, domain0, domain1 *fft.Domain) error { + qcpCount := len(trace.Qcp) + if pk.staticNumeratorCache != nil && + pk.staticNumeratorCache.domain0Cardinality == domain0.Cardinality && + pk.staticNumeratorCache.domain1Cardinality == domain1.Cardinality && + pk.staticNumeratorCache.qcpCount == qcpCount { + if len(pk.staticNumeratorCache.webgpuStaticMontKeys) != len(pk.staticNumeratorCache.cosets) { + pk.staticNumeratorCache.webgpuStaticMontKeys = make([]int, len(pk.staticNumeratorCache.cosets)) + for i := range pk.staticNumeratorCache.webgpuStaticMontKeys { + pk.staticNumeratorCache.webgpuStaticMontKeys[i] = nextCacheKey("ientStaticMontCacheKey) + } + pk.staticNumeratorCache.webgpuStaticMontPopulated = make([]bool, len(pk.staticNumeratorCache.cosets)) + } + if len(pk.staticNumeratorCache.webgpuStaticMontPopulated) != len(pk.staticNumeratorCache.cosets) { + pk.staticNumeratorCache.webgpuStaticMontPopulated = make([]bool, len(pk.staticNumeratorCache.cosets)) + } + if err := pk.staticNumeratorCache.ensureQuotientAuxCache(domain0, domain1); err != nil { + return err + } + pk.staticNumeratorCache.canonical.applyToTrace(trace) + return nil + } + + canonical := cloneStaticNumeratorPolys(trace) + if err := canonicalizePolynomialsRegularWithWebGPU(canonical.polynomials(), int(domain0.Cardinality)); err != nil { + return err + } + + rho := int(domain1.Cardinality / domain0.Cardinality) + cosets := make([]staticNumeratorPolys, rho) + webgpuStaticMontKeys := make([]int, rho) + for i := range webgpuStaticMontKeys { + webgpuStaticMontKeys[i] = nextCacheKey("ientStaticMontCacheKey) + } + + cosetTable, err := domain0.CosetTable() + if err != nil { + return err + } + scalingVector := cosetTable + working := canonical.clone() + for i := 0; i < rho; i++ { + if i == 1 { + w := domain1.Generator + scalingVector = make([]fr.Element, domain0.Cardinality) + fft.BuildExpTable(w, scalingVector) + } + + if err := transformPolynomialsToCoset(working.polynomials(), domain0, scalingVector); err != nil { + return err + } + cosets[i] = working.clone() + } + quotientAux, err := buildQuotientAuxCache(domain0, domain1) + if err != nil { + return err + } + + pk.staticNumeratorCache = &staticNumeratorCache{ + domain0Cardinality: domain0.Cardinality, + domain1Cardinality: domain1.Cardinality, + qcpCount: qcpCount, + canonical: canonical, + cosets: cosets, + quotientAux: quotientAux, + webgpuStaticMontKeys: webgpuStaticMontKeys, + webgpuStaticMontPopulated: make([]bool, rho), + webgpuAuxMontKey: nextCacheKey("ientAuxMontCacheKey), + } + pk.staticNumeratorCache.canonical.applyToTrace(trace) + return nil +} + +func (c *staticNumeratorCache) ensureQuotientAuxCache(domain0, domain1 *fft.Domain) error { + n := int(domain0.Cardinality) + rho := int(domain1.Cardinality / domain0.Cardinality) + if c.webgpuAuxMontKey <= 0 { + c.webgpuAuxMontKey = nextCacheKey("ientAuxMontCacheKey) + c.webgpuAuxMontPopulated = false + } + if c.quotientAux.valid(n, rho) { + return nil + } + aux, err := buildQuotientAuxCache(domain0, domain1) + if err != nil { + return err + } + c.quotientAux = aux + c.webgpuAuxMontPopulated = false + return nil +} + +func (a quotientAuxCache) valid(n, rho int) bool { + vectorBytes := n * frBytes + return rho > 0 && + len(a.twiddlesPacked) == vectorBytes && + len(a.scalingPacked) == rho*vectorBytes && + len(a.denominatorsPacked) == rho*vectorBytes && + len(a.cosets) == rho && + len(a.cosetExpMinusOnes) == rho && + len(a.lagrangeScales) == rho +} + +func buildQuotientAuxCache(domain0, domain1 *fft.Domain) (quotientAuxCache, error) { + n := int(domain0.Cardinality) + rho := int(domain1.Cardinality / domain0.Cardinality) + if n <= 0 || rho <= 0 { + return quotientAuxCache{}, fmt.Errorf("webgpu plonk bn254: invalid quotient auxiliary domain n=%d rho=%d", n, rho) + } + + twiddles0 := make([]fr.Element, n) + if n == 1 { + twiddles0[0].SetOne() + } else { + twiddles, err := domain0.Twiddles() + if err != nil { + return quotientAuxCache{}, err + } + copy(twiddles0, twiddles[0]) + w := twiddles0[1] + for i := len(twiddles[0]); i < len(twiddles0); i++ { + twiddles0[i].Mul(&twiddles0[i-1], &w) + } + } + + cosetTable, err := domain0.CosetTable() + if err != nil { + return quotientAuxCache{}, err + } + + vectorBytes := n * frBytes + aux := quotientAuxCache{ + twiddlesPacked: packFrVectorRegularLEInto(nil, twiddles0), + scalingPacked: make([]byte, rho*vectorBytes), + denominatorsPacked: make([]byte, rho*vectorBytes), + cosets: make([]fr.Element, rho), + cosetExpMinusOnes: make([]fr.Element, rho), + lagrangeScales: make([]fr.Element, rho), + } + aux.cs.Set(&domain1.FrMultiplicativeGen) + aux.css.Square(&aux.cs) + + shifters := make([]fr.Element, rho) + shifters[0].Set(&domain1.FrMultiplicativeGen) + for i := 1; i < rho; i++ { + shifters[i].Set(&domain1.Generator) + } + + denominators := make([]fr.Element, n) + bufBatchInvert := make([]fr.Element, n) + scalingVector := make([]fr.Element, n) + var coset, cosetExpMinusOne, one fr.Element + coset.SetOne() + one.SetOne() + bn := big.NewInt(int64(domain0.Cardinality)) + for i := 0; i < rho; i++ { + coset.Mul(&coset, &shifters[i]) + aux.cosets[i].Set(&coset) + cosetExpMinusOne.Exp(coset, bn).Sub(&cosetExpMinusOne, &one) + aux.cosetExpMinusOnes[i].Set(&cosetExpMinusOne) + aux.lagrangeScales[i].Mul(&cosetExpMinusOne, &domain0.CardinalityInv) + + for j := 0; j < n; j++ { + denominators[j].Mul(&coset, &twiddles0[j]).Sub(&denominators[j], &one) + } + batchInvert(denominators, bufBatchInvert) + packFrVectorRegularLEInto(aux.denominatorsPacked[i*vectorBytes:(i+1)*vectorBytes], denominators) + + currentScalingVector := scalingVector + if i == 0 { + currentScalingVector = cosetTable + } else { + fft.BuildExpTable(coset, scalingVector) + } + packFrVectorRegularLEInto(aux.scalingPacked[i*vectorBytes:(i+1)*vectorBytes], currentScalingVector) + } + return aux, nil +} + +func cloneStaticNumeratorPolys(trace *native.Trace) staticNumeratorPolys { + res := staticNumeratorPolys{ + ql: trace.Ql.Clone(), + qr: trace.Qr.Clone(), + qm: trace.Qm.Clone(), + qo: trace.Qo.Clone(), + s1: trace.S1.Clone(), + s2: trace.S2.Clone(), + s3: trace.S3.Clone(), + qcp: make([]*iop.Polynomial, len(trace.Qcp)), + } + for i := range trace.Qcp { + res.qcp[i] = trace.Qcp[i].Clone() + } + return res +} + +func (p staticNumeratorPolys) clone() staticNumeratorPolys { + res := staticNumeratorPolys{ + ql: p.ql.Clone(), + qr: p.qr.Clone(), + qm: p.qm.Clone(), + qo: p.qo.Clone(), + s1: p.s1.Clone(), + s2: p.s2.Clone(), + s3: p.s3.Clone(), + qcp: make([]*iop.Polynomial, len(p.qcp)), + } + for i := range p.qcp { + res.qcp[i] = p.qcp[i].Clone() + } + return res +} + +func (p staticNumeratorPolys) polynomials() []*iop.Polynomial { + res := []*iop.Polynomial{p.ql, p.qr, p.qm, p.qo, p.s1, p.s2, p.s3} + res = append(res, p.qcp...) + return res +} + +func (p staticNumeratorPolys) applyToTrace(trace *native.Trace) { + trace.Ql = p.ql + trace.Qr = p.qr + trace.Qm = p.qm + trace.Qo = p.qo + trace.S1 = p.s1 + trace.S2 = p.s2 + trace.S3 = p.s3 + trace.Qcp = p.qcp +} + +func transformPolynomialsToCoset(polys []*iop.Polynomial, domain *fft.Domain, scalingVector []fr.Element) error { + // shift polynomials to be in the correct coset + if err := canonicalizePolynomialsRegularWithWebGPU(polys, int(domain.Cardinality)); err != nil { + return err + } + + // scale by shifter + for _, p := range polys { + cp := p.Coefficients() + for j := range cp { + cp[j].Mul(&cp[j], &scalingVector[j]) + } + } + return lagrangePolynomialsRegularWithWebGPU(polys, int(domain.Cardinality)) +} + +func (pk *ProvingKey) ensureStaticNumeratorCacheForTrace(trace *native.Trace, domain0, domain1 *fft.Domain) error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + return pk.ensureStaticNumeratorCache(trace, domain0, domain1) +} + +func (pk *ProvingKey) preloadQuotientStaticCaches(trace *native.Trace, domain0, domain1 *fft.Domain) error { + staticCache := pk.staticNumeratorCache + if staticCache == nil { + return errors.New("webgpu plonk bn254: missing static numerator cache") + } + n := int(domain0.Cardinality) + rho := int(domain1.Cardinality / domain0.Cardinality) + if len(staticCache.cosets) != rho { + return fmt.Errorf("webgpu plonk bn254: static numerator cache has %d cosets, expected %d", len(staticCache.cosets), rho) + } + quotientAux := staticCache.quotientAux + if !quotientAux.valid(n, rho) { + return errors.New("webgpu plonk bn254: invalid quotient auxiliary cache") + } + auxMontCacheKey := staticCache.webgpuAuxMontKey + if auxMontCacheKey <= 0 || int(uint32(auxMontCacheKey)) != auxMontCacheKey { + return fmt.Errorf("webgpu plonk bn254: invalid quotient auxiliary WebGPU cache key %d", auxMontCacheKey) + } + + commitmentCount := len(trace.Qcp) + staticVectorCount := quotientBaseStaticVectorCount + commitmentCount + vectorBytes := n * frBytes + staticPacked, err := packStaticNumeratorCosets(staticCache, rho, staticVectorCount, commitmentCount, n, vectorBytes) + if err != nil { + return err + } + + staticMontCacheKeysPacked, err := packStaticMontCacheKeys(staticCache, rho) + if err != nil { + return err + } + + if err := bridge.Bridge.PreloadQuotientStaticAndAux( + "bn254", + staticPacked, + staticMontCacheKeysPacked, + quotientAux.scalingPacked, + quotientAux.twiddlesPacked, + quotientAux.denominatorsPacked, + n, + staticVectorCount, + rho, + auxMontCacheKey, + ); err != nil { + return err + } + + for i := range staticCache.webgpuStaticMontPopulated { + staticCache.webgpuStaticMontPopulated[i] = true + } + staticCache.webgpuAuxMontPopulated = true + return nil +} + +type quotientStaticBridgeInputs struct { + staticCache *staticNumeratorCache + quotientAux quotientAuxCache + staticPacked []byte + staticMontCacheKeysPacked []byte + twiddlesPacked []byte + scalingPacked []byte + denominatorsPacked []byte + auxMontCacheKey int +} + +func (pk *ProvingKey) quotientStaticBridgeInputs(rho, staticVectorCount, commitmentCount, n, vectorBytes int) (quotientStaticBridgeInputs, error) { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + staticCache := pk.staticNumeratorCache + if staticCache == nil || len(staticCache.cosets) != rho { + return quotientStaticBridgeInputs{}, errors.New("missing static numerator cache") + } + quotientAux := staticCache.quotientAux + if !quotientAux.valid(n, rho) { + return quotientStaticBridgeInputs{}, errors.New("webgpu plonk bn254: invalid quotient auxiliary cache") + } + staticMontCacheKeysPacked, err := packStaticMontCacheKeys(staticCache, rho) + if err != nil { + return quotientStaticBridgeInputs{}, err + } + + reuseStaticMontCache := true + for i := 0; i < rho; i++ { + if !staticCache.webgpuStaticMontPopulated[i] { + reuseStaticMontCache = false + } + } + auxMontCacheKey := staticCache.webgpuAuxMontKey + if auxMontCacheKey <= 0 || int(uint32(auxMontCacheKey)) != auxMontCacheKey { + return quotientStaticBridgeInputs{}, fmt.Errorf("webgpu plonk bn254: invalid quotient auxiliary WebGPU cache key %d", auxMontCacheKey) + } + + var staticPacked []byte + if !reuseStaticMontCache { + staticPacked, err = packStaticNumeratorCosets(staticCache, rho, staticVectorCount, commitmentCount, n, vectorBytes) + if err != nil { + return quotientStaticBridgeInputs{}, err + } + } + + twiddlesPacked := quotientAux.twiddlesPacked + scalingPacked := quotientAux.scalingPacked + denominatorsPacked := quotientAux.denominatorsPacked + if staticCache.webgpuAuxMontPopulated { + twiddlesPacked = nil + scalingPacked = nil + denominatorsPacked = nil + } + + return quotientStaticBridgeInputs{ + staticCache: staticCache, + quotientAux: quotientAux, + staticPacked: staticPacked, + staticMontCacheKeysPacked: staticMontCacheKeysPacked, + twiddlesPacked: twiddlesPacked, + scalingPacked: scalingPacked, + denominatorsPacked: denominatorsPacked, + auxMontCacheKey: auxMontCacheKey, + }, nil +} + +func (pk *ProvingKey) markQuotientStaticBridgeInputsPopulated(staticCache *staticNumeratorCache) { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + for i := range staticCache.webgpuStaticMontPopulated { + staticCache.webgpuStaticMontPopulated[i] = true + } + staticCache.webgpuAuxMontPopulated = true +} + +func packStaticMontCacheKeys(staticCache *staticNumeratorCache, rho int) ([]byte, error) { + if len(staticCache.webgpuStaticMontKeys) != rho || len(staticCache.webgpuStaticMontPopulated) != rho { + return nil, errors.New("webgpu plonk bn254: invalid static numerator WebGPU cache metadata") + } + out := make([]byte, rho*4) + for i := 0; i < rho; i++ { + key := staticCache.webgpuStaticMontKeys[i] + if key <= 0 || int(uint32(key)) != key { + return nil, fmt.Errorf("webgpu plonk bn254: invalid static numerator WebGPU cache key %d", key) + } + binary.LittleEndian.PutUint32(out[i*4:(i+1)*4], uint32(key)) + } + return out, nil +} + +func packStaticNumeratorCosets(staticCache *staticNumeratorCache, rho, staticVectorCount, commitmentCount, n, vectorBytes int) ([]byte, error) { + staticPacked := make([]byte, rho*staticVectorCount*vectorBytes) + for i := 0; i < rho; i++ { + start := i * staticVectorCount * vectorBytes + if err := packStaticNumeratorPolys( + staticPacked[start:start+staticVectorCount*vectorBytes], + staticCache.cosets[i], + staticVectorCount, + commitmentCount, + n, + vectorBytes, + ); err != nil { + return nil, err + } + } + return staticPacked, nil +} + +func packStaticNumeratorPolys(dst []byte, polys staticNumeratorPolys, staticVectorCount, commitmentCount, n, vectorBytes int) error { + if len(polys.qcp) != commitmentCount { + return fmt.Errorf("webgpu plonk bn254: quotient evaluator expected %d qcp vectors, got %d", commitmentCount, len(polys.qcp)) + } + staticVectors := polys.polynomials() + if len(staticVectors) != staticVectorCount { + return fmt.Errorf("webgpu plonk bn254: quotient evaluator expected %d static vectors, got %d", staticVectorCount, len(staticVectors)) + } + for i, p := range staticVectors { + if p == nil { + return fmt.Errorf("webgpu plonk bn254: missing quotient static polynomial %d", i) + } + coeffs := p.Coefficients() + if len(coeffs) != n { + return fmt.Errorf("webgpu plonk bn254: quotient static polynomial %d has %d coefficients, expected %d", i, len(coeffs), n) + } + packFrVectorRegularLEInto(dst[i*vectorBytes:(i+1)*vectorBytes], coeffs) + } + return nil +} + +// batchInvert modifies in place vec, with vec[i]<-vec[i]^{-1}, using +// the Montgomery batch inversion trick. We don't use gnark-crypto's batchInvert +// because we want to use a buffer preallocated, to avoid wasting memory. +// /!\ it doesn't check that all vec's inputs or non zero, it is ensured by the size +// of the field /!\ +func batchInvert(vec, buf []fr.Element) { + // local function only, vec and buf are of the same size + copy(buf, vec) + for i := 1; i < len(vec); i++ { + vec[i].Mul(&vec[i], &vec[i-1]) + } + acc := vec[len(vec)-1] + acc.Inverse(&acc) + for i := len(vec) - 1; i > 0; i-- { + vec[i].Mul(&acc, &vec[i-1]) + acc.Mul(&acc, &buf[i]) + } + vec[0].Set(&acc) +} diff --git a/backend/accelerated/webgpu/plonk/bn254/prove.go b/backend/accelerated/webgpu/plonk/bn254/prove.go new file mode 100644 index 0000000000..25816b7d94 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bn254/prove.go @@ -0,0 +1,1304 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bn254 + +import ( + "errors" + "fmt" + "hash" + "math/big" + "math/bits" + + curve "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/hash_to_field" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/iop" + "github.com/consensys/gnark-crypto/ecc/bn254/kzg" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark/backend" + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" + native "github.com/consensys/gnark/backend/plonk/bn254" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + cs "github.com/consensys/gnark/constraint/bn254" + "github.com/consensys/gnark/constraint/solver" + fcs "github.com/consensys/gnark/frontend/cs" +) + +const ( + id_L int = iota + id_R + id_O + id_Z + id_ZS + id_Ql + id_Qr + id_Qm + id_Qo + id_Qk + id_S1 + id_S2 + id_S3 + id_Qci // [ .. , Qc_i, Pi_i, ...] +) + +// blinding factors +const ( + id_Bl int = iota + id_Br + id_Bo + id_Bz + nb_blinding_polynomials +) + +// blinding orders (-1 to deactivate) +const ( + order_blinding_L = 1 + order_blinding_R = 1 + order_blinding_O = 1 + order_blinding_Z = 2 +) + +func Prove(spr *cs.SparseR1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (proof *native.Proof, err error) { + // parse the options + opt, err := backend.NewProverConfig(opts...) + if err != nil { + return nil, fmt.Errorf("get prover options: %w", err) + } + + if err := pk.Prepare(); err != nil { + return nil, fmt.Errorf("prepare proving key: %w", err) + } + + // init instance + instance, err := newInstance(spr, pk, fullWitness, &opt) + if err != nil { + return nil, fmt.Errorf("new instance: %w", err) + } + + if err := instance.initBlindingPolynomials(); err != nil { + return nil, fmt.Errorf("init blinding polynomials: %w", err) + } + if err := instance.solveConstraints(); err != nil { + return nil, fmt.Errorf("solve constraints: %w", err) + } + if err := instance.completeQk(); err != nil { + return nil, fmt.Errorf("complete qk: %w", err) + } + if err := instance.deriveGammaAndBeta(); err != nil { + return nil, fmt.Errorf("derive gamma and beta: %w", err) + } + if err := instance.buildRatioCopyConstraint(); err != nil { + return nil, fmt.Errorf("build ratio copy constraint: %w", err) + } + if err := instance.computeQuotient(); err != nil { + return nil, fmt.Errorf("compute quotient: %w", err) + } + if err := instance.openZ(); err != nil { + return nil, fmt.Errorf("open z: %w", err) + } + if err := instance.computeLinearizedPolynomial(); err != nil { + return nil, fmt.Errorf("compute linearized polynomial: %w", err) + } + if err := instance.batchOpening(); err != nil { + return nil, fmt.Errorf("batch opening: %w", err) + } + + return instance.proof, nil +} + +// represents a Prover instance +type instance struct { + pk *ProvingKey + proof *native.Proof + spr *cs.SparseR1CS + opt *backend.ProverConfig + + fs *fiatshamir.Transcript + kzgFoldingHash hash.Hash // for KZG folding + htfFunc hash.Hash // hash to field function + + // polynomials + x []*iop.Polynomial // x stores tracks the polynomial we need + bp []*iop.Polynomial // blinding polynomials + h *iop.Polynomial // h is the quotient polynomial + blindedZ []fr.Element // blindedZ is the blinded version of Z + quotientShardsRandomizers [2]fr.Element // random elements for blinding the shards of the quotient + + precomputedDenominators []fr.Element // stores the denominators of the Lagrange polynomials + linearizedPolynomial []fr.Element + linearizedPolynomialDigest kzg.Digest + + fullWitness witness.Witness + + // bsb22 commitment stuff + commitmentInfo constraint.PlonkCommitments + commitmentVal []fr.Element + cCommitments []*iop.Polynomial + + // challenges + gamma, beta, alpha, zeta fr.Element + + domain0, domain1 *fft.Domain + + trace *native.Trace +} + +func newInstance(spr *cs.SparseR1CS, pk *ProvingKey, fullWitness witness.Witness, opts *backend.ProverConfig) (*instance, error) { + if opts.HashToFieldFn == nil { + opts.HashToFieldFn = hash_to_field.New([]byte("BSB22-Plonk")) + } + s := instance{ + pk: pk, + proof: &native.Proof{}, + spr: spr, + opt: opts, + fullWitness: fullWitness, + bp: make([]*iop.Polynomial, nb_blinding_polynomials), + fs: fiatshamir.NewTranscript(opts.ChallengeHash, "gamma", "beta", "alpha", "zeta"), + kzgFoldingHash: opts.KZGFoldingHash, + htfFunc: opts.HashToFieldFn, + } + s.initBSB22Commitments() + s.x = make([]*iop.Polynomial, id_Qci+2*len(s.commitmentInfo)) + + // init fft domains + s.domain0, s.domain1 = domainsForSPR(spr) + + // sampling random numbers for blinding the quotient + if opts.StatisticalZK { + s.quotientShardsRandomizers[0].SetRandom() + s.quotientShardsRandomizers[1].SetRandom() + } + + // build trace + s.trace = native.NewTrace(spr, s.domain0) + if err := pk.ensureStaticNumeratorCacheForTrace(s.trace, s.domain0, s.domain1); err != nil { + return nil, err + } + + return &s, nil +} + +func domainsForSPR(spr *cs.SparseR1CS) (*fft.Domain, *fft.Domain) { + nbConstraints := spr.GetNbConstraints() + sizeSystem := uint64(nbConstraints + len(spr.Public)) // len(spr.Public) is for the placeholder constraints + domain0 := fft.NewDomain(sizeSystem) + + // h, the quotient polynomial is of degree 3(n+1)+2, so it's in a 3(n+2) dim vector space, + // the domain is the next power of 2 superior to 3(n+2). 4*domainNum is enough in all cases + // except when n<6. + var domain1 *fft.Domain + if sizeSystem < 6 { + domain1 = fft.NewDomain(8*sizeSystem, fft.WithoutPrecompute()) + } else { + domain1 = fft.NewDomain(4*sizeSystem, fft.WithoutPrecompute()) + } + return domain0, domain1 +} + +func (s *instance) initBlindingPolynomials() error { + s.bp[id_Bl] = getRandomPolynomial(order_blinding_L) + s.bp[id_Br] = getRandomPolynomial(order_blinding_R) + s.bp[id_Bo] = getRandomPolynomial(order_blinding_O) + s.bp[id_Bz] = getRandomPolynomial(order_blinding_Z) + return nil +} + +func (s *instance) initBSB22Commitments() { + s.commitmentInfo = s.spr.CommitmentInfo.(constraint.PlonkCommitments) + s.commitmentVal = make([]fr.Element, len(s.commitmentInfo)) // TODO @Tabaie get rid of this + s.cCommitments = make([]*iop.Polynomial, len(s.commitmentInfo)) + s.proof.Bsb22Commitments = make([]kzg.Digest, len(s.commitmentInfo)) + + // override the hint for the commitment constraints + bsb22ID := solver.GetHintID(fcs.Bsb22CommitmentComputePlaceholder) + s.opt.SolverOpts = append(s.opt.SolverOpts, solver.OverrideHint(bsb22ID, s.bsb22Hint)) +} + +// Computing and verifying Bsb22 multi-commits explained in https://hackmd.io/x8KsadW3RRyX7YTCFJIkHg +func (s *instance) bsb22Hint(_ *big.Int, ins, outs []*big.Int) error { + var err error + commDepth := int(ins[0].Int64()) + ins = ins[1:] + + res := &s.commitmentVal[commDepth] + + commitmentInfo := s.spr.CommitmentInfo.(constraint.PlonkCommitments)[commDepth] + committedValues := make([]fr.Element, s.domain0.Cardinality) + offset := s.spr.GetNbPublicVariables() + for i := range ins { + committedValues[offset+commitmentInfo.Committed[i]].SetBigInt(ins[i]) + } + if _, err = committedValues[offset+commitmentInfo.CommitmentIndex].SetRandom(); err != nil { // Commitment injection constraint has qcp = 0. Safe to use for blinding. + return err + } + if _, err = committedValues[offset+s.spr.GetNbConstraints()-1].SetRandom(); err != nil { // Last constraint has qcp = 0. Safe to use for blinding + return err + } + s.cCommitments[commDepth] = iop.NewPolynomial(&committedValues, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + if s.proof.Bsb22Commitments[commDepth], err = kzg.Commit(s.cCommitments[commDepth].Coefficients(), s.pk.KzgLagrange, 1); err != nil { + return err + } + + s.htfFunc.Write(s.proof.Bsb22Commitments[commDepth].Marshal()) + hashBts := s.htfFunc.Sum(nil) + s.htfFunc.Reset() + nbBuf := fr.Bytes + if s.htfFunc.Size() < fr.Bytes { + nbBuf = s.htfFunc.Size() + } + res.SetBytes(hashBts[:nbBuf]) // TODO @Tabaie use CommitmentIndex for this; create a new variable CommitmentConstraintIndex for other uses + res.BigInt(outs[0]) + + return nil +} + +// solveConstraints computes the evaluation of the polynomials L, R, O +// and sets x[id_L], x[id_R], x[id_O] in Lagrange form +func (s *instance) solveConstraints() error { + _solution, err := s.spr.Solve(s.fullWitness, s.opt.SolverOpts...) + if err != nil { + return err + } + solution := _solution.(*cs.SparseR1CSSolution) + evaluationLDomainSmall := []fr.Element(solution.L) + evaluationRDomainSmall := []fr.Element(solution.R) + evaluationODomainSmall := []fr.Element(solution.O) + s.x[id_L] = iop.NewPolynomial(&evaluationLDomainSmall, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + s.x[id_R] = iop.NewPolynomial(&evaluationRDomainSmall, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + s.x[id_O] = iop.NewPolynomial(&evaluationODomainSmall, iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}) + + // commit to l, r, o and add blinding factors + if err := s.commitToLRO(); err != nil { + return err + } + return nil +} + +func (s *instance) completeQk() error { + qk := s.trace.Qk.Clone() + qkCoeffs := qk.Coefficients() + + wWitness, ok := s.fullWitness.Vector().(fr.Vector) + if !ok { + return witness.ErrInvalidWitness + } + + copy(qkCoeffs, wWitness[:len(s.spr.Public)]) + + for i := range s.commitmentInfo { + qkCoeffs[s.spr.GetNbPublicVariables()+s.commitmentInfo[i].CommitmentIndex] = s.commitmentVal[i] + } + + s.x[id_Qk] = qk + + return nil +} + +// commitToLRO commits to L, R, O polynomials using reduced-size MSMs. +// +// L, R, O live on a domain of size n = 2^k, but only offset = nbPublic + nbConstraints +// entries carry actual values. The rest are s0 = witness[0] (first public input). +// For R and O, the first nbPublic entries (placeholders) are also s0. +// +// Key identity: Σ_{i=0}^{n-1} KzgLagrange.G1[i] = [Σ L_i(τ)]₁ = [1]₁ = Kzg.G1[0] +// +// So we can rewrite the commitment as: +// +// [P] = Σ P[i]·G1_lag[i] +// = Σ (P[i]-s0)·G1_lag[i] + s0·Σ G1_lag[i] +// = MSM((P[i]-s0), G1_lag[i]) + s0·Kzg.G1[0] +// +// The (P[i]-s0) terms are zero in the padding region, so the MSM only needs +// the non-padding entries. For a 2.2M-constraint circuit on a 4M domain, +// this nearly halves each MSM. +func (s *instance) commitToLRO() error { + n := int(s.domain0.Cardinality) + nbPublic := len(s.spr.Public) + offset := nbPublic + s.spr.GetNbConstraints() + + // s0 = witness[0] = first public input + wWitness, ok := s.fullWitness.Vector().(fr.Vector) + if !ok { + return witness.ErrInvalidWitness + } + s0 := wWitness[0] + + // correctionPoint = s0 · [1]₁ = s0 · Kzg.G1[0] + var s0BigInt big.Int + s0.BigInt(&s0BigInt) + var correctionPoint curve.G1Affine + correctionPoint.ScalarMultiplication(&s.pk.Kzg.G1[0], &s0BigInt) + + // L: subtract s0, MSM on [0:offset], add correction + blinding, restore + coeffs := s.x[id_L].Coefficients() + for i := 0; i < offset; i++ { + coeffs[i].Sub(&coeffs[i], &s0) + } + var commit curve.G1Affine + commit, err := s.msmG1("kzgLagrange", 0, coeffs[:offset]) + if err != nil { + return err + } + for i := 0; i < offset; i++ { + coeffs[i].Add(&coeffs[i], &s0) + } + commit.Add(&commit, &correctionPoint) + cb := commitBlindingFactor(n, s.bp[id_Bl], s.pk.Kzg) + s.proof.LRO[0].Add(&commit, &cb) + + // R: subtract s0, MSM on [nbPublic:offset], add correction + blinding, restore + coeffs = s.x[id_R].Coefficients() + for i := nbPublic; i < offset; i++ { + coeffs[i].Sub(&coeffs[i], &s0) + } + commit, err = s.msmG1("kzgLagrange", nbPublic, coeffs[nbPublic:offset]) + if err != nil { + return err + } + for i := nbPublic; i < offset; i++ { + coeffs[i].Add(&coeffs[i], &s0) + } + commit.Add(&commit, &correctionPoint) + cb = commitBlindingFactor(n, s.bp[id_Br], s.pk.Kzg) + s.proof.LRO[1].Add(&commit, &cb) + + // O: same as R + coeffs = s.x[id_O].Coefficients() + for i := nbPublic; i < offset; i++ { + coeffs[i].Sub(&coeffs[i], &s0) + } + commit, err = s.msmG1("kzgLagrange", nbPublic, coeffs[nbPublic:offset]) + if err != nil { + return err + } + for i := nbPublic; i < offset; i++ { + coeffs[i].Add(&coeffs[i], &s0) + } + commit.Add(&commit, &correctionPoint) + cb = commitBlindingFactor(n, s.bp[id_Bo], s.pk.Kzg) + s.proof.LRO[2].Add(&commit, &cb) + + return nil +} + +func (s *instance) msmG1(vectorName string, start int, scalars []fr.Element) (curve.G1Affine, error) { + scalarsPacked := packFrVectorRegularLEInto(nil, scalars) + packed, err := bridge.Bridge.MSMG1Slice(s.pk.handle, vectorName, start, len(scalars), scalarsPacked) + return decodeG1AffineFromPacked(packed, err) +} + +func (s *instance) msmG1Batch(vectorName string, start int, scalarVectors ...[]fr.Element) ([]curve.G1Affine, error) { + if len(scalarVectors) == 0 { + return nil, errors.New("webgpu plonk bn254: empty MSM batch") + } + termCount := 0 + for _, scalars := range scalarVectors { + if len(scalars) > termCount { + termCount = len(scalars) + } + } + scalarsPacked, err := packFrVectorsRegularLEPaddedInto(nil, scalarVectors, termCount) + if err != nil { + return nil, err + } + packed, err := bridge.Bridge.MSMG1Batch(s.pk.handle, vectorName, start, termCount, len(scalarVectors), scalarsPacked) + return decodeG1AffineBatchFromPacked(packed, len(scalarVectors), err) +} + +// deriveGammaAndBeta (copy constraint) +func (s *instance) deriveGammaAndBeta() error { + wWitness, ok := s.fullWitness.Vector().(fr.Vector) + if !ok { + return witness.ErrInvalidWitness + } + + if err := bindPublicData(s.fs, "gamma", s.pk.Vk, wWitness[:len(s.spr.Public)]); err != nil { + return err + } + + gamma, err := deriveRandomness(s.fs, "gamma", &s.proof.LRO[0], &s.proof.LRO[1], &s.proof.LRO[2]) + if err != nil { + return err + } + + bbeta, err := s.fs.ComputeChallenge("beta") + if err != nil { + return err + } + s.gamma = gamma + s.beta.SetBytes(bbeta) + + return nil +} + +// commitToPolyAndBlinding computes the KZG commitment of a polynomial p +// in Lagrange form (large degree) +// and add the contribution of a blinding polynomial b (small degree) +// /!\ The polynomial p is supposed to be in Lagrange form. +func (s *instance) commitToPolyAndBlinding(p, b *iop.Polynomial) (commit curve.G1Affine, err error) { + + commit, err = s.msmG1("kzgLagrange", 0, p.Coefficients()) + + // we add in the blinding contribution + n := int(s.domain0.Cardinality) + cb := commitBlindingFactor(n, b, s.pk.Kzg) + commit.Add(&commit, &cb) + + return +} + +func (s *instance) deriveAlpha() (err error) { + alphaDeps := make([]*curve.G1Affine, len(s.proof.Bsb22Commitments)+1) + for i := range s.proof.Bsb22Commitments { + alphaDeps[i] = &s.proof.Bsb22Commitments[i] + } + alphaDeps[len(alphaDeps)-1] = &s.proof.Z + s.alpha, err = deriveRandomness(s.fs, "alpha", alphaDeps...) + return err +} + +func (s *instance) deriveZeta() (err error) { + s.zeta, err = deriveRandomness(s.fs, "zeta", &s.proof.H[0], &s.proof.H[1], &s.proof.H[2]) + return +} + +// computeQuotient computes H +func (s *instance) computeQuotient() (err error) { + s.x[id_Ql] = s.trace.Ql + s.x[id_Qr] = s.trace.Qr + s.x[id_Qm] = s.trace.Qm + s.x[id_Qo] = s.trace.Qo + s.x[id_S1] = s.trace.S1 + s.x[id_S2] = s.trace.S2 + s.x[id_S3] = s.trace.S3 + + for i := 0; i < len(s.commitmentInfo); i++ { + s.x[id_Qci+2*i] = s.trace.Qcp[i] + } + + n := s.domain0.Cardinality + lone := make([]fr.Element, n) + lone[0].SetOne() + + for i := 0; i < len(s.commitmentInfo); i++ { + s.x[id_Qci+2*i+1] = s.cCommitments[i] + } + + // derive alpha + if err = s.deriveAlpha(); err != nil { + return err + } + + // TODO complete waste of memory find another way to do that + identity := make([]fr.Element, n) + identity[1].Set(&s.beta) + + s.x[id_ZS] = s.x[id_Z].ShallowClone().Shift(1) + + numerator, err := s.computeNumerator() + if err != nil { + return err + } + + s.h, err = divideByZH(numerator, [2]*fft.Domain{s.domain0, s.domain1}) + if err != nil { + return err + } + + // commit to h + if err := s.commitToQuotient(s.h1(), s.h2(), s.h3()); err != nil { + return err + } + + if err := s.deriveZeta(); err != nil { + return err + } + + return nil +} + +func (s *instance) buildRatioCopyConstraint() (err error) { + // TODO @gbotrel having iop.BuildRatioCopyConstraint return something + // with capacity = len() + 4 would avoid extra alloc / copy during openZ + s.x[id_Z], err = iop.BuildRatioCopyConstraint( + []*iop.Polynomial{ + s.x[id_L], + s.x[id_R], + s.x[id_O], + }, + s.trace.S, + s.beta, + s.gamma, + iop.Form{Basis: iop.Lagrange, Layout: iop.Regular}, + s.domain0, + ) + if err != nil { + return err + } + + // commit to the blinded version of z + s.proof.Z, err = s.commitToPolyAndBlinding(s.x[id_Z], s.bp[id_Bz]) + + return +} + +// open Z (blinded) at ωζ +func (s *instance) openZ() (err error) { + var zetaShifted fr.Element + zetaShifted.Mul(&s.zeta, &s.pk.Vk.Generator) + s.blindedZ = getBlindedCoefficients(s.x[id_Z], s.bp[id_Bz]) + // open z at zeta + s.proof.ZShiftedOpening, err = s.openKZG(s.blindedZ, zetaShifted) + if err != nil { + return err + } + return nil +} + +func (s *instance) openKZG(p []fr.Element, point fr.Element) (kzg.OpeningProof, error) { + if len(p) > len(s.pk.Kzg.G1) { + return kzg.OpeningProof{}, kzg.ErrInvalidPolynomialSize + } + + var proof kzg.OpeningProof + proof.ClaimedValue = evalKZGPolynomial(p, point) + + cp := make([]fr.Element, len(p)) + copy(cp, p) + h := dividePolyByXMinusA(cp, proof.ClaimedValue, point) + + hCommit, err := s.msmG1("kzg", 0, h) + if err != nil { + return kzg.OpeningProof{}, err + } + proof.H.Set(&hCommit) + + return proof, nil +} + +func (s *instance) h1() []fr.Element { + var h1 []fr.Element + if !s.opt.StatisticalZK { + h1 = s.h.Coefficients()[:s.domain0.Cardinality+2] + } else { + h1 = make([]fr.Element, s.domain0.Cardinality+3) + copy(h1, s.h.Coefficients()[:s.domain0.Cardinality+2]) + h1[s.domain0.Cardinality+2].Set(&s.quotientShardsRandomizers[0]) + } + return h1 +} + +func (s *instance) h2() []fr.Element { + var h2 []fr.Element + if !s.opt.StatisticalZK { + h2 = s.h.Coefficients()[s.domain0.Cardinality+2 : 2*(s.domain0.Cardinality+2)] + } else { + h2 = make([]fr.Element, s.domain0.Cardinality+3) + copy(h2, s.h.Coefficients()[s.domain0.Cardinality+2:2*(s.domain0.Cardinality+2)]) + h2[0].Sub(&h2[0], &s.quotientShardsRandomizers[0]) + h2[s.domain0.Cardinality+2].Set(&s.quotientShardsRandomizers[1]) + } + return h2 +} + +func (s *instance) h3() []fr.Element { + var h3 []fr.Element + if !s.opt.StatisticalZK { + h3 = s.h.Coefficients()[2*(s.domain0.Cardinality+2) : 3*(s.domain0.Cardinality+2)] + } else { + h3 = make([]fr.Element, s.domain0.Cardinality+2) + copy(h3, s.h.Coefficients()[2*(s.domain0.Cardinality+2):3*(s.domain0.Cardinality+2)]) + h3[0].Sub(&h3[0], &s.quotientShardsRandomizers[1]) + } + return h3 +} + +func (s *instance) computeLinearizedPolynomial() error { + qcpzeta := make([]fr.Element, len(s.commitmentInfo)) + for i := range s.commitmentInfo { + qcpzeta[i] = s.trace.Qcp[i].Evaluate(s.zeta) + } + + blzeta := evaluateBlinded(s.x[id_L], s.bp[id_Bl], s.zeta) + brzeta := evaluateBlinded(s.x[id_R], s.bp[id_Br], s.zeta) + bozeta := evaluateBlinded(s.x[id_O], s.bp[id_Bo], s.zeta) + bzuzeta := s.proof.ZShiftedOpening.ClaimedValue + + linearizedPolynomial, err := s.innerComputeLinearizedPoly( + blzeta, + brzeta, + bozeta, + s.alpha, + s.beta, + s.gamma, + s.zeta, + bzuzeta, + qcpzeta, + s.blindedZ, + coefficients(s.cCommitments), + s.pk, + ) + if err != nil { + return err + } + s.linearizedPolynomial = linearizedPolynomial + + s.linearizedPolynomialDigest, err = s.msmG1("kzg", 0, s.linearizedPolynomial) + return err +} + +func (s *instance) batchOpening() error { + polysQcp := coefficients(s.trace.Qcp) + polysToOpen := make([][]fr.Element, 6+len(polysQcp)) + copy(polysToOpen[6:], polysQcp) + + polysToOpen[0] = s.linearizedPolynomial + polysToOpen[1] = getBlindedCoefficients(s.x[id_L], s.bp[id_Bl]) + polysToOpen[2] = getBlindedCoefficients(s.x[id_R], s.bp[id_Br]) + polysToOpen[3] = getBlindedCoefficients(s.x[id_O], s.bp[id_Bo]) + polysToOpen[4] = s.trace.S1.Coefficients() + polysToOpen[5] = s.trace.S2.Coefficients() + + digestsToOpen := make([]curve.G1Affine, len(s.pk.Vk.Qcp)+6) + copy(digestsToOpen[6:], s.pk.Vk.Qcp) + + digestsToOpen[0] = s.linearizedPolynomialDigest + digestsToOpen[1] = s.proof.LRO[0] + digestsToOpen[2] = s.proof.LRO[1] + digestsToOpen[3] = s.proof.LRO[2] + digestsToOpen[4] = s.pk.Vk.S[0] + digestsToOpen[5] = s.pk.Vk.S[1] + + var err error + s.proof.BatchedProof, err = s.batchOpenSinglePoint( + polysToOpen, + digestsToOpen, + s.zeta, + s.kzgFoldingHash, + s.proof.ZShiftedOpening.ClaimedValue.Marshal(), + ) + return err +} + +func (s *instance) batchOpenSinglePoint(polynomials [][]fr.Element, digests []curve.G1Affine, point fr.Element, hf hash.Hash, dataTranscript ...[]byte) (kzg.BatchOpeningProof, error) { + nbDigests := len(digests) + if nbDigests != len(polynomials) { + return kzg.BatchOpeningProof{}, kzg.ErrInvalidNbDigests + } + if nbDigests == 0 { + return kzg.BatchOpeningProof{}, kzg.ErrZeroNbDigests + } + + largestPoly := -1 + for _, p := range polynomials { + if len(p) > len(s.pk.Kzg.G1) { + return kzg.BatchOpeningProof{}, kzg.ErrInvalidPolynomialSize + } + if len(p) > largestPoly { + largestPoly = len(p) + } + } + + var res kzg.BatchOpeningProof + res.ClaimedValues = make([]fr.Element, len(polynomials)) + for i := range polynomials { + res.ClaimedValues[i] = evalKZGPolynomial(polynomials[i], point) + } + + gamma, err := deriveKZGBatchGamma(point, digests, res.ClaimedValues, hf, dataTranscript...) + if err != nil { + return kzg.BatchOpeningProof{}, err + } + + var foldedEvaluations fr.Element + foldedEvaluations = res.ClaimedValues[nbDigests-1] + for i := nbDigests - 2; i >= 0; i-- { + foldedEvaluations.Mul(&foldedEvaluations, &gamma). + Add(&foldedEvaluations, &res.ClaimedValues[i]) + } + + foldedPolynomials := make([]fr.Element, largestPoly) + copy(foldedPolynomials, polynomials[0]) + + gammaPower := gamma + for i := 1; i < len(polynomials); i++ { + var term fr.Element + for j := range polynomials[i] { + term.Mul(&polynomials[i][j], &gammaPower) + foldedPolynomials[j].Add(&foldedPolynomials[j], &term) + } + gammaPower.Mul(&gammaPower, &gamma) + } + + h := dividePolyByXMinusA(foldedPolynomials, foldedEvaluations, point) + + hCommit, err := s.msmG1("kzg", 0, h) + if err != nil { + return kzg.BatchOpeningProof{}, err + } + res.H.Set(&hCommit) + + return res, nil +} + +// evaluate the full set of constraints, all polynomials in x are back in +// canonical regular form at the end +func (s *instance) computeNumerator() (*iop.Polynomial, error) { + // init vectors that are used multiple times throughout the computation + n := s.domain0.Cardinality + + rho := int(s.domain1.Cardinality / n) + + // init the result polynomial & buffer + cres := make([]fr.Element, s.domain1.Cardinality) + buf := make([]fr.Element, n) + + // pre-computed to compute the bit reverse index + // of the result polynomial + m := uint64(s.domain1.Cardinality) + mm := uint64(64 - bits.TrailingZeros64(m)) + + dynamicPolyIDs := []int{id_L, id_R, id_O, id_Z, id_Qk} + commitmentValuePolyIDs := make([]int, 0, len(s.commitmentInfo)) + for i := range s.commitmentInfo { + commitmentValuePolyIDs = append(commitmentValuePolyIDs, id_Qci+2*i+1) + } + quotientDynamicPolyIDs := append(append([]int(nil), dynamicPolyIDs...), commitmentValuePolyIDs...) + dynamicTransformCacheKey := nextCacheKey("ientTransformCacheKey) + + vectorBytes := int(n) * frBytes + commitmentCount := len(quotientDynamicPolyIDs) - quotientBaseDynamicVectorCount + if commitmentCount < 0 { + return nil, fmt.Errorf("webgpu plonk bn254: quotient evaluator expected at least %d dynamic vectors, got %d", quotientBaseDynamicVectorCount, len(quotientDynamicPolyIDs)) + } + staticVectorCount := quotientBaseStaticVectorCount + commitmentCount + staticInputs, err := s.pk.quotientStaticBridgeInputs(rho, staticVectorCount, commitmentCount, int(n), vectorBytes) + if err != nil { + return nil, err + } + quotientAux := staticInputs.quotientAux + + dynamicPacked := make([]byte, len(quotientDynamicPolyIDs)*vectorBytes) + for i, id := range quotientDynamicPolyIDs { + if id >= len(s.x) || s.x[id] == nil { + return nil, fmt.Errorf("webgpu plonk bn254: missing quotient dynamic polynomial %d", id) + } + coeffs := s.x[id].Coefficients() + if len(coeffs) != int(n) { + return nil, fmt.Errorf("webgpu plonk bn254: quotient dynamic polynomial %d has %d coefficients, expected %d", id, len(coeffs), n) + } + packFrVectorRegularLEInto(dynamicPacked[i*vectorBytes:(i+1)*vectorBytes], coeffs) + } + + blinds := [][]fr.Element{ + s.bp[id_Bl].Coefficients(), + s.bp[id_Br].Coefficients(), + s.bp[id_Bo].Coefficients(), + s.bp[id_Bz].Coefficients(), + } + blindCoeffCount := 0 + for _, blind := range blinds { + if len(blind) > blindCoeffCount { + blindCoeffCount = len(blind) + } + } + + blindBytes := len(blinds) * blindCoeffCount * frBytes + blindsPacked := make([]byte, rho*blindBytes) + scalarBytes := quotientEvalScalarCount * frBytes + scalarsPacked := make([]byte, rho*scalarBytes) + + for i := 0; i < rho; i++ { + coset := quotientAux.cosets[i] + cosetExpMinusOne := quotientAux.cosetExpMinusOnes[i] + blindStart := i * blindBytes + for blindIndex, blind := range blinds { + start := blindStart + blindIndex*blindCoeffCount*frBytes + acc := cosetExpMinusOne + for j := range blind { + var scaled fr.Element + scaled.Mul(&blind[j], &acc) + writeFrRegularLE(blindsPacked[start+j*frBytes:start+(j+1)*frBytes], &scaled) + acc.Mul(&acc, &coset) + } + } + + packFrVectorRegularLEInto(scalarsPacked[i*scalarBytes:(i+1)*scalarBytes], []fr.Element{ + coset, + quotientAux.lagrangeScales[i], + quotientAux.cs, + quotientAux.css, + s.beta, + s.gamma, + s.alpha, + }) + } + + outputPacked, err := bridge.Bridge.TransformAndEvaluateQuotientCosets( + "bn254", + dynamicPacked, + staticInputs.scalingPacked, + staticInputs.staticPacked, + staticInputs.staticMontCacheKeysPacked, + staticInputs.twiddlesPacked, + staticInputs.denominatorsPacked, + blindsPacked, + scalarsPacked, + int(n), + blindCoeffCount, + commitmentCount, + dynamicTransformCacheKey, + rho, + staticInputs.auxMontCacheKey, + ) + if err != nil { + return nil, err + } + if len(outputPacked) != rho*vectorBytes { + return nil, fmt.Errorf("webgpu plonk bn254: quotient all-coset evaluator returned %d bytes, expected %d", len(outputPacked), rho*vectorBytes) + } + s.pk.markQuotientStaticBridgeInputsPopulated(staticInputs.staticCache) + for i := 0; i < rho; i++ { + if err := unpackFrVectorRegularLEInto(buf, outputPacked[i*vectorBytes:(i+1)*vectorBytes]); err != nil { + return nil, err + } + for j := 0; j < int(n); j++ { + // we build the polynomial in bit reverse order + cres[bits.Reverse64(uint64(rho*j+i))>>mm] = buf[j] + } + } + + canonicalizeGroup := func(ids []int) error { + polys := make([]*iop.Polynomial, 0, len(ids)) + for _, id := range ids { + if id >= len(s.x) || id == id_ZS || s.x[id] == nil { + continue + } + polys = append(polys, s.x[id]) + } + if err := canonicalizePolynomialsRegularWithWebGPU(polys, int(s.domain0.Cardinality)); err != nil { + return err + } + return nil + } + + s.x[id_ZS] = nil + s.x[id_Qk] = nil + + if err := canonicalizeGroup(dynamicPolyIDs); err != nil { + return nil, err + } + if len(commitmentValuePolyIDs) > 0 { + if err := canonicalizeGroup(commitmentValuePolyIDs); err != nil { + return nil, err + } + } + + res := iop.NewPolynomial(&cres, iop.Form{Basis: iop.LagrangeCoset, Layout: iop.BitReverse}) + + return res, nil + +} + +func evaluateBlinded(p, bp *iop.Polynomial, zeta fr.Element) fr.Element { + // Get the size of the polynomial + n := big.NewInt(int64(p.Size())) + + var pEvaluatedAtZeta fr.Element + + // Evaluate the polynomial and blinded polynomial at zeta + pEvaluatedAtZeta = p.Evaluate(zeta) + bpEvaluatedAtZeta := bp.Evaluate(zeta) + + // Multiply the evaluated blinded polynomial by tempElement + var t fr.Element + one := fr.One() + t.Exp(zeta, n).Sub(&t, &one) + bpEvaluatedAtZeta.Mul(&bpEvaluatedAtZeta, &t) + + // Add the evaluated polynomial and the evaluated blinded polynomial + pEvaluatedAtZeta.Add(&pEvaluatedAtZeta, &bpEvaluatedAtZeta) + + // Return the result + return pEvaluatedAtZeta +} + +// /!\ modifies the size +func getBlindedCoefficients(p, bp *iop.Polynomial) []fr.Element { + cp := p.Coefficients() + cbp := bp.Coefficients() + cp = append(cp, cbp...) + for i := 0; i < len(cbp); i++ { + cp[i].Sub(&cp[i], &cbp[i]) + } + return cp +} + +// commits to a polynomial of the form b*(Xⁿ-1) where b is of small degree +func commitBlindingFactor(n int, b *iop.Polynomial, key kzg.ProvingKey) curve.G1Affine { + cp := b.Coefficients() + np := b.Size() + + var res curve.G1Affine + for i := 0; i < np; i++ { + var scalar big.Int + cp[i].BigInt(&scalar) + + var hi, lo curve.G1Affine + hi.ScalarMultiplication(&key.G1[n+i], &scalar) + lo.ScalarMultiplication(&key.G1[i], &scalar) + hi.Sub(&hi, &lo) + res.Add(&res, &hi) + } + return res +} + +func evalKZGPolynomial(p []fr.Element, point fr.Element) fr.Element { + var res fr.Element + for i := len(p) - 1; i >= 0; i-- { + res.Mul(&res, &point).Add(&res, &p[i]) + } + return res +} + +// dividePolyByXMinusA computes (f-f(a))/(x-a), reusing f for the result. +func dividePolyByXMinusA(f []fr.Element, fa, a fr.Element) []fr.Element { + if len(f) == 0 { + return []fr.Element{} + } + + f[0].Sub(&f[0], &fa) + + var t fr.Element + for i := len(f) - 2; i >= 0; i-- { + t.Mul(&f[i+1], &a) + f[i].Add(&f[i], &t) + } + + return f[1:] +} + +func deriveKZGBatchGamma(point fr.Element, digests []curve.G1Affine, claimedValues []fr.Element, hf hash.Hash, dataTranscript ...[]byte) (fr.Element, error) { + fs := fiatshamir.NewTranscript(hf, "gamma") + if err := fs.Bind("gamma", point.Marshal()); err != nil { + return fr.Element{}, err + } + for i := range digests { + if err := fs.Bind("gamma", digests[i].Marshal()); err != nil { + return fr.Element{}, err + } + } + for i := range claimedValues { + if err := fs.Bind("gamma", claimedValues[i].Marshal()); err != nil { + return fr.Element{}, err + } + } + for i := range dataTranscript { + if err := fs.Bind("gamma", dataTranscript[i]); err != nil { + return fr.Element{}, err + } + } + + gammaBytes, err := fs.ComputeChallenge("gamma") + if err != nil { + return fr.Element{}, err + } + var gamma fr.Element + gamma.SetBytes(gammaBytes) + return gamma, nil +} + +// return a random polynomial of degree n, if n==-1 cancel the blinding +func getRandomPolynomial(n int) *iop.Polynomial { + var a []fr.Element + if n == -1 { + a = make([]fr.Element, 1) + a[0].SetZero() + } else { + a = make([]fr.Element, n+1) + for i := 0; i <= n; i++ { + a[i].SetRandom() + } + } + res := iop.NewPolynomial(&a, iop.Form{ + Basis: iop.Canonical, Layout: iop.Regular}) + return res +} + +func coefficients(p []*iop.Polynomial) [][]fr.Element { + res := make([][]fr.Element, len(p)) + for i, pI := range p { + res[i] = pI.Coefficients() + } + return res +} + +func (s *instance) commitToQuotient(h1, h2, h3 []fr.Element) error { + commits, err := s.msmG1Batch("kzg", 0, h1, h2, h3) + if err != nil { + return err + } + copy(s.proof.H[:], commits) + return nil +} + +// divideByZH +// The input must be in LagrangeCoset. +// The result is in Canonical Regular. (in place using a) +func divideByZH(a *iop.Polynomial, domains [2]*fft.Domain) (*iop.Polynomial, error) { + smallDomain, bigDomain := domains[0], domains[1] + if smallDomain == nil || bigDomain == nil { + return nil, errors.New("invalid domain") + } + if smallDomain.Cardinality == 0 || bigDomain.Cardinality == 0 { + return nil, errors.New("invalid domain cardinality") + } + if bigDomain.Cardinality%smallDomain.Cardinality != 0 { + return nil, errors.New("invalid domain ratio") + } + + // check that the basis is LagrangeCoset + if a.Basis != iop.LagrangeCoset || a.Layout != iop.BitReverse { + return nil, errors.New("invalid form") + } + + // prepare the evaluations of x^n-1 on the big domain's coset + xnMinusOneInverseLagrangeCoset := evaluateXnMinusOneDomainBigCoset(domains) + rho := int(bigDomain.Cardinality / smallDomain.Cardinality) + + r := a.Coefficients() + n := uint64(len(r)) + nn := uint64(64 - bits.TrailingZeros64(n)) + + for i := range r { + iRev := bits.Reverse64(uint64(i)) >> nn + r[i].Mul(&r[i], &xnMinusOneInverseLagrangeCoset[int(iRev)%rho]) + } + + if err := canonicalizeQuotientFromCosetWithWebGPU(a); err != nil { + return nil, err + } + + return a, nil + +} + +// evaluateXnMinusOneDomainBigCoset evaluates Xᵐ-1 on DomainBig coset +func evaluateXnMinusOneDomainBigCoset(domains [2]*fft.Domain) []fr.Element { + + rho := domains[1].Cardinality / domains[0].Cardinality + + res := make([]fr.Element, rho) + + expo := big.NewInt(int64(domains[0].Cardinality)) + res[0].Exp(domains[1].FrMultiplicativeGen, expo) + + var t fr.Element + t.Exp(domains[1].Generator, expo) + + one := fr.One() + + for i := 1; i < int(rho); i++ { + res[i].Mul(&res[i-1], &t) + res[i-1].Sub(&res[i-1], &one) + } + res[len(res)-1].Sub(&res[len(res)-1], &one) + + res = fr.BatchInvert(res) + + return res +} + +// innerComputeLinearizedPoly computes the linearized polynomial in canonical basis. +// The purpose is to commit and open all in one ql, qr, qm, qo, qk. +// * lZeta, rZeta, oZeta are the evaluation of l, r, o at zeta +// * z is the permutation polynomial, zu is Z(μX), the shifted version of Z +// * pk is the proving key: the linearized polynomial is a linear combination of ql, qr, qm, qo, qk. +// +// The Linearized polynomial is: +// +// α²*L₁(ζ)*Z(X) +// + α*( (l(ζ)+β*s1(ζ)+γ)*(r(ζ)+β*s2(ζ)+γ)*(β*s3(X))*Z(μζ) - Z(X)*(l(ζ)+β*id1(ζ)+γ)*(r(ζ)+β*id2(ζ)+γ)*(o(ζ)+β*id3(ζ)+γ)) +// + l(ζ)*Ql(X) + l(ζ)r(ζ)*Qm(X) + r(ζ)*Qr(X) + o(ζ)*Qo(X) + Qk(X) + ∑ᵢQcp_(ζ)Pi_(X) +// - Z_{H}(ζ)*((H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) +// +// /!\ blindedZCanonical is modified +func (s *instance) innerComputeLinearizedPoly(lZeta, rZeta, oZeta, alpha, beta, gamma, zeta, zu fr.Element, qcpZeta, blindedZCanonical []fr.Element, pi2Canonical [][]fr.Element, pk *ProvingKey) ([]fr.Element, error) { + + // l(ζ)r(ζ) + var rl fr.Element + rl.Mul(&rZeta, &lZeta) + + // s1 = α*(l(ζ)+β*s1(β)+γ)*(r(ζ)+β*s2(β)+γ)*β*Z(μζ) + // s2 = -α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + // the linearised polynomial is + // α²*L₁(ζ)*Z(X) + + // s1*s3(X)+s2*Z(X) + l(ζ)*Ql(X) + + // l(ζ)r(ζ)*Qm(X) + r(ζ)*Qr(X) + o(ζ)*Qo(X) + Qk(X) + ∑ᵢQcp_(ζ)Pi_(X) - + // Z_{H}(ζ)*((H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) + var s1, s2 fr.Element + s1 = s.trace.S1.Evaluate(zeta) // s1(ζ) + s1.Mul(&s1, &beta).Add(&s1, &lZeta).Add(&s1, &gamma) // (l(ζ)+β*s1(ζ)+γ) + tmp := s.trace.S2.Evaluate(zeta) // s2(ζ) + tmp.Mul(&tmp, &beta).Add(&tmp, &rZeta).Add(&tmp, &gamma) // (r(ζ)+β*s2(ζ)+γ) + s1.Mul(&s1, &tmp).Mul(&s1, &zu).Mul(&s1, &beta).Mul(&s1, &alpha) // (l(ζ)+β*s1(ζ)+γ)*(r(ζ)+β*s2(ζ)+γ)*β*Z(μζ)*α + + var uzeta, uuzeta fr.Element + uzeta.Mul(&zeta, &pk.Vk.CosetShift) + uuzeta.Mul(&uzeta, &pk.Vk.CosetShift) + + s2.Mul(&beta, &zeta).Add(&s2, &lZeta).Add(&s2, &gamma) // (l(ζ)+β*ζ+γ) + tmp.Mul(&beta, &uzeta).Add(&tmp, &rZeta).Add(&tmp, &gamma) // (r(ζ)+β*u*ζ+γ) + s2.Mul(&s2, &tmp) // (l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ) + tmp.Mul(&beta, &uuzeta).Add(&tmp, &oZeta).Add(&tmp, &gamma) // (o(ζ)+β*u²*ζ+γ) + s2.Mul(&s2, &tmp) // (l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + s2.Neg(&s2).Mul(&s2, &alpha) + + // Z_h(ζ), ζⁿ⁺², L₁(ζ)*α²*Z + var zhZeta, zetaNPlusTwo, alphaSquareLagrangeZero, one, den, frNbElmt fr.Element + one.SetOne() + nbElmt := int64(s.domain0.Cardinality) + alphaSquareLagrangeZero.Set(&zeta).Exp(alphaSquareLagrangeZero, big.NewInt(nbElmt)) // ζⁿ + zetaNPlusTwo.Mul(&alphaSquareLagrangeZero, &zeta).Mul(&zetaNPlusTwo, &zeta) // ζⁿ⁺² + alphaSquareLagrangeZero.Sub(&alphaSquareLagrangeZero, &one) // ζⁿ - 1 + zhZeta.Set(&alphaSquareLagrangeZero) // Z_h(ζ) = ζⁿ - 1 + frNbElmt.SetUint64(uint64(nbElmt)) + den.Sub(&zeta, &one).Inverse(&den) // 1/(ζ-1) + alphaSquareLagrangeZero.Mul(&alphaSquareLagrangeZero, &den). // L₁ = (ζⁿ - 1)/(ζ-1) + Mul(&alphaSquareLagrangeZero, &alpha). + Mul(&alphaSquareLagrangeZero, &alpha). + Mul(&alphaSquareLagrangeZero, &s.domain0.CardinalityInv) // α²*L₁(ζ) + + s3canonical := s.trace.S3.Coefficients() + + if err := canonicalizePolynomialsRegularWithWebGPU([]*iop.Polynomial{s.trace.Qk}, int(s.domain0.Cardinality)); err != nil { + return nil, err + } + + // len(h1)=len(h2)=len(blindedZCanonical)=len(h3)+1 when Statistical ZK is activated + // len(h1)=len(h2)=len(h3)=len(blindedZCanonical)-1 when Statistical ZK is deactivated + h1 := s.h1() + h2 := s.h2() + h3 := s.h3() + + // at this stage we have + // s1 = α*(l(ζ)+β*s1(β)+γ)*(r(ζ)+β*s2(β)+γ)*β*Z(μζ) + // s2 = -α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + cql := s.trace.Ql.Coefficients() + cqr := s.trace.Qr.Coefficients() + cqm := s.trace.Qm.Coefficients() + cqo := s.trace.Qo.Coefficients() + cqk := s.trace.Qk.Coefficients() + + var t, t0, t1 fr.Element + + for i := range blindedZCanonical { + t.Mul(&blindedZCanonical[i], &s2) // -Z(X)*α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + if i < len(s3canonical) { + t0.Mul(&s3canonical[i], &s1) // α*(l(ζ)+β*s1(β)+γ)*(r(ζ)+β*s2(β)+γ)*β*Z(μζ)*β*s3(X) + t.Add(&t, &t0) + } + if i < len(cqm) { + t1.Mul(&cqm[i], &rl) // l(ζ)r(ζ)*Qm(X) + t.Add(&t, &t1) // linPol += l(ζ)r(ζ)*Qm(X) + t0.Mul(&cql[i], &lZeta) // l(ζ)Q_l(X) + t.Add(&t, &t0) // linPol += l(ζ)*Ql(X) + t0.Mul(&cqr[i], &rZeta) //r(ζ)*Qr(X) + t.Add(&t, &t0) // linPol += r(ζ)*Qr(X) + t0.Mul(&cqo[i], &oZeta) // o(ζ)*Qo(X) + t.Add(&t, &t0) // linPol += o(ζ)*Qo(X) + t.Add(&t, &cqk[i]) // linPol += Qk(X) + for j := range qcpZeta { // linPol += ∑ᵢQcp_(ζ)Pi_(X) + t0.Mul(&pi2Canonical[j][i], &qcpZeta[j]) + t.Add(&t, &t0) + } + } + + t0.Mul(&blindedZCanonical[i], &alphaSquareLagrangeZero) // α²L₁(ζ)Z(X) + blindedZCanonical[i].Add(&t, &t0) // linPol += α²L₁(ζ)Z(X) + + // if statistical zeroknowledge is deactivated, len(h1)=len(h2)=len(h3)=len(blindedZ)-1. + // Else len(h1)=len(h2)=len(blindedZCanonical)=len(h3)+1 + if i < len(h3) { + t.Mul(&h3[i], &zetaNPlusTwo). + Add(&t, &h2[i]). + Mul(&t, &zetaNPlusTwo). + Add(&t, &h1[i]). + Mul(&t, &zhZeta) + blindedZCanonical[i].Sub(&blindedZCanonical[i], &t) // linPol -= Z_h(ζ)*(H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) + } else if s.opt.StatisticalZK { + t.Mul(&h2[i], &zetaNPlusTwo). + Add(&t, &h1[i]). + Mul(&t, &zhZeta) + blindedZCanonical[i].Sub(&blindedZCanonical[i], &t) // linPol -= Z_h(ζ)*(H₀(X) + ζᵐ⁺²*H₁(X) + ζ²⁽ᵐ⁺²⁾*H₂(X)) + } + } + + return blindedZCanonical, nil +} + +func bindPublicData(fs *fiatshamir.Transcript, challenge string, vk *native.VerifyingKey, publicInputs []fr.Element) error { + if err := fs.Bind(challenge, vk.S[0].Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.S[1].Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.S[2].Marshal()); err != nil { + return err + } + + if err := fs.Bind(challenge, vk.Ql.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qr.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qm.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qo.Marshal()); err != nil { + return err + } + if err := fs.Bind(challenge, vk.Qk.Marshal()); err != nil { + return err + } + for i := range vk.Qcp { + if err := fs.Bind(challenge, vk.Qcp[i].Marshal()); err != nil { + return err + } + } + + for i := 0; i < len(publicInputs); i++ { + if err := fs.Bind(challenge, publicInputs[i].Marshal()); err != nil { + return err + } + } + + return nil +} + +func deriveRandomness(fs *fiatshamir.Transcript, challenge string, points ...*curve.G1Affine) (fr.Element, error) { + var buf [curve.SizeOfG1AffineUncompressed]byte + var r fr.Element + + for _, p := range points { + buf = p.RawBytes() + if err := fs.Bind(challenge, buf[:]); err != nil { + return r, err + } + } + + b, err := fs.ComputeChallenge(challenge) + if err != nil { + return r, err + } + r.SetBytes(b) + return r, nil +} diff --git a/backend/accelerated/webgpu/plonk/bn254/provingkey.go b/backend/accelerated/webgpu/plonk/bn254/provingkey.go new file mode 100644 index 0000000000..baf6aa58a5 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bn254/provingkey.go @@ -0,0 +1,72 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bn254 + +import ( + "sync" + + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" + native "github.com/consensys/gnark/backend/plonk/bn254" + cs "github.com/consensys/gnark/constraint/bn254" +) + +type ProvingKey struct { + native.ProvingKey + prepareMu sync.Mutex + handle string + staticNumeratorCache *staticNumeratorCache +} + +func (pk *ProvingKey) Prepare() error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + return pk.ensurePreparedLocked() +} + +func (pk *ProvingKey) ensurePreparedLocked() error { + if pk.handle != "" { + return nil + } + if err := bridge.Bridge.Init("bn254"); err != nil { + return err + } + payload := bridge.JSObject() + payload.Set("kzg", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.Kzg.G1))) + payload.Set("kzgCount", len(pk.Kzg.G1)) + payload.Set("kzgLagrange", bridge.JSUint8Array(packG1AffineJacobianBatch(pk.KzgLagrange.G1))) + payload.Set("kzgLagrangeCount", len(pk.KzgLagrange.G1)) + handle, err := bridge.Bridge.PrepareKey("bn254", payload) + if err != nil { + return err + } + pk.handle = handle + return nil +} + +func (pk *ProvingKey) PrepareWithCS(spr *cs.SparseR1CS) error { + pk.prepareMu.Lock() + defer pk.prepareMu.Unlock() + + if err := pk.ensurePreparedLocked(); err != nil { + return err + } + domain0, domain1 := domainsForSPR(spr) + trace := native.NewTrace(spr, domain0) + if err := pk.ensureStaticNumeratorCache(trace, domain0, domain1); err != nil { + return err + } + if err := pk.preloadQuotientStaticCaches(trace, domain0, domain1); err != nil { + return err + } + if err := bridge.Bridge.PrewarmQuotientTransformDomain("bn254", int(domain0.Cardinality)); err != nil { + return err + } + if err := bridge.Bridge.PrewarmQuotientEvaluateKernel("bn254", len(trace.Qcp)); err != nil { + return err + } + return bridge.Bridge.PrewarmQuotientCanonicalizeDomain("bn254", int(domain1.Cardinality)) +} diff --git a/backend/accelerated/webgpu/plonk/bn254/serialize.go b/backend/accelerated/webgpu/plonk/bn254/serialize.go new file mode 100644 index 0000000000..4bdbce49cb --- /dev/null +++ b/backend/accelerated/webgpu/plonk/bn254/serialize.go @@ -0,0 +1,265 @@ +//go:build js && wasm + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bn254 + +import ( + "encoding/binary" + "errors" + "fmt" + + curve "github.com/consensys/gnark-crypto/ecc/bn254" + bn254fp "github.com/consensys/gnark-crypto/ecc/bn254/fp" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/iop" + "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/internal/bridge" +) + +const ( + frBytes = fr.Bytes + g1CoordinateBytes = bn254fp.Bytes + g1PointBytes = 3 * g1CoordinateBytes +) + +func packFrVectorRegularLEInto(dst []byte, values []fr.Element) []byte { + required := len(values) * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + } + for i := range values { + base := i * frBytes + writeFrRegularLE(dst[base:base+frBytes], &values[i]) + } + return dst +} + +func packFrVectorsRegularLEPaddedInto(dst []byte, vectors [][]fr.Element, elementCount int) ([]byte, error) { + if elementCount <= 0 { + return nil, errors.New("webgpu plonk bn254: empty MSM batch") + } + required := len(vectors) * elementCount * frBytes + if cap(dst) < required { + dst = make([]byte, required) + } else { + dst = dst[:required] + clear(dst) + } + for i, values := range vectors { + if len(values) > elementCount { + return nil, fmt.Errorf("webgpu plonk bn254: MSM batch vector %d has %d elements, expected at most %d", i, len(values), elementCount) + } + start := i * elementCount * frBytes + packFrVectorRegularLEInto(dst[start:start+len(values)*frBytes], values) + } + return dst, nil +} + +func writeFrRegularLE(dst []byte, value *fr.Element) { + be := value.Bytes() + for i := 0; i < frBytes; i++ { + dst[i] = be[frBytes-1-i] + } +} + +func readFrRegularLE(src []byte) (fr.Element, error) { + if len(src) != frBytes { + return fr.Element{}, fmt.Errorf("webgpu plonk bn254: expected %d Fr bytes, got %d", frBytes, len(src)) + } + var le [frBytes]byte + copy(le[:], src) + return fr.LittleEndian.Element(&le) +} + +func unpackFrVectorRegularLEInto(dst []fr.Element, src []byte) error { + if len(src) != len(dst)*frBytes { + return fmt.Errorf("webgpu plonk bn254: expected %d Fr vector bytes, got %d", len(dst)*frBytes, len(src)) + } + for i := range dst { + value, err := readFrRegularLE(src[i*frBytes : (i+1)*frBytes]) + if err != nil { + return err + } + dst[i] = value + } + return nil +} + +type canonicalizeGroupKey struct { + inputBitReversed bool + inverseCoset bool +} + +func canonicalizePolynomialsRegularWithWebGPU(polys []*iop.Polynomial, elementCount int) error { + n := elementCount + groups := make(map[canonicalizeGroupKey][]*iop.Polynomial) + for _, p := range polys { + if p == nil { + continue + } + if p.Basis == iop.Canonical { + p.ToRegular() + continue + } + coeffs := p.Coefficients() + if len(coeffs) != n { + return fmt.Errorf("webgpu plonk bn254: canonicalize polynomial has %d coefficients, expected %d", len(coeffs), n) + } + switch p.Basis { + case iop.Lagrange: + case iop.LagrangeCoset: + default: + return fmt.Errorf("webgpu plonk bn254: unsupported polynomial basis %d", p.Basis) + } + switch p.Layout { + case iop.Regular: + case iop.BitReverse: + default: + return fmt.Errorf("webgpu plonk bn254: unsupported polynomial layout %d", p.Layout) + } + key := canonicalizeGroupKey{ + inputBitReversed: p.Layout == iop.BitReverse, + inverseCoset: p.Basis == iop.LagrangeCoset, + } + groups[key] = append(groups[key], p) + } + + vectorBytes := n * frBytes + for key, group := range groups { + valuesPacked := make([]byte, len(group)*vectorBytes) + for i, p := range group { + packFrVectorRegularLEInto(valuesPacked[i*vectorBytes:(i+1)*vectorBytes], p.Coefficients()) + } + canonicalPacked, err := bridge.Bridge.CanonicalizeQuotientVectors("bn254", valuesPacked, len(group), n, key.inputBitReversed, key.inverseCoset) + if err != nil { + return err + } + if len(canonicalPacked) != len(valuesPacked) { + return fmt.Errorf("webgpu plonk bn254: quotient canonicalize returned %d bytes, expected %d", len(canonicalPacked), len(valuesPacked)) + } + for i, p := range group { + if err := unpackFrVectorRegularLEInto(p.Coefficients(), canonicalPacked[i*vectorBytes:(i+1)*vectorBytes]); err != nil { + return err + } + p.Basis = iop.Canonical + p.Layout = iop.Regular + } + } + return nil +} + +func canonicalizeQuotientFromCosetWithWebGPU(p *iop.Polynomial) error { + return canonicalizePolynomialsRegularWithWebGPU([]*iop.Polynomial{p}, len(p.Coefficients())) +} + +func lagrangePolynomialsRegularWithWebGPU(polys []*iop.Polynomial, elementCount int) error { + n := elementCount + filtered := make([]*iop.Polynomial, 0, len(polys)) + for _, p := range polys { + if p == nil { + continue + } + if p.Basis == iop.Lagrange { + p.ToRegular() + continue + } + if p.Basis != iop.Canonical || p.Layout != iop.Regular { + return fmt.Errorf("webgpu plonk bn254: expected canonical regular polynomial, got basis %d layout %d", p.Basis, p.Layout) + } + if len(p.Coefficients()) != n { + return fmt.Errorf("webgpu plonk bn254: lagrange polynomial has %d coefficients, expected %d", len(p.Coefficients()), n) + } + filtered = append(filtered, p) + } + if len(filtered) == 0 { + return nil + } + + vectorBytes := n * frBytes + valuesPacked := make([]byte, len(filtered)*vectorBytes) + for i, p := range filtered { + packFrVectorRegularLEInto(valuesPacked[i*vectorBytes:(i+1)*vectorBytes], p.Coefficients()) + } + lagrangePacked, err := bridge.Bridge.LagrangeQuotientVectors("bn254", valuesPacked, len(filtered), n) + if err != nil { + return err + } + if len(lagrangePacked) != len(valuesPacked) { + return fmt.Errorf("webgpu plonk bn254: quotient lagrange returned %d bytes, expected %d", len(lagrangePacked), len(valuesPacked)) + } + for i, p := range filtered { + if err := unpackFrVectorRegularLEInto(p.Coefficients(), lagrangePacked[i*vectorBytes:(i+1)*vectorBytes]); err != nil { + return err + } + p.Basis = iop.Lagrange + p.Layout = iop.Regular + } + return nil +} + +func packG1AffineJacobianBatch(points []curve.G1Affine) []byte { + out := make([]byte, len(points)*g1PointBytes) + for i := range points { + base := i * g1PointBytes + writeFPMontLE(out[base:base+g1CoordinateBytes], &points[i].X) + writeFPMontLE(out[base+g1CoordinateBytes:base+2*g1CoordinateBytes], &points[i].Y) + writeG1JacobianZOne(out[base+2*g1CoordinateBytes : base+3*g1CoordinateBytes]) + } + return out +} + +func decodeG1AffineFromPacked(packed []byte, err error) (curve.G1Affine, error) { + if err != nil { + return curve.G1Affine{}, err + } + if len(packed) != 2*g1CoordinateBytes { + return curve.G1Affine{}, fmt.Errorf("webgpu plonk bn254: expected %d G1 bytes, got %d", 2*g1CoordinateBytes, len(packed)) + } + return curve.G1Affine{ + X: readFPMontLE(packed[:g1CoordinateBytes]), + Y: readFPMontLE(packed[g1CoordinateBytes:]), + }, nil +} + +func decodeG1AffineBatchFromPacked(packed []byte, count int, err error) ([]curve.G1Affine, error) { + if err != nil { + return nil, err + } + expected := count * 2 * g1CoordinateBytes + if len(packed) != expected { + return nil, fmt.Errorf("webgpu plonk bn254: expected %d G1 batch bytes, got %d", expected, len(packed)) + } + res := make([]curve.G1Affine, count) + for i := range res { + start := i * 2 * g1CoordinateBytes + res[i] = curve.G1Affine{ + X: readFPMontLE(packed[start : start+g1CoordinateBytes]), + Y: readFPMontLE(packed[start+g1CoordinateBytes : start+2*g1CoordinateBytes]), + } + } + return res, nil +} + +func readFPMontLE(src []byte) bn254fp.Element { + var words [4]uint64 + for i := range words { + words[i] = binary.LittleEndian.Uint64(src[i*8 : (i+1)*8]) + } + return bn254fp.Element(words) +} + +func writeFPMontLE(dst []byte, value *bn254fp.Element) { + words := [4]uint64(*value) + for i := range words { + binary.LittleEndian.PutUint64(dst[i*8:(i+1)*8], words[i]) + } +} + +func writeG1JacobianZOne(dst []byte) { + var one bn254fp.Element + one.SetOne() + writeFPMontLE(dst, &one) +} diff --git a/backend/accelerated/webgpu/plonk/doc.go b/backend/accelerated/webgpu/plonk/doc.go new file mode 100644 index 0000000000..93f120270b --- /dev/null +++ b/backend/accelerated/webgpu/plonk/doc.go @@ -0,0 +1,8 @@ +//go:build js && wasm + +// Package plonk provides the browser/WASM entry point for an experimental +// WebGPU-accelerated PLONK prover. +// +// Curve-specific proving code lives in subpackages such as bn254, keeping the +// package root as the browser/WASM dispatcher. +package plonk diff --git a/backend/accelerated/webgpu/plonk/internal/bridge/bridge.go b/backend/accelerated/webgpu/plonk/internal/bridge/bridge.go new file mode 100644 index 0000000000..8e3f431b45 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/internal/bridge/bridge.go @@ -0,0 +1,149 @@ +//go:build js && wasm + +package bridge + +import ( + "syscall/js" + + webgpubridge "github.com/consensys/gnark/backend/accelerated/webgpu/internal/bridge" +) + +var Bridge = Client{Client: webgpubridge.NewClient("gnarkPlonkWebGPU", "webgpu plonk")} + +type Client struct { + webgpubridge.Client +} + +func JSUint8Array(src []byte) js.Value { + return webgpubridge.JSUint8Array(src) +} + +func JSObject() js.Value { + return webgpubridge.JSObject() +} + +func (c Client) MSMG1Slice(handle, vectorName string, start, count int, scalarsPacked []byte) ([]byte, error) { + value, err := c.CallPromise( + "msmG1", + handle, + vectorName, + webgpubridge.JSUint8Array(scalarsPacked), + start, + count, + ) + if err != nil { + return nil, err + } + return webgpubridge.GoBytes(c.ErrorPrefix, value) +} + +func (c Client) MSMG1Batch(handle, vectorName string, start, termsPerInstance, instanceCount int, scalarsPacked []byte) ([]byte, error) { + value, err := c.CallPromise( + "msmG1Batch", + handle, + vectorName, + webgpubridge.JSUint8Array(scalarsPacked), + start, + termsPerInstance, + instanceCount, + ) + if err != nil { + return nil, err + } + return webgpubridge.GoBytes(c.ErrorPrefix, value) +} + +func (c Client) CanonicalizeQuotientVectors(curve string, valuesPacked []byte, vectorCount, elementCount int, inputBitReversed, inverseCoset bool) ([]byte, error) { + value, err := c.CallPromise( + "canonicalizeQuotientVectors", + curve, + webgpubridge.JSUint8Array(valuesPacked), + vectorCount, + elementCount, + inputBitReversed, + inverseCoset, + ) + if err != nil { + return nil, err + } + return webgpubridge.GoBytes(c.ErrorPrefix, value) +} + +func (c Client) LagrangeQuotientVectors(curve string, valuesPacked []byte, vectorCount, elementCount int) ([]byte, error) { + value, err := c.CallPromise( + "lagrangeQuotientVectors", + curve, + webgpubridge.JSUint8Array(valuesPacked), + vectorCount, + elementCount, + ) + if err != nil { + return nil, err + } + return webgpubridge.GoBytes(c.ErrorPrefix, value) +} + +func (c Client) TransformAndEvaluateQuotientCosets( + curve string, + dynamicValuesPacked, scalingPacked, staticValuesPacked, staticMontCacheKeysPacked, twiddlesPacked, denominatorsPacked, blindsPacked, scalarsPacked []byte, + elementCount, blindCoeffCount, commitmentCount, dynamicTransformCacheKey, cosetCount, auxMontCacheKey int, +) ([]byte, error) { + value, err := c.CallPromise( + "transformAndEvaluateQuotientCosets", + curve, + webgpubridge.JSUint8Array(dynamicValuesPacked), + webgpubridge.JSUint8Array(scalingPacked), + webgpubridge.JSUint8Array(staticValuesPacked), + webgpubridge.JSUint8Array(staticMontCacheKeysPacked), + webgpubridge.JSUint8Array(twiddlesPacked), + webgpubridge.JSUint8Array(denominatorsPacked), + webgpubridge.JSUint8Array(blindsPacked), + webgpubridge.JSUint8Array(scalarsPacked), + elementCount, + blindCoeffCount, + commitmentCount, + dynamicTransformCacheKey, + cosetCount, + auxMontCacheKey, + ) + if err != nil { + return nil, err + } + return webgpubridge.GoBytes(c.ErrorPrefix, value) +} + +func (c Client) PreloadQuotientStaticAndAux( + curve string, + staticValuesPacked, staticMontCacheKeysPacked, scalingPacked, twiddlesPacked, denominatorsPacked []byte, + elementCount, staticVectorCount, cosetCount, auxMontCacheKey int, +) error { + _, err := c.CallPromise( + "preloadQuotientStaticAndAux", + curve, + webgpubridge.JSUint8Array(staticValuesPacked), + webgpubridge.JSUint8Array(staticMontCacheKeysPacked), + webgpubridge.JSUint8Array(scalingPacked), + webgpubridge.JSUint8Array(twiddlesPacked), + webgpubridge.JSUint8Array(denominatorsPacked), + elementCount, + staticVectorCount, + cosetCount, + auxMontCacheKey, + ) + return err +} + +func (c Client) PrewarmQuotientTransformDomain(curve string, elementCount int) error { + _, err := c.CallPromise("prewarmQuotientTransformDomain", curve, elementCount) + return err +} + +func (c Client) PrewarmQuotientCanonicalizeDomain(curve string, elementCount int) error { + _, err := c.CallPromise("prewarmQuotientCanonicalizeDomain", curve, elementCount) + return err +} + +func (c Client) PrewarmQuotientEvaluateKernel(curve string, commitmentCount int) error { + _, err := c.CallPromise("prewarmQuotientEvaluateKernel", curve, commitmentCount) + return err +} diff --git a/backend/accelerated/webgpu/plonk/internal/wasmruntime/native/main.go b/backend/accelerated/webgpu/plonk/internal/wasmruntime/native/main.go new file mode 100644 index 0000000000..cb2c689f53 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/internal/wasmruntime/native/main.go @@ -0,0 +1,52 @@ +//go:build js && wasm + +package main + +import ( + "bytes" + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/backend/accelerated/webgpu/internal/wasmruntime" + gnarkplonk "github.com/consensys/gnark/backend/plonk" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" +) + +func main() { + if err := wasmruntime.Install(wasmruntime.Config[gnarkplonk.ProvingKey, gnarkplonk.VerifyingKey, gnarkplonk.Proof]{ + GlobalName: "gnarkPlonkRuntimeNative", + SupportedCurves: map[string]ecc.ID{ + "bn254": ecc.BN254, + "bls12_377": ecc.BLS12_377, + "bls12_381": ecc.BLS12_381, + }, + CSFactory: gnarkplonk.NewCS, + PKFactory: gnarkplonk.NewProvingKey, + VKFactory: gnarkplonk.NewVerifyingKey, + ProofFactory: gnarkplonk.NewProof, + ReadProvingKey: func(pk gnarkplonk.ProvingKey, format string, data []byte) error { + switch format { + case "serialized": + if _, err := pk.ReadFrom(bytes.NewReader(data)); err != nil { + return fmt.Errorf("read pk: %w", err) + } + case "unsafe": + if _, err := pk.UnsafeReadFrom(bytes.NewReader(data)); err != nil { + return fmt.Errorf("read pk unsafe: %w", err) + } + default: + return fmt.Errorf("unsupported proving key format %q", format) + } + return nil + }, + Prove: func(ccs constraint.ConstraintSystem, pk gnarkplonk.ProvingKey, fullWitness witness.Witness) (gnarkplonk.Proof, error) { + return gnarkplonk.Prove(ccs, pk, fullWitness) + }, + Verify: func(proof gnarkplonk.Proof, vk gnarkplonk.VerifyingKey, publicWitness witness.Witness) error { + return gnarkplonk.Verify(proof, vk, publicWitness) + }, + }); err != nil { + panic(err) + } +} diff --git a/backend/accelerated/webgpu/plonk/internal/wasmruntime/webgpu/main.go b/backend/accelerated/webgpu/plonk/internal/wasmruntime/webgpu/main.go new file mode 100644 index 0000000000..e33c4863e4 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/internal/wasmruntime/webgpu/main.go @@ -0,0 +1,55 @@ +//go:build js && wasm + +package main + +import ( + "bytes" + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/backend/accelerated/webgpu/internal/wasmruntime" + webgpuplonk "github.com/consensys/gnark/backend/accelerated/webgpu/plonk" + gnarkplonk "github.com/consensys/gnark/backend/plonk" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" +) + +func main() { + if err := wasmruntime.Install(wasmruntime.Config[gnarkplonk.ProvingKey, gnarkplonk.VerifyingKey, gnarkplonk.Proof]{ + GlobalName: "gnarkPlonkRuntimeWebGPU", + SupportedCurves: map[string]ecc.ID{ + "bn254": ecc.BN254, + "bls12_377": ecc.BLS12_377, + "bls12_381": ecc.BLS12_381, + }, + CSFactory: gnarkplonk.NewCS, + PKFactory: webgpuplonk.NewProvingKey, + VKFactory: gnarkplonk.NewVerifyingKey, + ProofFactory: gnarkplonk.NewProof, + ReadProvingKey: func(pk gnarkplonk.ProvingKey, format string, data []byte) error { + switch format { + case "serialized": + if _, err := pk.ReadFrom(bytes.NewReader(data)); err != nil { + return fmt.Errorf("read pk: %w", err) + } + case "unsafe": + if _, err := pk.UnsafeReadFrom(bytes.NewReader(data)); err != nil { + return fmt.Errorf("read pk unsafe: %w", err) + } + default: + return fmt.Errorf("unsupported proving key format %q", format) + } + return nil + }, + Prepare: webgpuplonk.Prepare, + PrepareWithCS: webgpuplonk.PrepareWithCS, + Prove: func(ccs constraint.ConstraintSystem, pk gnarkplonk.ProvingKey, fullWitness witness.Witness) (gnarkplonk.Proof, error) { + return webgpuplonk.Prove(ccs, pk, fullWitness) + }, + Verify: func(proof gnarkplonk.Proof, vk gnarkplonk.VerifyingKey, publicWitness witness.Witness) error { + return gnarkplonk.Verify(proof, vk, publicWitness) + }, + }); err != nil { + panic(err) + } +} diff --git a/backend/accelerated/webgpu/plonk/plonk.go b/backend/accelerated/webgpu/plonk/plonk.go new file mode 100644 index 0000000000..0d5078aa70 --- /dev/null +++ b/backend/accelerated/webgpu/plonk/plonk.go @@ -0,0 +1,100 @@ +//go:build js && wasm + +package plonk + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + webgpu_bls12377 "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/bls12-377" + webgpu_bls12381 "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/bls12-381" + webgpu_bn254 "github.com/consensys/gnark/backend/accelerated/webgpu/plonk/bn254" + "github.com/consensys/gnark/backend/plonk" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + csbls12377 "github.com/consensys/gnark/constraint/bls12-377" + csbls12381 "github.com/consensys/gnark/constraint/bls12-381" + csbn254 "github.com/consensys/gnark/constraint/bn254" +) + +// Prove runs the PLONK prover for supported curves. +func Prove(spr constraint.ConstraintSystem, pk plonk.ProvingKey, fullWitness witness.Witness) (plonk.Proof, error) { + switch typedSPR := spr.(type) { + case *csbn254.SparseR1CS: + typedPK, ok := pk.(*webgpu_bn254.ProvingKey) + if !ok { + return nil, fmt.Errorf("webgpu plonk: expected *webgpu_bn254.ProvingKey, got %T", pk) + } + return webgpu_bn254.Prove(typedSPR, typedPK, fullWitness) + case *csbls12377.SparseR1CS: + typedPK, ok := pk.(*webgpu_bls12377.ProvingKey) + if !ok { + return nil, fmt.Errorf("webgpu plonk: expected *webgpu_bls12377.ProvingKey, got %T", pk) + } + return webgpu_bls12377.Prove(typedSPR, typedPK, fullWitness) + case *csbls12381.SparseR1CS: + typedPK, ok := pk.(*webgpu_bls12381.ProvingKey) + if !ok { + return nil, fmt.Errorf("webgpu plonk: expected *webgpu_bls12381.ProvingKey, got %T", pk) + } + return webgpu_bls12381.Prove(typedSPR, typedPK, fullWitness) + default: + return nil, fmt.Errorf("webgpu plonk: unsupported constraint system %T", spr) + } +} + +// PrepareWithCS initializes browser-side caches that need both the proving key +// and the constraint system. For PLONK this includes the static quotient +// numerator polynomials derived from the trace. +func PrepareWithCS(spr constraint.ConstraintSystem, pk plonk.ProvingKey) error { + switch typedSPR := spr.(type) { + case *csbn254.SparseR1CS: + typedPK, ok := pk.(*webgpu_bn254.ProvingKey) + if !ok { + return fmt.Errorf("webgpu plonk: expected *webgpu_bn254.ProvingKey, got %T", pk) + } + return typedPK.PrepareWithCS(typedSPR) + case *csbls12377.SparseR1CS: + typedPK, ok := pk.(*webgpu_bls12377.ProvingKey) + if !ok { + return fmt.Errorf("webgpu plonk: expected *webgpu_bls12377.ProvingKey, got %T", pk) + } + return typedPK.PrepareWithCS(typedSPR) + case *csbls12381.SparseR1CS: + typedPK, ok := pk.(*webgpu_bls12381.ProvingKey) + if !ok { + return fmt.Errorf("webgpu plonk: expected *webgpu_bls12381.ProvingKey, got %T", pk) + } + return typedPK.PrepareWithCS(typedSPR) + default: + return fmt.Errorf("webgpu plonk: unsupported constraint system %T", spr) + } +} + +// NewProvingKey returns an empty proving-key wrapper for supported curves. +func NewProvingKey(curveID ecc.ID) plonk.ProvingKey { + switch curveID { + case ecc.BN254: + return &webgpu_bn254.ProvingKey{} + case ecc.BLS12_377: + return &webgpu_bls12377.ProvingKey{} + case ecc.BLS12_381: + return &webgpu_bls12381.ProvingKey{} + default: + panic("webgpu plonk: unsupported curve") + } +} + +// Prepare initializes browser-side caches for a deserialized proving key. +func Prepare(pk plonk.ProvingKey) error { + switch typedPK := pk.(type) { + case *webgpu_bn254.ProvingKey: + return typedPK.Prepare() + case *webgpu_bls12377.ProvingKey: + return typedPK.Prepare() + case *webgpu_bls12381.ProvingKey: + return typedPK.Prepare() + default: + return fmt.Errorf("webgpu plonk: unsupported proving key type %T", pk) + } +} diff --git a/backend/accelerated/webgpu/shaders/common/g1_core.wgsl b/backend/accelerated/webgpu/shaders/common/g1_core.wgsl new file mode 100644 index 0000000000..c450eb95e2 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/common/g1_core.wgsl @@ -0,0 +1,259 @@ +struct G1Point { + x: Fp, + y: Fp, + z: Fp, +} + +const G1_OP_COPY: u32 = 0u; +const G1_OP_JAC_INFINITY: u32 = 1u; +const G1_OP_AFFINE_TO_JAC: u32 = 2u; +const G1_OP_NEG_JAC: u32 = 3u; +const G1_OP_DOUBLE_JAC: u32 = 4u; +const G1_OP_ADD_MIXED: u32 = 5u; +const G1_OP_JAC_TO_AFFINE: u32 = 6u; +const G1_OP_AFFINE_ADD: u32 = 7u; + +fn g1_jac_infinity() -> G1Point { + var p: G1Point; + p.x = fp_one(); + p.y = fp_one(); + p.z = fp_zero(); + return p; +} + +fn g1_affine_is_infinity(a: G1Point) -> bool { + return fp_is_zero(a.z); +} + +fn g1_jac_is_infinity(p: G1Point) -> bool { + return fp_is_zero(p.z); +} + +fn g1_affine_to_jac(a: G1Point) -> G1Point { + if (g1_affine_is_infinity(a)) { + return g1_jac_infinity(); + } + var p: G1Point; + p.x = a.x; + p.y = a.y; + p.z = fp_one(); + return p; +} + +fn g1_jac_to_affine(p: G1Point) -> G1Point { + if (g1_jac_is_infinity(p)) { + var inf: G1Point; + inf.x = fp_zero(); + inf.y = fp_zero(); + inf.z = fp_zero(); + return inf; + } + let a = fp_inverse(p.z); + let b = fp_square(a); + var out: G1Point; + out.x = fp_mul(p.x, b); + out.y = fp_mul(fp_mul(p.y, b), a); + out.z = fp_one(); + return out; +} + +fn g1_neg_affine(q: G1Point) -> G1Point { + if (g1_affine_is_infinity(q)) { + return q; + } + var p = q; + p.y = fp_neg(q.y); + return p; +} + +fn g1_neg_jac(q: G1Point) -> G1Point { + var p = q; + p.y = fp_neg(q.y); + return p; +} + +fn g1_double_mixed(a: G1Point) -> G1Point { + if (g1_affine_is_infinity(a)) { + return g1_jac_infinity(); + } + var xx = fp_square(a.x); + let yy = fp_square(a.y); + var yyyy = fp_square(yy); + var s = fp_add(a.x, yy); + s = fp_square(s); + s = fp_sub(s, xx); + s = fp_sub(s, yyyy); + s = fp_double(s); + var m = fp_double(xx); + m = fp_add(m, xx); + let t = fp_sub(fp_sub(fp_square(m), s), s); + + var p: G1Point; + p.x = t; + p.y = fp_mul(fp_sub(s, t), m); + yyyy = fp_double(fp_double(fp_double(yyyy))); + p.y = fp_sub(p.y, yyyy); + p.z = fp_double(a.y); + return p; +} + +fn g1_double_jac(q: G1Point) -> G1Point { + var a = fp_square(q.x); + let b = fp_square(q.y); + let c = fp_square(b); + var d = fp_add(q.x, b); + d = fp_square(d); + d = fp_sub(d, a); + d = fp_sub(d, c); + d = fp_double(d); + var e = fp_double(a); + e = fp_add(e, a); + let f = fp_square(e); + var t = fp_double(d); + + var p: G1Point; + p.z = fp_double(fp_mul(q.y, q.z)); + p.x = fp_sub(f, t); + p.y = fp_mul(fp_sub(d, p.x), e); + t = fp_double(fp_double(fp_double(c))); + p.y = fp_sub(p.y, t); + return p; +} + +fn g1_add_mixed(p: G1Point, a: G1Point) -> G1Point { + if (g1_affine_is_infinity(a)) { + return p; + } + if (g1_jac_is_infinity(p)) { + return g1_affine_to_jac(a); + } + + let z1z1 = fp_square(p.z); + let u2 = fp_mul(a.x, z1z1); + let s2 = fp_mul(fp_mul(a.y, p.z), z1z1); + + if (fp_equal(u2, p.x) && fp_equal(s2, p.y)) { + return g1_double_mixed(a); + } + + let h = fp_sub(u2, p.x); + let hh = fp_square(h); + let i = fp_double(fp_double(hh)); + let j = fp_mul(h, i); + let r = fp_double(fp_sub(s2, p.y)); + let v = fp_mul(p.x, i); + + var out: G1Point; + out.x = fp_sub(fp_sub(fp_sub(fp_square(r), j), v), v); + out.y = fp_sub(fp_mul(fp_sub(v, out.x), r), fp_double(fp_mul(j, p.y))); + out.z = fp_square(fp_add(p.z, h)); + out.z = fp_sub(out.z, z1z1); + out.z = fp_sub(out.z, hh); + return out; +} + +fn g1_add_affine(a: G1Point, b: G1Point) -> G1Point { + return g1_jac_to_affine(g1_add_mixed(g1_affine_to_jac(a), b)); +} + +fn g1_add_jac(p: G1Point, q: G1Point) -> G1Point { + if (g1_jac_is_infinity(p)) { + return q; + } + if (g1_jac_is_infinity(q)) { + return p; + } + let z1z1 = fp_square(p.z); + let z2z2 = fp_square(q.z); + let u1 = fp_mul(p.x, z2z2); + let u2 = fp_mul(q.x, z1z1); + let s1 = fp_mul(fp_mul(p.y, q.z), z2z2); + let s2 = fp_mul(fp_mul(q.y, p.z), z1z1); + let h = fp_sub(u2, u1); + let r = fp_double(fp_sub(s2, s1)); + if (fp_is_zero(h)) { + if (fp_is_zero(r)) { + return g1_double_jac(p); + } + return g1_jac_infinity(); + } + let i = fp_double(fp_double(fp_square(h))); + let j = fp_mul(h, i); + let v = fp_mul(u1, i); + var out: G1Point; + out.x = fp_sub(fp_sub(fp_sub(fp_square(r), j), v), v); + out.y = fp_sub(fp_mul(fp_sub(v, out.x), r), fp_double(fp_mul(s1, j))); + let z_sum = fp_add(p.z, q.z); + out.z = fp_mul(fp_sub(fp_sub(fp_square(z_sum), z1z1), z2z2), h); + return out; +} + +fn g1_scalar_mul_affine_small(base: G1Point, scalar: u32) -> G1Point { + if (scalar == 0u || g1_affine_is_infinity(base)) { + return g1_jac_to_affine(g1_jac_infinity()); + } + var acc = g1_jac_infinity(); + var cur_jac = g1_affine_to_jac(base); + var cur_aff = base; + var k = scalar; + loop { + if ((k & 1u) != 0u) { + acc = g1_add_mixed(acc, cur_aff); + } + k = k >> 1u; + if (k == 0u) { + break; + } + cur_jac = g1_double_jac(cur_jac); + cur_aff = g1_jac_to_affine(cur_jac); + } + return g1_jac_to_affine(acc); +} + +fn g1_scalar_mul_jac_small(base: G1Point, scalar: u32) -> G1Point { + if (scalar == 0u || g1_jac_is_infinity(base)) { + return g1_jac_infinity(); + } + var acc = g1_jac_infinity(); + var b = base; + var k = scalar; + loop { + if (k == 0u) { + break; + } + if ((k & 1u) != 0u) { + acc = g1_add_jac(acc, b); + } + b = g1_double_jac(b); + k = k >> 1u; + } + return acc; +} + +fn g1_dispatch(opcode: u32, a: G1Point, b: G1Point) -> G1Point { + if (opcode == G1_OP_COPY) { + return a; + } + if (opcode == G1_OP_JAC_INFINITY) { + return g1_jac_infinity(); + } + if (opcode == G1_OP_AFFINE_TO_JAC) { + return g1_affine_to_jac(a); + } + if (opcode == G1_OP_NEG_JAC) { + return g1_neg_jac(a); + } + if (opcode == G1_OP_DOUBLE_JAC) { + return g1_double_jac(a); + } + if (opcode == G1_OP_ADD_MIXED) { + return g1_add_mixed(a, b); + } + if (opcode == G1_OP_JAC_TO_AFFINE) { + return g1_jac_to_affine(a); + } + if (opcode == G1_OP_AFFINE_ADD) { + return g1_add_affine(a, b); + } + return g1_jac_infinity(); +} diff --git a/backend/accelerated/webgpu/shaders/common/g1_msm_bindings.wgsl b/backend/accelerated/webgpu/shaders/common/g1_msm_bindings.wgsl new file mode 100644 index 0000000000..8611529758 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/common/g1_msm_bindings.wgsl @@ -0,0 +1,40 @@ +struct Params { + lane0: vec4, + lane1: vec4, +} + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; +@group(0) @binding(4) var input_meta0: array; +@group(0) @binding(5) var input_meta1: array; +@group(0) @binding(6) var input_meta2: array; + +fn params_count() -> u32 { + return params.lane0.x; +} + +fn params_opcode() -> u32 { + return params.lane0.y; +} + +fn params_terms_per_instance() -> u32 { + return params.lane0.z; +} + +fn params_window() -> u32 { + return params.lane0.w; +} + +fn params_num_windows() -> u32 { + return params.lane1.x; +} + +fn params_bucket_count() -> u32 { + return params.lane1.y; +} + +fn params_row_width() -> u32 { + return params.lane1.z; +} diff --git a/backend/accelerated/webgpu/shaders/common/g1_msm_jac.wgsl b/backend/accelerated/webgpu/shaders/common/g1_msm_jac.wgsl new file mode 100644 index 0000000000..46adf88dd6 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/common/g1_msm_jac.wgsl @@ -0,0 +1,94 @@ +var g1_jac_wg: array; + +// Bucket accumulation: sparse signed bases → Jacobian bucket sums (no g1_jac_to_affine). +@compute @workgroup_size(64) +fn g1_msm_bucket_jac_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params_count()) { return; } + let start = input_meta1[i]; + let size = input_meta2[i]; + var acc = g1_jac_infinity(); + for (var j = 0u; j < size; j = j + 1u) { + let raw = input_meta0[start + j]; + let idx = raw & 0x7fffffffu; + let neg = (raw & 0x80000000u) != 0u; + var point = g1_load_from(0u, idx); + if (neg) { point = g1_neg_affine(point); } + acc = g1_add_mixed(acc, point); + } + g1_store(i, acc); +} + +// Weight buckets: multiply each Jacobian bucket sum by its bucket index. +@compute @workgroup_size(64) +fn g1_msm_weight_jac_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params_count()) { return; } + let point = g1_load_from(0u, i); + let value = input_meta0[i]; + if (g1_jac_is_infinity(point) || value == 0u) { + g1_store(i, g1_jac_infinity()); + return; + } + g1_store(i, g1_scalar_mul_jac_small(point, value)); +} + +// Subsum: reduce weighted Jacobian buckets into one Jacobian sum per window. +// One workgroup per window; 64 threads stride + internal tree reduction. +@compute @workgroup_size(64) +fn g1_msm_subsum_jac_main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + let i = wg_id.x; + let tid = local_id.x; + if (i >= params_count()) { return; } + let start = input_meta1[i]; + let count = input_meta2[i]; + var local_sum = g1_jac_infinity(); + var j = tid; + loop { + if (j >= count) { break; } + let point = g1_load_from(0u, start + j); + if (!g1_jac_is_infinity(point)) { + local_sum = g1_add_jac(local_sum, point); + } + j = j + 64u; + } + g1_jac_wg[tid] = local_sum; + workgroupBarrier(); + + var stride = 32u; + loop { + if (stride == 0u) { break; } + if (tid < stride) { + g1_jac_wg[tid] = g1_add_jac(g1_jac_wg[tid], g1_jac_wg[tid + stride]); + } + workgroupBarrier(); + stride = stride >> 1u; + } + if (tid == 0u) { + g1_store(i, g1_jac_wg[0u]); + } +} + +// Combine: Horner evaluation over Jacobian window sums. One g1_jac_to_affine per instance. +@compute @workgroup_size(64) +fn g1_msm_combine_jac_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params_count()) { return; } + let num_windows = params_num_windows(); + let window = params_window(); + var acc = g1_jac_infinity(); + for (var win: i32 = i32(num_windows) - 1; win >= 0; win = win - 1) { + if (u32(win) != (num_windows - 1u)) { + for (var step = 0u; step < window; step = step + 1u) { + acc = g1_double_jac(acc); + } + } + let point = g1_load_from(0u, i * num_windows + u32(win)); + if (g1_jac_is_infinity(point)) { continue; } + acc = g1_add_jac(acc, point); + } + g1_store(i, g1_jac_to_affine(acc)); +} diff --git a/backend/accelerated/webgpu/shaders/common/g1_ops_bindings.wgsl b/backend/accelerated/webgpu/shaders/common/g1_ops_bindings.wgsl new file mode 100644 index 0000000000..d2269a675c --- /dev/null +++ b/backend/accelerated/webgpu/shaders/common/g1_ops_bindings.wgsl @@ -0,0 +1,11 @@ +struct Params { + count: u32, + opcode: u32, + _pad0: u32, + _pad1: u32, +} + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; diff --git a/backend/accelerated/webgpu/shaders/common/g1_ops_main.wgsl b/backend/accelerated/webgpu/shaders/common/g1_ops_main.wgsl new file mode 100644 index 0000000000..db741418ef --- /dev/null +++ b/backend/accelerated/webgpu/shaders/common/g1_ops_main.wgsl @@ -0,0 +1,10 @@ +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn g1_ops_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + g1_store(i, g1_dispatch(params.opcode, g1_load_from(0u, i), g1_load_from(1u, i))); +} diff --git a/backend/accelerated/webgpu/shaders/common/g2_msm_bindings.wgsl b/backend/accelerated/webgpu/shaders/common/g2_msm_bindings.wgsl new file mode 100644 index 0000000000..8611529758 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/common/g2_msm_bindings.wgsl @@ -0,0 +1,40 @@ +struct Params { + lane0: vec4, + lane1: vec4, +} + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; +@group(0) @binding(4) var input_meta0: array; +@group(0) @binding(5) var input_meta1: array; +@group(0) @binding(6) var input_meta2: array; + +fn params_count() -> u32 { + return params.lane0.x; +} + +fn params_opcode() -> u32 { + return params.lane0.y; +} + +fn params_terms_per_instance() -> u32 { + return params.lane0.z; +} + +fn params_window() -> u32 { + return params.lane0.w; +} + +fn params_num_windows() -> u32 { + return params.lane1.x; +} + +fn params_bucket_count() -> u32 { + return params.lane1.y; +} + +fn params_row_width() -> u32 { + return params.lane1.z; +} diff --git a/backend/accelerated/webgpu/shaders/common/g2_msm_jac.wgsl b/backend/accelerated/webgpu/shaders/common/g2_msm_jac.wgsl new file mode 100644 index 0000000000..212dfe2bb8 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/common/g2_msm_jac.wgsl @@ -0,0 +1,128 @@ +var g2_jac_wg: array; + +fn msm_jac_params_window() -> u32 { + return params.lane0.w; +} + +fn msm_jac_params_num_windows() -> u32 { + return params.lane1.x; +} + +// Bucket accumulation: sparse signed base list → Jacobian bucket sums. +// Bases in input_a are stored as Jacobian with z=1 (affine encoding). +// Output stored as Jacobian (no g2_jac_to_affine inversion). +@compute @workgroup_size(32) +fn g2_msm_bucket_jac_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params_count()) { + return; + } + let start = input_meta1[i]; + let size = input_meta2[i]; + var acc = g2_jac_infinity(); + for (var j = 0u; j < size; j = j + 1u) { + let raw = input_meta0[start + j]; + let idx = raw & 0x7fffffffu; + let neg = (raw & 0x80000000u) != 0u; + var point = g2_load_from(0u, idx); + if (neg) { + point = g2_neg_affine(point); + } + acc = g2_add_mixed(acc, point); + } + g2_store(i, acc); +} + +// Weight buckets: multiply each Jacobian bucket sum by its scalar bucket index. +// Input (input_a) and output are Jacobian. No inversions. +@compute @workgroup_size(32) +fn g2_msm_weight_jac_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params_count()) { + return; + } + let point = g2_load_from(0u, i); + let value = input_meta0[i]; + if (g2_jac_is_infinity(point) || value == 0u) { + g2_store(i, g2_jac_infinity()); + return; + } + g2_store(i, g2_scalar_mul_jac_small(point, value)); +} + +// Subsum: reduce weighted Jacobian bucket sums into one Jacobian sum per window. +// One workgroup per window. 32 threads stride through the window's bucket range, +// then a tree reduction collapses 32 partial sums into one. +@compute @workgroup_size(32) +fn g2_msm_subsum_jac_main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + let i = wg_id.x; + let tid = local_id.x; + if (i >= params_count()) { + return; + } + let start = input_meta1[i]; + let count = input_meta2[i]; + + // Each thread accumulates its strided portion of the window's buckets. + var local_sum = g2_jac_infinity(); + var j = tid; + loop { + if (j >= count) { + break; + } + let point = g2_load_from(0u, start + j); + if (!g2_jac_is_infinity(point)) { + local_sum = g2_add_jac(local_sum, point); + } + j = j + 32u; + } + + g2_jac_wg[tid] = local_sum; + workgroupBarrier(); + + // Tree reduction over 32 partial sums. + var stride = 16u; + loop { + if (stride == 0u) { + break; + } + if (tid < stride) { + g2_jac_wg[tid] = g2_add_jac(g2_jac_wg[tid], g2_jac_wg[tid + stride]); + } + workgroupBarrier(); + stride = stride >> 1u; + } + + if (tid == 0u) { + g2_store(i, g2_jac_wg[0u]); + } +} + +// Combine: Horner evaluation over window sums → single MSM result per instance. +// Reads Jacobian window sums. Performs one g2_jac_to_affine per instance. +@compute @workgroup_size(32) +fn g2_msm_combine_jac_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params_count()) { + return; + } + let num_windows = msm_jac_params_num_windows(); + let window = msm_jac_params_window(); + var acc = g2_jac_infinity(); + for (var win: i32 = i32(num_windows) - 1; win >= 0; win = win - 1) { + if (u32(win) != (num_windows - 1u)) { + for (var step = 0u; step < window; step = step + 1u) { + acc = g2_double_jac(acc); + } + } + let point = g2_load_from(0u, i * num_windows + u32(win)); + if (g2_jac_is_infinity(point)) { + continue; + } + acc = g2_add_jac(acc, point); + } + g2_store(i, g2_jac_to_affine(acc)); +} diff --git a/backend/accelerated/webgpu/shaders/common/g2_ops_bindings.wgsl b/backend/accelerated/webgpu/shaders/common/g2_ops_bindings.wgsl new file mode 100644 index 0000000000..42c9585161 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/common/g2_ops_bindings.wgsl @@ -0,0 +1,19 @@ +struct Params { + count: u32, + opcode: u32, + _pad0: u32, + _pad1: u32, +} + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +fn params_count() -> u32 { + return params.count; +} + +fn params_opcode() -> u32 { + return params.opcode; +} diff --git a/backend/accelerated/webgpu/shaders/common/g2_ops_main.wgsl b/backend/accelerated/webgpu/shaders/common/g2_ops_main.wgsl new file mode 100644 index 0000000000..3b7c15648f --- /dev/null +++ b/backend/accelerated/webgpu/shaders/common/g2_ops_main.wgsl @@ -0,0 +1,10 @@ +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn g2_ops_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params_count()) { + return; + } + g2_store(i, g2_dispatch(params_opcode(), g2_load_from(0u, i), g2_load_from(1u, i))); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_377/fp_arith.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_377/fp_arith.wgsl new file mode 100644 index 0000000000..6f9368e75b --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_377/fp_arith.wgsl @@ -0,0 +1,444 @@ +// curvegpu:section fp-types begin +struct Fp { + limbs: array, +} + +struct Fp24 { + limbs: array, +} +// curvegpu:section fp-types end + +struct Params { + count: u32, + opcode: u32, + _pad0: u32, + _pad1: u32, +} + +const FP_OP_COPY: u32 = 0u; +const FP_OP_ZERO: u32 = 1u; +const FP_OP_ONE: u32 = 2u; +const FP_OP_ADD: u32 = 3u; +const FP_OP_SUB: u32 = 4u; +const FP_OP_NEG: u32 = 5u; +const FP_OP_DOUBLE: u32 = 6u; +const FP_OP_NORMALIZE: u32 = 7u; +const FP_OP_EQUAL: u32 = 8u; +const FP_OP_MUL: u32 = 9u; +const FP_OP_SQUARE: u32 = 10u; +const FP_OP_TO_MONT: u32 = 11u; +const FP_OP_FROM_MONT: u32 = 12u; + +// curvegpu:section fp-consts begin +const FP_LIMB16_MASK: u32 = 0xffffu; +const FP_QINV_NEG_16: u32 = 0xffffu; + +const FP_MODULUS16: array = array( + 0x0001u, 0x0000u, + 0xc000u, 0x8508u, + 0x0000u, 0x3000u, + 0x5d44u, 0x170bu, + 0x4800u, 0xba09u, + 0x622fu, 0x1ef3u, + 0x138fu, 0x00f5u, + 0xd9f3u, 0x1a22u, + 0x493bu, 0x6ca1u, + 0x05c0u, 0xc63bu, + 0x10eau, 0x17c5u, + 0x3a46u, 0x01aeu, +); + +const FP_MODULUS_MINUS_TWO: array = array( + 0xffffffffu, + 0x8508bfffu, + 0x30000000u, + 0x170b5d44u, + 0xba094800u, + 0x1ef3622fu, + 0x00f5138fu, + 0x1a22d9f3u, + 0x6ca1493bu, + 0xc63b05c0u, + 0x17c510eau, + 0x01ae3a46u, +); +// curvegpu:section fp-consts end + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +// curvegpu:section fp-core begin +fn fp_zero() -> Fp { + var z: Fp; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = 0u; + } + return z; +} + +fn fp_one() -> Fp { + var z: Fp; + z.limbs[0] = 0xffffff68u; + z.limbs[1] = 0x02cdffffu; + z.limbs[2] = 0x7fffffb1u; + z.limbs[3] = 0x51409f83u; + z.limbs[4] = 0x8a7d3ff2u; + z.limbs[5] = 0x9f7db3a9u; + z.limbs[6] = 0x6e7c6305u; + z.limbs[7] = 0x7b4e97b7u; + z.limbs[8] = 0x803c84e8u; + z.limbs[9] = 0x4cf495bfu; + z.limbs[10] = 0xe2fdf49au; + z.limbs[11] = 0x008d6661u; + return z; +} + +fn fp_one_regular() -> Fp { + var z = fp_zero(); + z.limbs[0] = 1u; + return z; +} + +fn fp_rsquare_regular() -> Fp { + var z: Fp; + z.limbs[0] = 0x9400cd22u; + z.limbs[1] = 0xb786686cu; + z.limbs[2] = 0xb00431b1u; + z.limbs[3] = 0x0329fcaau; + z.limbs[4] = 0x62d6b46du; + z.limbs[5] = 0x22a5f111u; + z.limbs[6] = 0x827dc3acu; + z.limbs[7] = 0xbfdf7d03u; + z.limbs[8] = 0x41790bf9u; + z.limbs[9] = 0x837e92f0u; + z.limbs[10] = 0x1e914b88u; + z.limbs[11] = 0x006dfccbu; + return z; +} + +fn fp_modulus() -> Fp { + var z: Fp; + z.limbs[0] = 0x00000001u; + z.limbs[1] = 0x8508c000u; + z.limbs[2] = 0x30000000u; + z.limbs[3] = 0x170b5d44u; + z.limbs[4] = 0xba094800u; + z.limbs[5] = 0x1ef3622fu; + z.limbs[6] = 0x00f5138fu; + z.limbs[7] = 0x1a22d9f3u; + z.limbs[8] = 0x6ca1493bu; + z.limbs[9] = 0xc63b05c0u; + z.limbs[10] = 0x17c510eau; + z.limbs[11] = 0x01ae3a46u; + return z; +} + +fn fp_predicate(value: bool) -> Fp { + var z = fp_zero(); + if (value) { + z = fp_one(); + } + return z; +} + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fp_is_zero(x: Fp) -> bool { + var acc = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let limb = x.limbs[i]; + acc = acc | limb; + } + return acc == 0u; +} + +fn fp_equal(x: Fp, y: Fp) -> bool { + for (var i = 0u; i < 12u; i = i + 1u) { + let xLimb = x.limbs[i]; + let yLimb = y.limbs[i]; + if (xLimb != yLimb) { + return false; + } + } + return true; +} + +fn fp_gte(x: Fp, y: Fp) -> bool { + for (var i: i32 = 11; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let yLimb = y.limbs[idx]; + if (xLimb != yLimb) { + return xLimb > yLimb; + } + } + return true; +} + +fn fp_add_modulus(x: Fp) -> Fp { + let q = fp_modulus(); + var z: Fp; + var carry = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let lane = adc(x.limbs[i], q.limbs[i], carry); + z.limbs[i] = lane.x; + carry = lane.y; + } + return z; +} + +fn fp_sub_modulus(x: Fp) -> Fp { + let q = fp_modulus(); + var z: Fp; + var borrow = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let lane = sbb(x.limbs[i], q.limbs[i], borrow); + z.limbs[i] = lane.x; + borrow = lane.y; + } + return z; +} + +fn fp_add(x: Fp, y: Fp) -> Fp { + var z: Fp; + var carry = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let lane = adc(x.limbs[i], y.limbs[i], carry); + z.limbs[i] = lane.x; + carry = lane.y; + } + if ((carry != 0u) || fp_gte(z, fp_modulus())) { + return fp_sub_modulus(z); + } + return z; +} + +fn fp_sub(x: Fp, y: Fp) -> Fp { + var z: Fp; + var borrow = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let lane = sbb(x.limbs[i], y.limbs[i], borrow); + z.limbs[i] = lane.x; + borrow = lane.y; + } + if (borrow != 0u) { + return fp_add_modulus(z); + } + return z; +} + +fn fp_neg(x: Fp) -> Fp { + if (fp_is_zero(x)) { + return fp_zero(); + } + return fp_sub(fp_modulus(), x); +} + +fn fp_double(x: Fp) -> Fp { + return fp_add(x, x); +} + +fn fp_normalize(x: Fp) -> Fp { + if (fp_gte(x, fp_modulus())) { + return fp_sub_modulus(x); + } + return x; +} + +fn fp_unpack16(x: Fp) -> Fp24 { + var z: Fp24; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FP_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fp_pack16(x: Fp24) -> Fp { + var z: Fp; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fp24_gte_modulus(x: Fp24) -> bool { + for (var i: i32 = 23; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FP_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fp24_sub_modulus(x: Fp24) -> Fp24 { + var z: Fp24; + var borrow = 0u; + for (var i = 0u; i < 24u; i = i + 1u) { + let lane = sbb(x.limbs[i], FP_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FP_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fp_mul(x: Fp, y: Fp) -> Fp { + let a = fp_unpack16(x); + let b = fp_unpack16(y); + var t: array; + + for (var i = 0u; i < 24u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 24u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FP_LIMB16_MASK; + carry = uv >> 16u; + } + t[24] = carry; + + let m = (t[0] * FP_QINV_NEG_16) & FP_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 24u; j = j + 1u) { + let qLimb = FP_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FP_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[24] + carry; + t[23] = uv & FP_LIMB16_MASK; + t[24] = uv >> 16u; + } + + var z24: Fp24; + for (var i = 0u; i < 24u; i = i + 1u) { + z24.limbs[i] = t[i]; + } + if ((t[24] != 0u) || fp24_gte_modulus(z24)) { + z24 = fp24_sub_modulus(z24); + } + return fp_pack16(z24); +} + +fn fp_square(x: Fp) -> Fp { + return fp_mul(x, x); +} + +fn fp_inverse(x: Fp) -> Fp { + if (fp_is_zero(x)) { + return fp_zero(); + } + var acc = fp_one(); + for (var word_index: i32 = 11; word_index >= 0; word_index = word_index - 1) { + let word = FP_MODULUS_MINUS_TWO[u32(word_index)]; + for (var bit_index: i32 = 31; bit_index >= 0; bit_index = bit_index - 1) { + acc = fp_square(acc); + if (((word >> u32(bit_index)) & 1u) != 0u) { + acc = fp_mul(acc, x); + } + } + } + return acc; +} +// curvegpu:section fp-core end + +fn fp_dispatch(opcode: u32, a: Fp, b: Fp) -> Fp { + if (opcode == FP_OP_COPY) { + return a; + } + if (opcode == FP_OP_ZERO) { + return fp_zero(); + } + if (opcode == FP_OP_ONE) { + return fp_one(); + } + if (opcode == FP_OP_ADD) { + return fp_add(a, b); + } + if (opcode == FP_OP_SUB) { + return fp_sub(a, b); + } + if (opcode == FP_OP_NEG) { + return fp_neg(a); + } + if (opcode == FP_OP_DOUBLE) { + return fp_double(a); + } + if (opcode == FP_OP_NORMALIZE) { + return fp_normalize(a); + } + if (opcode == FP_OP_EQUAL) { + return fp_predicate(fp_equal(a, b)); + } + if (opcode == FP_OP_MUL) { + return fp_mul(a, b); + } + if (opcode == FP_OP_SQUARE) { + return fp_square(a); + } + if (opcode == FP_OP_TO_MONT) { + return fp_mul(a, fp_rsquare_regular()); + } + if (opcode == FP_OP_FROM_MONT) { + return fp_mul(a, fp_one_regular()); + } + return fp_zero(); +} + +fn fp_load_a(index: u32) -> Fp { + let base = index * 12u; + var z: Fp; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_a[base + i]; + } + return z; +} + +fn fp_load_b(index: u32) -> Fp { + let base = index * 12u; + var z: Fp; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_b[base + i]; + } + return z; +} + +fn fp_store(index: u32, value: Fp) { + let base = index * 12u; + for (var i = 0u; i < 12u; i = i + 1u) { + output[base + i] = value.limbs[i]; + } +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fp_ops_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + fp_store(i, fp_dispatch(params.opcode, fp_load_a(i), fp_load_b(i))); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_arith.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_arith.wgsl new file mode 100644 index 0000000000..321d990ac9 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_arith.wgsl @@ -0,0 +1,492 @@ +// curvegpu:section fr_types begin +struct Fr { + limbs: array, +} + +struct Fr16 { + limbs: array, +} +// curvegpu:section fr_types end + +struct Params { + count: u32, + opcode: u32, + _pad0: u32, + _pad1: u32, +} + +const FR_OP_COPY: u32 = 0u; +const FR_OP_ZERO: u32 = 1u; +const FR_OP_ONE: u32 = 2u; +const FR_OP_ADD: u32 = 3u; +const FR_OP_SUB: u32 = 4u; +const FR_OP_NEG: u32 = 5u; +const FR_OP_DOUBLE: u32 = 6u; +const FR_OP_NORMALIZE: u32 = 7u; +const FR_OP_EQUAL: u32 = 8u; +const FR_OP_MUL: u32 = 9u; +const FR_OP_SQUARE: u32 = 10u; +const FR_OP_TO_MONT: u32 = 11u; +const FR_OP_FROM_MONT: u32 = 12u; +// curvegpu:section fr_constants begin +const FR_LIMB16_MASK: u32 = 0xffffu; +const FR_QINV_NEG_16: u32 = 0xffffu; + +const FR_MODULUS16: array = array( + 0x0001u, 0x0000u, + 0x8000u, 0x0a11u, + 0x0001u, 0xd000u, + 0x76feu, 0x59aau, + 0xb001u, 0x5c37u, + 0x4d1eu, 0x60b4u, + 0xa556u, 0x9a2cu, + 0x655eu, 0x12abu, +); +// curvegpu:section fr_constants end + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +// curvegpu:section fr_core begin +fn fr_zero() -> Fr { + var z: Fr; + z.limbs[0] = 0u; + z.limbs[1] = 0u; + z.limbs[2] = 0u; + z.limbs[3] = 0u; + z.limbs[4] = 0u; + z.limbs[5] = 0u; + z.limbs[6] = 0u; + z.limbs[7] = 0u; + return z; +} + +fn fr_one() -> Fr { + var z: Fr; + z.limbs[0] = 0xfffffff3u; + z.limbs[1] = 0x7d1c7fffu; + z.limbs[2] = 0x6ffffff2u; + z.limbs[3] = 0x7257f50fu; + z.limbs[4] = 0x512c0feeu; + z.limbs[5] = 0x16d81575u; + z.limbs[6] = 0x2bbb9a9du; + z.limbs[7] = 0x0d4bda32u; + return z; +} + +fn fr_one_regular() -> Fr { + var z = fr_zero(); + z.limbs[0] = 1u; + return z; +} + +fn fr_rsquare_regular() -> Fr { + var z: Fr; + z.limbs[0] = 0xb861857bu; + z.limbs[1] = 0x25d577bau; + z.limbs[2] = 0x8860591fu; + z.limbs[3] = 0xcc2c27b5u; + z.limbs[4] = 0xe5dc8593u; + z.limbs[5] = 0xa7cc008fu; + z.limbs[6] = 0xeff1c939u; + z.limbs[7] = 0x011fdae7u; + return z; +} + +fn fr_modulus() -> Fr { + var z: Fr; + z.limbs[0] = 0x00000001u; + z.limbs[1] = 0x0a118000u; + z.limbs[2] = 0xd0000001u; + z.limbs[3] = 0x59aa76feu; + z.limbs[4] = 0x5c37b001u; + z.limbs[5] = 0x60b44d1eu; + z.limbs[6] = 0x9a2ca556u; + z.limbs[7] = 0x12ab655eu; + return z; +} + +fn fr_predicate(value: bool) -> Fr { + var z = fr_zero(); + if (value) { + z = fr_one(); + } + return z; +} + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fr_is_zero(x: Fr) -> bool { + return (x.limbs[0] | x.limbs[1] | x.limbs[2] | x.limbs[3] | + x.limbs[4] | x.limbs[5] | x.limbs[6] | x.limbs[7]) == 0u; +} + +fn fr_equal(x: Fr, y: Fr) -> bool { + return (x.limbs[0] == y.limbs[0]) && + (x.limbs[1] == y.limbs[1]) && + (x.limbs[2] == y.limbs[2]) && + (x.limbs[3] == y.limbs[3]) && + (x.limbs[4] == y.limbs[4]) && + (x.limbs[5] == y.limbs[5]) && + (x.limbs[6] == y.limbs[6]) && + (x.limbs[7] == y.limbs[7]); +} + +fn fr_gte(x: Fr, y: Fr) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fr_add_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fr_sub_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fr_add(x: Fr, y: Fr) -> Fr { + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fr_gte(z, fr_modulus())) { + return fr_sub_modulus(z); + } + return z; +} + +fn fr_sub(x: Fr, y: Fr) -> Fr { + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fr_add_modulus(z); + } + return z; +} + +fn fr_neg(x: Fr) -> Fr { + if (fr_is_zero(x)) { + return fr_zero(); + } + return fr_sub(fr_modulus(), x); +} + +fn fr_double(x: Fr) -> Fr { + return fr_add(x, x); +} + +fn fr_normalize(x: Fr) -> Fr { + if (fr_gte(x, fr_modulus())) { + return fr_sub_modulus(x); + } + return x; +} + +fn fr_unpack16(x: Fr) -> Fr16 { + var z: Fr16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FR_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fr_pack16(x: Fr16) -> Fr { + var z: Fr; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fr16_gte_modulus(x: Fr16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FR_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fr16_sub_modulus(x: Fr16) -> Fr16 { + var z: Fr16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FR_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FR_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fr_mul(x: Fr, y: Fr) -> Fr { + let a = fr_unpack16(x); + let b = fr_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FR_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FR_QINV_NEG_16) & FR_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FR_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FR_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FR_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fr16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fr16_gte_modulus(z16)) { + z16 = fr16_sub_modulus(z16); + } + return fr_pack16(z16); +} +// curvegpu:section fr_core end + +fn fr_dispatch(opcode: u32, a: Fr, b: Fr) -> Fr { + if (opcode == FR_OP_COPY) { + return a; + } + if (opcode == FR_OP_ZERO) { + return fr_zero(); + } + if (opcode == FR_OP_ONE) { + return fr_one(); + } + if (opcode == FR_OP_ADD) { + return fr_add(a, b); + } + if (opcode == FR_OP_SUB) { + return fr_sub(a, b); + } + if (opcode == FR_OP_NEG) { + return fr_neg(a); + } + if (opcode == FR_OP_DOUBLE) { + return fr_double(a); + } + if (opcode == FR_OP_NORMALIZE) { + return fr_normalize(a); + } + if (opcode == FR_OP_EQUAL) { + return fr_predicate(fr_equal(a, b)); + } + if (opcode == FR_OP_MUL) { + return fr_mul(a, b); + } + if (opcode == FR_OP_SQUARE) { + return fr_mul(a, a); + } + if (opcode == FR_OP_TO_MONT) { + return fr_mul(a, fr_rsquare_regular()); + } + if (opcode == FR_OP_FROM_MONT) { + return fr_mul(a, fr_one_regular()); + } + return fr_zero(); +} + +fn fr_load_a(index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + z.limbs[0] = input_a[base + 0u]; + z.limbs[1] = input_a[base + 1u]; + z.limbs[2] = input_a[base + 2u]; + z.limbs[3] = input_a[base + 3u]; + z.limbs[4] = input_a[base + 4u]; + z.limbs[5] = input_a[base + 5u]; + z.limbs[6] = input_a[base + 6u]; + z.limbs[7] = input_a[base + 7u]; + return z; +} + +fn fr_load_b(index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + z.limbs[0] = input_b[base + 0u]; + z.limbs[1] = input_b[base + 1u]; + z.limbs[2] = input_b[base + 2u]; + z.limbs[3] = input_b[base + 3u]; + z.limbs[4] = input_b[base + 4u]; + z.limbs[5] = input_b[base + 5u]; + z.limbs[6] = input_b[base + 6u]; + z.limbs[7] = input_b[base + 7u]; + return z; +} + +fn fr_store(index: u32, value: Fr) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_ops_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + fr_store(i, fr_dispatch(params.opcode, fr_load_a(i), fr_load_b(i))); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_ntt.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_ntt.wgsl new file mode 100644 index 0000000000..f70c6a10a4 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_ntt.wgsl @@ -0,0 +1,356 @@ +struct Fr { + limbs: array, +} + +struct Fr16 { + limbs: array, +} + +struct Params { + count: u32, + m: u32, + _pad0: u32, + _pad1: u32, +} + +const FR_LIMB16_MASK: u32 = 0xffffu; +const FR_QINV_NEG_16: u32 = 0xffffu; + +const FR_MODULUS16: array = array( + 0x0001u, 0x0000u, + 0x8000u, 0x0a11u, + 0x0001u, 0xd000u, + 0x76feu, 0x59aau, + 0xb001u, 0x5c37u, + 0x4d1eu, 0x60b4u, + 0xa556u, 0x9a2cu, + 0x655eu, 0x12abu, +); + +@group(0) @binding(0) var input_values: array; +@group(0) @binding(1) var input_twiddles: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fr_modulus() -> Fr { + var z: Fr; + z.limbs[0] = 0x00000001u; + z.limbs[1] = 0x0a118000u; + z.limbs[2] = 0xd0000001u; + z.limbs[3] = 0x59aa76feu; + z.limbs[4] = 0x5c37b001u; + z.limbs[5] = 0x60b44d1eu; + z.limbs[6] = 0x9a2ca556u; + z.limbs[7] = 0x12ab655eu; + return z; +} + +fn fr_gte(x: Fr, y: Fr) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fr_add_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fr_sub_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fr_add(x: Fr, y: Fr) -> Fr { + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fr_gte(z, fr_modulus())) { + return fr_sub_modulus(z); + } + return z; +} + +fn fr_sub(x: Fr, y: Fr) -> Fr { + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fr_add_modulus(z); + } + return z; +} + +fn fr_unpack16(x: Fr) -> Fr16 { + var z: Fr16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FR_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fr_pack16(x: Fr16) -> Fr { + var z: Fr; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fr16_gte_modulus(x: Fr16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FR_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fr16_sub_modulus(x: Fr16) -> Fr16 { + var z: Fr16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FR_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FR_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fr_mul(x: Fr, y: Fr) -> Fr { + let a = fr_unpack16(x); + let b = fr_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FR_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FR_QINV_NEG_16) & FR_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FR_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FR_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FR_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fr16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fr16_gte_modulus(z16)) { + z16 = fr16_sub_modulus(z16); + } + return fr_pack16(z16); +} + +fn fr_load_from(buffer_kind: u32, index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + if (buffer_kind == 0u) { + z.limbs[0] = input_values[base + 0u]; + z.limbs[1] = input_values[base + 1u]; + z.limbs[2] = input_values[base + 2u]; + z.limbs[3] = input_values[base + 3u]; + z.limbs[4] = input_values[base + 4u]; + z.limbs[5] = input_values[base + 5u]; + z.limbs[6] = input_values[base + 6u]; + z.limbs[7] = input_values[base + 7u]; + return z; + } + z.limbs[0] = input_twiddles[base + 0u]; + z.limbs[1] = input_twiddles[base + 1u]; + z.limbs[2] = input_twiddles[base + 2u]; + z.limbs[3] = input_twiddles[base + 3u]; + z.limbs[4] = input_twiddles[base + 4u]; + z.limbs[5] = input_twiddles[base + 5u]; + z.limbs[6] = input_twiddles[base + 6u]; + z.limbs[7] = input_twiddles[base + 7u]; + return z; +} + +fn fr_store(index: u32, value: Fr) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_ntt_stage_main(@builtin(global_invocation_id) id: vec3) { + let pair = id.x; + let vector = id.y; + let half_count = params.count / 2u; + let batch_count = max(params._pad0, 1u); + if (pair >= half_count || vector >= batch_count) { + return; + } + + let m = params.m; + let j = pair % m; + let block = pair / m; + let vector_base = vector * params.count; + let left_index = vector_base + block * 2u * m + j; + let right_index = left_index + m; + + let left = fr_load_from(0u, left_index); + let twiddle = fr_load_from(1u, j); + let right = fr_mul(fr_load_from(0u, right_index), twiddle); + + fr_store(left_index, fr_add(left, right)); + fr_store(right_index, fr_sub(left, right)); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_plonk_quotient.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_plonk_quotient.wgsl new file mode 100644 index 0000000000..bd192ff047 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_plonk_quotient.wgsl @@ -0,0 +1,228 @@ +struct PlonkQuotientParams { + count: u32, + blind_count: u32, + coset_count: u32, + _pad1: u32, +} + +override COMMITMENT_COUNT: u32 = 0u; + +const PLONK_FR_WORDS: u32 = 8u; +const PLONK_BASE_DYNAMIC_VECTOR_COUNT: u32 = 5u; +const PLONK_BASE_STATIC_VECTOR_COUNT: u32 = 7u; +const PLONK_BLIND_COUNT: u32 = 4u; +const PLONK_SCALAR_COUNT: u32 = 7u; + +const PLONK_VEC_L: u32 = 0u; +const PLONK_VEC_R: u32 = 1u; +const PLONK_VEC_O: u32 = 2u; +const PLONK_VEC_Z: u32 = 3u; +const PLONK_VEC_QK: u32 = 4u; + +const PLONK_BLIND_L: u32 = 0u; +const PLONK_BLIND_R: u32 = 1u; +const PLONK_BLIND_O: u32 = 2u; +const PLONK_BLIND_Z: u32 = 3u; + +const PLONK_SCALAR_COSET: u32 = 0u; +const PLONK_SCALAR_LAGRANGE_SCALE: u32 = 1u; +const PLONK_SCALAR_CS: u32 = 2u; +const PLONK_SCALAR_CSS: u32 = 3u; +const PLONK_SCALAR_BETA: u32 = 4u; +const PLONK_SCALAR_GAMMA: u32 = 5u; +const PLONK_SCALAR_ALPHA: u32 = 6u; + +@group(0) @binding(0) var plonk_vectors: array; +@group(0) @binding(1) var plonk_blinds: array; +@group(0) @binding(2) var plonk_scalars: array; +@group(0) @binding(3) var plonk_output: array; +@group(0) @binding(4) var plonk_params: PlonkQuotientParams; + +fn plonk_static_base() -> u32 { + return PLONK_BASE_DYNAMIC_VECTOR_COUNT + COMMITMENT_COUNT; +} + +fn plonk_vec_ql() -> u32 { + return plonk_static_base(); +} + +fn plonk_vec_qr() -> u32 { + return plonk_static_base() + 1u; +} + +fn plonk_vec_qm() -> u32 { + return plonk_static_base() + 2u; +} + +fn plonk_vec_qo() -> u32 { + return plonk_static_base() + 3u; +} + +fn plonk_vec_s1() -> u32 { + return plonk_static_base() + 4u; +} + +fn plonk_vec_s2() -> u32 { + return plonk_static_base() + 5u; +} + +fn plonk_vec_s3() -> u32 { + return plonk_static_base() + 6u; +} + +fn plonk_vec_commitment_value(index: u32) -> u32 { + return PLONK_BASE_DYNAMIC_VECTOR_COUNT + index; +} + +fn plonk_vec_qcp(index: u32) -> u32 { + return plonk_static_base() + PLONK_BASE_STATIC_VECTOR_COUNT + index; +} + +fn plonk_vec_twiddles() -> u32 { + return plonk_static_base() + PLONK_BASE_STATIC_VECTOR_COUNT + COMMITMENT_COUNT; +} + +fn plonk_vec_denominators() -> u32 { + return plonk_vec_twiddles() + 1u; +} + +fn plonk_vector_count() -> u32 { + return plonk_vec_denominators() + 1u; +} + +fn fr_from_mont(x: Fr) -> Fr { + return fr_mul(x, fr_one_regular()); +} + +fn plonk_load_words(base: u32) -> Fr { + var z: Fr; + z.limbs[0] = plonk_vectors[base + 0u]; + z.limbs[1] = plonk_vectors[base + 1u]; + z.limbs[2] = plonk_vectors[base + 2u]; + z.limbs[3] = plonk_vectors[base + 3u]; + z.limbs[4] = plonk_vectors[base + 4u]; + z.limbs[5] = plonk_vectors[base + 5u]; + z.limbs[6] = plonk_vectors[base + 6u]; + z.limbs[7] = plonk_vectors[base + 7u]; + return z; +} + +fn plonk_load_vector_mont(coset: u32, vector: u32, index: u32) -> Fr { + let base = (((coset * plonk_vector_count() + vector) * plonk_params.count) + index) * PLONK_FR_WORDS; + return plonk_load_words(base); +} + +fn plonk_load_blind_mont(coset: u32, poly: u32, index: u32) -> Fr { + let base = (((coset * PLONK_BLIND_COUNT + poly) * plonk_params.blind_count) + index) * PLONK_FR_WORDS; + var z: Fr; + z.limbs[0] = plonk_blinds[base + 0u]; + z.limbs[1] = plonk_blinds[base + 1u]; + z.limbs[2] = plonk_blinds[base + 2u]; + z.limbs[3] = plonk_blinds[base + 3u]; + z.limbs[4] = plonk_blinds[base + 4u]; + z.limbs[5] = plonk_blinds[base + 5u]; + z.limbs[6] = plonk_blinds[base + 6u]; + z.limbs[7] = plonk_blinds[base + 7u]; + return z; +} + +fn plonk_load_scalar_mont(coset: u32, index: u32) -> Fr { + let base = ((coset * PLONK_SCALAR_COUNT) + index) * PLONK_FR_WORDS; + var z: Fr; + z.limbs[0] = plonk_scalars[base + 0u]; + z.limbs[1] = plonk_scalars[base + 1u]; + z.limbs[2] = plonk_scalars[base + 2u]; + z.limbs[3] = plonk_scalars[base + 3u]; + z.limbs[4] = plonk_scalars[base + 4u]; + z.limbs[5] = plonk_scalars[base + 5u]; + z.limbs[6] = plonk_scalars[base + 6u]; + z.limbs[7] = plonk_scalars[base + 7u]; + return z; +} + +fn plonk_store_regular(coset: u32, index: u32, value: Fr) { + let regular = fr_from_mont(value); + let base = ((coset * plonk_params.count) + index) * PLONK_FR_WORDS; + plonk_output[base + 0u] = regular.limbs[0]; + plonk_output[base + 1u] = regular.limbs[1]; + plonk_output[base + 2u] = regular.limbs[2]; + plonk_output[base + 3u] = regular.limbs[3]; + plonk_output[base + 4u] = regular.limbs[4]; + plonk_output[base + 5u] = regular.limbs[5]; + plonk_output[base + 6u] = regular.limbs[6]; + plonk_output[base + 7u] = regular.limbs[7]; +} + +fn plonk_eval_blind(coset: u32, poly: u32, point: Fr) -> Fr { + var res = fr_zero(); + var i = plonk_params.blind_count; + loop { + if (i == 0u) { + break; + } + i = i - 1u; + res = fr_add(fr_mul(res, point), plonk_load_blind_mont(coset, poly, i)); + } + return res; +} + +fn plonk_evaluate_quotient(coset: u32, index: u32) -> Fr { + let twiddle = plonk_load_vector_mont(coset, plonk_vec_twiddles(), index); + let next_index = (index + 1u) % plonk_params.count; + let next_twiddle = plonk_load_vector_mont(coset, plonk_vec_twiddles(), next_index); + + var l = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_L, index), plonk_eval_blind(coset, PLONK_BLIND_L, twiddle)); + var r = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_R, index), plonk_eval_blind(coset, PLONK_BLIND_R, twiddle)); + var o = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_O, index), plonk_eval_blind(coset, PLONK_BLIND_O, twiddle)); + var z = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_Z, index), plonk_eval_blind(coset, PLONK_BLIND_Z, twiddle)); + let zs = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_Z, next_index), plonk_eval_blind(coset, PLONK_BLIND_Z, next_twiddle)); + + var gate = fr_mul(plonk_load_vector_mont(coset, plonk_vec_ql(), index), l); + gate = fr_add(gate, fr_mul(plonk_load_vector_mont(coset, plonk_vec_qr(), index), r)); + gate = fr_add(gate, fr_mul(fr_mul(plonk_load_vector_mont(coset, plonk_vec_qm(), index), l), r)); + gate = fr_add(gate, fr_mul(plonk_load_vector_mont(coset, plonk_vec_qo(), index), o)); + gate = fr_add(gate, plonk_load_vector_mont(coset, PLONK_VEC_QK, index)); + var commitment_index = 0u; + loop { + if (commitment_index >= COMMITMENT_COUNT) { + break; + } + let qcp = plonk_load_vector_mont(coset, plonk_vec_qcp(commitment_index), index); + let commitment_value = plonk_load_vector_mont(coset, plonk_vec_commitment_value(commitment_index), index); + gate = fr_add(gate, fr_mul(qcp, commitment_value)); + commitment_index = commitment_index + 1u; + } + + let beta = plonk_load_scalar_mont(coset, PLONK_SCALAR_BETA); + let gamma = plonk_load_scalar_mont(coset, PLONK_SCALAR_GAMMA); + let alpha = plonk_load_scalar_mont(coset, PLONK_SCALAR_ALPHA); + let id = fr_mul(fr_mul(twiddle, plonk_load_scalar_mont(coset, PLONK_SCALAR_COSET)), beta); + + var a = fr_add(fr_add(gamma, l), id); + var b = fr_add(fr_add(fr_mul(id, plonk_load_scalar_mont(coset, PLONK_SCALAR_CS)), r), gamma); + var c = fr_add(fr_add(fr_mul(id, plonk_load_scalar_mont(coset, PLONK_SCALAR_CSS)), o), gamma); + let right = fr_mul(fr_mul(fr_mul(a, b), c), z); + + a = fr_add(fr_add(fr_mul(plonk_load_vector_mont(coset, plonk_vec_s1(), index), beta), l), gamma); + b = fr_add(fr_add(fr_mul(plonk_load_vector_mont(coset, plonk_vec_s2(), index), beta), r), gamma); + c = fr_add(fr_add(fr_mul(plonk_load_vector_mont(coset, plonk_vec_s3(), index), beta), o), gamma); + let left = fr_mul(fr_mul(fr_mul(a, b), c), zs); + + let ordering = fr_sub(left, right); + let lone = fr_mul(plonk_load_scalar_mont(coset, PLONK_SCALAR_LAGRANGE_SCALE), plonk_load_vector_mont(coset, plonk_vec_denominators(), index)); + var local = fr_mul(fr_sub(z, fr_one()), lone); + local = fr_add(fr_mul(local, alpha), ordering); + return fr_add(fr_mul(local, alpha), gate); +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_plonk_quotient_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + let coset = id.y; + if (i >= plonk_params.count || coset >= plonk_params.coset_count) { + return; + } + plonk_store_regular(coset, i, plonk_evaluate_quotient(coset, i)); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_vector.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_vector.wgsl new file mode 100644 index 0000000000..ef780989e4 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_377/fr_vector.wgsl @@ -0,0 +1,391 @@ +struct Fr { + limbs: array, +} + +struct Fr16 { + limbs: array, +} + +struct Params { + count: u32, + opcode: u32, + log_count: u32, + _pad0: u32, +} + +const FR_VECTOR_OP_COPY: u32 = 0u; +const FR_VECTOR_OP_ADD: u32 = 1u; +const FR_VECTOR_OP_SUB: u32 = 2u; +const FR_VECTOR_OP_MUL_FACTORS: u32 = 3u; +const FR_VECTOR_OP_BIT_REVERSE_COPY: u32 = 4u; +const FR_LIMB16_MASK: u32 = 0xffffu; +const FR_QINV_NEG_16: u32 = 0xffffu; + +const FR_MODULUS16: array = array( + 0x0001u, 0x0000u, + 0x8000u, 0x0a11u, + 0x0001u, 0xd000u, + 0x76feu, 0x59aau, + 0xb001u, 0x5c37u, + 0x4d1eu, 0x60b4u, + 0xa556u, 0x9a2cu, + 0x655eu, 0x12abu, +); + +@group(0) @binding(0) var input_values: array; +@group(0) @binding(1) var input_aux: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +fn fr_zero() -> Fr { + var z: Fr; + z.limbs[0] = 0u; + z.limbs[1] = 0u; + z.limbs[2] = 0u; + z.limbs[3] = 0u; + z.limbs[4] = 0u; + z.limbs[5] = 0u; + z.limbs[6] = 0u; + z.limbs[7] = 0u; + return z; +} + +fn fr_modulus() -> Fr { + var z: Fr; + z.limbs[0] = 0x00000001u; + z.limbs[1] = 0x0a118000u; + z.limbs[2] = 0xd0000001u; + z.limbs[3] = 0x59aa76feu; + z.limbs[4] = 0x5c37b001u; + z.limbs[5] = 0x60b44d1eu; + z.limbs[6] = 0x9a2ca556u; + z.limbs[7] = 0x12ab655eu; + return z; +} + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fr_gte(x: Fr, y: Fr) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fr_add_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fr_sub_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fr_add(x: Fr, y: Fr) -> Fr { + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fr_gte(z, fr_modulus())) { + return fr_sub_modulus(z); + } + return z; +} + +fn fr_sub(x: Fr, y: Fr) -> Fr { + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fr_add_modulus(z); + } + return z; +} + +fn fr_unpack16(x: Fr) -> Fr16 { + var z: Fr16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FR_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fr_pack16(x: Fr16) -> Fr { + var z: Fr; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fr16_gte_modulus(x: Fr16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FR_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fr16_sub_modulus(x: Fr16) -> Fr16 { + var z: Fr16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FR_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FR_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fr_mul(x: Fr, y: Fr) -> Fr { + let a = fr_unpack16(x); + let b = fr_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FR_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FR_QINV_NEG_16) & FR_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FR_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FR_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FR_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fr16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fr16_gte_modulus(z16)) { + z16 = fr16_sub_modulus(z16); + } + return fr_pack16(z16); +} + +fn reverse_bits(index: u32, log_count: u32) -> u32 { + var out = 0u; + for (var bit = 0u; bit < log_count; bit = bit + 1u) { + out = (out << 1u) | ((index >> bit) & 1u); + } + return out; +} + +fn fr_load_from(buffer_kind: u32, index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + if (buffer_kind == 0u) { + z.limbs[0] = input_values[base + 0u]; + z.limbs[1] = input_values[base + 1u]; + z.limbs[2] = input_values[base + 2u]; + z.limbs[3] = input_values[base + 3u]; + z.limbs[4] = input_values[base + 4u]; + z.limbs[5] = input_values[base + 5u]; + z.limbs[6] = input_values[base + 6u]; + z.limbs[7] = input_values[base + 7u]; + return z; + } + z.limbs[0] = input_aux[base + 0u]; + z.limbs[1] = input_aux[base + 1u]; + z.limbs[2] = input_aux[base + 2u]; + z.limbs[3] = input_aux[base + 3u]; + z.limbs[4] = input_aux[base + 4u]; + z.limbs[5] = input_aux[base + 5u]; + z.limbs[6] = input_aux[base + 6u]; + z.limbs[7] = input_aux[base + 7u]; + return z; +} + +fn fr_store(index: u32, value: Fr) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +fn fr_dispatch(index: u32) -> Fr { + if (params.opcode == FR_VECTOR_OP_COPY) { + return fr_load_from(0u, index); + } + if (params.opcode == FR_VECTOR_OP_ADD) { + return fr_add(fr_load_from(0u, index), fr_load_from(1u, index)); + } + if (params.opcode == FR_VECTOR_OP_SUB) { + return fr_sub(fr_load_from(0u, index), fr_load_from(1u, index)); + } + if (params.opcode == FR_VECTOR_OP_MUL_FACTORS) { + return fr_mul(fr_load_from(0u, index), fr_load_from(1u, index)); + } + if (params.opcode == FR_VECTOR_OP_BIT_REVERSE_COPY) { + if (params._pad0 != 0u) { + let vector_size = params._pad0; + let vector_base = (index / vector_size) * vector_size; + let lane = index % vector_size; + return fr_load_from(0u, vector_base + reverse_bits(lane, params.log_count)); + } + return fr_load_from(0u, reverse_bits(index, params.log_count)); + } + return fr_zero(); +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_vector_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + fr_store(i, fr_dispatch(i)); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_377/g1_io.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_377/g1_io.wgsl new file mode 100644 index 0000000000..21cb7f5ac7 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_377/g1_io.wgsl @@ -0,0 +1,35 @@ +fn fp_load_from(buffer_kind: u32, base: u32) -> Fp { + var z: Fp; + if (buffer_kind == 0u) { + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_a[base + i]; + } + return z; + } + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_b[base + i]; + } + return z; +} + +fn g1_load_from(buffer_kind: u32, index: u32) -> G1Point { + let base = index * 36u; + var p: G1Point; + p.x = fp_load_from(buffer_kind, base + 0u); + p.y = fp_load_from(buffer_kind, base + 12u); + p.z = fp_load_from(buffer_kind, base + 24u); + return p; +} + +fn fp_store(base: u32, value: Fp) { + for (var i = 0u; i < 12u; i = i + 1u) { + output[base + i] = value.limbs[i]; + } +} + +fn g1_store(index: u32, value: G1Point) { + let base = index * 36u; + fp_store(base + 0u, value.x); + fp_store(base + 12u, value.y); + fp_store(base + 24u, value.z); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_377/g2_arith.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_377/g2_arith.wgsl new file mode 100644 index 0000000000..a90e1b78bd --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_377/g2_arith.wgsl @@ -0,0 +1,349 @@ +struct Fp2 { + c0: Fp, + c1: Fp, +} + +struct G2Point { + x: Fp2, + y: Fp2, + z: Fp2, +} + +const G2_OP_COPY: u32 = 0u; +const G2_OP_JAC_INFINITY: u32 = 1u; +const G2_OP_AFFINE_TO_JAC: u32 = 2u; +const G2_OP_NEG_JAC: u32 = 3u; +const G2_OP_DOUBLE_JAC: u32 = 4u; +const G2_OP_ADD_MIXED: u32 = 5u; +const G2_OP_JAC_TO_AFFINE: u32 = 6u; +const G2_OP_AFFINE_ADD: u32 = 7u; + +fn fp2_zero() -> Fp2 { + var z: Fp2; + z.c0 = fp_zero(); + z.c1 = fp_zero(); + return z; +} + +fn fp2_one() -> Fp2 { + var z: Fp2; + z.c0 = fp_one(); + z.c1 = fp_zero(); + return z; +} + +fn fp2_is_zero(x: Fp2) -> bool { + return fp_is_zero(x.c0) && fp_is_zero(x.c1); +} + +fn fp2_equal(x: Fp2, y: Fp2) -> bool { + return fp_equal(x.c0, y.c0) && fp_equal(x.c1, y.c1); +} + +fn fp2_add(x: Fp2, y: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_add(x.c0, y.c0); + z.c1 = fp_add(x.c1, y.c1); + return z; +} + +fn fp2_sub(x: Fp2, y: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_sub(x.c0, y.c0); + z.c1 = fp_sub(x.c1, y.c1); + return z; +} + +fn fp2_neg(x: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_neg(x.c0); + z.c1 = fp_neg(x.c1); + return z; +} + +fn fp2_double(x: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_double(x.c0); + z.c1 = fp_double(x.c1); + return z; +} + +fn fp_mul_by_5(x: Fp) -> Fp { + return fp_add(fp_double(fp_double(x)), x); +} + +fn fp_non_residue_inv() -> Fp { + var z: Fp; + z.limbs[0] = 0x66666685u; + z.limbs[1] = 0x80722666u; + z.limbs[2] = 0x899999a9u; + z.limbs[3] = 0x8df55926u; + z.limbs[4] = 0xd64f34cfu; + z.limbs[5] = 0x7fe4561au; + z.limbs[6] = 0xb6e4f01bu; + z.limbs[7] = 0xb95da6d8u; + z.limbs[8] = 0xfc142743u; + z.limbs[9] = 0x4b747cccu; + z.limbs[10] = 0x70f49f43u; + z.limbs[11] = 0x0039c3fau; + return z; +} + +fn fp_mul_by_non_residue_inv(x: Fp) -> Fp { + return fp_mul(x, fp_non_residue_inv()); +} + +fn fp2_mul(x: Fp2, y: Fp2) -> Fp2 { + let a = fp_mul(x.c0, y.c0); + let b = fp_mul(x.c1, y.c1); + let ab = fp_mul(fp_add(x.c0, x.c1), fp_add(y.c0, y.c1)); + var z: Fp2; + z.c1 = fp_sub(fp_sub(ab, a), b); + z.c0 = fp_sub(a, fp_mul_by_5(b)); + return z; +} + +fn fp2_square(x: Fp2) -> Fp2 { + let a = fp_mul(fp_add(x.c0, x.c1), fp_sub(x.c0, fp_mul_by_5(x.c1))); + let b = fp_double(fp_mul(x.c0, x.c1)); + var z: Fp2; + z.c1 = b; + z.c0 = fp_add(a, fp_double(b)); + return z; +} + +fn fp2_inverse(x: Fp2) -> Fp2 { + let t0 = fp_square(x.c0); + let t1 = fp_square(x.c1); + let inv = fp_inverse(fp_add(t0, fp_mul_by_5(t1))); + var z: Fp2; + z.c0 = fp_mul(x.c0, inv); + z.c1 = fp_neg(fp_mul(x.c1, inv)); + return z; +} + +fn fp2_mul_by_b_twist(x: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = x.c1; + z.c1 = fp_mul_by_non_residue_inv(x.c0); + return z; +} + +fn g2_jac_infinity() -> G2Point { + var p: G2Point; + p.x = fp2_one(); + p.y = fp2_one(); + p.z = fp2_zero(); + return p; +} + +fn g2_affine_is_infinity(a: G2Point) -> bool { + return fp2_is_zero(a.z); +} + +fn g2_jac_is_infinity(p: G2Point) -> bool { + return fp2_is_zero(p.z); +} + +fn g2_affine_to_jac(a: G2Point) -> G2Point { + if (g2_affine_is_infinity(a)) { + return g2_jac_infinity(); + } + var p: G2Point; + p.x = a.x; + p.y = a.y; + p.z = fp2_one(); + return p; +} + +fn g2_jac_to_affine(p: G2Point) -> G2Point { + if (g2_jac_is_infinity(p)) { + var inf: G2Point; + inf.x = fp2_zero(); + inf.y = fp2_zero(); + inf.z = fp2_zero(); + return inf; + } + let a = fp2_inverse(p.z); + let b = fp2_square(a); + var out: G2Point; + out.x = fp2_mul(p.x, b); + out.y = fp2_mul(fp2_mul(p.y, b), a); + out.z = fp2_one(); + return out; +} + +fn g2_neg_affine(q: G2Point) -> G2Point { + if (g2_affine_is_infinity(q)) { + return q; + } + var p = q; + p.y = fp2_neg(q.y); + return p; +} + +fn g2_neg_jac(q: G2Point) -> G2Point { + var p = q; + p.y = fp2_neg(q.y); + return p; +} + +fn g2_double_mixed(a: G2Point) -> G2Point { + if (g2_affine_is_infinity(a)) { + return g2_jac_infinity(); + } + var xx = fp2_square(a.x); + let yy = fp2_square(a.y); + var yyyy = fp2_square(yy); + var s = fp2_add(a.x, yy); + s = fp2_square(s); + s = fp2_sub(s, xx); + s = fp2_sub(s, yyyy); + s = fp2_double(s); + var m = fp2_double(xx); + m = fp2_add(m, xx); + let t = fp2_sub(fp2_sub(fp2_square(m), s), s); + + var p: G2Point; + p.x = t; + p.y = fp2_mul(fp2_sub(s, t), m); + yyyy = fp2_double(fp2_double(fp2_double(yyyy))); + p.y = fp2_sub(p.y, yyyy); + p.z = fp2_double(a.y); + return p; +} + +fn g2_double_jac(q: G2Point) -> G2Point { + var a = fp2_square(q.x); + let b = fp2_square(q.y); + let c = fp2_square(b); + var d = fp2_add(q.x, b); + d = fp2_square(d); + d = fp2_sub(d, a); + d = fp2_sub(d, c); + d = fp2_double(d); + var e = fp2_double(a); + e = fp2_add(e, a); + let f = fp2_square(e); + let t = fp2_double(d); + + var p: G2Point; + p.z = fp2_double(fp2_mul(q.y, q.z)); + p.x = fp2_sub(f, t); + p.y = fp2_mul(fp2_sub(d, p.x), e); + let c8 = fp2_double(fp2_double(fp2_double(c))); + p.y = fp2_sub(p.y, c8); + return p; +} + +fn g2_add_mixed(p: G2Point, a: G2Point) -> G2Point { + if (g2_affine_is_infinity(a)) { + return p; + } + if (g2_jac_is_infinity(p)) { + return g2_affine_to_jac(a); + } + + let z1z1 = fp2_square(p.z); + let u2 = fp2_mul(a.x, z1z1); + let s2 = fp2_mul(fp2_mul(a.y, p.z), z1z1); + + if (fp2_equal(u2, p.x) && fp2_equal(s2, p.y)) { + return g2_double_mixed(a); + } + + let h = fp2_sub(u2, p.x); + let hh = fp2_square(h); + let i = fp2_double(fp2_double(hh)); + let j = fp2_mul(h, i); + let r = fp2_double(fp2_sub(s2, p.y)); + let v = fp2_mul(p.x, i); + + var out: G2Point; + out.x = fp2_sub(fp2_sub(fp2_sub(fp2_square(r), j), v), v); + out.y = fp2_sub(fp2_mul(fp2_sub(v, out.x), r), fp2_double(fp2_mul(j, p.y))); + out.z = fp2_square(fp2_add(p.z, h)); + out.z = fp2_sub(out.z, z1z1); + out.z = fp2_sub(out.z, hh); + return out; +} + +fn g2_add_jac(p: G2Point, q: G2Point) -> G2Point { + if (g2_jac_is_infinity(p)) { + return q; + } + if (g2_jac_is_infinity(q)) { + return p; + } + let z1z1 = fp2_square(p.z); + let z2z2 = fp2_square(q.z); + let u1 = fp2_mul(p.x, z2z2); + let u2 = fp2_mul(q.x, z1z1); + let s1 = fp2_mul(fp2_mul(p.y, q.z), z2z2); + let s2 = fp2_mul(fp2_mul(q.y, p.z), z1z1); + let h = fp2_sub(u2, u1); + let r = fp2_double(fp2_sub(s2, s1)); + if (fp2_is_zero(h)) { + if (fp2_is_zero(r)) { + return g2_double_jac(p); + } + return g2_jac_infinity(); + } + let i = fp2_double(fp2_double(fp2_square(h))); + let j = fp2_mul(h, i); + let v = fp2_mul(u1, i); + var out: G2Point; + out.x = fp2_sub(fp2_sub(fp2_sub(fp2_square(r), j), v), v); + out.y = fp2_sub(fp2_mul(fp2_sub(v, out.x), r), fp2_double(fp2_mul(s1, j))); + let z_sum = fp2_add(p.z, q.z); + out.z = fp2_mul(fp2_sub(fp2_sub(fp2_square(z_sum), z1z1), z2z2), h); + return out; +} + +fn g2_scalar_mul_jac_small(base: G2Point, scalar: u32) -> G2Point { + if (scalar == 0u || g2_jac_is_infinity(base)) { + return g2_jac_infinity(); + } + var acc = g2_jac_infinity(); + var b = base; + var k = scalar; + loop { + if (k == 0u) { + break; + } + if ((k & 1u) != 0u) { + acc = g2_add_jac(acc, b); + } + b = g2_double_jac(b); + k = k >> 1u; + } + return acc; +} + +fn g2_dispatch(opcode: u32, a: G2Point, b: G2Point) -> G2Point { + if (opcode == G2_OP_COPY) { + return a; + } + if (opcode == G2_OP_JAC_INFINITY) { + return g2_jac_infinity(); + } + if (opcode == G2_OP_AFFINE_TO_JAC) { + return g2_affine_to_jac(a); + } + if (opcode == G2_OP_NEG_JAC) { + return g2_neg_jac(a); + } + if (opcode == G2_OP_DOUBLE_JAC) { + return g2_double_jac(a); + } + if (opcode == G2_OP_ADD_MIXED) { + return g2_add_mixed(a, b); + } + if (opcode == G2_OP_JAC_TO_AFFINE) { + return g2_jac_to_affine(a); + } + if (opcode == G2_OP_AFFINE_ADD) { + return g2_jac_to_affine(g2_add_mixed(g2_affine_to_jac(a), b)); + } + return g2_jac_infinity(); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_377/g2_io.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_377/g2_io.wgsl new file mode 100644 index 0000000000..bc977d00b0 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_377/g2_io.wgsl @@ -0,0 +1,47 @@ +fn fp_load_from(buffer_kind: u32, base: u32) -> Fp { + var z: Fp; + if (buffer_kind == 0u) { + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_a[base + i]; + } + return z; + } + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_b[base + i]; + } + return z; +} + +fn fp2_load_from(buffer_kind: u32, base: u32) -> Fp2 { + var z: Fp2; + z.c0 = fp_load_from(buffer_kind, base); + z.c1 = fp_load_from(buffer_kind, base + 12u); + return z; +} + +fn g2_load_from(buffer_kind: u32, index: u32) -> G2Point { + let base = index * 72u; + var p: G2Point; + p.x = fp2_load_from(buffer_kind, base + 0u); + p.y = fp2_load_from(buffer_kind, base + 24u); + p.z = fp2_load_from(buffer_kind, base + 48u); + return p; +} + +fn fp_store(base: u32, value: Fp) { + for (var i = 0u; i < 12u; i = i + 1u) { + output[base + i] = value.limbs[i]; + } +} + +fn fp2_store(base: u32, value: Fp2) { + fp_store(base, value.c0); + fp_store(base + 12u, value.c1); +} + +fn g2_store(index: u32, value: G2Point) { + let base = index * 72u; + fp2_store(base + 0u, value.x); + fp2_store(base + 24u, value.y); + fp2_store(base + 48u, value.z); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_381/fp_arith.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_381/fp_arith.wgsl new file mode 100644 index 0000000000..cd39fe1d15 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_381/fp_arith.wgsl @@ -0,0 +1,444 @@ +// curvegpu:section fp-types begin +struct Fp { + limbs: array, +} + +struct Fp24 { + limbs: array, +} +// curvegpu:section fp-types end + +struct Params { + count: u32, + opcode: u32, + _pad0: u32, + _pad1: u32, +} + +const FP_OP_COPY: u32 = 0u; +const FP_OP_ZERO: u32 = 1u; +const FP_OP_ONE: u32 = 2u; +const FP_OP_ADD: u32 = 3u; +const FP_OP_SUB: u32 = 4u; +const FP_OP_NEG: u32 = 5u; +const FP_OP_DOUBLE: u32 = 6u; +const FP_OP_NORMALIZE: u32 = 7u; +const FP_OP_EQUAL: u32 = 8u; +const FP_OP_MUL: u32 = 9u; +const FP_OP_SQUARE: u32 = 10u; +const FP_OP_TO_MONT: u32 = 11u; +const FP_OP_FROM_MONT: u32 = 12u; + +// curvegpu:section fp-consts begin +const FP_LIMB16_MASK: u32 = 0xffffu; +const FP_QINV_NEG_16: u32 = 0xfffdu; + +const FP_MODULUS16: array = array( + 0xaaabu, 0xffffu, + 0xffffu, 0xb9feu, + 0xffffu, 0xb153u, + 0xfffeu, 0x1eabu, + 0xf624u, 0xf6b0u, + 0xd2a0u, 0x6730u, + 0x12bfu, 0xf385u, + 0x4b84u, 0x6477u, + 0xacd7u, 0x434bu, + 0xa7b6u, 0x4b1bu, + 0xe69au, 0x397fu, + 0x11eau, 0x1a01u, +); + +const FP_MODULUS_MINUS_TWO: array = array( + 0xffffaaa9u, + 0xb9feffffu, + 0xb153ffffu, + 0x1eabfffeu, + 0xf6b0f624u, + 0x6730d2a0u, + 0xf38512bfu, + 0x64774b84u, + 0x434bacd7u, + 0x4b1ba7b6u, + 0x397fe69au, + 0x1a0111eau, +); +// curvegpu:section fp-consts end + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +// curvegpu:section fp-core begin +fn fp_zero() -> Fp { + var z: Fp; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = 0u; + } + return z; +} + +fn fp_one() -> Fp { + var z: Fp; + z.limbs[0] = 0x0002fffdu; + z.limbs[1] = 0x76090000u; + z.limbs[2] = 0xc40c0002u; + z.limbs[3] = 0xebf4000bu; + z.limbs[4] = 0x53c758bau; + z.limbs[5] = 0x5f489857u; + z.limbs[6] = 0x70525745u; + z.limbs[7] = 0x77ce5853u; + z.limbs[8] = 0xa256ec6du; + z.limbs[9] = 0x5c071a97u; + z.limbs[10] = 0xfa80e493u; + z.limbs[11] = 0x15f65ec3u; + return z; +} + +fn fp_one_regular() -> Fp { + var z = fp_zero(); + z.limbs[0] = 1u; + return z; +} + +fn fp_rsquare_regular() -> Fp { + var z: Fp; + z.limbs[0] = 0x1c341746u; + z.limbs[1] = 0xf4df1f34u; + z.limbs[2] = 0x09d104f1u; + z.limbs[3] = 0x0a76e6a6u; + z.limbs[4] = 0x4c95b6d5u; + z.limbs[5] = 0x8de5476cu; + z.limbs[6] = 0x939d83c0u; + z.limbs[7] = 0x67eb88a9u; + z.limbs[8] = 0xb519952du; + z.limbs[9] = 0x9a793e85u; + z.limbs[10] = 0x92cae3aau; + z.limbs[11] = 0x11988fe5u; + return z; +} + +fn fp_modulus() -> Fp { + var z: Fp; + z.limbs[0] = 0xffffaaabu; + z.limbs[1] = 0xb9feffffu; + z.limbs[2] = 0xb153ffffu; + z.limbs[3] = 0x1eabfffeu; + z.limbs[4] = 0xf6b0f624u; + z.limbs[5] = 0x6730d2a0u; + z.limbs[6] = 0xf38512bfu; + z.limbs[7] = 0x64774b84u; + z.limbs[8] = 0x434bacd7u; + z.limbs[9] = 0x4b1ba7b6u; + z.limbs[10] = 0x397fe69au; + z.limbs[11] = 0x1a0111eau; + return z; +} + +fn fp_predicate(value: bool) -> Fp { + var z = fp_zero(); + if (value) { + z = fp_one(); + } + return z; +} + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fp_is_zero(x: Fp) -> bool { + var acc = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let limb = x.limbs[i]; + acc = acc | limb; + } + return acc == 0u; +} + +fn fp_equal(x: Fp, y: Fp) -> bool { + for (var i = 0u; i < 12u; i = i + 1u) { + let xLimb = x.limbs[i]; + let yLimb = y.limbs[i]; + if (xLimb != yLimb) { + return false; + } + } + return true; +} + +fn fp_gte(x: Fp, y: Fp) -> bool { + for (var i: i32 = 11; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let yLimb = y.limbs[idx]; + if (xLimb != yLimb) { + return xLimb > yLimb; + } + } + return true; +} + +fn fp_add_modulus(x: Fp) -> Fp { + let q = fp_modulus(); + var z: Fp; + var carry = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let lane = adc(x.limbs[i], q.limbs[i], carry); + z.limbs[i] = lane.x; + carry = lane.y; + } + return z; +} + +fn fp_sub_modulus(x: Fp) -> Fp { + let q = fp_modulus(); + var z: Fp; + var borrow = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let lane = sbb(x.limbs[i], q.limbs[i], borrow); + z.limbs[i] = lane.x; + borrow = lane.y; + } + return z; +} + +fn fp_add(x: Fp, y: Fp) -> Fp { + var z: Fp; + var carry = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let lane = adc(x.limbs[i], y.limbs[i], carry); + z.limbs[i] = lane.x; + carry = lane.y; + } + if ((carry != 0u) || fp_gte(z, fp_modulus())) { + return fp_sub_modulus(z); + } + return z; +} + +fn fp_sub(x: Fp, y: Fp) -> Fp { + var z: Fp; + var borrow = 0u; + for (var i = 0u; i < 12u; i = i + 1u) { + let lane = sbb(x.limbs[i], y.limbs[i], borrow); + z.limbs[i] = lane.x; + borrow = lane.y; + } + if (borrow != 0u) { + return fp_add_modulus(z); + } + return z; +} + +fn fp_neg(x: Fp) -> Fp { + if (fp_is_zero(x)) { + return fp_zero(); + } + return fp_sub(fp_modulus(), x); +} + +fn fp_double(x: Fp) -> Fp { + return fp_add(x, x); +} + +fn fp_normalize(x: Fp) -> Fp { + if (fp_gte(x, fp_modulus())) { + return fp_sub_modulus(x); + } + return x; +} + +fn fp_unpack16(x: Fp) -> Fp24 { + var z: Fp24; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FP_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fp_pack16(x: Fp24) -> Fp { + var z: Fp; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fp24_gte_modulus(x: Fp24) -> bool { + for (var i: i32 = 23; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FP_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fp24_sub_modulus(x: Fp24) -> Fp24 { + var z: Fp24; + var borrow = 0u; + for (var i = 0u; i < 24u; i = i + 1u) { + let lane = sbb(x.limbs[i], FP_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FP_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fp_mul(x: Fp, y: Fp) -> Fp { + let a = fp_unpack16(x); + let b = fp_unpack16(y); + var t: array; + + for (var i = 0u; i < 24u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 24u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FP_LIMB16_MASK; + carry = uv >> 16u; + } + t[24] = carry; + + let m = (t[0] * FP_QINV_NEG_16) & FP_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 24u; j = j + 1u) { + let qLimb = FP_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FP_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[24] + carry; + t[23] = uv & FP_LIMB16_MASK; + t[24] = uv >> 16u; + } + + var z24: Fp24; + for (var i = 0u; i < 24u; i = i + 1u) { + z24.limbs[i] = t[i]; + } + if ((t[24] != 0u) || fp24_gte_modulus(z24)) { + z24 = fp24_sub_modulus(z24); + } + return fp_pack16(z24); +} + +fn fp_square(x: Fp) -> Fp { + return fp_mul(x, x); +} + +fn fp_inverse(x: Fp) -> Fp { + if (fp_is_zero(x)) { + return fp_zero(); + } + var acc = fp_one(); + for (var word_index: i32 = 11; word_index >= 0; word_index = word_index - 1) { + let word = FP_MODULUS_MINUS_TWO[u32(word_index)]; + for (var bit_index: i32 = 31; bit_index >= 0; bit_index = bit_index - 1) { + acc = fp_square(acc); + if (((word >> u32(bit_index)) & 1u) != 0u) { + acc = fp_mul(acc, x); + } + } + } + return acc; +} +// curvegpu:section fp-core end + +fn fp_dispatch(opcode: u32, a: Fp, b: Fp) -> Fp { + if (opcode == FP_OP_COPY) { + return a; + } + if (opcode == FP_OP_ZERO) { + return fp_zero(); + } + if (opcode == FP_OP_ONE) { + return fp_one(); + } + if (opcode == FP_OP_ADD) { + return fp_add(a, b); + } + if (opcode == FP_OP_SUB) { + return fp_sub(a, b); + } + if (opcode == FP_OP_NEG) { + return fp_neg(a); + } + if (opcode == FP_OP_DOUBLE) { + return fp_double(a); + } + if (opcode == FP_OP_NORMALIZE) { + return fp_normalize(a); + } + if (opcode == FP_OP_EQUAL) { + return fp_predicate(fp_equal(a, b)); + } + if (opcode == FP_OP_MUL) { + return fp_mul(a, b); + } + if (opcode == FP_OP_SQUARE) { + return fp_square(a); + } + if (opcode == FP_OP_TO_MONT) { + return fp_mul(a, fp_rsquare_regular()); + } + if (opcode == FP_OP_FROM_MONT) { + return fp_mul(a, fp_one_regular()); + } + return fp_zero(); +} + +fn fp_load_a(index: u32) -> Fp { + let base = index * 12u; + var z: Fp; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_a[base + i]; + } + return z; +} + +fn fp_load_b(index: u32) -> Fp { + let base = index * 12u; + var z: Fp; + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_b[base + i]; + } + return z; +} + +fn fp_store(index: u32, value: Fp) { + let base = index * 12u; + for (var i = 0u; i < 12u; i = i + 1u) { + output[base + i] = value.limbs[i]; + } +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fp_ops_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + fp_store(i, fp_dispatch(params.opcode, fp_load_a(i), fp_load_b(i))); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_arith.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_arith.wgsl new file mode 100644 index 0000000000..3e66b46f34 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_arith.wgsl @@ -0,0 +1,492 @@ +// curvegpu:section fr_types begin +struct Fr { + limbs: array, +} + +struct Fr16 { + limbs: array, +} +// curvegpu:section fr_types end + +struct Params { + count: u32, + opcode: u32, + _pad0: u32, + _pad1: u32, +} + +const FR_OP_COPY: u32 = 0u; +const FR_OP_ZERO: u32 = 1u; +const FR_OP_ONE: u32 = 2u; +const FR_OP_ADD: u32 = 3u; +const FR_OP_SUB: u32 = 4u; +const FR_OP_NEG: u32 = 5u; +const FR_OP_DOUBLE: u32 = 6u; +const FR_OP_NORMALIZE: u32 = 7u; +const FR_OP_EQUAL: u32 = 8u; +const FR_OP_MUL: u32 = 9u; +const FR_OP_SQUARE: u32 = 10u; +const FR_OP_TO_MONT: u32 = 11u; +const FR_OP_FROM_MONT: u32 = 12u; +// curvegpu:section fr_constants begin +const FR_LIMB16_MASK: u32 = 0xffffu; +const FR_QINV_NEG_16: u32 = 0xffffu; + +const FR_MODULUS16: array = array( + 0x0001u, 0x0000u, + 0xffffu, 0xffffu, + 0x5bfeu, 0xfffeu, + 0xa402u, 0x53bdu, + 0xd805u, 0x09a1u, + 0xd808u, 0x3339u, + 0x7d48u, 0x299du, + 0xa753u, 0x73edu, +); +// curvegpu:section fr_constants end + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +// curvegpu:section fr_core begin +fn fr_zero() -> Fr { + var z: Fr; + z.limbs[0] = 0u; + z.limbs[1] = 0u; + z.limbs[2] = 0u; + z.limbs[3] = 0u; + z.limbs[4] = 0u; + z.limbs[5] = 0u; + z.limbs[6] = 0u; + z.limbs[7] = 0u; + return z; +} + +fn fr_one() -> Fr { + var z: Fr; + z.limbs[0] = 0xfffffffeu; + z.limbs[1] = 0x00000001u; + z.limbs[2] = 0x00034802u; + z.limbs[3] = 0x5884b7fau; + z.limbs[4] = 0xecbc4ff5u; + z.limbs[5] = 0x998c4fefu; + z.limbs[6] = 0xacc5056fu; + z.limbs[7] = 0x1824b159u; + return z; +} + +fn fr_one_regular() -> Fr { + var z = fr_zero(); + z.limbs[0] = 1u; + return z; +} + +fn fr_rsquare_regular() -> Fr { + var z: Fr; + z.limbs[0] = 0xf3f29c6du; + z.limbs[1] = 0xc999e990u; + z.limbs[2] = 0x87925c23u; + z.limbs[3] = 0x2b6cedcbu; + z.limbs[4] = 0x7254398fu; + z.limbs[5] = 0x05d31496u; + z.limbs[6] = 0x9f59ff11u; + z.limbs[7] = 0x0748d9d9u; + return z; +} + +fn fr_modulus() -> Fr { + var z: Fr; + z.limbs[0] = 0x00000001u; + z.limbs[1] = 0xffffffffu; + z.limbs[2] = 0xfffe5bfeu; + z.limbs[3] = 0x53bda402u; + z.limbs[4] = 0x09a1d805u; + z.limbs[5] = 0x3339d808u; + z.limbs[6] = 0x299d7d48u; + z.limbs[7] = 0x73eda753u; + return z; +} + +fn fr_predicate(value: bool) -> Fr { + var z = fr_zero(); + if (value) { + z = fr_one(); + } + return z; +} + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fr_is_zero(x: Fr) -> bool { + return (x.limbs[0] | x.limbs[1] | x.limbs[2] | x.limbs[3] | + x.limbs[4] | x.limbs[5] | x.limbs[6] | x.limbs[7]) == 0u; +} + +fn fr_equal(x: Fr, y: Fr) -> bool { + return (x.limbs[0] == y.limbs[0]) && + (x.limbs[1] == y.limbs[1]) && + (x.limbs[2] == y.limbs[2]) && + (x.limbs[3] == y.limbs[3]) && + (x.limbs[4] == y.limbs[4]) && + (x.limbs[5] == y.limbs[5]) && + (x.limbs[6] == y.limbs[6]) && + (x.limbs[7] == y.limbs[7]); +} + +fn fr_gte(x: Fr, y: Fr) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fr_add_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fr_sub_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fr_add(x: Fr, y: Fr) -> Fr { + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fr_gte(z, fr_modulus())) { + return fr_sub_modulus(z); + } + return z; +} + +fn fr_sub(x: Fr, y: Fr) -> Fr { + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fr_add_modulus(z); + } + return z; +} + +fn fr_neg(x: Fr) -> Fr { + if (fr_is_zero(x)) { + return fr_zero(); + } + return fr_sub(fr_modulus(), x); +} + +fn fr_double(x: Fr) -> Fr { + return fr_add(x, x); +} + +fn fr_normalize(x: Fr) -> Fr { + if (fr_gte(x, fr_modulus())) { + return fr_sub_modulus(x); + } + return x; +} + +fn fr_unpack16(x: Fr) -> Fr16 { + var z: Fr16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FR_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fr_pack16(x: Fr16) -> Fr { + var z: Fr; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fr16_gte_modulus(x: Fr16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FR_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fr16_sub_modulus(x: Fr16) -> Fr16 { + var z: Fr16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FR_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FR_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fr_mul(x: Fr, y: Fr) -> Fr { + let a = fr_unpack16(x); + let b = fr_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FR_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FR_QINV_NEG_16) & FR_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FR_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FR_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FR_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fr16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fr16_gte_modulus(z16)) { + z16 = fr16_sub_modulus(z16); + } + return fr_pack16(z16); +} +// curvegpu:section fr_core end + +fn fr_dispatch(opcode: u32, a: Fr, b: Fr) -> Fr { + if (opcode == FR_OP_COPY) { + return a; + } + if (opcode == FR_OP_ZERO) { + return fr_zero(); + } + if (opcode == FR_OP_ONE) { + return fr_one(); + } + if (opcode == FR_OP_ADD) { + return fr_add(a, b); + } + if (opcode == FR_OP_SUB) { + return fr_sub(a, b); + } + if (opcode == FR_OP_NEG) { + return fr_neg(a); + } + if (opcode == FR_OP_DOUBLE) { + return fr_double(a); + } + if (opcode == FR_OP_NORMALIZE) { + return fr_normalize(a); + } + if (opcode == FR_OP_EQUAL) { + return fr_predicate(fr_equal(a, b)); + } + if (opcode == FR_OP_MUL) { + return fr_mul(a, b); + } + if (opcode == FR_OP_SQUARE) { + return fr_mul(a, a); + } + if (opcode == FR_OP_TO_MONT) { + return fr_mul(a, fr_rsquare_regular()); + } + if (opcode == FR_OP_FROM_MONT) { + return fr_mul(a, fr_one_regular()); + } + return fr_zero(); +} + +fn fr_load_a(index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + z.limbs[0] = input_a[base + 0u]; + z.limbs[1] = input_a[base + 1u]; + z.limbs[2] = input_a[base + 2u]; + z.limbs[3] = input_a[base + 3u]; + z.limbs[4] = input_a[base + 4u]; + z.limbs[5] = input_a[base + 5u]; + z.limbs[6] = input_a[base + 6u]; + z.limbs[7] = input_a[base + 7u]; + return z; +} + +fn fr_load_b(index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + z.limbs[0] = input_b[base + 0u]; + z.limbs[1] = input_b[base + 1u]; + z.limbs[2] = input_b[base + 2u]; + z.limbs[3] = input_b[base + 3u]; + z.limbs[4] = input_b[base + 4u]; + z.limbs[5] = input_b[base + 5u]; + z.limbs[6] = input_b[base + 6u]; + z.limbs[7] = input_b[base + 7u]; + return z; +} + +fn fr_store(index: u32, value: Fr) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_ops_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + fr_store(i, fr_dispatch(params.opcode, fr_load_a(i), fr_load_b(i))); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_ntt.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_ntt.wgsl new file mode 100644 index 0000000000..46127703ac --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_ntt.wgsl @@ -0,0 +1,356 @@ +struct Fr { + limbs: array, +} + +struct Fr16 { + limbs: array, +} + +struct Params { + count: u32, + m: u32, + _pad0: u32, + _pad1: u32, +} + +const FR_LIMB16_MASK: u32 = 0xffffu; +const FR_QINV_NEG_16: u32 = 0xffffu; + +const FR_MODULUS16: array = array( + 0x0001u, 0x0000u, + 0xffffu, 0xffffu, + 0x5bfeu, 0xfffeu, + 0xa402u, 0x53bdu, + 0xd805u, 0x09a1u, + 0xd808u, 0x3339u, + 0x7d48u, 0x299du, + 0xa753u, 0x73edu, +); + +@group(0) @binding(0) var input_values: array; +@group(0) @binding(1) var input_twiddles: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fr_modulus() -> Fr { + var z: Fr; + z.limbs[0] = 0x00000001u; + z.limbs[1] = 0xffffffffu; + z.limbs[2] = 0xfffe5bfeu; + z.limbs[3] = 0x53bda402u; + z.limbs[4] = 0x09a1d805u; + z.limbs[5] = 0x3339d808u; + z.limbs[6] = 0x299d7d48u; + z.limbs[7] = 0x73eda753u; + return z; +} + +fn fr_gte(x: Fr, y: Fr) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fr_add_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fr_sub_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fr_add(x: Fr, y: Fr) -> Fr { + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fr_gte(z, fr_modulus())) { + return fr_sub_modulus(z); + } + return z; +} + +fn fr_sub(x: Fr, y: Fr) -> Fr { + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fr_add_modulus(z); + } + return z; +} + +fn fr_unpack16(x: Fr) -> Fr16 { + var z: Fr16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FR_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fr_pack16(x: Fr16) -> Fr { + var z: Fr; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fr16_gte_modulus(x: Fr16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FR_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fr16_sub_modulus(x: Fr16) -> Fr16 { + var z: Fr16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FR_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FR_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fr_mul(x: Fr, y: Fr) -> Fr { + let a = fr_unpack16(x); + let b = fr_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FR_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FR_QINV_NEG_16) & FR_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FR_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FR_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FR_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fr16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fr16_gte_modulus(z16)) { + z16 = fr16_sub_modulus(z16); + } + return fr_pack16(z16); +} + +fn fr_load_from(buffer_kind: u32, index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + if (buffer_kind == 0u) { + z.limbs[0] = input_values[base + 0u]; + z.limbs[1] = input_values[base + 1u]; + z.limbs[2] = input_values[base + 2u]; + z.limbs[3] = input_values[base + 3u]; + z.limbs[4] = input_values[base + 4u]; + z.limbs[5] = input_values[base + 5u]; + z.limbs[6] = input_values[base + 6u]; + z.limbs[7] = input_values[base + 7u]; + return z; + } + z.limbs[0] = input_twiddles[base + 0u]; + z.limbs[1] = input_twiddles[base + 1u]; + z.limbs[2] = input_twiddles[base + 2u]; + z.limbs[3] = input_twiddles[base + 3u]; + z.limbs[4] = input_twiddles[base + 4u]; + z.limbs[5] = input_twiddles[base + 5u]; + z.limbs[6] = input_twiddles[base + 6u]; + z.limbs[7] = input_twiddles[base + 7u]; + return z; +} + +fn fr_store(index: u32, value: Fr) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_ntt_stage_main(@builtin(global_invocation_id) id: vec3) { + let pair = id.x; + let vector = id.y; + let half_count = params.count / 2u; + let batch_count = max(params._pad0, 1u); + if (pair >= half_count || vector >= batch_count) { + return; + } + + let m = params.m; + let j = pair % m; + let block = pair / m; + let vector_base = vector * params.count; + let left_index = vector_base + block * 2u * m + j; + let right_index = left_index + m; + + let left = fr_load_from(0u, left_index); + let twiddle = fr_load_from(1u, j); + let right = fr_mul(fr_load_from(0u, right_index), twiddle); + + fr_store(left_index, fr_add(left, right)); + fr_store(right_index, fr_sub(left, right)); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_plonk_quotient.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_plonk_quotient.wgsl new file mode 100644 index 0000000000..bd192ff047 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_plonk_quotient.wgsl @@ -0,0 +1,228 @@ +struct PlonkQuotientParams { + count: u32, + blind_count: u32, + coset_count: u32, + _pad1: u32, +} + +override COMMITMENT_COUNT: u32 = 0u; + +const PLONK_FR_WORDS: u32 = 8u; +const PLONK_BASE_DYNAMIC_VECTOR_COUNT: u32 = 5u; +const PLONK_BASE_STATIC_VECTOR_COUNT: u32 = 7u; +const PLONK_BLIND_COUNT: u32 = 4u; +const PLONK_SCALAR_COUNT: u32 = 7u; + +const PLONK_VEC_L: u32 = 0u; +const PLONK_VEC_R: u32 = 1u; +const PLONK_VEC_O: u32 = 2u; +const PLONK_VEC_Z: u32 = 3u; +const PLONK_VEC_QK: u32 = 4u; + +const PLONK_BLIND_L: u32 = 0u; +const PLONK_BLIND_R: u32 = 1u; +const PLONK_BLIND_O: u32 = 2u; +const PLONK_BLIND_Z: u32 = 3u; + +const PLONK_SCALAR_COSET: u32 = 0u; +const PLONK_SCALAR_LAGRANGE_SCALE: u32 = 1u; +const PLONK_SCALAR_CS: u32 = 2u; +const PLONK_SCALAR_CSS: u32 = 3u; +const PLONK_SCALAR_BETA: u32 = 4u; +const PLONK_SCALAR_GAMMA: u32 = 5u; +const PLONK_SCALAR_ALPHA: u32 = 6u; + +@group(0) @binding(0) var plonk_vectors: array; +@group(0) @binding(1) var plonk_blinds: array; +@group(0) @binding(2) var plonk_scalars: array; +@group(0) @binding(3) var plonk_output: array; +@group(0) @binding(4) var plonk_params: PlonkQuotientParams; + +fn plonk_static_base() -> u32 { + return PLONK_BASE_DYNAMIC_VECTOR_COUNT + COMMITMENT_COUNT; +} + +fn plonk_vec_ql() -> u32 { + return plonk_static_base(); +} + +fn plonk_vec_qr() -> u32 { + return plonk_static_base() + 1u; +} + +fn plonk_vec_qm() -> u32 { + return plonk_static_base() + 2u; +} + +fn plonk_vec_qo() -> u32 { + return plonk_static_base() + 3u; +} + +fn plonk_vec_s1() -> u32 { + return plonk_static_base() + 4u; +} + +fn plonk_vec_s2() -> u32 { + return plonk_static_base() + 5u; +} + +fn plonk_vec_s3() -> u32 { + return plonk_static_base() + 6u; +} + +fn plonk_vec_commitment_value(index: u32) -> u32 { + return PLONK_BASE_DYNAMIC_VECTOR_COUNT + index; +} + +fn plonk_vec_qcp(index: u32) -> u32 { + return plonk_static_base() + PLONK_BASE_STATIC_VECTOR_COUNT + index; +} + +fn plonk_vec_twiddles() -> u32 { + return plonk_static_base() + PLONK_BASE_STATIC_VECTOR_COUNT + COMMITMENT_COUNT; +} + +fn plonk_vec_denominators() -> u32 { + return plonk_vec_twiddles() + 1u; +} + +fn plonk_vector_count() -> u32 { + return plonk_vec_denominators() + 1u; +} + +fn fr_from_mont(x: Fr) -> Fr { + return fr_mul(x, fr_one_regular()); +} + +fn plonk_load_words(base: u32) -> Fr { + var z: Fr; + z.limbs[0] = plonk_vectors[base + 0u]; + z.limbs[1] = plonk_vectors[base + 1u]; + z.limbs[2] = plonk_vectors[base + 2u]; + z.limbs[3] = plonk_vectors[base + 3u]; + z.limbs[4] = plonk_vectors[base + 4u]; + z.limbs[5] = plonk_vectors[base + 5u]; + z.limbs[6] = plonk_vectors[base + 6u]; + z.limbs[7] = plonk_vectors[base + 7u]; + return z; +} + +fn plonk_load_vector_mont(coset: u32, vector: u32, index: u32) -> Fr { + let base = (((coset * plonk_vector_count() + vector) * plonk_params.count) + index) * PLONK_FR_WORDS; + return plonk_load_words(base); +} + +fn plonk_load_blind_mont(coset: u32, poly: u32, index: u32) -> Fr { + let base = (((coset * PLONK_BLIND_COUNT + poly) * plonk_params.blind_count) + index) * PLONK_FR_WORDS; + var z: Fr; + z.limbs[0] = plonk_blinds[base + 0u]; + z.limbs[1] = plonk_blinds[base + 1u]; + z.limbs[2] = plonk_blinds[base + 2u]; + z.limbs[3] = plonk_blinds[base + 3u]; + z.limbs[4] = plonk_blinds[base + 4u]; + z.limbs[5] = plonk_blinds[base + 5u]; + z.limbs[6] = plonk_blinds[base + 6u]; + z.limbs[7] = plonk_blinds[base + 7u]; + return z; +} + +fn plonk_load_scalar_mont(coset: u32, index: u32) -> Fr { + let base = ((coset * PLONK_SCALAR_COUNT) + index) * PLONK_FR_WORDS; + var z: Fr; + z.limbs[0] = plonk_scalars[base + 0u]; + z.limbs[1] = plonk_scalars[base + 1u]; + z.limbs[2] = plonk_scalars[base + 2u]; + z.limbs[3] = plonk_scalars[base + 3u]; + z.limbs[4] = plonk_scalars[base + 4u]; + z.limbs[5] = plonk_scalars[base + 5u]; + z.limbs[6] = plonk_scalars[base + 6u]; + z.limbs[7] = plonk_scalars[base + 7u]; + return z; +} + +fn plonk_store_regular(coset: u32, index: u32, value: Fr) { + let regular = fr_from_mont(value); + let base = ((coset * plonk_params.count) + index) * PLONK_FR_WORDS; + plonk_output[base + 0u] = regular.limbs[0]; + plonk_output[base + 1u] = regular.limbs[1]; + plonk_output[base + 2u] = regular.limbs[2]; + plonk_output[base + 3u] = regular.limbs[3]; + plonk_output[base + 4u] = regular.limbs[4]; + plonk_output[base + 5u] = regular.limbs[5]; + plonk_output[base + 6u] = regular.limbs[6]; + plonk_output[base + 7u] = regular.limbs[7]; +} + +fn plonk_eval_blind(coset: u32, poly: u32, point: Fr) -> Fr { + var res = fr_zero(); + var i = plonk_params.blind_count; + loop { + if (i == 0u) { + break; + } + i = i - 1u; + res = fr_add(fr_mul(res, point), plonk_load_blind_mont(coset, poly, i)); + } + return res; +} + +fn plonk_evaluate_quotient(coset: u32, index: u32) -> Fr { + let twiddle = plonk_load_vector_mont(coset, plonk_vec_twiddles(), index); + let next_index = (index + 1u) % plonk_params.count; + let next_twiddle = plonk_load_vector_mont(coset, plonk_vec_twiddles(), next_index); + + var l = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_L, index), plonk_eval_blind(coset, PLONK_BLIND_L, twiddle)); + var r = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_R, index), plonk_eval_blind(coset, PLONK_BLIND_R, twiddle)); + var o = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_O, index), plonk_eval_blind(coset, PLONK_BLIND_O, twiddle)); + var z = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_Z, index), plonk_eval_blind(coset, PLONK_BLIND_Z, twiddle)); + let zs = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_Z, next_index), plonk_eval_blind(coset, PLONK_BLIND_Z, next_twiddle)); + + var gate = fr_mul(plonk_load_vector_mont(coset, plonk_vec_ql(), index), l); + gate = fr_add(gate, fr_mul(plonk_load_vector_mont(coset, plonk_vec_qr(), index), r)); + gate = fr_add(gate, fr_mul(fr_mul(plonk_load_vector_mont(coset, plonk_vec_qm(), index), l), r)); + gate = fr_add(gate, fr_mul(plonk_load_vector_mont(coset, plonk_vec_qo(), index), o)); + gate = fr_add(gate, plonk_load_vector_mont(coset, PLONK_VEC_QK, index)); + var commitment_index = 0u; + loop { + if (commitment_index >= COMMITMENT_COUNT) { + break; + } + let qcp = plonk_load_vector_mont(coset, plonk_vec_qcp(commitment_index), index); + let commitment_value = plonk_load_vector_mont(coset, plonk_vec_commitment_value(commitment_index), index); + gate = fr_add(gate, fr_mul(qcp, commitment_value)); + commitment_index = commitment_index + 1u; + } + + let beta = plonk_load_scalar_mont(coset, PLONK_SCALAR_BETA); + let gamma = plonk_load_scalar_mont(coset, PLONK_SCALAR_GAMMA); + let alpha = plonk_load_scalar_mont(coset, PLONK_SCALAR_ALPHA); + let id = fr_mul(fr_mul(twiddle, plonk_load_scalar_mont(coset, PLONK_SCALAR_COSET)), beta); + + var a = fr_add(fr_add(gamma, l), id); + var b = fr_add(fr_add(fr_mul(id, plonk_load_scalar_mont(coset, PLONK_SCALAR_CS)), r), gamma); + var c = fr_add(fr_add(fr_mul(id, plonk_load_scalar_mont(coset, PLONK_SCALAR_CSS)), o), gamma); + let right = fr_mul(fr_mul(fr_mul(a, b), c), z); + + a = fr_add(fr_add(fr_mul(plonk_load_vector_mont(coset, plonk_vec_s1(), index), beta), l), gamma); + b = fr_add(fr_add(fr_mul(plonk_load_vector_mont(coset, plonk_vec_s2(), index), beta), r), gamma); + c = fr_add(fr_add(fr_mul(plonk_load_vector_mont(coset, plonk_vec_s3(), index), beta), o), gamma); + let left = fr_mul(fr_mul(fr_mul(a, b), c), zs); + + let ordering = fr_sub(left, right); + let lone = fr_mul(plonk_load_scalar_mont(coset, PLONK_SCALAR_LAGRANGE_SCALE), plonk_load_vector_mont(coset, plonk_vec_denominators(), index)); + var local = fr_mul(fr_sub(z, fr_one()), lone); + local = fr_add(fr_mul(local, alpha), ordering); + return fr_add(fr_mul(local, alpha), gate); +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_plonk_quotient_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + let coset = id.y; + if (i >= plonk_params.count || coset >= plonk_params.coset_count) { + return; + } + plonk_store_regular(coset, i, plonk_evaluate_quotient(coset, i)); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_vector.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_vector.wgsl new file mode 100644 index 0000000000..6ee5f21f42 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_381/fr_vector.wgsl @@ -0,0 +1,391 @@ +struct Fr { + limbs: array, +} + +struct Fr16 { + limbs: array, +} + +struct Params { + count: u32, + opcode: u32, + log_count: u32, + _pad0: u32, +} + +const FR_VECTOR_OP_COPY: u32 = 0u; +const FR_VECTOR_OP_ADD: u32 = 1u; +const FR_VECTOR_OP_SUB: u32 = 2u; +const FR_VECTOR_OP_MUL_FACTORS: u32 = 3u; +const FR_VECTOR_OP_BIT_REVERSE_COPY: u32 = 4u; +const FR_LIMB16_MASK: u32 = 0xffffu; +const FR_QINV_NEG_16: u32 = 0xffffu; + +const FR_MODULUS16: array = array( + 0x0001u, 0x0000u, + 0xffffu, 0xffffu, + 0x5bfeu, 0xfffeu, + 0xa402u, 0x53bdu, + 0xd805u, 0x09a1u, + 0xd808u, 0x3339u, + 0x7d48u, 0x299du, + 0xa753u, 0x73edu, +); + +@group(0) @binding(0) var input_values: array; +@group(0) @binding(1) var input_aux: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +fn fr_zero() -> Fr { + var z: Fr; + z.limbs[0] = 0u; + z.limbs[1] = 0u; + z.limbs[2] = 0u; + z.limbs[3] = 0u; + z.limbs[4] = 0u; + z.limbs[5] = 0u; + z.limbs[6] = 0u; + z.limbs[7] = 0u; + return z; +} + +fn fr_modulus() -> Fr { + var z: Fr; + z.limbs[0] = 0x00000001u; + z.limbs[1] = 0xffffffffu; + z.limbs[2] = 0xfffe5bfeu; + z.limbs[3] = 0x53bda402u; + z.limbs[4] = 0x09a1d805u; + z.limbs[5] = 0x3339d808u; + z.limbs[6] = 0x299d7d48u; + z.limbs[7] = 0x73eda753u; + return z; +} + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fr_gte(x: Fr, y: Fr) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fr_add_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fr_sub_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fr_add(x: Fr, y: Fr) -> Fr { + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fr_gte(z, fr_modulus())) { + return fr_sub_modulus(z); + } + return z; +} + +fn fr_sub(x: Fr, y: Fr) -> Fr { + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fr_add_modulus(z); + } + return z; +} + +fn fr_unpack16(x: Fr) -> Fr16 { + var z: Fr16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FR_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fr_pack16(x: Fr16) -> Fr { + var z: Fr; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fr16_gte_modulus(x: Fr16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FR_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fr16_sub_modulus(x: Fr16) -> Fr16 { + var z: Fr16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FR_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FR_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fr_mul(x: Fr, y: Fr) -> Fr { + let a = fr_unpack16(x); + let b = fr_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FR_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FR_QINV_NEG_16) & FR_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FR_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FR_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FR_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fr16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fr16_gte_modulus(z16)) { + z16 = fr16_sub_modulus(z16); + } + return fr_pack16(z16); +} + +fn reverse_bits(index: u32, log_count: u32) -> u32 { + var out = 0u; + for (var bit = 0u; bit < log_count; bit = bit + 1u) { + out = (out << 1u) | ((index >> bit) & 1u); + } + return out; +} + +fn fr_load_from(buffer_kind: u32, index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + if (buffer_kind == 0u) { + z.limbs[0] = input_values[base + 0u]; + z.limbs[1] = input_values[base + 1u]; + z.limbs[2] = input_values[base + 2u]; + z.limbs[3] = input_values[base + 3u]; + z.limbs[4] = input_values[base + 4u]; + z.limbs[5] = input_values[base + 5u]; + z.limbs[6] = input_values[base + 6u]; + z.limbs[7] = input_values[base + 7u]; + return z; + } + z.limbs[0] = input_aux[base + 0u]; + z.limbs[1] = input_aux[base + 1u]; + z.limbs[2] = input_aux[base + 2u]; + z.limbs[3] = input_aux[base + 3u]; + z.limbs[4] = input_aux[base + 4u]; + z.limbs[5] = input_aux[base + 5u]; + z.limbs[6] = input_aux[base + 6u]; + z.limbs[7] = input_aux[base + 7u]; + return z; +} + +fn fr_store(index: u32, value: Fr) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +fn fr_dispatch(index: u32) -> Fr { + if (params.opcode == FR_VECTOR_OP_COPY) { + return fr_load_from(0u, index); + } + if (params.opcode == FR_VECTOR_OP_ADD) { + return fr_add(fr_load_from(0u, index), fr_load_from(1u, index)); + } + if (params.opcode == FR_VECTOR_OP_SUB) { + return fr_sub(fr_load_from(0u, index), fr_load_from(1u, index)); + } + if (params.opcode == FR_VECTOR_OP_MUL_FACTORS) { + return fr_mul(fr_load_from(0u, index), fr_load_from(1u, index)); + } + if (params.opcode == FR_VECTOR_OP_BIT_REVERSE_COPY) { + if (params._pad0 != 0u) { + let vector_size = params._pad0; + let vector_base = (index / vector_size) * vector_size; + let lane = index % vector_size; + return fr_load_from(0u, vector_base + reverse_bits(lane, params.log_count)); + } + return fr_load_from(0u, reverse_bits(index, params.log_count)); + } + return fr_zero(); +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_vector_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + fr_store(i, fr_dispatch(i)); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_381/g1_io.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_381/g1_io.wgsl new file mode 100644 index 0000000000..21cb7f5ac7 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_381/g1_io.wgsl @@ -0,0 +1,35 @@ +fn fp_load_from(buffer_kind: u32, base: u32) -> Fp { + var z: Fp; + if (buffer_kind == 0u) { + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_a[base + i]; + } + return z; + } + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_b[base + i]; + } + return z; +} + +fn g1_load_from(buffer_kind: u32, index: u32) -> G1Point { + let base = index * 36u; + var p: G1Point; + p.x = fp_load_from(buffer_kind, base + 0u); + p.y = fp_load_from(buffer_kind, base + 12u); + p.z = fp_load_from(buffer_kind, base + 24u); + return p; +} + +fn fp_store(base: u32, value: Fp) { + for (var i = 0u; i < 12u; i = i + 1u) { + output[base + i] = value.limbs[i]; + } +} + +fn g1_store(index: u32, value: G1Point) { + let base = index * 36u; + fp_store(base + 0u, value.x); + fp_store(base + 12u, value.y); + fp_store(base + 24u, value.z); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_381/g2_arith.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_381/g2_arith.wgsl new file mode 100644 index 0000000000..45ae3eed48 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_381/g2_arith.wgsl @@ -0,0 +1,325 @@ +struct Fp2 { + c0: Fp, + c1: Fp, +} + +struct G2Point { + x: Fp2, + y: Fp2, + z: Fp2, +} + +const G2_OP_COPY: u32 = 0u; +const G2_OP_JAC_INFINITY: u32 = 1u; +const G2_OP_AFFINE_TO_JAC: u32 = 2u; +const G2_OP_NEG_JAC: u32 = 3u; +const G2_OP_DOUBLE_JAC: u32 = 4u; +const G2_OP_ADD_MIXED: u32 = 5u; +const G2_OP_JAC_TO_AFFINE: u32 = 6u; +const G2_OP_AFFINE_ADD: u32 = 7u; + +fn fp2_zero() -> Fp2 { + var z: Fp2; + z.c0 = fp_zero(); + z.c1 = fp_zero(); + return z; +} + +fn fp2_one() -> Fp2 { + var z: Fp2; + z.c0 = fp_one(); + z.c1 = fp_zero(); + return z; +} + +fn fp2_is_zero(x: Fp2) -> bool { + return fp_is_zero(x.c0) && fp_is_zero(x.c1); +} + +fn fp2_equal(x: Fp2, y: Fp2) -> bool { + return fp_equal(x.c0, y.c0) && fp_equal(x.c1, y.c1); +} + +fn fp2_add(x: Fp2, y: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_add(x.c0, y.c0); + z.c1 = fp_add(x.c1, y.c1); + return z; +} + +fn fp2_sub(x: Fp2, y: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_sub(x.c0, y.c0); + z.c1 = fp_sub(x.c1, y.c1); + return z; +} + +fn fp2_neg(x: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_neg(x.c0); + z.c1 = fp_neg(x.c1); + return z; +} + +fn fp2_double(x: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_double(x.c0); + z.c1 = fp_double(x.c1); + return z; +} + +fn fp2_mul(x: Fp2, y: Fp2) -> Fp2 { + let a = fp_mul(x.c0, y.c0); + let b = fp_mul(x.c1, y.c1); + let ab = fp_mul(fp_add(x.c0, x.c1), fp_add(y.c0, y.c1)); + var z: Fp2; + z.c1 = fp_sub(fp_sub(ab, a), b); + z.c0 = fp_sub(a, b); + return z; +} + +fn fp2_square(x: Fp2) -> Fp2 { + let a = fp_mul(fp_add(x.c0, x.c1), fp_sub(x.c0, x.c1)); + let b = fp_double(fp_mul(x.c0, x.c1)); + var z: Fp2; + z.c0 = a; + z.c1 = b; + return z; +} + +fn fp2_inverse(x: Fp2) -> Fp2 { + let t0 = fp_square(x.c0); + let t1 = fp_square(x.c1); + let inv = fp_inverse(fp_add(t0, t1)); + var z: Fp2; + z.c0 = fp_mul(x.c0, inv); + z.c1 = fp_neg(fp_mul(x.c1, inv)); + return z; +} + +fn fp2_mul_by_b_twist(x: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_sub(x.c0, x.c1); + z.c1 = fp_add(x.c0, x.c1); + z = fp2_double(fp2_double(z)); + return z; +} + +fn g2_jac_infinity() -> G2Point { + var p: G2Point; + p.x = fp2_one(); + p.y = fp2_one(); + p.z = fp2_zero(); + return p; +} + +fn g2_affine_is_infinity(a: G2Point) -> bool { + return fp2_is_zero(a.z); +} + +fn g2_jac_is_infinity(p: G2Point) -> bool { + return fp2_is_zero(p.z); +} + +fn g2_affine_to_jac(a: G2Point) -> G2Point { + if (g2_affine_is_infinity(a)) { + return g2_jac_infinity(); + } + var p: G2Point; + p.x = a.x; + p.y = a.y; + p.z = fp2_one(); + return p; +} + +fn g2_jac_to_affine(p: G2Point) -> G2Point { + if (g2_jac_is_infinity(p)) { + var inf: G2Point; + inf.x = fp2_zero(); + inf.y = fp2_zero(); + inf.z = fp2_zero(); + return inf; + } + let a = fp2_inverse(p.z); + let b = fp2_square(a); + var out: G2Point; + out.x = fp2_mul(p.x, b); + out.y = fp2_mul(fp2_mul(p.y, b), a); + out.z = fp2_one(); + return out; +} + +fn g2_neg_affine(q: G2Point) -> G2Point { + if (g2_affine_is_infinity(q)) { + return q; + } + var p = q; + p.y = fp2_neg(q.y); + return p; +} + +fn g2_neg_jac(q: G2Point) -> G2Point { + var p = q; + p.y = fp2_neg(q.y); + return p; +} + +fn g2_double_mixed(a: G2Point) -> G2Point { + if (g2_affine_is_infinity(a)) { + return g2_jac_infinity(); + } + var xx = fp2_square(a.x); + let yy = fp2_square(a.y); + var yyyy = fp2_square(yy); + var s = fp2_add(a.x, yy); + s = fp2_square(s); + s = fp2_sub(s, xx); + s = fp2_sub(s, yyyy); + s = fp2_double(s); + var m = fp2_double(xx); + m = fp2_add(m, xx); + let t = fp2_sub(fp2_sub(fp2_square(m), s), s); + + var p: G2Point; + p.x = t; + p.y = fp2_mul(fp2_sub(s, t), m); + yyyy = fp2_double(fp2_double(fp2_double(yyyy))); + p.y = fp2_sub(p.y, yyyy); + p.z = fp2_double(a.y); + return p; +} + +fn g2_double_jac(q: G2Point) -> G2Point { + var a = fp2_square(q.x); + let b = fp2_square(q.y); + let c = fp2_square(b); + var d = fp2_add(q.x, b); + d = fp2_square(d); + d = fp2_sub(d, a); + d = fp2_sub(d, c); + d = fp2_double(d); + var e = fp2_double(a); + e = fp2_add(e, a); + let f = fp2_square(e); + let t = fp2_double(d); + + var p: G2Point; + p.z = fp2_double(fp2_mul(q.y, q.z)); + p.x = fp2_sub(f, t); + p.y = fp2_mul(fp2_sub(d, p.x), e); + let c8 = fp2_double(fp2_double(fp2_double(c))); + p.y = fp2_sub(p.y, c8); + return p; +} + +fn g2_add_mixed(p: G2Point, a: G2Point) -> G2Point { + if (g2_affine_is_infinity(a)) { + return p; + } + if (g2_jac_is_infinity(p)) { + return g2_affine_to_jac(a); + } + + let z1z1 = fp2_square(p.z); + let u2 = fp2_mul(a.x, z1z1); + let s2 = fp2_mul(fp2_mul(a.y, p.z), z1z1); + + if (fp2_equal(u2, p.x) && fp2_equal(s2, p.y)) { + return g2_double_mixed(a); + } + + let h = fp2_sub(u2, p.x); + let hh = fp2_square(h); + let i = fp2_double(fp2_double(hh)); + let j = fp2_mul(h, i); + let r = fp2_double(fp2_sub(s2, p.y)); + let v = fp2_mul(p.x, i); + + var out: G2Point; + out.x = fp2_sub(fp2_sub(fp2_sub(fp2_square(r), j), v), v); + out.y = fp2_sub(fp2_mul(fp2_sub(v, out.x), r), fp2_double(fp2_mul(j, p.y))); + out.z = fp2_square(fp2_add(p.z, h)); + out.z = fp2_sub(out.z, z1z1); + out.z = fp2_sub(out.z, hh); + return out; +} + +fn g2_add_jac(p: G2Point, q: G2Point) -> G2Point { + if (g2_jac_is_infinity(p)) { + return q; + } + if (g2_jac_is_infinity(q)) { + return p; + } + let z1z1 = fp2_square(p.z); + let z2z2 = fp2_square(q.z); + let u1 = fp2_mul(p.x, z2z2); + let u2 = fp2_mul(q.x, z1z1); + let s1 = fp2_mul(fp2_mul(p.y, q.z), z2z2); + let s2 = fp2_mul(fp2_mul(q.y, p.z), z1z1); + let h = fp2_sub(u2, u1); + let r = fp2_double(fp2_sub(s2, s1)); + if (fp2_is_zero(h)) { + if (fp2_is_zero(r)) { + return g2_double_jac(p); + } + return g2_jac_infinity(); + } + let i = fp2_double(fp2_double(fp2_square(h))); + let j = fp2_mul(h, i); + let v = fp2_mul(u1, i); + var out: G2Point; + out.x = fp2_sub(fp2_sub(fp2_sub(fp2_square(r), j), v), v); + out.y = fp2_sub(fp2_mul(fp2_sub(v, out.x), r), fp2_double(fp2_mul(s1, j))); + let z_sum = fp2_add(p.z, q.z); + out.z = fp2_mul(fp2_sub(fp2_sub(fp2_square(z_sum), z1z1), z2z2), h); + return out; +} + +fn g2_scalar_mul_jac_small(base: G2Point, scalar: u32) -> G2Point { + if (scalar == 0u || g2_jac_is_infinity(base)) { + return g2_jac_infinity(); + } + var acc = g2_jac_infinity(); + var b = base; + var k = scalar; + loop { + if (k == 0u) { + break; + } + if ((k & 1u) != 0u) { + acc = g2_add_jac(acc, b); + } + b = g2_double_jac(b); + k = k >> 1u; + } + return acc; +} + +fn g2_dispatch(opcode: u32, a: G2Point, b: G2Point) -> G2Point { + if (opcode == G2_OP_COPY) { + return a; + } + if (opcode == G2_OP_JAC_INFINITY) { + return g2_jac_infinity(); + } + if (opcode == G2_OP_AFFINE_TO_JAC) { + return g2_affine_to_jac(a); + } + if (opcode == G2_OP_NEG_JAC) { + return g2_neg_jac(a); + } + if (opcode == G2_OP_DOUBLE_JAC) { + return g2_double_jac(a); + } + if (opcode == G2_OP_ADD_MIXED) { + return g2_add_mixed(a, b); + } + if (opcode == G2_OP_JAC_TO_AFFINE) { + return g2_jac_to_affine(a); + } + if (opcode == G2_OP_AFFINE_ADD) { + return g2_jac_to_affine(g2_add_mixed(g2_affine_to_jac(a), b)); + } + return g2_jac_infinity(); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bls12_381/g2_io.wgsl b/backend/accelerated/webgpu/shaders/curves/bls12_381/g2_io.wgsl new file mode 100644 index 0000000000..bc977d00b0 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bls12_381/g2_io.wgsl @@ -0,0 +1,47 @@ +fn fp_load_from(buffer_kind: u32, base: u32) -> Fp { + var z: Fp; + if (buffer_kind == 0u) { + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_a[base + i]; + } + return z; + } + for (var i = 0u; i < 12u; i = i + 1u) { + z.limbs[i] = input_b[base + i]; + } + return z; +} + +fn fp2_load_from(buffer_kind: u32, base: u32) -> Fp2 { + var z: Fp2; + z.c0 = fp_load_from(buffer_kind, base); + z.c1 = fp_load_from(buffer_kind, base + 12u); + return z; +} + +fn g2_load_from(buffer_kind: u32, index: u32) -> G2Point { + let base = index * 72u; + var p: G2Point; + p.x = fp2_load_from(buffer_kind, base + 0u); + p.y = fp2_load_from(buffer_kind, base + 24u); + p.z = fp2_load_from(buffer_kind, base + 48u); + return p; +} + +fn fp_store(base: u32, value: Fp) { + for (var i = 0u; i < 12u; i = i + 1u) { + output[base + i] = value.limbs[i]; + } +} + +fn fp2_store(base: u32, value: Fp2) { + fp_store(base, value.c0); + fp_store(base + 12u, value.c1); +} + +fn g2_store(index: u32, value: G2Point) { + let base = index * 72u; + fp2_store(base + 0u, value.x); + fp2_store(base + 24u, value.y); + fp2_store(base + 48u, value.z); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bn254/fp_arith.wgsl b/backend/accelerated/webgpu/shaders/curves/bn254/fp_arith.wgsl new file mode 100644 index 0000000000..11846249ce --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bn254/fp_arith.wgsl @@ -0,0 +1,525 @@ +// curvegpu:section fp-types begin +struct Fp { + limbs: array, +} + +struct Fp16 { + limbs: array, +} +// curvegpu:section fp-types end + +struct Params { + count: u32, + opcode: u32, + _pad0: u32, + _pad1: u32, +} + +const FP_OP_COPY: u32 = 0u; +const FP_OP_ZERO: u32 = 1u; +const FP_OP_ONE: u32 = 2u; +const FP_OP_ADD: u32 = 3u; +const FP_OP_SUB: u32 = 4u; +const FP_OP_NEG: u32 = 5u; +const FP_OP_DOUBLE: u32 = 6u; +const FP_OP_NORMALIZE: u32 = 7u; +const FP_OP_EQUAL: u32 = 8u; +const FP_OP_MUL: u32 = 9u; +const FP_OP_SQUARE: u32 = 10u; +const FP_OP_TO_MONT: u32 = 11u; +const FP_OP_FROM_MONT: u32 = 12u; + +// curvegpu:section fp-consts begin +const FP_LIMB16_MASK: u32 = 0xffffu; +const FP_QINV_NEG_16: u32 = 0x6389u; + +const FP_MODULUS16: array = array( + 0xfd47u, 0xd87cu, + 0x8c16u, 0x3c20u, + 0xca8du, 0x6871u, + 0x6a91u, 0x9781u, + 0x585du, 0x8181u, + 0x45b6u, 0xb850u, + 0xa029u, 0xe131u, + 0x4e72u, 0x3064u, +); + +const FP_MODULUS_MINUS_TWO: array = array( + 0xd87cfd45u, + 0x3c208c16u, + 0x6871ca8du, + 0x97816a91u, + 0x8181585du, + 0xb85045b6u, + 0xe131a029u, + 0x30644e72u, +); +// curvegpu:section fp-consts end + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +// curvegpu:section fp-core begin +fn fp_zero() -> Fp { + var z: Fp; + z.limbs[0] = 0u; + z.limbs[1] = 0u; + z.limbs[2] = 0u; + z.limbs[3] = 0u; + z.limbs[4] = 0u; + z.limbs[5] = 0u; + z.limbs[6] = 0u; + z.limbs[7] = 0u; + return z; +} + +fn fp_one() -> Fp { + var z: Fp; + z.limbs[0] = 0xc58f0d9du; + z.limbs[1] = 0xd35d438du; + z.limbs[2] = 0xf5c70b3du; + z.limbs[3] = 0x0a78eb28u; + z.limbs[4] = 0x7879462cu; + z.limbs[5] = 0x666ea36fu; + z.limbs[6] = 0x9a07df2fu; + z.limbs[7] = 0x0e0a77c1u; + return z; +} + +fn fp_one_regular() -> Fp { + var z = fp_zero(); + z.limbs[0] = 1u; + return z; +} + +fn fp_rsquare_regular() -> Fp { + var z: Fp; + z.limbs[0] = 0x538afa89u; + z.limbs[1] = 0xf32cfc5bu; + z.limbs[2] = 0xd44501fbu; + z.limbs[3] = 0xb5e71911u; + z.limbs[4] = 0x0a417ff6u; + z.limbs[5] = 0x47ab1effu; + z.limbs[6] = 0xcab8351fu; + z.limbs[7] = 0x06d89f71u; + return z; +} + +fn fp_modulus() -> Fp { + var z: Fp; + z.limbs[0] = 0xd87cfd47u; + z.limbs[1] = 0x3c208c16u; + z.limbs[2] = 0x6871ca8du; + z.limbs[3] = 0x97816a91u; + z.limbs[4] = 0x8181585du; + z.limbs[5] = 0xb85045b6u; + z.limbs[6] = 0xe131a029u; + z.limbs[7] = 0x30644e72u; + return z; +} + +fn fp_predicate(value: bool) -> Fp { + var z = fp_zero(); + if (value) { + z = fp_one(); + } + return z; +} + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fp_is_zero(x: Fp) -> bool { + return (x.limbs[0] | x.limbs[1] | x.limbs[2] | x.limbs[3] | + x.limbs[4] | x.limbs[5] | x.limbs[6] | x.limbs[7]) == 0u; +} + +fn fp_equal(x: Fp, y: Fp) -> bool { + return (x.limbs[0] == y.limbs[0]) && + (x.limbs[1] == y.limbs[1]) && + (x.limbs[2] == y.limbs[2]) && + (x.limbs[3] == y.limbs[3]) && + (x.limbs[4] == y.limbs[4]) && + (x.limbs[5] == y.limbs[5]) && + (x.limbs[6] == y.limbs[6]) && + (x.limbs[7] == y.limbs[7]); +} + +fn fp_gte(x: Fp, y: Fp) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fp_add_modulus(x: Fp) -> Fp { + let q = fp_modulus(); + var z: Fp; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fp_sub_modulus(x: Fp) -> Fp { + let q = fp_modulus(); + var z: Fp; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fp_add(x: Fp, y: Fp) -> Fp { + var z: Fp; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fp_gte(z, fp_modulus())) { + return fp_sub_modulus(z); + } + return z; +} + +fn fp_sub(x: Fp, y: Fp) -> Fp { + var z: Fp; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fp_add_modulus(z); + } + return z; +} + +fn fp_neg(x: Fp) -> Fp { + if (fp_is_zero(x)) { + return fp_zero(); + } + return fp_sub(fp_modulus(), x); +} + +fn fp_double(x: Fp) -> Fp { + return fp_add(x, x); +} + +fn fp_normalize(x: Fp) -> Fp { + if (fp_gte(x, fp_modulus())) { + return fp_sub_modulus(x); + } + return x; +} + +fn fp_unpack16(x: Fp) -> Fp16 { + var z: Fp16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FP_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fp_pack16(x: Fp16) -> Fp { + var z: Fp; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fp16_gte_modulus(x: Fp16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FP_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fp16_sub_modulus(x: Fp16) -> Fp16 { + var z: Fp16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FP_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FP_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fp_mul(x: Fp, y: Fp) -> Fp { + let a = fp_unpack16(x); + let b = fp_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FP_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FP_QINV_NEG_16) & FP_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FP_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FP_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FP_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fp16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fp16_gte_modulus(z16)) { + z16 = fp16_sub_modulus(z16); + } + return fp_pack16(z16); +} + +fn fp_square(x: Fp) -> Fp { + return fp_mul(x, x); +} + +fn fp_inverse(x: Fp) -> Fp { + if (fp_is_zero(x)) { + return fp_zero(); + } + var acc = fp_one(); + for (var wordIndex: i32 = 7; wordIndex >= 0; wordIndex = wordIndex - 1) { + let word = FP_MODULUS_MINUS_TWO[u32(wordIndex)]; + for (var bitIndex: i32 = 31; bitIndex >= 0; bitIndex = bitIndex - 1) { + acc = fp_square(acc); + if (((word >> u32(bitIndex)) & 1u) != 0u) { + acc = fp_mul(acc, x); + } + } + } + return acc; +} +// curvegpu:section fp-core end + +fn fp_dispatch(opcode: u32, a: Fp, b: Fp) -> Fp { + if (opcode == FP_OP_COPY) { + return a; + } + if (opcode == FP_OP_ZERO) { + return fp_zero(); + } + if (opcode == FP_OP_ONE) { + return fp_one(); + } + if (opcode == FP_OP_ADD) { + return fp_add(a, b); + } + if (opcode == FP_OP_SUB) { + return fp_sub(a, b); + } + if (opcode == FP_OP_NEG) { + return fp_neg(a); + } + if (opcode == FP_OP_DOUBLE) { + return fp_double(a); + } + if (opcode == FP_OP_NORMALIZE) { + return fp_normalize(a); + } + if (opcode == FP_OP_EQUAL) { + return fp_predicate(fp_equal(a, b)); + } + if (opcode == FP_OP_MUL) { + return fp_mul(a, b); + } + if (opcode == FP_OP_SQUARE) { + return fp_square(a); + } + if (opcode == FP_OP_TO_MONT) { + return fp_mul(a, fp_rsquare_regular()); + } + if (opcode == FP_OP_FROM_MONT) { + return fp_mul(a, fp_one_regular()); + } + return fp_zero(); +} + +fn fp_load_a(index: u32) -> Fp { + let base = index * 8u; + var z: Fp; + z.limbs[0] = input_a[base + 0u]; + z.limbs[1] = input_a[base + 1u]; + z.limbs[2] = input_a[base + 2u]; + z.limbs[3] = input_a[base + 3u]; + z.limbs[4] = input_a[base + 4u]; + z.limbs[5] = input_a[base + 5u]; + z.limbs[6] = input_a[base + 6u]; + z.limbs[7] = input_a[base + 7u]; + return z; +} + +fn fp_load_b(index: u32) -> Fp { + let base = index * 8u; + var z: Fp; + z.limbs[0] = input_b[base + 0u]; + z.limbs[1] = input_b[base + 1u]; + z.limbs[2] = input_b[base + 2u]; + z.limbs[3] = input_b[base + 3u]; + z.limbs[4] = input_b[base + 4u]; + z.limbs[5] = input_b[base + 5u]; + z.limbs[6] = input_b[base + 6u]; + z.limbs[7] = input_b[base + 7u]; + return z; +} + +fn fp_store(index: u32, value: Fp) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fp_ops_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + fp_store(i, fp_dispatch(params.opcode, fp_load_a(i), fp_load_b(i))); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bn254/fr_arith.wgsl b/backend/accelerated/webgpu/shaders/curves/bn254/fr_arith.wgsl new file mode 100644 index 0000000000..1a054206db --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bn254/fr_arith.wgsl @@ -0,0 +1,492 @@ +// curvegpu:section fr_types begin +struct Fr { + limbs: array, +} + +struct Fr16 { + limbs: array, +} +// curvegpu:section fr_types end + +struct Params { + count: u32, + opcode: u32, + _pad0: u32, + _pad1: u32, +} + +const FR_OP_COPY: u32 = 0u; +const FR_OP_ZERO: u32 = 1u; +const FR_OP_ONE: u32 = 2u; +const FR_OP_ADD: u32 = 3u; +const FR_OP_SUB: u32 = 4u; +const FR_OP_NEG: u32 = 5u; +const FR_OP_DOUBLE: u32 = 6u; +const FR_OP_NORMALIZE: u32 = 7u; +const FR_OP_EQUAL: u32 = 8u; +const FR_OP_MUL: u32 = 9u; +const FR_OP_SQUARE: u32 = 10u; +const FR_OP_TO_MONT: u32 = 11u; +const FR_OP_FROM_MONT: u32 = 12u; +// curvegpu:section fr_constants begin +const FR_LIMB16_MASK: u32 = 0xffffu; +const FR_QINV_NEG_16: u32 = 0xffffu; + +const FR_MODULUS16: array = array( + 0x0001u, 0xf000u, + 0xf593u, 0x43e1u, + 0x7091u, 0x79b9u, + 0xe848u, 0x2833u, + 0x585du, 0x8181u, + 0x45b6u, 0xb850u, + 0xa029u, 0xe131u, + 0x4e72u, 0x3064u, +); +// curvegpu:section fr_constants end + +@group(0) @binding(0) var input_a: array; +@group(0) @binding(1) var input_b: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +// curvegpu:section fr_core begin +fn fr_zero() -> Fr { + var z: Fr; + z.limbs[0] = 0u; + z.limbs[1] = 0u; + z.limbs[2] = 0u; + z.limbs[3] = 0u; + z.limbs[4] = 0u; + z.limbs[5] = 0u; + z.limbs[6] = 0u; + z.limbs[7] = 0u; + return z; +} + +fn fr_one() -> Fr { + var z: Fr; + z.limbs[0] = 0x4ffffffbu; + z.limbs[1] = 0xac96341cu; + z.limbs[2] = 0x9f60cd29u; + z.limbs[3] = 0x36fc7695u; + z.limbs[4] = 0x7879462eu; + z.limbs[5] = 0x666ea36fu; + z.limbs[6] = 0x9a07df2fu; + z.limbs[7] = 0x0e0a77c1u; + return z; +} + +fn fr_one_regular() -> Fr { + var z = fr_zero(); + z.limbs[0] = 1u; + return z; +} + +fn fr_rsquare_regular() -> Fr { + var z: Fr; + z.limbs[0] = 0xae216da7u; + z.limbs[1] = 0x1bb8e645u; + z.limbs[2] = 0xe35c59e3u; + z.limbs[3] = 0x53fe3ab1u; + z.limbs[4] = 0x53bb8085u; + z.limbs[5] = 0x8c49833du; + z.limbs[6] = 0x7f4e44a5u; + z.limbs[7] = 0x0216d0b1u; + return z; +} + +fn fr_modulus() -> Fr { + var z: Fr; + z.limbs[0] = 0xf0000001u; + z.limbs[1] = 0x43e1f593u; + z.limbs[2] = 0x79b97091u; + z.limbs[3] = 0x2833e848u; + z.limbs[4] = 0x8181585du; + z.limbs[5] = 0xb85045b6u; + z.limbs[6] = 0xe131a029u; + z.limbs[7] = 0x30644e72u; + return z; +} + +fn fr_predicate(value: bool) -> Fr { + var z = fr_zero(); + if (value) { + z = fr_one(); + } + return z; +} + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fr_is_zero(x: Fr) -> bool { + return (x.limbs[0] | x.limbs[1] | x.limbs[2] | x.limbs[3] | + x.limbs[4] | x.limbs[5] | x.limbs[6] | x.limbs[7]) == 0u; +} + +fn fr_equal(x: Fr, y: Fr) -> bool { + return (x.limbs[0] == y.limbs[0]) && + (x.limbs[1] == y.limbs[1]) && + (x.limbs[2] == y.limbs[2]) && + (x.limbs[3] == y.limbs[3]) && + (x.limbs[4] == y.limbs[4]) && + (x.limbs[5] == y.limbs[5]) && + (x.limbs[6] == y.limbs[6]) && + (x.limbs[7] == y.limbs[7]); +} + +fn fr_gte(x: Fr, y: Fr) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fr_add_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fr_sub_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fr_add(x: Fr, y: Fr) -> Fr { + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fr_gte(z, fr_modulus())) { + return fr_sub_modulus(z); + } + return z; +} + +fn fr_sub(x: Fr, y: Fr) -> Fr { + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fr_add_modulus(z); + } + return z; +} + +fn fr_neg(x: Fr) -> Fr { + if (fr_is_zero(x)) { + return fr_zero(); + } + return fr_sub(fr_modulus(), x); +} + +fn fr_double(x: Fr) -> Fr { + return fr_add(x, x); +} + +fn fr_normalize(x: Fr) -> Fr { + if (fr_gte(x, fr_modulus())) { + return fr_sub_modulus(x); + } + return x; +} + +fn fr_unpack16(x: Fr) -> Fr16 { + var z: Fr16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FR_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fr_pack16(x: Fr16) -> Fr { + var z: Fr; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fr16_gte_modulus(x: Fr16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FR_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fr16_sub_modulus(x: Fr16) -> Fr16 { + var z: Fr16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FR_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FR_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fr_mul(x: Fr, y: Fr) -> Fr { + let a = fr_unpack16(x); + let b = fr_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FR_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FR_QINV_NEG_16) & FR_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FR_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FR_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FR_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fr16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fr16_gte_modulus(z16)) { + z16 = fr16_sub_modulus(z16); + } + return fr_pack16(z16); +} +// curvegpu:section fr_core end + +fn fr_dispatch(opcode: u32, a: Fr, b: Fr) -> Fr { + if (opcode == FR_OP_COPY) { + return a; + } + if (opcode == FR_OP_ZERO) { + return fr_zero(); + } + if (opcode == FR_OP_ONE) { + return fr_one(); + } + if (opcode == FR_OP_ADD) { + return fr_add(a, b); + } + if (opcode == FR_OP_SUB) { + return fr_sub(a, b); + } + if (opcode == FR_OP_NEG) { + return fr_neg(a); + } + if (opcode == FR_OP_DOUBLE) { + return fr_double(a); + } + if (opcode == FR_OP_NORMALIZE) { + return fr_normalize(a); + } + if (opcode == FR_OP_EQUAL) { + return fr_predicate(fr_equal(a, b)); + } + if (opcode == FR_OP_MUL) { + return fr_mul(a, b); + } + if (opcode == FR_OP_SQUARE) { + return fr_mul(a, a); + } + if (opcode == FR_OP_TO_MONT) { + return fr_mul(a, fr_rsquare_regular()); + } + if (opcode == FR_OP_FROM_MONT) { + return fr_mul(a, fr_one_regular()); + } + return fr_zero(); +} + +fn fr_load_a(index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + z.limbs[0] = input_a[base + 0u]; + z.limbs[1] = input_a[base + 1u]; + z.limbs[2] = input_a[base + 2u]; + z.limbs[3] = input_a[base + 3u]; + z.limbs[4] = input_a[base + 4u]; + z.limbs[5] = input_a[base + 5u]; + z.limbs[6] = input_a[base + 6u]; + z.limbs[7] = input_a[base + 7u]; + return z; +} + +fn fr_load_b(index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + z.limbs[0] = input_b[base + 0u]; + z.limbs[1] = input_b[base + 1u]; + z.limbs[2] = input_b[base + 2u]; + z.limbs[3] = input_b[base + 3u]; + z.limbs[4] = input_b[base + 4u]; + z.limbs[5] = input_b[base + 5u]; + z.limbs[6] = input_b[base + 6u]; + z.limbs[7] = input_b[base + 7u]; + return z; +} + +fn fr_store(index: u32, value: Fr) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_ops_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + fr_store(i, fr_dispatch(params.opcode, fr_load_a(i), fr_load_b(i))); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bn254/fr_ntt.wgsl b/backend/accelerated/webgpu/shaders/curves/bn254/fr_ntt.wgsl new file mode 100644 index 0000000000..36a876ec06 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bn254/fr_ntt.wgsl @@ -0,0 +1,356 @@ +struct Fr { + limbs: array, +} + +struct Fr16 { + limbs: array, +} + +struct Params { + count: u32, + m: u32, + _pad0: u32, + _pad1: u32, +} + +const FR_LIMB16_MASK: u32 = 0xffffu; +const FR_QINV_NEG_16: u32 = 0xffffu; + +const FR_MODULUS16: array = array( + 0x0001u, 0xf000u, + 0xf593u, 0x43e1u, + 0x7091u, 0x79b9u, + 0xe848u, 0x2833u, + 0x585du, 0x8181u, + 0x45b6u, 0xb850u, + 0xa029u, 0xe131u, + 0x4e72u, 0x3064u, +); + +@group(0) @binding(0) var input_values: array; +@group(0) @binding(1) var input_twiddles: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fr_modulus() -> Fr { + var z: Fr; + z.limbs[0] = 0xf0000001u; + z.limbs[1] = 0x43e1f593u; + z.limbs[2] = 0x79b97091u; + z.limbs[3] = 0x2833e848u; + z.limbs[4] = 0x8181585du; + z.limbs[5] = 0xb85045b6u; + z.limbs[6] = 0xe131a029u; + z.limbs[7] = 0x30644e72u; + return z; +} + +fn fr_gte(x: Fr, y: Fr) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fr_add_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fr_sub_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fr_add(x: Fr, y: Fr) -> Fr { + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fr_gte(z, fr_modulus())) { + return fr_sub_modulus(z); + } + return z; +} + +fn fr_sub(x: Fr, y: Fr) -> Fr { + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fr_add_modulus(z); + } + return z; +} + +fn fr_unpack16(x: Fr) -> Fr16 { + var z: Fr16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FR_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fr_pack16(x: Fr16) -> Fr { + var z: Fr; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fr16_gte_modulus(x: Fr16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FR_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fr16_sub_modulus(x: Fr16) -> Fr16 { + var z: Fr16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FR_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FR_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fr_mul(x: Fr, y: Fr) -> Fr { + let a = fr_unpack16(x); + let b = fr_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FR_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FR_QINV_NEG_16) & FR_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FR_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FR_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FR_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fr16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fr16_gte_modulus(z16)) { + z16 = fr16_sub_modulus(z16); + } + return fr_pack16(z16); +} + +fn fr_load_from(buffer_kind: u32, index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + if (buffer_kind == 0u) { + z.limbs[0] = input_values[base + 0u]; + z.limbs[1] = input_values[base + 1u]; + z.limbs[2] = input_values[base + 2u]; + z.limbs[3] = input_values[base + 3u]; + z.limbs[4] = input_values[base + 4u]; + z.limbs[5] = input_values[base + 5u]; + z.limbs[6] = input_values[base + 6u]; + z.limbs[7] = input_values[base + 7u]; + return z; + } + z.limbs[0] = input_twiddles[base + 0u]; + z.limbs[1] = input_twiddles[base + 1u]; + z.limbs[2] = input_twiddles[base + 2u]; + z.limbs[3] = input_twiddles[base + 3u]; + z.limbs[4] = input_twiddles[base + 4u]; + z.limbs[5] = input_twiddles[base + 5u]; + z.limbs[6] = input_twiddles[base + 6u]; + z.limbs[7] = input_twiddles[base + 7u]; + return z; +} + +fn fr_store(index: u32, value: Fr) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_ntt_stage_main(@builtin(global_invocation_id) id: vec3) { + let pair = id.x; + let vector = id.y; + let half_count = params.count / 2u; + let batch_count = max(params._pad0, 1u); + if (pair >= half_count || vector >= batch_count) { + return; + } + + let m = params.m; + let j = pair % m; + let block = pair / m; + let vector_base = vector * params.count; + let left_index = vector_base + block * 2u * m + j; + let right_index = left_index + m; + + let left = fr_load_from(0u, left_index); + let twiddle = fr_load_from(1u, j); + let right = fr_mul(fr_load_from(0u, right_index), twiddle); + + fr_store(left_index, fr_add(left, right)); + fr_store(right_index, fr_sub(left, right)); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bn254/fr_plonk_quotient.wgsl b/backend/accelerated/webgpu/shaders/curves/bn254/fr_plonk_quotient.wgsl new file mode 100644 index 0000000000..bd192ff047 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bn254/fr_plonk_quotient.wgsl @@ -0,0 +1,228 @@ +struct PlonkQuotientParams { + count: u32, + blind_count: u32, + coset_count: u32, + _pad1: u32, +} + +override COMMITMENT_COUNT: u32 = 0u; + +const PLONK_FR_WORDS: u32 = 8u; +const PLONK_BASE_DYNAMIC_VECTOR_COUNT: u32 = 5u; +const PLONK_BASE_STATIC_VECTOR_COUNT: u32 = 7u; +const PLONK_BLIND_COUNT: u32 = 4u; +const PLONK_SCALAR_COUNT: u32 = 7u; + +const PLONK_VEC_L: u32 = 0u; +const PLONK_VEC_R: u32 = 1u; +const PLONK_VEC_O: u32 = 2u; +const PLONK_VEC_Z: u32 = 3u; +const PLONK_VEC_QK: u32 = 4u; + +const PLONK_BLIND_L: u32 = 0u; +const PLONK_BLIND_R: u32 = 1u; +const PLONK_BLIND_O: u32 = 2u; +const PLONK_BLIND_Z: u32 = 3u; + +const PLONK_SCALAR_COSET: u32 = 0u; +const PLONK_SCALAR_LAGRANGE_SCALE: u32 = 1u; +const PLONK_SCALAR_CS: u32 = 2u; +const PLONK_SCALAR_CSS: u32 = 3u; +const PLONK_SCALAR_BETA: u32 = 4u; +const PLONK_SCALAR_GAMMA: u32 = 5u; +const PLONK_SCALAR_ALPHA: u32 = 6u; + +@group(0) @binding(0) var plonk_vectors: array; +@group(0) @binding(1) var plonk_blinds: array; +@group(0) @binding(2) var plonk_scalars: array; +@group(0) @binding(3) var plonk_output: array; +@group(0) @binding(4) var plonk_params: PlonkQuotientParams; + +fn plonk_static_base() -> u32 { + return PLONK_BASE_DYNAMIC_VECTOR_COUNT + COMMITMENT_COUNT; +} + +fn plonk_vec_ql() -> u32 { + return plonk_static_base(); +} + +fn plonk_vec_qr() -> u32 { + return plonk_static_base() + 1u; +} + +fn plonk_vec_qm() -> u32 { + return plonk_static_base() + 2u; +} + +fn plonk_vec_qo() -> u32 { + return plonk_static_base() + 3u; +} + +fn plonk_vec_s1() -> u32 { + return plonk_static_base() + 4u; +} + +fn plonk_vec_s2() -> u32 { + return plonk_static_base() + 5u; +} + +fn plonk_vec_s3() -> u32 { + return plonk_static_base() + 6u; +} + +fn plonk_vec_commitment_value(index: u32) -> u32 { + return PLONK_BASE_DYNAMIC_VECTOR_COUNT + index; +} + +fn plonk_vec_qcp(index: u32) -> u32 { + return plonk_static_base() + PLONK_BASE_STATIC_VECTOR_COUNT + index; +} + +fn plonk_vec_twiddles() -> u32 { + return plonk_static_base() + PLONK_BASE_STATIC_VECTOR_COUNT + COMMITMENT_COUNT; +} + +fn plonk_vec_denominators() -> u32 { + return plonk_vec_twiddles() + 1u; +} + +fn plonk_vector_count() -> u32 { + return plonk_vec_denominators() + 1u; +} + +fn fr_from_mont(x: Fr) -> Fr { + return fr_mul(x, fr_one_regular()); +} + +fn plonk_load_words(base: u32) -> Fr { + var z: Fr; + z.limbs[0] = plonk_vectors[base + 0u]; + z.limbs[1] = plonk_vectors[base + 1u]; + z.limbs[2] = plonk_vectors[base + 2u]; + z.limbs[3] = plonk_vectors[base + 3u]; + z.limbs[4] = plonk_vectors[base + 4u]; + z.limbs[5] = plonk_vectors[base + 5u]; + z.limbs[6] = plonk_vectors[base + 6u]; + z.limbs[7] = plonk_vectors[base + 7u]; + return z; +} + +fn plonk_load_vector_mont(coset: u32, vector: u32, index: u32) -> Fr { + let base = (((coset * plonk_vector_count() + vector) * plonk_params.count) + index) * PLONK_FR_WORDS; + return plonk_load_words(base); +} + +fn plonk_load_blind_mont(coset: u32, poly: u32, index: u32) -> Fr { + let base = (((coset * PLONK_BLIND_COUNT + poly) * plonk_params.blind_count) + index) * PLONK_FR_WORDS; + var z: Fr; + z.limbs[0] = plonk_blinds[base + 0u]; + z.limbs[1] = plonk_blinds[base + 1u]; + z.limbs[2] = plonk_blinds[base + 2u]; + z.limbs[3] = plonk_blinds[base + 3u]; + z.limbs[4] = plonk_blinds[base + 4u]; + z.limbs[5] = plonk_blinds[base + 5u]; + z.limbs[6] = plonk_blinds[base + 6u]; + z.limbs[7] = plonk_blinds[base + 7u]; + return z; +} + +fn plonk_load_scalar_mont(coset: u32, index: u32) -> Fr { + let base = ((coset * PLONK_SCALAR_COUNT) + index) * PLONK_FR_WORDS; + var z: Fr; + z.limbs[0] = plonk_scalars[base + 0u]; + z.limbs[1] = plonk_scalars[base + 1u]; + z.limbs[2] = plonk_scalars[base + 2u]; + z.limbs[3] = plonk_scalars[base + 3u]; + z.limbs[4] = plonk_scalars[base + 4u]; + z.limbs[5] = plonk_scalars[base + 5u]; + z.limbs[6] = plonk_scalars[base + 6u]; + z.limbs[7] = plonk_scalars[base + 7u]; + return z; +} + +fn plonk_store_regular(coset: u32, index: u32, value: Fr) { + let regular = fr_from_mont(value); + let base = ((coset * plonk_params.count) + index) * PLONK_FR_WORDS; + plonk_output[base + 0u] = regular.limbs[0]; + plonk_output[base + 1u] = regular.limbs[1]; + plonk_output[base + 2u] = regular.limbs[2]; + plonk_output[base + 3u] = regular.limbs[3]; + plonk_output[base + 4u] = regular.limbs[4]; + plonk_output[base + 5u] = regular.limbs[5]; + plonk_output[base + 6u] = regular.limbs[6]; + plonk_output[base + 7u] = regular.limbs[7]; +} + +fn plonk_eval_blind(coset: u32, poly: u32, point: Fr) -> Fr { + var res = fr_zero(); + var i = plonk_params.blind_count; + loop { + if (i == 0u) { + break; + } + i = i - 1u; + res = fr_add(fr_mul(res, point), plonk_load_blind_mont(coset, poly, i)); + } + return res; +} + +fn plonk_evaluate_quotient(coset: u32, index: u32) -> Fr { + let twiddle = plonk_load_vector_mont(coset, plonk_vec_twiddles(), index); + let next_index = (index + 1u) % plonk_params.count; + let next_twiddle = plonk_load_vector_mont(coset, plonk_vec_twiddles(), next_index); + + var l = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_L, index), plonk_eval_blind(coset, PLONK_BLIND_L, twiddle)); + var r = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_R, index), plonk_eval_blind(coset, PLONK_BLIND_R, twiddle)); + var o = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_O, index), plonk_eval_blind(coset, PLONK_BLIND_O, twiddle)); + var z = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_Z, index), plonk_eval_blind(coset, PLONK_BLIND_Z, twiddle)); + let zs = fr_add(plonk_load_vector_mont(coset, PLONK_VEC_Z, next_index), plonk_eval_blind(coset, PLONK_BLIND_Z, next_twiddle)); + + var gate = fr_mul(plonk_load_vector_mont(coset, plonk_vec_ql(), index), l); + gate = fr_add(gate, fr_mul(plonk_load_vector_mont(coset, plonk_vec_qr(), index), r)); + gate = fr_add(gate, fr_mul(fr_mul(plonk_load_vector_mont(coset, plonk_vec_qm(), index), l), r)); + gate = fr_add(gate, fr_mul(plonk_load_vector_mont(coset, plonk_vec_qo(), index), o)); + gate = fr_add(gate, plonk_load_vector_mont(coset, PLONK_VEC_QK, index)); + var commitment_index = 0u; + loop { + if (commitment_index >= COMMITMENT_COUNT) { + break; + } + let qcp = plonk_load_vector_mont(coset, plonk_vec_qcp(commitment_index), index); + let commitment_value = plonk_load_vector_mont(coset, plonk_vec_commitment_value(commitment_index), index); + gate = fr_add(gate, fr_mul(qcp, commitment_value)); + commitment_index = commitment_index + 1u; + } + + let beta = plonk_load_scalar_mont(coset, PLONK_SCALAR_BETA); + let gamma = plonk_load_scalar_mont(coset, PLONK_SCALAR_GAMMA); + let alpha = plonk_load_scalar_mont(coset, PLONK_SCALAR_ALPHA); + let id = fr_mul(fr_mul(twiddle, plonk_load_scalar_mont(coset, PLONK_SCALAR_COSET)), beta); + + var a = fr_add(fr_add(gamma, l), id); + var b = fr_add(fr_add(fr_mul(id, plonk_load_scalar_mont(coset, PLONK_SCALAR_CS)), r), gamma); + var c = fr_add(fr_add(fr_mul(id, plonk_load_scalar_mont(coset, PLONK_SCALAR_CSS)), o), gamma); + let right = fr_mul(fr_mul(fr_mul(a, b), c), z); + + a = fr_add(fr_add(fr_mul(plonk_load_vector_mont(coset, plonk_vec_s1(), index), beta), l), gamma); + b = fr_add(fr_add(fr_mul(plonk_load_vector_mont(coset, plonk_vec_s2(), index), beta), r), gamma); + c = fr_add(fr_add(fr_mul(plonk_load_vector_mont(coset, plonk_vec_s3(), index), beta), o), gamma); + let left = fr_mul(fr_mul(fr_mul(a, b), c), zs); + + let ordering = fr_sub(left, right); + let lone = fr_mul(plonk_load_scalar_mont(coset, PLONK_SCALAR_LAGRANGE_SCALE), plonk_load_vector_mont(coset, plonk_vec_denominators(), index)); + var local = fr_mul(fr_sub(z, fr_one()), lone); + local = fr_add(fr_mul(local, alpha), ordering); + return fr_add(fr_mul(local, alpha), gate); +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_plonk_quotient_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + let coset = id.y; + if (i >= plonk_params.count || coset >= plonk_params.coset_count) { + return; + } + plonk_store_regular(coset, i, plonk_evaluate_quotient(coset, i)); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bn254/fr_vector.wgsl b/backend/accelerated/webgpu/shaders/curves/bn254/fr_vector.wgsl new file mode 100644 index 0000000000..eda45cc992 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bn254/fr_vector.wgsl @@ -0,0 +1,391 @@ +struct Fr { + limbs: array, +} + +struct Fr16 { + limbs: array, +} + +struct Params { + count: u32, + opcode: u32, + log_count: u32, + _pad0: u32, +} + +const FR_VECTOR_OP_COPY: u32 = 0u; +const FR_VECTOR_OP_ADD: u32 = 1u; +const FR_VECTOR_OP_SUB: u32 = 2u; +const FR_VECTOR_OP_MUL_FACTORS: u32 = 3u; +const FR_VECTOR_OP_BIT_REVERSE_COPY: u32 = 4u; +const FR_LIMB16_MASK: u32 = 0xffffu; +const FR_QINV_NEG_16: u32 = 0xffffu; + +const FR_MODULUS16: array = array( + 0x0001u, 0xf000u, + 0xf593u, 0x43e1u, + 0x7091u, 0x79b9u, + 0xe848u, 0x2833u, + 0x585du, 0x8181u, + 0x45b6u, 0xb850u, + 0xa029u, 0xe131u, + 0x4e72u, 0x3064u, +); + +@group(0) @binding(0) var input_values: array; +@group(0) @binding(1) var input_aux: array; +@group(0) @binding(2) var output: array; +@group(0) @binding(3) var params: Params; + +fn fr_zero() -> Fr { + var z: Fr; + z.limbs[0] = 0u; + z.limbs[1] = 0u; + z.limbs[2] = 0u; + z.limbs[3] = 0u; + z.limbs[4] = 0u; + z.limbs[5] = 0u; + z.limbs[6] = 0u; + z.limbs[7] = 0u; + return z; +} + +fn fr_modulus() -> Fr { + var z: Fr; + z.limbs[0] = 0xf0000001u; + z.limbs[1] = 0x43e1f593u; + z.limbs[2] = 0x79b97091u; + z.limbs[3] = 0x2833e848u; + z.limbs[4] = 0x8181585du; + z.limbs[5] = 0xb85045b6u; + z.limbs[6] = 0xe131a029u; + z.limbs[7] = 0x30644e72u; + return z; +} + +fn adc(a: u32, b: u32, carry: u32) -> vec2 { + let sum0 = a + b; + let carry0 = select(0u, 1u, sum0 < a); + let sum1 = sum0 + carry; + let carry1 = select(0u, 1u, sum1 < sum0); + return vec2(sum1, carry0 | carry1); +} + +fn sbb(a: u32, b: u32, borrow: u32) -> vec2 { + let diff0 = a - b; + let borrow0 = select(0u, 1u, a < b); + let diff1 = diff0 - borrow; + let borrow1 = select(0u, 1u, diff1 > diff0); + return vec2(diff1, borrow0 | borrow1); +} + +fn fr_gte(x: Fr, y: Fr) -> bool { + if (x.limbs[7] != y.limbs[7]) { + return x.limbs[7] > y.limbs[7]; + } + if (x.limbs[6] != y.limbs[6]) { + return x.limbs[6] > y.limbs[6]; + } + if (x.limbs[5] != y.limbs[5]) { + return x.limbs[5] > y.limbs[5]; + } + if (x.limbs[4] != y.limbs[4]) { + return x.limbs[4] > y.limbs[4]; + } + if (x.limbs[3] != y.limbs[3]) { + return x.limbs[3] > y.limbs[3]; + } + if (x.limbs[2] != y.limbs[2]) { + return x.limbs[2] > y.limbs[2]; + } + if (x.limbs[1] != y.limbs[1]) { + return x.limbs[1] > y.limbs[1]; + } + return x.limbs[0] >= y.limbs[0]; +} + +fn fr_add_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], q.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], q.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], q.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], q.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], q.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], q.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], q.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], q.limbs[7], carry); + z.limbs[7] = lane.x; + return z; +} + +fn fr_sub_modulus(x: Fr) -> Fr { + let q = fr_modulus(); + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], q.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], q.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], q.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], q.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], q.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], q.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], q.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], q.limbs[7], borrow); + z.limbs[7] = lane.x; + return z; +} + +fn fr_add(x: Fr, y: Fr) -> Fr { + var z: Fr; + var carry = 0u; + var lane = adc(x.limbs[0], y.limbs[0], carry); + z.limbs[0] = lane.x; + carry = lane.y; + lane = adc(x.limbs[1], y.limbs[1], carry); + z.limbs[1] = lane.x; + carry = lane.y; + lane = adc(x.limbs[2], y.limbs[2], carry); + z.limbs[2] = lane.x; + carry = lane.y; + lane = adc(x.limbs[3], y.limbs[3], carry); + z.limbs[3] = lane.x; + carry = lane.y; + lane = adc(x.limbs[4], y.limbs[4], carry); + z.limbs[4] = lane.x; + carry = lane.y; + lane = adc(x.limbs[5], y.limbs[5], carry); + z.limbs[5] = lane.x; + carry = lane.y; + lane = adc(x.limbs[6], y.limbs[6], carry); + z.limbs[6] = lane.x; + carry = lane.y; + lane = adc(x.limbs[7], y.limbs[7], carry); + z.limbs[7] = lane.x; + if ((lane.y != 0u) || fr_gte(z, fr_modulus())) { + return fr_sub_modulus(z); + } + return z; +} + +fn fr_sub(x: Fr, y: Fr) -> Fr { + var z: Fr; + var borrow = 0u; + var lane = sbb(x.limbs[0], y.limbs[0], borrow); + z.limbs[0] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[1], y.limbs[1], borrow); + z.limbs[1] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[2], y.limbs[2], borrow); + z.limbs[2] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[3], y.limbs[3], borrow); + z.limbs[3] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[4], y.limbs[4], borrow); + z.limbs[4] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[5], y.limbs[5], borrow); + z.limbs[5] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[6], y.limbs[6], borrow); + z.limbs[6] = lane.x; + borrow = lane.y; + lane = sbb(x.limbs[7], y.limbs[7], borrow); + z.limbs[7] = lane.x; + if (lane.y != 0u) { + return fr_add_modulus(z); + } + return z; +} + +fn fr_unpack16(x: Fr) -> Fr16 { + var z: Fr16; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[2u * i] = x.limbs[i] & FR_LIMB16_MASK; + z.limbs[2u * i + 1u] = x.limbs[i] >> 16u; + } + return z; +} + +fn fr_pack16(x: Fr16) -> Fr { + var z: Fr; + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = x.limbs[2u * i] | (x.limbs[2u * i + 1u] << 16u); + } + return z; +} + +fn fr16_gte_modulus(x: Fr16) -> bool { + for (var i: i32 = 15; i >= 0; i = i - 1) { + let idx = u32(i); + let xLimb = x.limbs[idx]; + let qLimb = FR_MODULUS16[idx]; + if (xLimb != qLimb) { + return xLimb > qLimb; + } + } + return true; +} + +fn fr16_sub_modulus(x: Fr16) -> Fr16 { + var z: Fr16; + var borrow = 0u; + for (var i = 0u; i < 16u; i = i + 1u) { + let lane = sbb(x.limbs[i], FR_MODULUS16[i], borrow); + z.limbs[i] = lane.x & FR_LIMB16_MASK; + borrow = lane.y; + } + return z; +} + +fn fr_mul(x: Fr, y: Fr) -> Fr { + let a = fr_unpack16(x); + let b = fr_unpack16(y); + var t: array; + + for (var i = 0u; i < 16u; i = i + 1u) { + var carry = 0u; + let bi = b.limbs[i]; + for (var j = 0u; j < 16u; j = j + 1u) { + let aLimb = a.limbs[j]; + let uv = t[j] + (aLimb * bi) + carry; + t[j] = uv & FR_LIMB16_MASK; + carry = uv >> 16u; + } + t[16] = carry; + + let m = (t[0] * FR_QINV_NEG_16) & FR_LIMB16_MASK; + carry = 0u; + for (var j = 0u; j < 16u; j = j + 1u) { + let qLimb = FR_MODULUS16[j]; + let uv = t[j] + (m * qLimb) + carry; + if (j > 0u) { + t[j - 1u] = uv & FR_LIMB16_MASK; + } + carry = uv >> 16u; + } + let uv = t[16] + carry; + t[15] = uv & FR_LIMB16_MASK; + t[16] = uv >> 16u; + } + + var z16: Fr16; + for (var i = 0u; i < 16u; i = i + 1u) { + z16.limbs[i] = t[i]; + } + if ((t[16] != 0u) || fr16_gte_modulus(z16)) { + z16 = fr16_sub_modulus(z16); + } + return fr_pack16(z16); +} + +fn reverse_bits(index: u32, log_count: u32) -> u32 { + var out = 0u; + for (var bit = 0u; bit < log_count; bit = bit + 1u) { + out = (out << 1u) | ((index >> bit) & 1u); + } + return out; +} + +fn fr_load_from(buffer_kind: u32, index: u32) -> Fr { + let base = index * 8u; + var z: Fr; + if (buffer_kind == 0u) { + z.limbs[0] = input_values[base + 0u]; + z.limbs[1] = input_values[base + 1u]; + z.limbs[2] = input_values[base + 2u]; + z.limbs[3] = input_values[base + 3u]; + z.limbs[4] = input_values[base + 4u]; + z.limbs[5] = input_values[base + 5u]; + z.limbs[6] = input_values[base + 6u]; + z.limbs[7] = input_values[base + 7u]; + return z; + } + z.limbs[0] = input_aux[base + 0u]; + z.limbs[1] = input_aux[base + 1u]; + z.limbs[2] = input_aux[base + 2u]; + z.limbs[3] = input_aux[base + 3u]; + z.limbs[4] = input_aux[base + 4u]; + z.limbs[5] = input_aux[base + 5u]; + z.limbs[6] = input_aux[base + 6u]; + z.limbs[7] = input_aux[base + 7u]; + return z; +} + +fn fr_store(index: u32, value: Fr) { + let base = index * 8u; + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +fn fr_dispatch(index: u32) -> Fr { + if (params.opcode == FR_VECTOR_OP_COPY) { + return fr_load_from(0u, index); + } + if (params.opcode == FR_VECTOR_OP_ADD) { + return fr_add(fr_load_from(0u, index), fr_load_from(1u, index)); + } + if (params.opcode == FR_VECTOR_OP_SUB) { + return fr_sub(fr_load_from(0u, index), fr_load_from(1u, index)); + } + if (params.opcode == FR_VECTOR_OP_MUL_FACTORS) { + return fr_mul(fr_load_from(0u, index), fr_load_from(1u, index)); + } + if (params.opcode == FR_VECTOR_OP_BIT_REVERSE_COPY) { + if (params._pad0 != 0u) { + let vector_size = params._pad0; + let vector_base = (index / vector_size) * vector_size; + let lane = index % vector_size; + return fr_load_from(0u, vector_base + reverse_bits(lane, params.log_count)); + } + return fr_load_from(0u, reverse_bits(index, params.log_count)); + } + return fr_zero(); +} + +override WORKGROUP_SIZE: u32 = 64; + +@compute @workgroup_size(WORKGROUP_SIZE) +fn fr_vector_main(@builtin(global_invocation_id) id: vec3) { + let i = id.x; + if (i >= params.count) { + return; + } + fr_store(i, fr_dispatch(i)); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bn254/g1_io.wgsl b/backend/accelerated/webgpu/shaders/curves/bn254/g1_io.wgsl new file mode 100644 index 0000000000..bdde2646cd --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bn254/g1_io.wgsl @@ -0,0 +1,50 @@ +fn fp_load_from(buffer_kind: u32, base: u32) -> Fp { + var z: Fp; + if (buffer_kind == 0u) { + z.limbs[0] = input_a[base + 0u]; + z.limbs[1] = input_a[base + 1u]; + z.limbs[2] = input_a[base + 2u]; + z.limbs[3] = input_a[base + 3u]; + z.limbs[4] = input_a[base + 4u]; + z.limbs[5] = input_a[base + 5u]; + z.limbs[6] = input_a[base + 6u]; + z.limbs[7] = input_a[base + 7u]; + return z; + } + z.limbs[0] = input_b[base + 0u]; + z.limbs[1] = input_b[base + 1u]; + z.limbs[2] = input_b[base + 2u]; + z.limbs[3] = input_b[base + 3u]; + z.limbs[4] = input_b[base + 4u]; + z.limbs[5] = input_b[base + 5u]; + z.limbs[6] = input_b[base + 6u]; + z.limbs[7] = input_b[base + 7u]; + return z; +} + +fn g1_load_from(buffer_kind: u32, index: u32) -> G1Point { + let base = index * 24u; + var p: G1Point; + p.x = fp_load_from(buffer_kind, base + 0u); + p.y = fp_load_from(buffer_kind, base + 8u); + p.z = fp_load_from(buffer_kind, base + 16u); + return p; +} + +fn fp_store(base: u32, value: Fp) { + output[base + 0u] = value.limbs[0]; + output[base + 1u] = value.limbs[1]; + output[base + 2u] = value.limbs[2]; + output[base + 3u] = value.limbs[3]; + output[base + 4u] = value.limbs[4]; + output[base + 5u] = value.limbs[5]; + output[base + 6u] = value.limbs[6]; + output[base + 7u] = value.limbs[7]; +} + +fn g1_store(index: u32, value: G1Point) { + let base = index * 24u; + fp_store(base + 0u, value.x); + fp_store(base + 8u, value.y); + fp_store(base + 16u, value.z); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bn254/g2_arith.wgsl b/backend/accelerated/webgpu/shaders/curves/bn254/g2_arith.wgsl new file mode 100644 index 0000000000..932d7b08c2 --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bn254/g2_arith.wgsl @@ -0,0 +1,346 @@ +struct Fp2 { + c0: Fp, + c1: Fp, +} + +struct G2Point { + x: Fp2, + y: Fp2, + z: Fp2, +} + +const G2_OP_COPY: u32 = 0u; +const G2_OP_JAC_INFINITY: u32 = 1u; +const G2_OP_AFFINE_TO_JAC: u32 = 2u; +const G2_OP_NEG_JAC: u32 = 3u; +const G2_OP_DOUBLE_JAC: u32 = 4u; +const G2_OP_ADD_MIXED: u32 = 5u; +const G2_OP_JAC_TO_AFFINE: u32 = 6u; +const G2_OP_AFFINE_ADD: u32 = 7u; + +fn fp2_zero() -> Fp2 { + var z: Fp2; + z.c0 = fp_zero(); + z.c1 = fp_zero(); + return z; +} + +fn fp2_one() -> Fp2 { + var z: Fp2; + z.c0 = fp_one(); + z.c1 = fp_zero(); + return z; +} + +fn fp2_is_zero(x: Fp2) -> bool { + return fp_is_zero(x.c0) && fp_is_zero(x.c1); +} + +fn fp2_equal(x: Fp2, y: Fp2) -> bool { + return fp_equal(x.c0, y.c0) && fp_equal(x.c1, y.c1); +} + +fn fp2_add(x: Fp2, y: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_add(x.c0, y.c0); + z.c1 = fp_add(x.c1, y.c1); + return z; +} + +fn fp2_sub(x: Fp2, y: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_sub(x.c0, y.c0); + z.c1 = fp_sub(x.c1, y.c1); + return z; +} + +fn fp2_neg(x: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_neg(x.c0); + z.c1 = fp_neg(x.c1); + return z; +} + +fn fp2_double(x: Fp2) -> Fp2 { + var z: Fp2; + z.c0 = fp_double(x.c0); + z.c1 = fp_double(x.c1); + return z; +} + +fn fp2_mul(x: Fp2, y: Fp2) -> Fp2 { + let a = fp_mul(x.c0, y.c0); + let b = fp_mul(x.c1, y.c1); + let ab = fp_mul(fp_add(x.c0, x.c1), fp_add(y.c0, y.c1)); + var z: Fp2; + z.c1 = fp_sub(fp_sub(ab, a), b); + z.c0 = fp_sub(a, b); + return z; +} + +fn fp2_square(x: Fp2) -> Fp2 { + let a = fp_mul(fp_add(x.c0, x.c1), fp_sub(x.c0, x.c1)); + let b = fp_double(fp_mul(x.c0, x.c1)); + var z: Fp2; + z.c0 = a; + z.c1 = b; + return z; +} + +fn fp2_inverse(x: Fp2) -> Fp2 { + let t0 = fp_square(x.c0); + let t1 = fp_square(x.c1); + let inv = fp_inverse(fp_add(t0, t1)); + var z: Fp2; + z.c0 = fp_mul(x.c0, inv); + z.c1 = fp_neg(fp_mul(x.c1, inv)); + return z; +} + +fn bn254_non_residue_inverse() -> Fp2 { + var z: Fp2; + z.c0 = fp_zero(); + z.c0.limbs[0] = 0x62e5ff12u; + z.c0.limbs[1] = 0x9168c5b0u; + z.c0.limbs[2] = 0xad07a2d2u; + z.c0.limbs[3] = 0x65af5018u; + z.c0.limbs[4] = 0x197d565eu; + z.c0.limbs[5] = 0x3272d31fu; + z.c0.limbs[6] = 0x01f7f840u; + z.c0.limbs[7] = 0x2c9f2108u; + z.c1 = fp_zero(); + z.c1.limbs[0] = 0xf09effcdu; + z.c1.limbs[1] = 0x684d4eeeu; + z.c1.limbs[2] = 0xdbef59bfu; + z.c1.limbs[3] = 0xcca59129u; + z.c1.limbs[4] = 0x60e40038u; + z.c1.limbs[5] = 0x9d189af4u; + z.c1.limbs[6] = 0x6e22d9c4u; + z.c1.limbs[7] = 0x006b3defu; + return z; +} + +fn fp2_mul_by_b_twist(x: Fp2) -> Fp2 { + let inv = bn254_non_residue_inverse(); + let scaled = fp2_mul(x, inv); + return fp2_add(fp2_double(scaled), scaled); +} + +fn g2_jac_infinity() -> G2Point { + var p: G2Point; + p.x = fp2_one(); + p.y = fp2_one(); + p.z = fp2_zero(); + return p; +} + +fn g2_affine_is_infinity(a: G2Point) -> bool { + return fp2_is_zero(a.z); +} + +fn g2_jac_is_infinity(p: G2Point) -> bool { + return fp2_is_zero(p.z); +} + +fn g2_affine_to_jac(a: G2Point) -> G2Point { + if (g2_affine_is_infinity(a)) { + return g2_jac_infinity(); + } + var p: G2Point; + p.x = a.x; + p.y = a.y; + p.z = fp2_one(); + return p; +} + +fn g2_jac_to_affine(p: G2Point) -> G2Point { + if (g2_jac_is_infinity(p)) { + var inf: G2Point; + inf.x = fp2_zero(); + inf.y = fp2_zero(); + inf.z = fp2_zero(); + return inf; + } + let a = fp2_inverse(p.z); + let b = fp2_square(a); + var out: G2Point; + out.x = fp2_mul(p.x, b); + out.y = fp2_mul(fp2_mul(p.y, b), a); + out.z = fp2_one(); + return out; +} + +fn g2_neg_affine(q: G2Point) -> G2Point { + if (g2_affine_is_infinity(q)) { + return q; + } + var p = q; + p.y = fp2_neg(q.y); + return p; +} + +fn g2_neg_jac(q: G2Point) -> G2Point { + var p = q; + p.y = fp2_neg(q.y); + return p; +} + +fn g2_double_mixed(a: G2Point) -> G2Point { + if (g2_affine_is_infinity(a)) { + return g2_jac_infinity(); + } + var xx = fp2_square(a.x); + let yy = fp2_square(a.y); + var yyyy = fp2_square(yy); + var s = fp2_add(a.x, yy); + s = fp2_square(s); + s = fp2_sub(s, xx); + s = fp2_sub(s, yyyy); + s = fp2_double(s); + var m = fp2_double(xx); + m = fp2_add(m, xx); + let t = fp2_sub(fp2_sub(fp2_square(m), s), s); + + var p: G2Point; + p.x = t; + p.y = fp2_mul(fp2_sub(s, t), m); + yyyy = fp2_double(fp2_double(fp2_double(yyyy))); + p.y = fp2_sub(p.y, yyyy); + p.z = fp2_double(a.y); + return p; +} + +fn g2_double_jac(q: G2Point) -> G2Point { + var a = fp2_square(q.x); + let b = fp2_square(q.y); + let c = fp2_square(b); + var d = fp2_add(q.x, b); + d = fp2_square(d); + d = fp2_sub(d, a); + d = fp2_sub(d, c); + d = fp2_double(d); + var e = fp2_double(a); + e = fp2_add(e, a); + let f = fp2_square(e); + let t = fp2_double(d); + + var p: G2Point; + p.z = fp2_double(fp2_mul(q.y, q.z)); + p.x = fp2_sub(f, t); + p.y = fp2_mul(fp2_sub(d, p.x), e); + let c8 = fp2_double(fp2_double(fp2_double(c))); + p.y = fp2_sub(p.y, c8); + return p; +} + +fn g2_add_mixed(p: G2Point, a: G2Point) -> G2Point { + if (g2_affine_is_infinity(a)) { + return p; + } + if (g2_jac_is_infinity(p)) { + return g2_affine_to_jac(a); + } + + let z1z1 = fp2_square(p.z); + let u2 = fp2_mul(a.x, z1z1); + let s2 = fp2_mul(fp2_mul(a.y, p.z), z1z1); + + if (fp2_equal(u2, p.x) && fp2_equal(s2, p.y)) { + return g2_double_mixed(a); + } + + let h = fp2_sub(u2, p.x); + let hh = fp2_square(h); + let i = fp2_double(fp2_double(hh)); + let j = fp2_mul(h, i); + let r = fp2_double(fp2_sub(s2, p.y)); + let v = fp2_mul(p.x, i); + + var out: G2Point; + out.x = fp2_sub(fp2_sub(fp2_sub(fp2_square(r), j), v), v); + out.y = fp2_sub(fp2_mul(fp2_sub(v, out.x), r), fp2_double(fp2_mul(j, p.y))); + out.z = fp2_square(fp2_add(p.z, h)); + out.z = fp2_sub(out.z, z1z1); + out.z = fp2_sub(out.z, hh); + return out; +} + +fn g2_add_jac(p: G2Point, q: G2Point) -> G2Point { + if (g2_jac_is_infinity(p)) { + return q; + } + if (g2_jac_is_infinity(q)) { + return p; + } + let z1z1 = fp2_square(p.z); + let z2z2 = fp2_square(q.z); + let u1 = fp2_mul(p.x, z2z2); + let u2 = fp2_mul(q.x, z1z1); + let s1 = fp2_mul(fp2_mul(p.y, q.z), z2z2); + let s2 = fp2_mul(fp2_mul(q.y, p.z), z1z1); + let h = fp2_sub(u2, u1); + let r = fp2_double(fp2_sub(s2, s1)); + if (fp2_is_zero(h)) { + if (fp2_is_zero(r)) { + return g2_double_jac(p); + } + return g2_jac_infinity(); + } + let i = fp2_double(fp2_double(fp2_square(h))); + let j = fp2_mul(h, i); + let v = fp2_mul(u1, i); + var out: G2Point; + out.x = fp2_sub(fp2_sub(fp2_sub(fp2_square(r), j), v), v); + out.y = fp2_sub(fp2_mul(fp2_sub(v, out.x), r), fp2_double(fp2_mul(s1, j))); + let z_sum = fp2_add(p.z, q.z); + out.z = fp2_mul(fp2_sub(fp2_sub(fp2_square(z_sum), z1z1), z2z2), h); + return out; +} + +fn g2_scalar_mul_jac_small(base: G2Point, scalar: u32) -> G2Point { + if (scalar == 0u || g2_jac_is_infinity(base)) { + return g2_jac_infinity(); + } + var acc = g2_jac_infinity(); + var b = base; + var k = scalar; + loop { + if (k == 0u) { + break; + } + if ((k & 1u) != 0u) { + acc = g2_add_jac(acc, b); + } + b = g2_double_jac(b); + k = k >> 1u; + } + return acc; +} + +fn g2_dispatch(opcode: u32, a: G2Point, b: G2Point) -> G2Point { + if (opcode == G2_OP_COPY) { + return a; + } + if (opcode == G2_OP_JAC_INFINITY) { + return g2_jac_infinity(); + } + if (opcode == G2_OP_AFFINE_TO_JAC) { + return g2_affine_to_jac(a); + } + if (opcode == G2_OP_NEG_JAC) { + return g2_neg_jac(a); + } + if (opcode == G2_OP_DOUBLE_JAC) { + return g2_double_jac(a); + } + if (opcode == G2_OP_ADD_MIXED) { + return g2_add_mixed(a, b); + } + if (opcode == G2_OP_JAC_TO_AFFINE) { + return g2_jac_to_affine(a); + } + if (opcode == G2_OP_AFFINE_ADD) { + return g2_jac_to_affine(g2_add_mixed(g2_affine_to_jac(a), b)); + } + return g2_jac_infinity(); +} diff --git a/backend/accelerated/webgpu/shaders/curves/bn254/g2_io.wgsl b/backend/accelerated/webgpu/shaders/curves/bn254/g2_io.wgsl new file mode 100644 index 0000000000..8b2d6f03ab --- /dev/null +++ b/backend/accelerated/webgpu/shaders/curves/bn254/g2_io.wgsl @@ -0,0 +1,47 @@ +fn fp_load_from(buffer_kind: u32, base: u32) -> Fp { + var z: Fp; + if (buffer_kind == 0u) { + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = input_a[base + i]; + } + return z; + } + for (var i = 0u; i < 8u; i = i + 1u) { + z.limbs[i] = input_b[base + i]; + } + return z; +} + +fn fp2_load_from(buffer_kind: u32, base: u32) -> Fp2 { + var z: Fp2; + z.c0 = fp_load_from(buffer_kind, base); + z.c1 = fp_load_from(buffer_kind, base + 8u); + return z; +} + +fn g2_load_from(buffer_kind: u32, index: u32) -> G2Point { + let base = index * 48u; + var p: G2Point; + p.x = fp2_load_from(buffer_kind, base + 0u); + p.y = fp2_load_from(buffer_kind, base + 16u); + p.z = fp2_load_from(buffer_kind, base + 32u); + return p; +} + +fn fp_store(base: u32, value: Fp) { + for (var i = 0u; i < 8u; i = i + 1u) { + output[base + i] = value.limbs[i]; + } +} + +fn fp2_store(base: u32, value: Fp2) { + fp_store(base, value.c0); + fp_store(base + 8u, value.c1); +} + +fn g2_store(index: u32, value: G2Point) { + let base = index * 48u; + fp2_store(base + 0u, value.x); + fp2_store(base + 16u, value.y); + fp2_store(base + 32u, value.z); +} diff --git a/backend/accelerated/webgpu/web/.npmrc b/backend/accelerated/webgpu/web/.npmrc new file mode 100644 index 0000000000..b58537fb2a --- /dev/null +++ b/backend/accelerated/webgpu/web/.npmrc @@ -0,0 +1 @@ +install-strategy=linked diff --git a/backend/accelerated/webgpu/web/eslint.config.js b/backend/accelerated/webgpu/web/eslint.config.js new file mode 100644 index 0000000000..9b1047eb7a --- /dev/null +++ b/backend/accelerated/webgpu/web/eslint.config.js @@ -0,0 +1,53 @@ +import js from "@eslint/js"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import tseslint from "typescript-eslint"; + +const tsconfigRootDir = dirname(fileURLToPath(import.meta.url)); + +export default tseslint.config( + { + ignores: [ + "dist/**", + "src/curvegpu/shader_bundle.generated.ts", + ], + }, + js.configs.recommended, + { + files: ["scripts/**/*.mjs"], + languageOptions: { + globals: { + console: "readonly", + }, + }, + }, + ...tseslint.configs.recommended, + { + files: ["**/*.ts"], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir, + }, + }, + rules: { + "@typescript-eslint/consistent-type-imports": [ + "warn", + { + fixStyle: "inline-type-imports", + prefer: "type-imports", + }, + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + }, + }, +); diff --git a/backend/accelerated/webgpu/web/index.ts b/backend/accelerated/webgpu/web/index.ts new file mode 100644 index 0000000000..e258df15c9 --- /dev/null +++ b/backend/accelerated/webgpu/web/index.ts @@ -0,0 +1 @@ +export * from "./src/index.js"; diff --git a/backend/accelerated/webgpu/web/package-lock.json b/backend/accelerated/webgpu/web/package-lock.json new file mode 100644 index 0000000000..84b937d7b0 --- /dev/null +++ b/backend/accelerated/webgpu/web/package-lock.json @@ -0,0 +1,1493 @@ +{ + "name": "gnark-webgpu", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gnark-webgpu", + "version": "0.1.0", + "devDependencies": { + "@eslint/js": "^9.0.0", + "@webgpu/types": "^0.1.70", + "eslint": "^9.0.0", + "typescript": "^5.8.0", + "typescript-eslint": "^8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz", + "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/accelerated/webgpu/web/package.json b/backend/accelerated/webgpu/web/package.json new file mode 100644 index 0000000000..2df77a7d6d --- /dev/null +++ b/backend/accelerated/webgpu/web/package.json @@ -0,0 +1,50 @@ +{ + "name": "gnark-webgpu", + "version": "0.1.0", + "description": "WebGPU/WASM implementation of gnark Groth16 and PLONK backend over BN254, BLS12-377 and BLS12-381 curves.", + "private": true, + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./shader_bundle": { + "types": "./dist/src/curvegpu/shader_bundle.generated.d.ts", + "import": "./dist/src/curvegpu/shader_bundle.generated.js" + } + }, + "files": [ + "dist/index.js", + "dist/index.d.ts", + "dist/index.d.ts.map", + "dist/src/**/*.js", + "dist/src/**/*.d.ts", + "dist/src/**/*.d.ts.map", + "dist/assets/**/*" + ], + "scripts": { + "build": "npm run build:shaders && rm -rf dist && tsc -p tsconfig.json", + "build:all": "npm run lint && npm run build && npm run build:wasm", + "build:shaders": "node scripts/bundle-shaders.mjs", + "build:wasm": "npm run build:wasm:groth16 && npm run build:wasm:plonk", + "build:wasm:assets": "mkdir -p dist/assets && cp \"$(go env GOROOT)/lib/wasm/wasm_exec.js\" dist/assets/wasm_exec.js", + "build:wasm:groth16": "npm run build:wasm:assets && npm run build:wasm:groth16:webgpu && npm run build:wasm:groth16:native", + "build:wasm:groth16:native": "GOOS=js GOARCH=wasm go build -o dist/assets/groth16-native.wasm ../groth16/internal/wasmruntime/native", + "build:wasm:groth16:webgpu": "GOOS=js GOARCH=wasm go build -o dist/assets/groth16-webgpu.wasm ../groth16/internal/wasmruntime/webgpu", + "build:wasm:plonk": "npm run build:wasm:assets && npm run build:wasm:plonk:webgpu && npm run build:wasm:plonk:native", + "build:wasm:plonk:native": "GOOS=js GOARCH=wasm go build -o dist/assets/plonk-native.wasm ../plonk/internal/wasmruntime/native", + "build:wasm:plonk:webgpu": "GOOS=js GOARCH=wasm go build -o dist/assets/plonk-webgpu.wasm ../plonk/internal/wasmruntime/webgpu", + "lint": "eslint ." + }, + "devDependencies": { + "@eslint/js": "^9.0.0", + "@webgpu/types": "^0.1.70", + "eslint": "^9.0.0", + "typescript": "^5.8.0", + "typescript-eslint": "^8.0.0" + } +} \ No newline at end of file diff --git a/backend/accelerated/webgpu/web/scripts/bundle-shaders.mjs b/backend/accelerated/webgpu/web/scripts/bundle-shaders.mjs new file mode 100644 index 0000000000..cb139aa1ff --- /dev/null +++ b/backend/accelerated/webgpu/web/scripts/bundle-shaders.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node +// Generates web/src/curvegpu/shader_bundle.generated.ts by inlining all WGSL +// shader files referenced by the library. Run via `npm run build:shaders`. +// +// The generated file installs the bundle via setBundledShaders() so that the +// library can operate without a web server for shader files. + +import { readFileSync, writeFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const webDir = resolve(__dirname, ".."); +const repoRoot = resolve(webDir, ".."); +const outPath = resolve(webDir, "src/curvegpu/shader_bundle.generated.ts"); + +// All unique WGSL file paths served under /shaders/ by the dev server. +// Update this list when new shaders are added to curves.ts. +const SHADER_PATHS = [ + "/shaders/curves/bn254/fp_arith.wgsl", + "/shaders/curves/bn254/fr_arith.wgsl", + "/shaders/curves/bn254/fr_vector.wgsl", + "/shaders/curves/bn254/fr_ntt.wgsl", + "/shaders/curves/bn254/fr_plonk_quotient.wgsl", + "/shaders/curves/bn254/g1_io.wgsl", + "/shaders/curves/bn254/g2_arith.wgsl", + "/shaders/curves/bn254/g2_io.wgsl", + "/shaders/curves/bls12_381/fp_arith.wgsl", + "/shaders/curves/bls12_381/fr_arith.wgsl", + "/shaders/curves/bls12_381/fr_vector.wgsl", + "/shaders/curves/bls12_381/fr_ntt.wgsl", + "/shaders/curves/bls12_381/fr_plonk_quotient.wgsl", + "/shaders/curves/bls12_381/g1_io.wgsl", + "/shaders/curves/bls12_381/g2_arith.wgsl", + "/shaders/curves/bls12_381/g2_io.wgsl", + "/shaders/curves/bls12_377/fp_arith.wgsl", + "/shaders/curves/bls12_377/fr_arith.wgsl", + "/shaders/curves/bls12_377/fr_vector.wgsl", + "/shaders/curves/bls12_377/fr_ntt.wgsl", + "/shaders/curves/bls12_377/fr_plonk_quotient.wgsl", + "/shaders/curves/bls12_377/g1_io.wgsl", + "/shaders/curves/bls12_377/g2_arith.wgsl", + "/shaders/curves/bls12_377/g2_io.wgsl", + "/shaders/common/g1_core.wgsl", + "/shaders/common/g1_ops_bindings.wgsl", + "/shaders/common/g1_ops_main.wgsl", + "/shaders/common/g1_msm_bindings.wgsl", + "/shaders/common/g2_ops_bindings.wgsl", + "/shaders/common/g2_ops_main.wgsl", + "/shaders/common/g2_msm_bindings.wgsl", + "/shaders/common/g1_msm_jac.wgsl", + "/shaders/common/g2_msm_jac.wgsl", +]; + +function escapeForTemplateLiteral(text) { + return text.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${"); +} + +const entries = []; +for (const urlPath of SHADER_PATHS) { + const filePath = resolve(repoRoot, urlPath.slice(1)); // strip leading / + const content = readFileSync(filePath, "utf8"); + const escaped = escapeForTemplateLiteral(content); + entries.push(` ${JSON.stringify(urlPath)}: \`${escaped}\``); +} + +const output = `\ +// Auto-generated by web/scripts/bundle-shaders.mjs — do not edit manually. +// Run \`npm run build:shaders\` to regenerate. +import { setBundledShaders } from "./shaders.js"; + +setBundledShaders({ +${entries.join(",\n")}, +}); +`; + +writeFileSync(outPath, output, "utf8"); +console.log(`Wrote ${SHADER_PATHS.length} shaders to ${outPath}`); diff --git a/backend/accelerated/webgpu/web/src/curvegpu/api.ts b/backend/accelerated/webgpu/web/src/curvegpu/api.ts new file mode 100644 index 0000000000..1e6851f548 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/api.ts @@ -0,0 +1,721 @@ +import type { FieldShape } from "./types.js"; +import type { BufferPool } from "./buffer_pool.js"; + +export type { CurveGPUError, CurveGPUNotSupportedError, CurveGPUDeviceLostError, CurveGPUShaderError } from "./errors.js"; + +/** + * Curves currently exposed by the browser library surface. + */ +export type SupportedCurveID = "bn254" | "bls12_381" | "bls12_377"; + +/** + * Canonical byte representation for field and scalar values. + * + * For `fr` and `fp` module operations, values are little-endian byte strings + * in Montgomery form unless explicitly converted with + * `toMontgomery` / `fromMontgomery`. + * + * For G1 scalar multiplication and MSM, scalars are little-endian 32-byte + * scalar-field elements in regular form. + */ +export type CurveGPUElementBytes = Uint8Array; + +/** + * Affine G1 point represented as little-endian field-element byte strings. + * + * Coordinates use the same field representation as the curve fixtures and + * shader interfaces for the selected curve. + */ +export interface CurveGPUAffinePoint { + x: Uint8Array; + y: Uint8Array; +} + +/** + * Jacobian G1 point represented as little-endian field-element byte strings. + */ +export interface CurveGPUJacobianPoint { + x: Uint8Array; + y: Uint8Array; + z: Uint8Array; +} + +/** + * Quadratic-extension field element represented as two base-field coordinates. + * + * Values are little-endian byte strings in the same base-field representation + * used by the selected curve's `fp` module. + */ +export interface CurveGPUFp2Element { + c0: Uint8Array; + c1: Uint8Array; +} + +/** + * Affine G2 point represented over the quadratic extension field. + */ +export interface CurveGPUG2AffinePoint { + x: CurveGPUFp2Element; + y: CurveGPUFp2Element; +} + +/** + * Jacobian G2 point represented over the quadratic extension field. + */ +export interface CurveGPUG2JacobianPoint { + x: CurveGPUFp2Element; + y: CurveGPUFp2Element; + z: CurveGPUFp2Element; +} + +/** + * Options for affine MSM execution. + * + * All fields are optional; sensible defaults are chosen automatically. + */ +export type CurveGPUMSMOptions = { + /** + * Number of independent MSM instances to compute in a single call. + * Each instance uses `termsPerInstance` consecutive base/scalar pairs + * from the input arrays. Defaults to `1`. + */ + count?: number; + /** + * Number of (base, scalar) pairs per MSM instance. When `count` is 1 and + * this field is omitted, the full length of the input arrays is used. + */ + termsPerInstance?: number; + /** + * Pippenger window size in bits. If omitted, the library selects a window + * size based on `termsPerInstance` via `bestWindow()`. + */ + window?: number; + /** + * Maximum number of terms processed per GPU dispatch chunk. Smaller values + * reduce peak GPU memory usage at the cost of more dispatches. Defaults to + * `256`. + */ + maxChunkSize?: number; +}; + +/** + * Supported packed point encodings for bulk APIs. + * + * `"jacobian_x_y_z_le"` — Six consecutive little-endian field-element byte + * strings in the order `x, y, z`. For affine points represented in Jacobian + * form set `z` to the Montgomery-form one element; for the point at infinity + * leave all six components zero-filled. + */ +export type CurveGPUPackedPointLayout = "jacobian_x_y_z_le"; + +/** + * Subset of device limits that matter for the current curve workloads. + */ +export type CurveGPURequestedLimits = { + /** Maximum byte size of a single storage buffer binding. */ + maxStorageBufferBindingSize?: number; + /** Maximum byte size of a GPU buffer. */ + maxBufferSize?: number; +}; + +/** + * Human-readable adapter details useful for logging, debugging, and telemetry. + */ +export type CurveGPUAdapterDiagnostics = { + /** GPU vendor string, e.g. `"apple"`, `"nvidia"`, `"intel"`. */ + vendor?: string; + /** GPU architecture string, e.g. `"common-3"`. */ + architecture?: string; + /** Free-form GPU description provided by the driver. */ + description?: string; + /** Whether the browser selected a software (fallback) adapter. */ + isFallbackAdapter?: boolean; +}; + +/** + * Options for acquiring a browser WebGPU context. + */ +export type CurveGPUContextOptions = { + /** + * Hint to the browser about which GPU to prefer on multi-GPU systems. + * `"high-performance"` requests a discrete GPU; `"low-power"` requests an + * integrated GPU. Defaults to the browser's own selection. + */ + powerPreference?: GPUPowerPreference; + /** + * When `true` (the default), the adapter's reported limits for + * `maxStorageBufferBindingSize` and `maxBufferSize` are propagated to + * `requestDevice`. Set to `false` to request a device with default limits, + * which may restrict the maximum MSM size. + */ + requireAdapterLimits?: boolean; + /** + * Explicit device limits to request, overriding the adapter-derived values. + * Useful when you know the exact buffer sizes your workload needs. + */ + requiredLimits?: CurveGPURequestedLimits; + /** Enable verbose debug logging from GPU operations. Defaults to `false`. */ + debug?: boolean; +}; + +/** + * Shared browser WebGPU context for all curve operations. + * + * This is the top-level object a consumer creates once, then reuses for + * field, group, NTT, and MSM work. + */ +export interface CurveGPUContext { + /** The underlying WebGPU adapter selected by the browser. */ + readonly adapter: GPUAdapter; + /** The WebGPU logical device used for all GPU operations. */ + readonly device: GPUDevice; + /** Adapter metadata, or `null` if `requestAdapterInfo()` is unavailable. */ + readonly adapterInfo: GPUAdapterInfo | null; + /** Human-readable diagnostics derived from the adapter. */ + readonly diagnostics: CurveGPUAdapterDiagnostics; + /** Limits that were requested when the device was created. */ + readonly requestedLimits: CurveGPURequestedLimits; + /** Whether verbose debug logging is enabled for GPU operations. */ + readonly debug: boolean; + /** Maximum compute workgroup size supported by the device. */ + readonly maxWorkgroupSize: number; + /** GPU buffer pool shared across all operations on this context. */ + readonly bufferPool: BufferPool; + /** + * Resolves when the GPU device is lost. + * + * Consumers can attach a handler to this promise to react to unexpected + * device loss (driver crash, GPU reset, tab backgrounded on mobile, etc.). + * The resolved value is the browser's `GPUDeviceLostInfo` object. + */ + readonly deviceLost: Promise; + /** + * Release any library-owned resources associated with the context. + * + * Drains the buffer pool and performs any other cleanup. Browser WebGPU + * device lifetime is still managed by the browser, so this is a logical + * shutdown hook rather than a hard device destroy. + */ + close(): void; +} + +/** + * Field arithmetic bound to a specific curve field. + * + * All methods except `toMontgomery` and `fromMontgomery` operate on + * Montgomery-form little-endian byte strings. + * + * Batch variants execute the same operation element-wise over equal-length + * slices. + */ +export interface FieldModule { + readonly context: CurveGPUContext; + readonly curve: SupportedCurveID; + readonly field: "fr" | "fp"; + readonly shape: FieldShape; + readonly byteSize: number; + /** Return the additive identity as a zero-filled byte string. */ + zero(): CurveGPUElementBytes; + /** Copy one element through the GPU implementation. */ + copy(value: CurveGPUElementBytes): Promise; + copyBatch(values: readonly CurveGPUElementBytes[]): Promise; + /** Return the multiplicative identity in Montgomery form. */ + montOne(): Promise; + /** Check modular equality. */ + equal(a: CurveGPUElementBytes, b: CurveGPUElementBytes): Promise; + equalBatch(a: readonly CurveGPUElementBytes[], b: readonly CurveGPUElementBytes[]): Promise; + /** Modular addition. */ + add(a: CurveGPUElementBytes, b: CurveGPUElementBytes): Promise; + addBatch(a: readonly CurveGPUElementBytes[], b: readonly CurveGPUElementBytes[]): Promise; + /** Modular subtraction. */ + sub(a: CurveGPUElementBytes, b: CurveGPUElementBytes): Promise; + subBatch(a: readonly CurveGPUElementBytes[], b: readonly CurveGPUElementBytes[]): Promise; + /** Modular negation. */ + neg(value: CurveGPUElementBytes): Promise; + negBatch(values: readonly CurveGPUElementBytes[]): Promise; + /** Modular doubling. */ + double(value: CurveGPUElementBytes): Promise; + doubleBatch(values: readonly CurveGPUElementBytes[]): Promise; + /** Modular multiplication. */ + mul(a: CurveGPUElementBytes, b: CurveGPUElementBytes): Promise; + mulBatch(a: readonly CurveGPUElementBytes[], b: readonly CurveGPUElementBytes[]): Promise; + /** Element-wise multiplication over packed Montgomery-form field elements. */ + mulPackedMont(a: Uint8Array, b: Uint8Array): Promise; + /** Modular squaring. */ + square(value: CurveGPUElementBytes): Promise; + squareBatch(values: readonly CurveGPUElementBytes[]): Promise; + /** Reduce a value into canonical Montgomery form. */ + normalizeMont(value: CurveGPUElementBytes): Promise; + normalizeMontBatch(values: readonly CurveGPUElementBytes[]): Promise; + /** Convert a regular little-endian field element into Montgomery form. */ + toMontgomery(value: CurveGPUElementBytes): Promise; + toMontgomeryBatch(values: readonly CurveGPUElementBytes[]): Promise; + /** + * Convert a packed sequence of regular little-endian field elements into + * Montgomery form. + */ + toMontgomeryPacked(values: Uint8Array): Promise; + /** Convert a Montgomery-form element back into regular little-endian bytes. */ + fromMontgomery(value: CurveGPUElementBytes): Promise; + fromMontgomeryBatch(values: readonly CurveGPUElementBytes[]): Promise; + /** + * Convert a packed sequence of Montgomery-form field elements back into + * regular little-endian bytes. + */ + fromMontgomeryPacked(values: Uint8Array): Promise; +} + +/** + * G1 point operations for a specific curve. + * + * Affine inputs are passed as `x` and `y` byte strings. Jacobian outputs use + * three coordinates in the same field representation as the selected curve. + */ +export interface G1Module { + readonly context: CurveGPUContext; + readonly curve: SupportedCurveID; + readonly coordinateBytes: number; + readonly pointBytes: number; + readonly zeroHex: string; + /** Return the affine point at infinity (all-zero coordinates). */ + affineInfinity(): CurveGPUAffinePoint; + /** Return the zero Jacobian point (all-zero coordinates) synchronously. */ + jacobianZero(): CurveGPUJacobianPoint; + /** Copy a Jacobian point through the GPU implementation. */ + copy(point: CurveGPUJacobianPoint): Promise; + copyBatch(points: readonly CurveGPUJacobianPoint[]): Promise; + /** Construct the Jacobian point at infinity via the GPU. */ + jacobianInfinity(): Promise; + jacobianInfinityBatch(count: number): Promise; + /** Lift affine points into Jacobian coordinates. */ + affineToJacobian(point: CurveGPUAffinePoint): Promise; + affineToJacobianBatch(points: readonly CurveGPUAffinePoint[]): Promise; + /** Negate Jacobian points. */ + negJacobian(point: CurveGPUJacobianPoint): Promise; + negJacobianBatch(points: readonly CurveGPUJacobianPoint[]): Promise; + /** Double Jacobian points. */ + doubleJacobian(point: CurveGPUJacobianPoint): Promise; + doubleJacobianBatch(points: readonly CurveGPUJacobianPoint[]): Promise; + /** Add an affine point into a Jacobian accumulator (mixed addition). */ + addMixed(point: CurveGPUJacobianPoint, affine: CurveGPUAffinePoint): Promise; + addMixedBatch(points: readonly CurveGPUJacobianPoint[], affine: readonly CurveGPUAffinePoint[]): Promise; + /** + * Convert Jacobian points to affine coordinates. + * + * The returned object keeps the `z` field for compatibility with existing + * fixtures; consumers that need a strict affine point should use `x` and `y`. + */ + jacobianToAffine(point: CurveGPUJacobianPoint): Promise; + jacobianToAffineBatch(points: readonly CurveGPUJacobianPoint[]): Promise; + /** Add two affine points and return the result in Jacobian form. */ + affineAdd(a: CurveGPUAffinePoint, b: CurveGPUAffinePoint): Promise; + affineAddBatch(a: readonly CurveGPUAffinePoint[], b: readonly CurveGPUAffinePoint[]): Promise; + /** Multiply an affine base by a scalar and return the result in Jacobian form. */ + scalarMulAffine(base: CurveGPUAffinePoint, scalar: CurveGPUElementBytes): Promise; + scalarMulAffineBatch(bases: readonly CurveGPUAffinePoint[], scalars: readonly CurveGPUElementBytes[]): Promise; + /** Add two affine points and return the result in affine form. */ + addAffine(a: CurveGPUAffinePoint, b: CurveGPUAffinePoint): Promise; + addAffineBatch(a: readonly CurveGPUAffinePoint[], b: readonly CurveGPUAffinePoint[]): Promise; + /** Negate an affine point and return the result in affine form. */ + negAffine(point: CurveGPUAffinePoint): Promise; + negAffineBatch(points: readonly CurveGPUAffinePoint[]): Promise; + /** Double an affine point and return the result in affine form. */ + doubleAffine(point: CurveGPUAffinePoint): Promise; + doubleAffineBatch(points: readonly CurveGPUAffinePoint[]): Promise; + /** Multiply an affine base by a scalar and return the result in affine form. */ + scalarMulAffineResult(base: CurveGPUAffinePoint, scalar: CurveGPUElementBytes): Promise; + scalarMulAffineResultBatch(bases: readonly CurveGPUAffinePoint[], scalars: readonly CurveGPUElementBytes[]): Promise; +} + +/** + * G2 point operations for a specific curve. + * + * Coordinates are represented over the quadratic extension field as `{c0, c1}` + * byte-string pairs. The arithmetic rules mirror `G1Module` but operate on + * `CurveGPUG2AffinePoint` and `CurveGPUG2JacobianPoint` types. + */ +export interface G2Module { + readonly context: CurveGPUContext; + readonly curve: SupportedCurveID; + /** Byte size of one base-field component (`c0` or `c1`). */ + readonly componentBytes: number; + /** Byte size of one G2 coordinate (two components: `2 * componentBytes`). */ + readonly coordinateBytes: number; + /** Byte size of one G2 Jacobian point (six components: `6 * componentBytes`). */ + readonly pointBytes: number; + /** Return the affine G2 point at infinity (all-zero components). */ + affineInfinity(): CurveGPUG2AffinePoint; + /** Return the zero G2 Jacobian point (all-zero components) synchronously. */ + jacobianZero(): CurveGPUG2JacobianPoint; + /** Copy a G2 Jacobian point through the GPU implementation. */ + copy(point: CurveGPUG2JacobianPoint): Promise; + copyBatch(points: readonly CurveGPUG2JacobianPoint[]): Promise; + /** Construct the G2 Jacobian point at infinity via the GPU. */ + jacobianInfinity(): Promise; + jacobianInfinityBatch(count: number): Promise; + /** Lift affine G2 points into Jacobian coordinates. */ + affineToJacobian(point: CurveGPUG2AffinePoint): Promise; + affineToJacobianBatch(points: readonly CurveGPUG2AffinePoint[]): Promise; + /** Negate G2 Jacobian points. */ + negJacobian(point: CurveGPUG2JacobianPoint): Promise; + negJacobianBatch(points: readonly CurveGPUG2JacobianPoint[]): Promise; + /** Double G2 Jacobian points. */ + doubleJacobian(point: CurveGPUG2JacobianPoint): Promise; + doubleJacobianBatch(points: readonly CurveGPUG2JacobianPoint[]): Promise; + /** Add an affine G2 point into a Jacobian accumulator (mixed addition). */ + addMixed(point: CurveGPUG2JacobianPoint, affine: CurveGPUG2AffinePoint): Promise; + addMixedBatch(points: readonly CurveGPUG2JacobianPoint[], affine: readonly CurveGPUG2AffinePoint[]): Promise; + /** + * Convert G2 Jacobian points to affine coordinates. + * + * Returns affine points; the `z` component is not present in the result type. + */ + jacobianToAffine(point: CurveGPUG2JacobianPoint): Promise; + jacobianToAffineBatch(points: readonly CurveGPUG2JacobianPoint[]): Promise; + /** Add two affine G2 points and return the result in Jacobian form. */ + affineAdd(a: CurveGPUG2AffinePoint, b: CurveGPUG2AffinePoint): Promise; + affineAddBatch(a: readonly CurveGPUG2AffinePoint[], b: readonly CurveGPUG2AffinePoint[]): Promise; + /** Multiply an affine G2 base by a scalar and return the result in Jacobian form. */ + scalarMulAffine(base: CurveGPUG2AffinePoint, scalar: CurveGPUElementBytes): Promise; + scalarMulAffineBatch(bases: readonly CurveGPUG2AffinePoint[], scalars: readonly CurveGPUElementBytes[]): Promise; + /** Add two affine G2 points and return the result in affine form. */ + addAffine(a: CurveGPUG2AffinePoint, b: CurveGPUG2AffinePoint): Promise; + addAffineBatch(a: readonly CurveGPUG2AffinePoint[], b: readonly CurveGPUG2AffinePoint[]): Promise; + /** Negate an affine G2 point and return the result in affine form. */ + negAffine(point: CurveGPUG2AffinePoint): Promise; + negAffineBatch(points: readonly CurveGPUG2AffinePoint[]): Promise; + /** Double an affine G2 point and return the result in affine form. */ + doubleAffine(point: CurveGPUG2AffinePoint): Promise; + doubleAffineBatch(points: readonly CurveGPUG2AffinePoint[]): Promise; + /** Multiply an affine G2 base by a scalar and return the result in affine form. */ + scalarMulAffineResult(base: CurveGPUG2AffinePoint, scalar: CurveGPUElementBytes): Promise; + scalarMulAffineResultBatch(bases: readonly CurveGPUG2AffinePoint[], scalars: readonly CurveGPUElementBytes[]): Promise; +} + +/** + * Scalar-field NTT module for a specific curve. + */ +export interface NTTModule { + readonly context: CurveGPUContext; + readonly curve: SupportedCurveID; + readonly field: "fr"; + /** Report the power-of-two domain sizes available from loaded metadata. */ + supportedSizes(): Promise; + /** Run the forward NTT over a power-of-two batch of Montgomery-form values. */ + forward(values: readonly CurveGPUElementBytes[]): Promise; + /** Run the inverse NTT over a power-of-two batch of Montgomery-form values. */ + inverse(values: readonly CurveGPUElementBytes[]): Promise; + /** Run the forward NTT over packed regular little-endian field elements. */ + forwardPackedRegular(values: Uint8Array): Promise; + /** Run the inverse NTT over packed regular little-endian field elements. */ + inversePackedRegular(values: Uint8Array): Promise; + /** Run the inverse NTT over bit-reversed packed regular little-endian field elements. */ + inverseBitReversePackedRegular(values: Uint8Array): Promise; + /** Convert packed regular little-endian values from Lagrange coset form to canonical regular form. */ + inverseCosetPackedRegular(values: Uint8Array): Promise; + /** Run the forward NTT over packed Montgomery-form field elements. */ + forwardPackedMont(values: Uint8Array): Promise; + /** Run the inverse NTT over packed Montgomery-form field elements. */ + inversePackedMont(values: Uint8Array): Promise; + /** Run forward NTTs over packed Montgomery-form vectors of equal size. */ + forwardPackedMontBatch(values: Uint8Array, vectorSize: number, vectorCount: number): Promise; + /** Run inverse NTTs over packed Montgomery-form vectors of equal size. */ + inversePackedMontBatch(values: Uint8Array, vectorSize: number, vectorCount: number): Promise; + /** + * Convert packed regular little-endian values from bit-reversed Lagrange + * coset form to canonical regular form. + */ + inverseCosetBitReversePackedRegular(values: Uint8Array): Promise; + /** Precompute and cache domain metadata for a power-of-two domain size. */ + prewarmDomain(size: number): Promise; +} + +/** + * Groth16 quotient helpers. + * + * These methods are separated from the generic NTT module even though they reuse + * the same NTT/vector kernels internally. + */ +export interface Groth16QuotientModule { + readonly context: CurveGPUContext; + readonly curve: SupportedCurveID; + /** + * Compute the Groth16 quotient vector H from packed regular little-endian + * A, B, and C witness polynomials already padded to the FFT domain size. + * + * The returned packed vector is in regular little-endian coefficient form + * and has the same element count as the padded inputs. + */ + computeGroth16QuotientPackedRegular(a: Uint8Array, b: Uint8Array, c: Uint8Array): Promise; + /** + * Compute the Groth16 quotient vector H from packed Montgomery little-endian + * A, B, and C witness polynomials already padded to the FFT domain size. + * + * The returned packed vector is in regular little-endian coefficient form + * and has the same element count as the padded inputs. + */ + computeGroth16QuotientPackedMont(a: Uint8Array, b: Uint8Array, c: Uint8Array): Promise; + /** Precompute and cache Groth16 quotient-domain data for a power-of-two domain size. */ + prewarmGroth16QuotientDomain(size: number): Promise; +} + +export type Groth16ProvingKeyFormat = "serialized" | "dump"; +export type Groth16RuntimeKind = "webgpu" | "native"; + +export type Groth16RuntimeOptions = { + /** Optional URL for Go's wasm_exec.js runtime shim. Defaults to the package asset. */ + wasmExecURL?: string; + /** Optional URL for the WebGPU-accelerated Groth16 Go WASM runtime. Defaults to the package asset. */ + webgpuWasmURL?: string; + /** Optional URL for the native gnark Groth16 Go WASM runtime. Defaults to the package asset. */ + nativeWasmURL?: string; +}; + +export interface Groth16Handle { + /** Release the corresponding Go WASM runtime handle. */ + dispose(): Promise; +} + +export interface Groth16ConstraintSystem extends Groth16Handle { + /** Number of constraints reported by the deserialized constraint system. */ + readonly constraints: number; +} + +export type Groth16ProvingKey = Groth16Handle; +export type Groth16VerificationKey = Groth16Handle; + +/** + * Browser Groth16 proof helpers backed by a long-lived Go WASM runtime. + */ +export interface Groth16Module extends Groth16QuotientModule { + /** + * Load the Go WASM Groth16 runtime. + * + * Defaults to the WebGPU runtime and package-shipped assets. Override URLs + * when serving the runtime from an application asset path or CDN. + */ + loadRuntime(options?: Groth16RuntimeOptions & { kind?: Groth16RuntimeKind }): Promise; + /** Deserialize a gnark Groth16 constraint system. */ + readConstraintSystem(bytes: Uint8Array): Promise; + /** Deserialize a gnark Groth16 proving key. */ + readProvingKey(bytes: Uint8Array, options?: { format?: Groth16ProvingKeyFormat }): Promise; + /** Deserialize a gnark Groth16 verification key. */ + readVerificationKey(bytes: Uint8Array): Promise; + /** Precompute browser-side proving key caches. */ + prepareProvingKey(pk: Groth16ProvingKey): Promise; + /** Prove with a gnark binary witness and return gnark-serialized proof bytes. */ + prove(ccs: Groth16ConstraintSystem, pk: Groth16ProvingKey, witness: Uint8Array): Promise; + /** Verify gnark-serialized proof bytes against a gnark binary public witness. */ + verify(proof: Uint8Array, vk: Groth16VerificationKey, publicWitness: Uint8Array): Promise; + /** + * Encode flat regular field values as a gnark binary witness. + * + * Values must be ordered `[public | private]`. The binary witness protocol + * stores field elements as fixed-width big-endian bytes. + */ + encodeWitness(values: readonly bigint[], options: { publicCount: number }): Uint8Array; +} + +export type PlonkProvingKeyFormat = "serialized" | "unsafe"; +export type PlonkRuntimeKind = "webgpu" | "native"; + +export type PlonkRuntimeOptions = { + /** Optional URL for Go's wasm_exec.js runtime shim. Defaults to the package asset. */ + wasmExecURL?: string; + /** Optional URL for the WebGPU-accelerated PLONK Go WASM runtime. Defaults to the package asset. */ + webgpuWasmURL?: string; + /** Optional URL for the native gnark PLONK Go WASM runtime. Defaults to the package asset. */ + nativeWasmURL?: string; +}; + +export interface PlonkHandle { + /** Release the corresponding Go WASM runtime handle. */ + dispose(): Promise; +} + +export interface PlonkConstraintSystem extends PlonkHandle { + /** Number of constraints reported by the deserialized constraint system. */ + readonly constraints: number; +} + +export type PlonkProvingKey = PlonkHandle; +export type PlonkVerificationKey = PlonkHandle; + +/** + * Browser PLONK proof helpers backed by a long-lived Go WASM runtime. + */ +export interface PlonkModule { + readonly context: CurveGPUContext; + readonly curve: SupportedCurveID; + /** + * Load the Go WASM PLONK runtime. + * + * Defaults to the WebGPU runtime and package-shipped assets. Override URLs + * when serving the runtime from an application asset path or CDN. + */ + loadRuntime(options?: PlonkRuntimeOptions & { kind?: PlonkRuntimeKind }): Promise; + /** Deserialize a gnark PLONK constraint system. */ + readConstraintSystem(bytes: Uint8Array): Promise; + /** Deserialize a gnark PLONK proving key. */ + readProvingKey(bytes: Uint8Array, options?: { format?: PlonkProvingKeyFormat }): Promise; + /** Deserialize a gnark PLONK verification key. */ + readVerificationKey(bytes: Uint8Array): Promise; + /** + * Precompute browser-side proving key caches. + * + * Passing the constraint system lets the WebGPU runtime prepare PLONK + * trace-derived caches outside the timed prove path. + */ + prepareProvingKey(pk: PlonkProvingKey, ccs?: PlonkConstraintSystem): Promise; + /** Prove with a gnark binary witness and return gnark-serialized proof bytes. */ + prove(ccs: PlonkConstraintSystem, pk: PlonkProvingKey, witness: Uint8Array): Promise; + /** Verify gnark-serialized proof bytes against a gnark binary public witness. */ + verify(proof: Uint8Array, vk: PlonkVerificationKey, publicWitness: Uint8Array): Promise; + /** + * Encode flat regular field values as a gnark binary witness. + * + * Values must be ordered `[public | private]`. The binary witness protocol + * stores field elements as fixed-width big-endian bytes. + */ + encodeWitness(values: readonly bigint[], options: { publicCount: number }): Uint8Array; +} + +/** + * Multi-scalar multiplication module over G1 affine bases. + */ +export interface G1MSMModule { + readonly context: CurveGPUContext; + readonly curve: SupportedCurveID; + readonly group: "g1"; + /** Choose the default Pippenger window size for a given term count. */ + bestWindow(termCount: number): number; + /** Run a single affine-base Pippenger MSM and return the result in Jacobian form. */ + pippengerAffine( + bases: readonly CurveGPUAffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options?: CurveGPUMSMOptions, + ): Promise; + /** Run a single affine-base Pippenger MSM and return the result in affine form. */ + pippengerAffineResult( + bases: readonly CurveGPUAffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options?: CurveGPUMSMOptions, + ): Promise; + /** + * Run a batched affine-base Pippenger MSM. + * + * `bases` and `scalars` are interleaved: the first `termsPerInstance` pairs + * belong to instance 0, the next `termsPerInstance` pairs to instance 1, etc. + * `options.count` and `options.termsPerInstance` must both be provided. + */ + pippengerAffineBatch( + bases: readonly CurveGPUAffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options: CurveGPUMSMOptions, + ): Promise; + /** + * Run affine-base Pippenger MSM from packed bytes. + * + * `basesPacked` is currently expected in `jacobian_x_y_z_le` layout with one + * packed point per term. For ordinary affine points, `z` should be the + * Montgomery-form one element and infinity points should remain zero-filled. + * + * `scalarsPacked` is a packed sequence of regular-form 32-byte scalars. + * + * The result is returned in the same packed `jacobian_x_y_z_le` layout. + */ + pippengerPackedJacobianBases( + basesPacked: Uint8Array, + scalarsPacked: Uint8Array, + options: CurveGPUMSMOptions & { layout?: CurveGPUPackedPointLayout }, + ): Promise; +} + +/** + * Multi-scalar multiplication module over G2 affine bases. + * + * The API mirrors `G1MSMModule` but operates on G2 points over the quadratic + * extension field. Bases are supplied in affine form; results are returned in + * Jacobian form unless an `AffineResult` variant is used. + */ +export interface G2MSMModule { + readonly context: CurveGPUContext; + readonly curve: SupportedCurveID; + readonly group: "g2"; + /** Choose the default Pippenger window size for a given term count. */ + bestWindow(termCount: number): number; + /** Run a single affine-base G2 Pippenger MSM and return the result in Jacobian form. */ + pippengerAffine( + bases: readonly CurveGPUG2AffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options?: CurveGPUMSMOptions, + ): Promise; + /** Run a single affine-base G2 Pippenger MSM and return the result in affine form. */ + pippengerAffineResult( + bases: readonly CurveGPUG2AffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options?: CurveGPUMSMOptions, + ): Promise; + /** + * Run a batched affine-base G2 Pippenger MSM. + * + * `bases` and `scalars` are interleaved: the first `termsPerInstance` pairs + * belong to instance 0, the next `termsPerInstance` pairs to instance 1, etc. + * `options.count` and `options.termsPerInstance` must both be provided. + */ + pippengerAffineBatch( + bases: readonly CurveGPUG2AffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options: CurveGPUMSMOptions, + ): Promise; + /** + * Run G2 Pippenger MSM from packed bytes. + * + * `basesPacked` must be in `jacobian_x_y_z_le` layout: six consecutive + * base-field components per point (`x.c0, x.c1, y.c0, y.c1, z.c0, z.c1`). + * Set `z.c0` to the Montgomery-form one element for affine inputs; leave all + * components zero for the point at infinity. + * + * `scalarsPacked` is a packed sequence of regular-form 32-byte scalars. + * + * The result is returned in the same packed `jacobian_x_y_z_le` layout, + * one Jacobian point per MSM instance. + */ + pippengerPackedJacobianBases( + basesPacked: Uint8Array, + scalarsPacked: Uint8Array, + options: CurveGPUMSMOptions & { layout?: CurveGPUPackedPointLayout }, + ): Promise; +} + +/** + * High-level curve module returned by the library. + * + * This groups the curve-specific submodules behind one stable object per + * supported curve. Obtain an instance via `createCurveModule` (or the + * curve-specific helpers `createBN254` / `createBLS12381`). + */ +export interface CurveModule { + /** The curve this module was created for. */ + readonly id: SupportedCurveID; + /** The WebGPU context shared across all submodules. */ + readonly context: CurveGPUContext; + /** Scalar-field (`Fr`) arithmetic. */ + readonly fr: FieldModule; + /** Base-field (`Fp`) arithmetic. */ + readonly fp: FieldModule; + /** G1 point operations. */ + readonly g1: G1Module; + /** G2 point operations over the quadratic extension field. */ + readonly g2: G2Module; + /** Scalar-field NTT. */ + readonly ntt: NTTModule; + /** Groth16-specific scalar-field helpers. */ + readonly groth16: Groth16Module; + /** PLONK proof helpers. */ + readonly plonk: PlonkModule; + /** Multi-scalar multiplication over G1. */ + readonly g1msm: G1MSMModule; + /** Multi-scalar multiplication over G2. */ + readonly g2msm: G2MSMModule; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/browser_utils.ts b/backend/accelerated/webgpu/web/src/curvegpu/browser_utils.ts new file mode 100644 index 0000000000..baddf2893a --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/browser_utils.ts @@ -0,0 +1,103 @@ +export function mustElement(value: T | null, name: string): T { + if (value === null) { + throw new Error(`missing element: ${name}`); + } + return value; +} + +export function createPageUI(statusEl: HTMLElement | null, logEl: HTMLElement | null): { + setStatus: (text: string) => void; + setPageState: (state: string) => void; + writeLog: (lines: string[]) => void; +} { + return { + setStatus(text: string): void { + mustElement(statusEl, "status").textContent = text; + }, + setPageState(state: string): void { + document.body.dataset.status = state; + }, + writeLog(lines: string[]): void { + mustElement(logEl, "log").textContent = lines.join("\n"); + }, + }; +} + +export async function fetchText(path: string): Promise { + const response = await fetch(path); + if (!response.ok) { + throw new Error(`failed to load ${path}: ${response.status} ${response.statusText}`); + } + return response.text(); +} + +export async function fetchJSON(path: string): Promise { + return JSON.parse(await fetchText(path)) as T; +} + +export async function fetchBytes(path: string): Promise { + const response = await fetch(path); + if (!response.ok) { + throw new Error(`failed to load ${path}: ${response.status} ${response.statusText}`); + } + return new Uint8Array(await response.arrayBuffer()); +} + +export async function getAdapterInfo(adapter: GPUAdapter): Promise { + const adapterWithInfo = adapter as GPUAdapter & { + info?: GPUAdapterInfo; + requestAdapterInfo?: () => Promise; + }; + if (adapterWithInfo.info) { + return adapterWithInfo.info; + } + if (typeof adapterWithInfo.requestAdapterInfo === "function") { + try { + return await adapterWithInfo.requestAdapterInfo(); + } catch { + return null; + } + } + return null; +} + +export async function appendAdapterDiagnostics(adapter: GPUAdapter, lines: string[]): Promise { + const adapterWithFallback = adapter as GPUAdapter & { isFallbackAdapter?: boolean }; + if ("isFallbackAdapter" in adapterWithFallback) { + lines.push(`adapter.isFallbackAdapter = ${String(adapterWithFallback.isFallbackAdapter)}`); + } + const info = await getAdapterInfo(adapter); + if (!info) { + lines.push("adapter.info = unavailable"); + return; + } + if (info.vendor) { + lines.push(`adapter.vendor = ${info.vendor}`); + } + if (info.architecture) { + lines.push(`adapter.architecture = ${info.architecture}`); + } +} + +export function hexToBytes(hex: string): Uint8Array { + if (hex.length % 2 !== 0) { + throw new Error(`invalid hex length ${hex.length}`); + } + const out = new Uint8Array(hex.length / 2); + for (let i = 0; i < out.length; i += 1) { + out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return out; +} + +export function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +export async function yieldToBrowser(): Promise { + await new Promise((resolve) => { + setTimeout(() => { + requestAnimationFrame(() => resolve()); + }, 0); + }); +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/buffer_pool.ts b/backend/accelerated/webgpu/web/src/curvegpu/buffer_pool.ts new file mode 100644 index 0000000000..e7943b6511 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/buffer_pool.ts @@ -0,0 +1,94 @@ +function nextPowerOfTwo(n: number): number { + let p = 1; + while (p < n) { + p *= 2; + } + return p; +} + +type PoolKey = string; +type PoolEntry = { buffer: GPUBuffer; size: number }; + +function poolKey(size: number, usage: number): PoolKey { + return `${size}:${usage}`; +} + +/** + * Per-device GPU buffer pool. Caches released buffers keyed by + * (rounded-size, usage) and re-issues them on acquire, avoiding + * repeated GPU allocations on hot paths. + * + * Sizes are rounded up to the next power of two to reduce fragmentation. + * Total pooled memory is capped at `maxPooledBytes` (default 64 MB). + * Buffers that would exceed the cap are destroyed rather than pooled. + */ +export class BufferPool { + private readonly device: GPUDevice; + private readonly maxBytes: number; + private readonly pool: Map = new Map(); + private readonly meta = new WeakMap(); + private totalBytes = 0; + + constructor(device: GPUDevice, options?: { maxPooledBytes?: number }) { + this.device = device; + this.maxBytes = options?.maxPooledBytes ?? 64 * 1024 * 1024; + } + + /** + * Return a buffer of at least `size` bytes with the given `usage`. + * May return a cached buffer from a previous `release` call. + */ + acquire(size: number, usage: number, label?: string): GPUBuffer { + const roundedSize = nextPowerOfTwo(Math.max(4, size)); + const key = poolKey(roundedSize, usage); + const entries = this.pool.get(key); + if (entries && entries.length > 0) { + const entry = entries.pop()!; + this.totalBytes -= entry.size; + return entry.buffer; + } + const buffer = this.device.createBuffer({ label, size: roundedSize, usage }); + this.meta.set(buffer, { size: roundedSize, usage }); + return buffer; + } + + /** + * Return a buffer to the pool. If the pool is at capacity, the buffer + * is destroyed instead. Do not use the buffer after calling `release`. + */ + release(buffer: GPUBuffer): void { + const m = this.meta.get(buffer); + if (!m) { + // Buffer was not created by this pool (e.g. staging buffers). Destroy it. + buffer.destroy(); + return; + } + if (this.totalBytes + m.size > this.maxBytes) { + buffer.destroy(); + this.meta.delete(buffer); + return; + } + const key = poolKey(m.size, m.usage); + let entries = this.pool.get(key); + if (!entries) { + entries = []; + this.pool.set(key, entries); + } + entries.push({ buffer, size: m.size }); + this.totalBytes += m.size; + } + + /** + * Destroy all pooled buffers and clear the pool. Call when the context + * is closed to avoid GPU memory leaks. + */ + destroy(): void { + for (const entries of this.pool.values()) { + for (const { buffer } of entries) { + buffer.destroy(); + } + } + this.pool.clear(); + this.totalBytes = 0; + } +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/context.ts b/backend/accelerated/webgpu/web/src/curvegpu/context.ts new file mode 100644 index 0000000000..9e915d2483 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/context.ts @@ -0,0 +1,110 @@ +import { getAdapterInfo } from "./browser_utils.js"; +import type { + CurveGPUAdapterDiagnostics, + CurveGPUContext, + CurveGPUContextOptions, + CurveGPURequestedLimits, +} from "./api.js"; +import { BufferPool } from "./buffer_pool.js"; +import { CurveGPUNotSupportedError } from "./errors.js"; + +type AdapterWithLimits = GPUAdapter & { + isFallbackAdapter?: boolean; + limits?: { + maxStorageBufferBindingSize?: number; + maxBufferSize?: number; + }; +}; + +function collectRequestedLimits(adapter: AdapterWithLimits, options: CurveGPUContextOptions): CurveGPURequestedLimits { + const requestedLimits: CurveGPURequestedLimits = {}; + if (options.requireAdapterLimits !== false) { + if (adapter.limits?.maxStorageBufferBindingSize !== undefined) { + requestedLimits.maxStorageBufferBindingSize = adapter.limits.maxStorageBufferBindingSize; + } + if (adapter.limits?.maxBufferSize !== undefined) { + requestedLimits.maxBufferSize = adapter.limits.maxBufferSize; + } + } + if (options.requiredLimits?.maxStorageBufferBindingSize !== undefined) { + requestedLimits.maxStorageBufferBindingSize = options.requiredLimits.maxStorageBufferBindingSize; + } + if (options.requiredLimits?.maxBufferSize !== undefined) { + requestedLimits.maxBufferSize = options.requiredLimits.maxBufferSize; + } + return requestedLimits; +} + +function buildDiagnostics(adapter: AdapterWithLimits, adapterInfo: GPUAdapterInfo | null): CurveGPUAdapterDiagnostics { + return { + vendor: adapterInfo?.vendor || undefined, + architecture: adapterInfo?.architecture || undefined, + description: adapterInfo?.description || undefined, + isFallbackAdapter: adapter.isFallbackAdapter, + }; +} + +/** + * Create the shared browser WebGPU context for the library. + * + * The context owns adapter and device acquisition. It is intended to be + * created once and reused across field, group, NTT, and MSM operations. + */ +export async function createCurveGPUContext(options: CurveGPUContextOptions = {}): Promise { + if (!navigator.gpu) { + throw new CurveGPUNotSupportedError( + "WebGPU is not supported in this browser. " + + "WebGPU requires Chrome 113+, Edge 113+, or Safari 18+. " + + "Firefox requires the dom.webgpu.enabled flag.", + ); + } + + const adapter = (await navigator.gpu.requestAdapter({ + powerPreference: options.powerPreference, + })) as AdapterWithLimits | null; + if (!adapter) { + throw new CurveGPUNotSupportedError( + "requestAdapter returned null. " + + "This can happen when no suitable GPU is available, " + + "when the browser is running in a context without GPU access, " + + "or when hardware acceleration is disabled in browser settings.", + ); + } + + const adapterInfo = await getAdapterInfo(adapter); + const requestedLimits = collectRequestedLimits(adapter, options); + const device = await adapter.requestDevice({ + requiredLimits: Object.keys(requestedLimits).length > 0 ? requestedLimits : undefined, + }); + + const debug = options.debug ?? false; + const maxWorkgroupSize = (device.limits as { maxComputeWorkgroupSizeX?: number }).maxComputeWorkgroupSizeX ?? 256; + const bufferPool = new BufferPool(device); + let closed = false; + + const deviceLost: Promise = device.lost.then((info) => { + if (debug) { + console.debug(`[curvegpu] device lost: reason=${info.reason} message=${info.message}`); + } + return info; + }); + + return { + adapter, + device, + adapterInfo, + diagnostics: buildDiagnostics(adapter, adapterInfo), + requestedLimits, + debug, + maxWorkgroupSize, + bufferPool, + deviceLost, + close(): void { + if (closed) { + return; + } + closed = true; + bufferPool.destroy(); + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/convert.ts b/backend/accelerated/webgpu/web/src/curvegpu/convert.ts new file mode 100644 index 0000000000..edc2b74d2a --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/convert.ts @@ -0,0 +1,57 @@ +export function splitBigUint64WordsToU32(words: readonly bigint[]): Uint32Array { + if (words.length !== 4 && words.length !== 6) { + throw new Error(`unsupported host word count ${words.length}`); + } + const out = new Uint32Array(words.length * 2); + for (let i = 0; i < words.length; i += 1) { + const word = words[i]; + out[2 * i] = Number(word & 0xffff_ffffn); + out[2 * i + 1] = Number((word >> 32n) & 0xffff_ffffn); + } + return out; +} + +export function splitBytesLEToU32(bytes: Uint8Array): Uint32Array { + if (bytes.length !== 32 && bytes.length !== 48) { + throw new Error(`unsupported byte length ${bytes.length}`); + } + const out = new Uint32Array(bytes.length / 4); + for (let i = 0; i < out.length; i += 1) { + const offset = i * 4; + out[i] = + bytes[offset] | + (bytes[offset + 1] << 8) | + (bytes[offset + 2] << 16) | + (bytes[offset + 3] << 24); + } + return out; +} + +export function joinU32LimbsToBigUint64(limbs: Uint32Array): bigint[] { + if (limbs.length !== 8 && limbs.length !== 12) { + throw new Error(`unsupported gpu limb count ${limbs.length}`); + } + const out: bigint[] = []; + for (let i = 0; i < limbs.length; i += 2) { + const lo = BigInt(limbs[i]); + const hi = BigInt(limbs[i + 1]) << 32n; + out.push(lo | hi); + } + return out; +} + +export function joinU32LimbsToBytesLE(limbs: Uint32Array): Uint8Array { + if (limbs.length !== 8 && limbs.length !== 12) { + throw new Error(`unsupported gpu limb count ${limbs.length}`); + } + const out = new Uint8Array(limbs.length * 4); + for (let i = 0; i < limbs.length; i += 1) { + const limb = limbs[i]; + const offset = i * 4; + out[offset] = limb & 0xff; + out[offset + 1] = (limb >>> 8) & 0xff; + out[offset + 2] = (limb >>> 16) & 0xff; + out[offset + 3] = (limb >>> 24) & 0xff; + } + return out; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/curves.ts b/backend/accelerated/webgpu/web/src/curvegpu/curves.ts new file mode 100644 index 0000000000..2d6f2d7d86 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/curves.ts @@ -0,0 +1,341 @@ +import type { CurveModule, CurveGPUContext, SupportedCurveID } from "./api.js"; +import { createFieldModule } from "./field_module.js"; +import { createG1Module } from "./g1_module.js"; +import { createG2Module } from "./g2_module.js"; +import { createG2MSMModule } from "./g2_msm_module.js"; +import { createGroth16Module } from "./groth16_module.js"; +import { createPlonkModule } from "./plonk_module.js"; +import { createPlonkQuotientModule } from "./plonk_quotient_module.js"; +import { createG1MSMModule } from "./msm_module.js"; +import { buildJacPippengerRuntime } from "./msm_pippenger.js"; +import { createNTTModule } from "./ntt_module.js"; +import { buildPipelineRegistry } from "./pipeline_registry.js"; +import { shapeFor } from "./types.js"; + +/** + * Runtime metadata for a supported curve. + * + * This is kept separate from the page harnesses so the library can evolve + * around one shared source of curve-specific facts. + */ +export interface CurveDefinition { + readonly id: SupportedCurveID; + readonly frArithShaderPath: string; + readonly frVectorShaderPath: string; + readonly frNTTShaderPath: string; + readonly frNTTDomainPath?: string; + readonly frModulusHex?: string; + readonly fpArithShaderPath: string; + readonly g1ArithShaderParts: readonly string[]; + readonly g1MSMShaderParts: readonly string[]; + readonly g2ArithShaderParts: readonly string[]; + readonly g2MSMShaderParts: readonly string[]; + readonly coordinateBytes: number; + readonly pointBytes: number; + readonly g2CoordinateBytes: number; + readonly g2PointBytes: number; + readonly zeroHex: string; +} + +function g1OpsShaderParts(fpArithShaderPath: string, g1IOPath: string): readonly string[] { + return [ + `${fpArithShaderPath}#section=fp-types`, + `${fpArithShaderPath}#section=fp-consts`, + `${fpArithShaderPath}#section=fp-core`, + "/shaders/common/g1_core.wgsl", + "/shaders/common/g1_ops_bindings.wgsl", + g1IOPath, + "/shaders/common/g1_ops_main.wgsl", + ]; +} + +function g1MSMShaderParts(fpArithShaderPath: string, g1IOPath: string): readonly string[] { + return [ + `${fpArithShaderPath}#section=fp-types`, + `${fpArithShaderPath}#section=fp-consts`, + `${fpArithShaderPath}#section=fp-core`, + "/shaders/common/g1_core.wgsl", + "/shaders/common/g1_msm_bindings.wgsl", + g1IOPath, + "/shaders/common/g1_msm_jac.wgsl", + ]; +} + +function g2OpsShaderParts(fpArithShaderPath: string, g2ArithPath: string, g2IOPath: string): readonly string[] { + return [ + `${fpArithShaderPath}#section=fp-types`, + `${fpArithShaderPath}#section=fp-consts`, + `${fpArithShaderPath}#section=fp-core`, + "/shaders/common/g2_ops_bindings.wgsl", + g2ArithPath, + g2IOPath, + "/shaders/common/g2_ops_main.wgsl", + ]; +} + +function g2MSMShaderParts(fpArithShaderPath: string, g2ArithPath: string, g2IOPath: string): readonly string[] { + return [ + `${fpArithShaderPath}#section=fp-types`, + `${fpArithShaderPath}#section=fp-consts`, + `${fpArithShaderPath}#section=fp-core`, + "/shaders/common/g2_msm_bindings.wgsl", + g2ArithPath, + g2IOPath, + "/shaders/common/g2_msm_jac.wgsl", + ]; +} + +const CURVE_DEFINITIONS: Record = { + bn254: { + id: "bn254", + frArithShaderPath: "/shaders/curves/bn254/fr_arith.wgsl", + frVectorShaderPath: "/shaders/curves/bn254/fr_vector.wgsl", + frNTTShaderPath: "/shaders/curves/bn254/fr_ntt.wgsl", + frNTTDomainPath: "/testdata/vectors/fr/bn254_ntt_domains.json", + frModulusHex: "0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", + fpArithShaderPath: "/shaders/curves/bn254/fp_arith.wgsl", + g1ArithShaderParts: g1OpsShaderParts("/shaders/curves/bn254/fp_arith.wgsl", "/shaders/curves/bn254/g1_io.wgsl"), + g1MSMShaderParts: g1MSMShaderParts("/shaders/curves/bn254/fp_arith.wgsl", "/shaders/curves/bn254/g1_io.wgsl"), + g2ArithShaderParts: g2OpsShaderParts("/shaders/curves/bn254/fp_arith.wgsl", "/shaders/curves/bn254/g2_arith.wgsl", "/shaders/curves/bn254/g2_io.wgsl"), + g2MSMShaderParts: g2MSMShaderParts("/shaders/curves/bn254/fp_arith.wgsl", "/shaders/curves/bn254/g2_arith.wgsl", "/shaders/curves/bn254/g2_io.wgsl"), + coordinateBytes: 32, + pointBytes: 96, + g2CoordinateBytes: 64, + g2PointBytes: 192, + zeroHex: "0000000000000000000000000000000000000000000000000000000000000000", + }, + bls12_381: { + id: "bls12_381", + frArithShaderPath: "/shaders/curves/bls12_381/fr_arith.wgsl", + frVectorShaderPath: "/shaders/curves/bls12_381/fr_vector.wgsl", + frNTTShaderPath: "/shaders/curves/bls12_381/fr_ntt.wgsl", + frNTTDomainPath: "/testdata/vectors/fr/bls12_381_ntt_domains.json", + frModulusHex: "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001", + fpArithShaderPath: "/shaders/curves/bls12_381/fp_arith.wgsl", + g1ArithShaderParts: g1OpsShaderParts("/shaders/curves/bls12_381/fp_arith.wgsl", "/shaders/curves/bls12_381/g1_io.wgsl"), + g1MSMShaderParts: g1MSMShaderParts("/shaders/curves/bls12_381/fp_arith.wgsl", "/shaders/curves/bls12_381/g1_io.wgsl"), + g2ArithShaderParts: g2OpsShaderParts("/shaders/curves/bls12_381/fp_arith.wgsl", "/shaders/curves/bls12_381/g2_arith.wgsl", "/shaders/curves/bls12_381/g2_io.wgsl"), + g2MSMShaderParts: g2MSMShaderParts("/shaders/curves/bls12_381/fp_arith.wgsl", "/shaders/curves/bls12_381/g2_arith.wgsl", "/shaders/curves/bls12_381/g2_io.wgsl"), + coordinateBytes: 48, + pointBytes: 144, + g2CoordinateBytes: 96, + g2PointBytes: 288, + zeroHex: + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + bls12_377: { + id: "bls12_377", + frArithShaderPath: "/shaders/curves/bls12_377/fr_arith.wgsl", + frVectorShaderPath: "/shaders/curves/bls12_377/fr_vector.wgsl", + frNTTShaderPath: "/shaders/curves/bls12_377/fr_ntt.wgsl", + frNTTDomainPath: "/testdata/vectors/fr/bls12_377_ntt_domains.json", + frModulusHex: "0x12ab655e9a2ca55660b44d1e5c37b00159aa76fed00000010a11800000000001", + fpArithShaderPath: "/shaders/curves/bls12_377/fp_arith.wgsl", + g1ArithShaderParts: g1OpsShaderParts("/shaders/curves/bls12_377/fp_arith.wgsl", "/shaders/curves/bls12_377/g1_io.wgsl"), + g1MSMShaderParts: g1MSMShaderParts("/shaders/curves/bls12_377/fp_arith.wgsl", "/shaders/curves/bls12_377/g1_io.wgsl"), + g2ArithShaderParts: g2OpsShaderParts("/shaders/curves/bls12_377/fp_arith.wgsl", "/shaders/curves/bls12_377/g2_arith.wgsl", "/shaders/curves/bls12_377/g2_io.wgsl"), + g2MSMShaderParts: g2MSMShaderParts("/shaders/curves/bls12_377/fp_arith.wgsl", "/shaders/curves/bls12_377/g2_arith.wgsl", "/shaders/curves/bls12_377/g2_io.wgsl"), + coordinateBytes: 48, + pointBytes: 144, + g2CoordinateBytes: 96, + g2PointBytes: 288, + zeroHex: + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, +}; + +/** + * Ordered list of curves currently exposed by the browser library. + */ +export const supportedCurveIds = Object.freeze(Object.keys(CURVE_DEFINITIONS)) as readonly SupportedCurveID[]; + +/** + * Return the runtime metadata for a supported curve. + */ +export function curveDefinition(curve: SupportedCurveID): CurveDefinition { + return CURVE_DEFINITIONS[curve]; +} + +/** + * Create the high-level curve module for a supported curve. + * + * This establishes the stable public object shape that later steps populate + * with concrete field, NTT, group, and MSM operations. + */ +export async function createCurveModule(context: CurveGPUContext, curve: SupportedCurveID): Promise { + const definition = curveDefinition(curve); + const frShape = shapeFor(curve, "fr"); + const fpShape = shapeFor(curve, "fp"); + + const opsWorkgroupSize = Math.min(context.maxWorkgroupSize, 256); + const registry = await buildPipelineRegistry({ + device: context.device, + opsWorkgroupSize, + opsShaders: [ + { shaderParts: [definition.frArithShaderPath], entryPoint: "fr_ops_main", useWorkgroupOverride: true }, + { shaderParts: [definition.fpArithShaderPath], entryPoint: "fp_ops_main", useWorkgroupOverride: true }, + { shaderParts: definition.g1ArithShaderParts, entryPoint: "g1_ops_main", useWorkgroupOverride: true }, + { shaderParts: definition.g2ArithShaderParts, entryPoint: "g2_ops_main", useWorkgroupOverride: true }, + { shaderParts: [definition.frVectorShaderPath], entryPoint: "fr_vector_main", useWorkgroupOverride: true }, + { shaderParts: [definition.frNTTShaderPath], entryPoint: "fr_ntt_stage_main", useWorkgroupOverride: true }, + ], + msmShaders: [ + { + shaderParts: definition.g1MSMShaderParts, + entryPoints: ["g1_msm_bucket_jac_main", "g1_msm_weight_jac_main", "g1_msm_subsum_jac_main", "g1_msm_combine_jac_main"], + }, + { + shaderParts: definition.g2MSMShaderParts, + entryPoints: ["g2_msm_bucket_jac_main", "g2_msm_weight_jac_main", "g2_msm_subsum_jac_main", "g2_msm_combine_jac_main"], + }, + ], + debug: context.debug, + }); + + const g1MsmRuntime = buildJacPippengerRuntime({ + bucket: registry.getMSMKernel("g1_msm_bucket_jac_main"), + weightJac: registry.getMSMKernel("g1_msm_weight_jac_main"), + subsumJac: registry.getMSMKernel("g1_msm_subsum_jac_main"), + combine: registry.getMSMKernel("g1_msm_combine_jac_main"), + }, 64, context.debug); + + const g2MsmRuntime = buildJacPippengerRuntime({ + bucket: registry.getMSMKernel("g2_msm_bucket_jac_main"), + weightJac: registry.getMSMKernel("g2_msm_weight_jac_main"), + subsumJac: registry.getMSMKernel("g2_msm_subsum_jac_main"), + combine: registry.getMSMKernel("g2_msm_combine_jac_main"), + }, 32, context.debug); + + const fr = createFieldModule(context, curve, "fr", { + byteSize: frShape.byteSize, + entryPoint: "fr_ops_main", + label: `${curve}-fr`, + shape: frShape, + kernel: registry.getOpsKernel("fr_ops_main"), + }); + const fp = createFieldModule(context, curve, "fp", { + byteSize: fpShape.byteSize, + entryPoint: "fp_ops_main", + label: `${curve}-fp`, + shape: fpShape, + kernel: registry.getOpsKernel("fp_ops_main"), + }); + const g1 = createG1Module( + context, + { + curve: definition.id, + coordinateBytes: definition.coordinateBytes, + pointBytes: definition.pointBytes, + zeroHex: definition.zeroHex, + kernel: registry.getOpsKernel("g1_ops_main"), + }, + fp, + ); + const g2 = createG2Module( + context, + { + curve: definition.id, + componentBytes: fpShape.byteSize, + coordinateBytes: definition.g2CoordinateBytes, + pointBytes: definition.g2PointBytes, + kernel: registry.getOpsKernel("g2_ops_main"), + }, + fp, + ); + const ntt = createNTTModule( + context, + { + curve: definition.id, + domainPath: definition.frNTTDomainPath ?? "", + modulusHex: definition.frModulusHex ?? "", + vectorKernel: registry.getOpsKernel("fr_vector_main"), + fieldKernel: registry.getOpsKernel("fr_ops_main"), + nttKernel: registry.getOpsKernel("fr_ntt_stage_main"), + }, + fr, + ); + const g1msm = createG1MSMModule( + context, + { + curve: definition.id, + coordinateBytes: definition.coordinateBytes, + pointBytes: definition.pointBytes, + runtime: g1MsmRuntime, + }, + fp, + g1, + ); + const g2msm = createG2MSMModule( + context, + { + curve: definition.id, + componentBytes: fpShape.byteSize, + pointBytes: definition.g2PointBytes, + runtime: g2MsmRuntime, + }, + g2, + fp, + ); + const groth16 = createGroth16Module({ + context, + curve: definition.id, + modulusHex: definition.frModulusHex ?? "", + frBytes: frShape.byteSize, + quotient: ntt, + g1, + g2, + g1msm, + g2msm, + }); + const plonkQuotient = createPlonkQuotientModule({ + context, + curve: definition.id, + fr, + ntt, + }); + const plonk = createPlonkModule({ + context, + curve: definition.id, + modulusHex: definition.frModulusHex ?? "", + frBytes: frShape.byteSize, + fr, + ntt, + quotient: plonkQuotient, + g1, + g1msm, + }); + return { + id: curve, + context, + fr, + fp, + g1, + g2, + ntt, + groth16, + plonk, + g1msm, + g2msm, + }; +} + +/** + * Create the BN254 module bound to an existing context. + */ +export function createBN254(context: CurveGPUContext): Promise { + return createCurveModule(context, "bn254"); +} + +/** + * Create the BLS12-381 module bound to an existing context. + */ +export function createBLS12381(context: CurveGPUContext): Promise { + return createCurveModule(context, "bls12_381"); +} + +/** + * Create the BLS12-377 module bound to an existing context. + */ +export function createBLS12377(context: CurveGPUContext): Promise { + return createCurveModule(context, "bls12_377"); +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/encoding.ts b/backend/accelerated/webgpu/web/src/curvegpu/encoding.ts new file mode 100644 index 0000000000..5fccad652a --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/encoding.ts @@ -0,0 +1,33 @@ +/** + * Documentation aliases for the byte encodings used at the WebGPU boundary. + * + * These are intentionally plain Uint8Array aliases, not branded types: the + * runtime representation stays simple while function names carry the encoding. + */ +export type RegularLEBytes = Uint8Array; +export type MontgomeryLEBytes = Uint8Array; +export type PackedRegularLEBytes = Uint8Array; +export type PackedMontgomeryLEBytes = Uint8Array; + +/** + * Convert an unsigned numeric hex string into a fixed-width little-endian byte + * string. The input may be odd-width and may include a `0x` prefix. + */ +export function hexToBytesLE(hex: string, byteSize: number): RegularLEBytes { + const digits = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex; + if (!/^[0-9a-fA-F]+$/.test(digits)) { + throw new Error(`expected a non-empty hex string, got ${JSON.stringify(hex)}`); + } + if (digits.length > byteSize * 2) { + throw new Error(`hex string is too wide for ${byteSize} bytes`); + } + + const out = new Uint8Array(byteSize); + let offset = 0; + for (let end = digits.length; end > 0; end -= 2) { + const start = Math.max(0, end - 2); + out[offset] = Number.parseInt(digits.slice(start, end), 16); + offset += 1; + } + return out; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/errors.ts b/backend/accelerated/webgpu/web/src/curvegpu/errors.ts new file mode 100644 index 0000000000..30f5b168d2 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/errors.ts @@ -0,0 +1,41 @@ +/** + * Base class for all errors thrown by the curvegpu library. + */ +export class CurveGPUError extends Error { + constructor(message: string) { + super(message); + this.name = "CurveGPUError"; + } +} + +/** + * Thrown when WebGPU is not available in the current environment, or when + * the adapter/device cannot be acquired. + */ +export class CurveGPUNotSupportedError extends CurveGPUError { + constructor(message: string) { + super(message); + this.name = "CurveGPUNotSupportedError"; + } +} + +/** + * Thrown when the GPU device is lost while an operation is in progress, + * or exposed on the context so callers can subscribe to device-loss events. + */ +export class CurveGPUDeviceLostError extends CurveGPUError { + constructor(message: string) { + super(message); + this.name = "CurveGPUDeviceLostError"; + } +} + +/** + * Thrown when a shader file cannot be fetched or a required section is missing. + */ +export class CurveGPUShaderError extends CurveGPUError { + constructor(message: string) { + super(message); + this.name = "CurveGPUShaderError"; + } +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/field_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/field_module.ts new file mode 100644 index 0000000000..a47f6c1821 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/field_module.ts @@ -0,0 +1,213 @@ +import type { CurveGPUContext, CurveGPUElementBytes, FieldModule, SupportedCurveID } from "./api.js"; +import type { SimpleKernel } from "./runtime_common.js"; +import { + cloneBytes, + ensureByteLength, + lazyAsync, + packElementBatch, + runSimpleKernel, + unpackElementBatch, +} from "./runtime_common.js"; + +const OP_COPY = 0; +const OP_ONE = 2; +const OP_ADD = 3; +const OP_SUB = 4; +const OP_NEG = 5; +const OP_DOUBLE = 6; +const OP_NORMALIZE = 7; +const OP_EQUAL = 8; +const OP_MUL = 9; +const OP_SQUARE = 10; +const OP_TO_MONT = 11; +const OP_FROM_MONT = 12; + +type FieldOpCode = + | typeof OP_COPY + | typeof OP_ONE + | typeof OP_ADD + | typeof OP_SUB + | typeof OP_NEG + | typeof OP_DOUBLE + | typeof OP_NORMALIZE + | typeof OP_EQUAL + | typeof OP_MUL + | typeof OP_SQUARE + | typeof OP_TO_MONT + | typeof OP_FROM_MONT; + +function zeros(count: number, byteSize: number): Uint8Array[] { + return Array.from({ length: count }, () => new Uint8Array(byteSize)); +} + +function isNonZero(bytes: Uint8Array): boolean { + return bytes.some((byte) => byte !== 0); +} + +function ensurePackedElements(bytes: Uint8Array, byteSize: number, label: string): number { + if (bytes.byteLength % byteSize !== 0) { + throw new Error(`${label}: expected a multiple of ${byteSize} bytes, got ${bytes.byteLength}`); + } + return bytes.byteLength / byteSize; +} + +export function createFieldModule( + context: CurveGPUContext, + curve: SupportedCurveID, + field: "fr" | "fp", + options: { + byteSize: number; + kernel: SimpleKernel; + entryPoint: "fr_ops_main" | "fp_ops_main"; + label: string; + shape: FieldModule["shape"]; + }, +): FieldModule { + const { byteSize, kernel, entryPoint: _entryPoint, label, shape } = options; + const zeroValue = new Uint8Array(byteSize); + + async function runPacked(opcode: FieldOpCode, inputA: Uint8Array, inputB?: Uint8Array): Promise { + const count = ensurePackedElements(inputA, byteSize, `${label}.packedA`); + const b = inputB ?? new Uint8Array(inputA.byteLength); + if (b.byteLength !== inputA.byteLength) { + throw new Error(`${label}.packedB: expected ${inputA.byteLength} bytes, got ${b.byteLength}`); + } + return runSimpleKernel({ + device: context.device, + pool: context.bufferPool, + kernel, + label: `${label}-packed-op-${opcode}`, + inputA, + inputB: b, + outputBytes: count * byteSize, + uniformWords: Uint32Array.from([count, opcode, 0, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + } + + async function runBatch(opcode: FieldOpCode, inputA: readonly CurveGPUElementBytes[], inputB: readonly CurveGPUElementBytes[]): Promise { + const count = Math.max(inputA.length, inputB.length); + if (count === 0) { + return []; + } + const a = inputA.length === 0 ? zeros(count, byteSize) : inputA; + const b = inputB.length === 0 ? zeros(count, byteSize) : inputB; + if (a.length !== count || b.length !== count) { + throw new Error(`${label}: mismatched batch lengths`); + } + + const output = await runSimpleKernel({ + device: context.device, + kernel, + label: `${label}-op-${opcode}`, + inputA: packElementBatch(a, byteSize, `${label}.inputA`), + inputB: packElementBatch(b, byteSize, `${label}.inputB`), + outputBytes: count * byteSize, + uniformWords: Uint32Array.from([count, opcode, 0, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + return unpackElementBatch(output, byteSize, count); + } + + async function runUnary(opcode: FieldOpCode, value: CurveGPUElementBytes): Promise { + ensureByteLength(value, byteSize, `${label}.value`); + return (await runBatch(opcode, [value], [zeroValue]))[0]; + } + + async function runBinary(opcode: FieldOpCode, a: CurveGPUElementBytes, b: CurveGPUElementBytes): Promise { + ensureByteLength(a, byteSize, `${label}.a`); + ensureByteLength(b, byteSize, `${label}.b`); + return (await runBatch(opcode, [a], [b]))[0]; + } + + const getMontOne = lazyAsync(async () => cloneBytes((await runBatch(OP_ONE, [zeroValue], [zeroValue]))[0])); + + return { + context, + curve, + field, + shape, + byteSize, + zero(): CurveGPUElementBytes { + return cloneBytes(zeroValue); + }, + async copy(value: CurveGPUElementBytes): Promise { + return runUnary(OP_COPY, value); + }, + async copyBatch(values: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_COPY, values, values); + }, + async montOne(): Promise { + return cloneBytes(await getMontOne()); + }, + async equal(a: CurveGPUElementBytes, b: CurveGPUElementBytes): Promise { + return isNonZero(await runBinary(OP_EQUAL, a, b)); + }, + async equalBatch(a: readonly CurveGPUElementBytes[], b: readonly CurveGPUElementBytes[]): Promise { + return (await runBatch(OP_EQUAL, a, b)).map(isNonZero); + }, + async add(a: CurveGPUElementBytes, b: CurveGPUElementBytes): Promise { + return runBinary(OP_ADD, a, b); + }, + async addBatch(a: readonly CurveGPUElementBytes[], b: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_ADD, a, b); + }, + async sub(a: CurveGPUElementBytes, b: CurveGPUElementBytes): Promise { + return runBinary(OP_SUB, a, b); + }, + async subBatch(a: readonly CurveGPUElementBytes[], b: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_SUB, a, b); + }, + async neg(value: CurveGPUElementBytes): Promise { + return runUnary(OP_NEG, value); + }, + async negBatch(values: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_NEG, values, zeros(values.length, byteSize)); + }, + async double(value: CurveGPUElementBytes): Promise { + return runUnary(OP_DOUBLE, value); + }, + async doubleBatch(values: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_DOUBLE, values, zeros(values.length, byteSize)); + }, + async mul(a: CurveGPUElementBytes, b: CurveGPUElementBytes): Promise { + return runBinary(OP_MUL, a, b); + }, + async mulBatch(a: readonly CurveGPUElementBytes[], b: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_MUL, a, b); + }, + async mulPackedMont(a: Uint8Array, b: Uint8Array): Promise { + return runPacked(OP_MUL, a, b); + }, + async square(value: CurveGPUElementBytes): Promise { + return runUnary(OP_SQUARE, value); + }, + async squareBatch(values: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_SQUARE, values, zeros(values.length, byteSize)); + }, + async normalizeMont(value: CurveGPUElementBytes): Promise { + return runUnary(OP_NORMALIZE, value); + }, + async normalizeMontBatch(values: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_NORMALIZE, values, zeros(values.length, byteSize)); + }, + async toMontgomery(value: CurveGPUElementBytes): Promise { + return runUnary(OP_TO_MONT, value); + }, + async toMontgomeryBatch(values: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_TO_MONT, values, zeros(values.length, byteSize)); + }, + async toMontgomeryPacked(values: Uint8Array): Promise { + return runPacked(OP_TO_MONT, values); + }, + async fromMontgomery(value: CurveGPUElementBytes): Promise { + return runUnary(OP_FROM_MONT, value); + }, + async fromMontgomeryBatch(values: readonly CurveGPUElementBytes[]): Promise { + return runBatch(OP_FROM_MONT, values, zeros(values.length, byteSize)); + }, + async fromMontgomeryPacked(values: Uint8Array): Promise { + return runPacked(OP_FROM_MONT, values); + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/g1_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/g1_module.ts new file mode 100644 index 0000000000..5ca895453b --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/g1_module.ts @@ -0,0 +1,330 @@ +import type { + CurveGPUAffinePoint, + CurveGPUContext, + CurveGPUElementBytes, + CurveGPUJacobianPoint, + FieldModule, + G1Module, + SupportedCurveID, +} from "./api.js"; +import type { SimpleKernel } from "./runtime_common.js"; +import { + cloneBytes, + ensureByteLength, + lazyAsync, + runSimpleKernel, +} from "./runtime_common.js"; + +const OP_COPY = 0; +const OP_JAC_INFINITY = 1; +const OP_AFFINE_TO_JAC = 2; +const OP_NEG_JAC = 3; +const OP_DOUBLE_JAC = 4; +const OP_ADD_MIXED = 5; +const OP_JAC_TO_AFFINE = 6; +const OP_AFFINE_ADD = 7; + +type G1OpCode = + | typeof OP_COPY + | typeof OP_JAC_INFINITY + | typeof OP_AFFINE_TO_JAC + | typeof OP_NEG_JAC + | typeof OP_DOUBLE_JAC + | typeof OP_ADD_MIXED + | typeof OP_JAC_TO_AFFINE + | typeof OP_AFFINE_ADD; + +function zeroBytes(size: number): Uint8Array { + return new Uint8Array(size); +} + +function clonePoint(point: CurveGPUJacobianPoint): CurveGPUJacobianPoint { + return { x: cloneBytes(point.x), y: cloneBytes(point.y), z: cloneBytes(point.z) }; +} + +function cloneAffine(point: CurveGPUAffinePoint): CurveGPUAffinePoint { + return { x: cloneBytes(point.x), y: cloneBytes(point.y) }; +} + +function packJacobianPoints(points: readonly CurveGPUJacobianPoint[], coordinateBytes: number, pointBytes: number, label: string): Uint8Array { + const out = new Uint8Array(points.length * pointBytes); + points.forEach((point, index) => { + ensureByteLength(point.x, coordinateBytes, `${label}[${index}].x`); + ensureByteLength(point.y, coordinateBytes, `${label}[${index}].y`); + ensureByteLength(point.z, coordinateBytes, `${label}[${index}].z`); + const base = index * pointBytes; + out.set(point.x, base); + out.set(point.y, base + coordinateBytes); + out.set(point.z, base + 2 * coordinateBytes); + }); + return out; +} + +function packAffinePoints( + points: readonly CurveGPUAffinePoint[], + coordinateBytes: number, + pointBytes: number, + oneMontZ: Uint8Array, + zeroCoordinate: Uint8Array, + label: string, +): Uint8Array { + const out = new Uint8Array(points.length * pointBytes); + points.forEach((point, index) => { + ensureByteLength(point.x, coordinateBytes, `${label}[${index}].x`); + ensureByteLength(point.y, coordinateBytes, `${label}[${index}].y`); + const isInfinity = point.x.every((byte) => byte === 0) && point.y.every((byte) => byte === 0); + const base = index * pointBytes; + out.set(point.x, base); + out.set(point.y, base + coordinateBytes); + out.set(isInfinity ? zeroCoordinate : oneMontZ, base + 2 * coordinateBytes); + }); + return out; +} + +function unpackJacobianPoints(bytes: Uint8Array, count: number, coordinateBytes: number, pointBytes: number): CurveGPUJacobianPoint[] { + const out: CurveGPUJacobianPoint[] = []; + for (let i = 0; i < count; i += 1) { + const base = i * pointBytes; + out.push({ + x: cloneBytes(bytes.slice(base, base + coordinateBytes)), + y: cloneBytes(bytes.slice(base + coordinateBytes, base + 2 * coordinateBytes)), + z: cloneBytes(bytes.slice(base + 2 * coordinateBytes, base + 3 * coordinateBytes)), + }); + } + return out; +} + +function affineFromJacobian(point: CurveGPUJacobianPoint): CurveGPUAffinePoint { + return { x: cloneBytes(point.x), y: cloneBytes(point.y) }; +} + +function isAffineInfinity(point: CurveGPUAffinePoint): boolean { + return point.x.every((byte) => byte === 0) && point.y.every((byte) => byte === 0); +} + +function scalarBit(scalar: Uint8Array, bit: number): boolean { + ensureByteLength(scalar, 32, "scalar"); + const byteIndex = Math.floor(bit / 8); + const bitIndex = bit % 8; + return ((scalar[byteIndex] >> bitIndex) & 1) !== 0; +} + +export function createG1Module( + context: CurveGPUContext, + options: { + curve: SupportedCurveID; + coordinateBytes: number; + pointBytes: number; + zeroHex: string; + kernel: SimpleKernel; + }, + fp: FieldModule, +): G1Module { + const { curve, coordinateBytes, pointBytes, zeroHex, kernel } = options; + const label = `${curve}-g1`; + const zeroCoordinate = zeroBytes(coordinateBytes); + const zeroJacobianPoint = { x: zeroBytes(coordinateBytes), y: zeroBytes(coordinateBytes), z: zeroBytes(coordinateBytes) }; + const zeroAffinePoint = { x: zeroBytes(coordinateBytes), y: zeroBytes(coordinateBytes) }; + + const getOneMontgomery = lazyAsync(async () => fp.montOne()); + + async function runJacobianBatch( + opcode: G1OpCode, + inputA: readonly CurveGPUJacobianPoint[], + inputB: readonly CurveGPUJacobianPoint[], + ): Promise { + const count = Math.max(inputA.length, inputB.length); + if (count === 0) { + return []; + } + if (inputA.length !== count || inputB.length !== count) { + throw new Error(`${label}: mismatched jacobian batch lengths`); + } + const output = await runSimpleKernel({ + device: context.device, + pool: context.bufferPool, + kernel, + label: `${label}-op-${opcode}`, + inputA: packJacobianPoints(inputA, coordinateBytes, pointBytes, `${label}.inputA`), + inputB: packJacobianPoints(inputB, coordinateBytes, pointBytes, `${label}.inputB`), + outputBytes: count * pointBytes, + uniformWords: Uint32Array.from([count, opcode, 0, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + return unpackJacobianPoints(output, count, coordinateBytes, pointBytes); + } + + async function runMixedBatch( + opcode: G1OpCode, + inputA: readonly CurveGPUJacobianPoint[], + inputB: readonly CurveGPUAffinePoint[], + ): Promise { + const count = Math.max(inputA.length, inputB.length); + if (count === 0) { + return []; + } + if (inputA.length !== count || inputB.length !== count) { + throw new Error(`${label}: mismatched mixed batch lengths`); + } + const oneMontZ = await getOneMontgomery(); + const output = await runSimpleKernel({ + device: context.device, + pool: context.bufferPool, + kernel, + label: `${label}-op-${opcode}`, + inputA: packJacobianPoints(inputA, coordinateBytes, pointBytes, `${label}.inputA`), + inputB: packAffinePoints(inputB, coordinateBytes, pointBytes, oneMontZ, zeroCoordinate, `${label}.inputB`), + outputBytes: count * pointBytes, + uniformWords: Uint32Array.from([count, opcode, 0, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + return unpackJacobianPoints(output, count, coordinateBytes, pointBytes); + } + + async function runAffineInputBatch( + opcode: G1OpCode, + inputA: readonly CurveGPUAffinePoint[], + ): Promise { + const count = inputA.length; + if (count === 0) { + return []; + } + const oneMontZ = await getOneMontgomery(); + const output = await runSimpleKernel({ + device: context.device, + pool: context.bufferPool, + kernel, + label: `${label}-op-${opcode}`, + inputA: packAffinePoints(inputA, coordinateBytes, pointBytes, oneMontZ, zeroCoordinate, `${label}.inputA`), + inputB: packJacobianPoints(makeZeroJacobianBatch(count), coordinateBytes, pointBytes, `${label}.inputB`), + outputBytes: count * pointBytes, + uniformWords: Uint32Array.from([count, opcode, 0, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + return unpackJacobianPoints(output, count, coordinateBytes, pointBytes); + } + + async function runJacobianUnary(opcode: G1OpCode, point: CurveGPUJacobianPoint): Promise { + return (await runJacobianBatch(opcode, [point], [zeroJacobianPoint]))[0]; + } + + async function runMixedUnary(opcode: G1OpCode, point: CurveGPUJacobianPoint, affine: CurveGPUAffinePoint): Promise { + return (await runMixedBatch(opcode, [point], [affine]))[0]; + } + + async function runAffineUnary(opcode: G1OpCode, affine: CurveGPUAffinePoint): Promise { + return (await runAffineInputBatch(opcode, [affine]))[0]; + } + + function makeZeroJacobianBatch(count: number): CurveGPUJacobianPoint[] { + return Array.from({ length: count }, () => clonePoint(zeroJacobianPoint)); + } + + return { + context, + curve, + coordinateBytes, + pointBytes, + zeroHex, + affineInfinity(): CurveGPUAffinePoint { + return cloneAffine(zeroAffinePoint); + }, + jacobianZero(): CurveGPUJacobianPoint { + return clonePoint(zeroJacobianPoint); + }, + async copy(point: CurveGPUJacobianPoint): Promise { + return runJacobianUnary(OP_COPY, point); + }, + async copyBatch(points: readonly CurveGPUJacobianPoint[]): Promise { + return runJacobianBatch(OP_COPY, points, Array.from({ length: points.length }, () => zeroJacobianPoint)); + }, + async jacobianInfinity(): Promise { + return (await runJacobianBatch(OP_JAC_INFINITY, makeZeroJacobianBatch(1), makeZeroJacobianBatch(1)))[0]; + }, + async jacobianInfinityBatch(count: number): Promise { + const zeros = makeZeroJacobianBatch(count); + return runJacobianBatch(OP_JAC_INFINITY, zeros, zeros); + }, + async affineToJacobian(point: CurveGPUAffinePoint): Promise { + return runAffineUnary(OP_AFFINE_TO_JAC, point); + }, + async affineToJacobianBatch(points: readonly CurveGPUAffinePoint[]): Promise { + return runAffineInputBatch(OP_AFFINE_TO_JAC, points); + }, + async negJacobian(point: CurveGPUJacobianPoint): Promise { + return runJacobianUnary(OP_NEG_JAC, point); + }, + async negJacobianBatch(points: readonly CurveGPUJacobianPoint[]): Promise { + return runJacobianBatch(OP_NEG_JAC, points, makeZeroJacobianBatch(points.length)); + }, + async doubleJacobian(point: CurveGPUJacobianPoint): Promise { + return runJacobianUnary(OP_DOUBLE_JAC, point); + }, + async doubleJacobianBatch(points: readonly CurveGPUJacobianPoint[]): Promise { + return runJacobianBatch(OP_DOUBLE_JAC, points, makeZeroJacobianBatch(points.length)); + }, + async addMixed(point: CurveGPUJacobianPoint, affine: CurveGPUAffinePoint): Promise { + return runMixedUnary(OP_ADD_MIXED, point, affine); + }, + async addMixedBatch(points: readonly CurveGPUJacobianPoint[], affine: readonly CurveGPUAffinePoint[]): Promise { + return runMixedBatch(OP_ADD_MIXED, points, affine); + }, + async jacobianToAffine(point: CurveGPUJacobianPoint): Promise { + return affineFromJacobian(await runJacobianUnary(OP_JAC_TO_AFFINE, point)); + }, + async jacobianToAffineBatch(points: readonly CurveGPUJacobianPoint[]): Promise { + return (await runJacobianBatch(OP_JAC_TO_AFFINE, points, makeZeroJacobianBatch(points.length))).map(affineFromJacobian); + }, + async affineAdd(a: CurveGPUAffinePoint, b: CurveGPUAffinePoint): Promise { + const left = await runAffineInputBatch(OP_AFFINE_TO_JAC, [a]); + return (await runMixedBatch(OP_AFFINE_ADD, left, [b]))[0]; + }, + async affineAddBatch(a: readonly CurveGPUAffinePoint[], b: readonly CurveGPUAffinePoint[]): Promise { + const left = await runAffineInputBatch(OP_AFFINE_TO_JAC, a); + return runMixedBatch(OP_AFFINE_ADD, left, b); + }, + async scalarMulAffine(base: CurveGPUAffinePoint, scalar: CurveGPUElementBytes): Promise { + return (await this.scalarMulAffineBatch([base], [scalar]))[0]; + }, + async scalarMulAffineBatch(bases: readonly CurveGPUAffinePoint[], scalars: readonly CurveGPUElementBytes[]): Promise { + if (bases.length !== scalars.length) { + throw new Error(`${label}: mismatched scalar-mul batch lengths`); + } + const zeros = makeZeroJacobianBatch(bases.length); + let acc = await runJacobianBatch(OP_JAC_INFINITY, zeros, zeros); + for (let bit = 255; bit >= 0; bit -= 1) { + acc = await runJacobianBatch(OP_DOUBLE_JAC, acc, zeros); + const activeBases = bases.map((point, index) => (scalarBit(scalars[index], bit) ? point : cloneAffine(zeroAffinePoint))); + if (activeBases.every(isAffineInfinity)) { + continue; + } + acc = await runMixedBatch(OP_ADD_MIXED, acc, activeBases); + } + return runJacobianBatch(OP_JAC_TO_AFFINE, acc, zeros); + }, + async addAffine(a: CurveGPUAffinePoint, b: CurveGPUAffinePoint): Promise { + return affineFromJacobian(await this.affineAdd(a, b)); + }, + async addAffineBatch(a: readonly CurveGPUAffinePoint[], b: readonly CurveGPUAffinePoint[]): Promise { + return (await this.affineAddBatch(a, b)).map(affineFromJacobian); + }, + async negAffine(point: CurveGPUAffinePoint): Promise { + return affineFromJacobian(await this.negJacobian(await this.affineToJacobian(point))); + }, + async negAffineBatch(points: readonly CurveGPUAffinePoint[]): Promise { + return (await this.negJacobianBatch(await this.affineToJacobianBatch(points))).map(affineFromJacobian); + }, + async doubleAffine(point: CurveGPUAffinePoint): Promise { + return affineFromJacobian(await this.doubleJacobian(await this.affineToJacobian(point))); + }, + async doubleAffineBatch(points: readonly CurveGPUAffinePoint[]): Promise { + return (await this.doubleJacobianBatch(await this.affineToJacobianBatch(points))).map(affineFromJacobian); + }, + async scalarMulAffineResult(base: CurveGPUAffinePoint, scalar: CurveGPUElementBytes): Promise { + return affineFromJacobian(await this.scalarMulAffine(base, scalar)); + }, + async scalarMulAffineResultBatch(bases: readonly CurveGPUAffinePoint[], scalars: readonly CurveGPUElementBytes[]): Promise { + return (await this.scalarMulAffineBatch(bases, scalars)).map(affineFromJacobian); + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/g2_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/g2_module.ts new file mode 100644 index 0000000000..78d59f4c37 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/g2_module.ts @@ -0,0 +1,400 @@ +import type { + CurveGPUContext, + CurveGPUFp2Element, + CurveGPUG2AffinePoint, + CurveGPUG2JacobianPoint, + FieldModule, + G2Module, + SupportedCurveID, +} from "./api.js"; +import type { SimpleKernel } from "./runtime_common.js"; +import { + cloneBytes, + ensureByteLength, + lazyAsync, + runSimpleKernel, +} from "./runtime_common.js"; + +const OP_COPY = 0; +const OP_JAC_INFINITY = 1; +const OP_AFFINE_TO_JAC = 2; +const OP_NEG_JAC = 3; +const OP_DOUBLE_JAC = 4; +const OP_ADD_MIXED = 5; +const OP_JAC_TO_AFFINE = 6; +const OP_AFFINE_ADD = 7; + +type G2OpCode = + | typeof OP_COPY + | typeof OP_JAC_INFINITY + | typeof OP_AFFINE_TO_JAC + | typeof OP_NEG_JAC + | typeof OP_DOUBLE_JAC + | typeof OP_ADD_MIXED + | typeof OP_JAC_TO_AFFINE + | typeof OP_AFFINE_ADD; + +function zeroBytes(size: number): Uint8Array { + return new Uint8Array(size); +} + +function zeroFp2(size: number): CurveGPUFp2Element { + return { c0: zeroBytes(size), c1: zeroBytes(size) }; +} + +function cloneFp2(value: CurveGPUFp2Element): CurveGPUFp2Element { + return { c0: cloneBytes(value.c0), c1: cloneBytes(value.c1) }; +} + +function cloneJacobian(point: CurveGPUG2JacobianPoint): CurveGPUG2JacobianPoint { + return { + x: cloneFp2(point.x), + y: cloneFp2(point.y), + z: cloneFp2(point.z), + }; +} + +function cloneAffine(point: CurveGPUG2AffinePoint): CurveGPUG2AffinePoint { + return { + x: cloneFp2(point.x), + y: cloneFp2(point.y), + }; +} + +function ensureFp2(value: CurveGPUFp2Element, componentBytes: number, label: string): void { + ensureByteLength(value.c0, componentBytes, `${label}.c0`); + ensureByteLength(value.c1, componentBytes, `${label}.c1`); +} + +function fp2IsZero(value: CurveGPUFp2Element): boolean { + return value.c0.every((byte) => byte === 0) && value.c1.every((byte) => byte === 0); +} + +function isAffineInfinity(point: CurveGPUG2AffinePoint): boolean { + return fp2IsZero(point.x) && fp2IsZero(point.y); +} + +function scalarBit(scalar: Uint8Array, bit: number): boolean { + ensureByteLength(scalar, 32, "scalar"); + const byteIndex = Math.floor(bit / 8); + const bitIndex = bit % 8; + return ((scalar[byteIndex] >> bitIndex) & 1) !== 0; +} + +function packFp2(out: Uint8Array, offset: number, value: CurveGPUFp2Element): void { + out.set(value.c0, offset); + out.set(value.c1, offset + value.c0.byteLength); +} + +function packJacobianPoints( + points: readonly CurveGPUG2JacobianPoint[], + componentBytes: number, + pointBytes: number, + label: string, +): Uint8Array { + const out = new Uint8Array(points.length * pointBytes); + points.forEach((point, index) => { + ensureFp2(point.x, componentBytes, `${label}[${index}].x`); + ensureFp2(point.y, componentBytes, `${label}[${index}].y`); + ensureFp2(point.z, componentBytes, `${label}[${index}].z`); + const base = index * pointBytes; + packFp2(out, base, point.x); + packFp2(out, base + 2 * componentBytes, point.y); + packFp2(out, base + 4 * componentBytes, point.z); + }); + return out; +} + +function packAffinePoints( + points: readonly CurveGPUG2AffinePoint[], + componentBytes: number, + pointBytes: number, + oneMontZ: CurveGPUFp2Element, + zeroCoordinate: CurveGPUFp2Element, + label: string, +): Uint8Array { + const out = new Uint8Array(points.length * pointBytes); + points.forEach((point, index) => { + ensureFp2(point.x, componentBytes, `${label}[${index}].x`); + ensureFp2(point.y, componentBytes, `${label}[${index}].y`); + const isInfinity = fp2IsZero(point.x) && fp2IsZero(point.y); + const base = index * pointBytes; + packFp2(out, base, point.x); + packFp2(out, base + 2 * componentBytes, point.y); + packFp2(out, base + 4 * componentBytes, isInfinity ? zeroCoordinate : oneMontZ); + }); + return out; +} + +function unpackFp2(bytes: Uint8Array, offset: number, componentBytes: number): CurveGPUFp2Element { + return { + c0: cloneBytes(bytes.slice(offset, offset + componentBytes)), + c1: cloneBytes(bytes.slice(offset + componentBytes, offset + 2 * componentBytes)), + }; +} + +function unpackJacobianPoints( + bytes: Uint8Array, + count: number, + componentBytes: number, + pointBytes: number, +): CurveGPUG2JacobianPoint[] { + const out: CurveGPUG2JacobianPoint[] = []; + for (let i = 0; i < count; i += 1) { + const base = i * pointBytes; + out.push({ + x: unpackFp2(bytes, base, componentBytes), + y: unpackFp2(bytes, base + 2 * componentBytes, componentBytes), + z: unpackFp2(bytes, base + 4 * componentBytes, componentBytes), + }); + } + return out; +} + +function affineFromJacobian(point: CurveGPUG2JacobianPoint): CurveGPUG2AffinePoint { + return { + x: cloneFp2(point.x), + y: cloneFp2(point.y), + }; +} + +export function createG2Module( + context: CurveGPUContext, + options: { + curve: SupportedCurveID; + componentBytes: number; + coordinateBytes: number; + pointBytes: number; + kernel: SimpleKernel; + }, + fp: FieldModule, +): G2Module { + const { curve, componentBytes, coordinateBytes, pointBytes, kernel } = options; + const label = `${curve}-g2`; + + const zeroCoordinate = zeroFp2(componentBytes); + const zeroJacobianPoint: CurveGPUG2JacobianPoint = { + x: zeroFp2(componentBytes), + y: zeroFp2(componentBytes), + z: zeroFp2(componentBytes), + }; + const zeroAffinePoint: CurveGPUG2AffinePoint = { + x: zeroFp2(componentBytes), + y: zeroFp2(componentBytes), + }; + + const getOneMontgomery = lazyAsync(async () => { + const c0 = await fp.montOne(); + const c1 = zeroBytes(componentBytes); + return { c0, c1 }; + }); + + function makeZeroJacobianBatch(count: number): CurveGPUG2JacobianPoint[] { + return Array.from({ length: count }, () => cloneJacobian(zeroJacobianPoint)); + } + + async function runJacobianBatch( + opcode: G2OpCode, + inputA: readonly CurveGPUG2JacobianPoint[], + inputB: readonly CurveGPUG2JacobianPoint[], + ): Promise { + const count = Math.max(inputA.length, inputB.length); + if (count === 0) { + return []; + } + if (inputA.length !== count || inputB.length !== count) { + throw new Error(`${label}: mismatched jacobian batch lengths`); + } + const output = await runSimpleKernel({ + device: context.device, + pool: context.bufferPool, + kernel, + label: `${label}-op-${opcode}`, + inputA: packJacobianPoints(inputA, componentBytes, pointBytes, `${label}.inputA`), + inputB: packJacobianPoints(inputB, componentBytes, pointBytes, `${label}.inputB`), + outputBytes: count * pointBytes, + uniformWords: Uint32Array.from([count, opcode, 0, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + return unpackJacobianPoints(output, count, componentBytes, pointBytes); + } + + async function runMixedBatch( + opcode: G2OpCode, + inputA: readonly CurveGPUG2JacobianPoint[], + inputB: readonly CurveGPUG2AffinePoint[], + ): Promise { + const count = Math.max(inputA.length, inputB.length); + if (count === 0) { + return []; + } + if (inputA.length !== count || inputB.length !== count) { + throw new Error(`${label}: mismatched mixed batch lengths`); + } + const oneMontZ = await getOneMontgomery(); + const output = await runSimpleKernel({ + device: context.device, + pool: context.bufferPool, + kernel, + label: `${label}-op-${opcode}`, + inputA: packJacobianPoints(inputA, componentBytes, pointBytes, `${label}.inputA`), + inputB: packAffinePoints(inputB, componentBytes, pointBytes, oneMontZ, zeroCoordinate, `${label}.inputB`), + outputBytes: count * pointBytes, + uniformWords: Uint32Array.from([count, opcode, 0, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + return unpackJacobianPoints(output, count, componentBytes, pointBytes); + } + + async function runAffineInputBatch( + opcode: G2OpCode, + inputA: readonly CurveGPUG2AffinePoint[], + ): Promise { + const count = inputA.length; + if (count === 0) { + return []; + } + const oneMontZ = await getOneMontgomery(); + const output = await runSimpleKernel({ + device: context.device, + pool: context.bufferPool, + kernel, + label: `${label}-op-${opcode}`, + inputA: packAffinePoints(inputA, componentBytes, pointBytes, oneMontZ, zeroCoordinate, `${label}.inputA`), + inputB: packJacobianPoints(makeZeroJacobianBatch(count), componentBytes, pointBytes, `${label}.inputB`), + outputBytes: count * pointBytes, + uniformWords: Uint32Array.from([count, opcode, 0, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + return unpackJacobianPoints(output, count, componentBytes, pointBytes); + } + + async function runJacobianUnary(opcode: G2OpCode, point: CurveGPUG2JacobianPoint): Promise { + return (await runJacobianBatch(opcode, [point], [zeroJacobianPoint]))[0]; + } + + async function runMixedUnary(opcode: G2OpCode, point: CurveGPUG2JacobianPoint, affine: CurveGPUG2AffinePoint): Promise { + return (await runMixedBatch(opcode, [point], [affine]))[0]; + } + + async function runAffineUnary(opcode: G2OpCode, affine: CurveGPUG2AffinePoint): Promise { + return (await runAffineInputBatch(opcode, [affine]))[0]; + } + + return { + context, + curve, + componentBytes, + coordinateBytes, + pointBytes, + affineInfinity(): CurveGPUG2AffinePoint { + return cloneAffine(zeroAffinePoint); + }, + jacobianZero(): CurveGPUG2JacobianPoint { + return cloneJacobian(zeroJacobianPoint); + }, + async copy(point: CurveGPUG2JacobianPoint): Promise { + return runJacobianUnary(OP_COPY, point); + }, + async copyBatch(points: readonly CurveGPUG2JacobianPoint[]): Promise { + return runJacobianBatch(OP_COPY, points, Array.from({ length: points.length }, () => zeroJacobianPoint)); + }, + async jacobianInfinity(): Promise { + return (await runJacobianBatch(OP_JAC_INFINITY, makeZeroJacobianBatch(1), makeZeroJacobianBatch(1)))[0]; + }, + async jacobianInfinityBatch(count: number): Promise { + const zeros = makeZeroJacobianBatch(count); + return runJacobianBatch(OP_JAC_INFINITY, zeros, zeros); + }, + async affineToJacobian(point: CurveGPUG2AffinePoint): Promise { + return runAffineUnary(OP_AFFINE_TO_JAC, point); + }, + async affineToJacobianBatch(points: readonly CurveGPUG2AffinePoint[]): Promise { + return runAffineInputBatch(OP_AFFINE_TO_JAC, points); + }, + async negJacobian(point: CurveGPUG2JacobianPoint): Promise { + return runJacobianUnary(OP_NEG_JAC, point); + }, + async negJacobianBatch(points: readonly CurveGPUG2JacobianPoint[]): Promise { + return runJacobianBatch(OP_NEG_JAC, points, Array.from({ length: points.length }, () => zeroJacobianPoint)); + }, + async doubleJacobian(point: CurveGPUG2JacobianPoint): Promise { + return runJacobianUnary(OP_DOUBLE_JAC, point); + }, + async doubleJacobianBatch(points: readonly CurveGPUG2JacobianPoint[]): Promise { + return runJacobianBatch(OP_DOUBLE_JAC, points, Array.from({ length: points.length }, () => zeroJacobianPoint)); + }, + async addMixed(point: CurveGPUG2JacobianPoint, affine: CurveGPUG2AffinePoint): Promise { + return runMixedUnary(OP_ADD_MIXED, point, affine); + }, + async addMixedBatch( + points: readonly CurveGPUG2JacobianPoint[], + affine: readonly CurveGPUG2AffinePoint[], + ): Promise { + return runMixedBatch(OP_ADD_MIXED, points, affine); + }, + async jacobianToAffine(point: CurveGPUG2JacobianPoint): Promise { + return affineFromJacobian((await runJacobianBatch(OP_JAC_TO_AFFINE, [point], [zeroJacobianPoint]))[0]); + }, + async jacobianToAffineBatch(points: readonly CurveGPUG2JacobianPoint[]): Promise { + return (await runJacobianBatch(OP_JAC_TO_AFFINE, points, Array.from({ length: points.length }, () => zeroJacobianPoint))).map(affineFromJacobian); + }, + async affineAdd(a: CurveGPUG2AffinePoint, b: CurveGPUG2AffinePoint): Promise { + const left = await runAffineInputBatch(OP_AFFINE_TO_JAC, [a]); + return (await runMixedBatch(OP_AFFINE_ADD, left, [b]))[0]; + }, + async affineAddBatch(a: readonly CurveGPUG2AffinePoint[], b: readonly CurveGPUG2AffinePoint[]): Promise { + if (a.length !== b.length) { + throw new Error(`${label}: mismatched affine batch lengths`); + } + const left = await runAffineInputBatch(OP_AFFINE_TO_JAC, a); + return runMixedBatch(OP_AFFINE_ADD, left, b); + }, + async scalarMulAffine(base: CurveGPUG2AffinePoint, scalar: Uint8Array): Promise { + return (await this.scalarMulAffineBatch([base], [scalar]))[0]; + }, + async scalarMulAffineBatch( + bases: readonly CurveGPUG2AffinePoint[], + scalars: readonly Uint8Array[], + ): Promise { + if (bases.length !== scalars.length) { + throw new Error(`${label}: mismatched scalar-mul batch lengths`); + } + const zeros = makeZeroJacobianBatch(bases.length); + let acc = await runJacobianBatch(OP_JAC_INFINITY, zeros, zeros); + for (let bit = 255; bit >= 0; bit -= 1) { + acc = await runJacobianBatch(OP_DOUBLE_JAC, acc, zeros); + const activeBases = bases.map((point, index) => (scalarBit(scalars[index], bit) ? point : cloneAffine(zeroAffinePoint))); + if (activeBases.every(isAffineInfinity)) { + continue; + } + acc = await runMixedBatch(OP_ADD_MIXED, acc, activeBases); + } + return runJacobianBatch(OP_JAC_TO_AFFINE, acc, zeros); + }, + async addAffine(a: CurveGPUG2AffinePoint, b: CurveGPUG2AffinePoint): Promise { + return affineFromJacobian(await this.affineAdd(a, b)); + }, + async addAffineBatch(a: readonly CurveGPUG2AffinePoint[], b: readonly CurveGPUG2AffinePoint[]): Promise { + return (await this.affineAddBatch(a, b)).map(affineFromJacobian); + }, + async negAffine(point: CurveGPUG2AffinePoint): Promise { + return affineFromJacobian(await this.negJacobian(await this.affineToJacobian(point))); + }, + async negAffineBatch(points: readonly CurveGPUG2AffinePoint[]): Promise { + return (await this.negJacobianBatch(await this.affineToJacobianBatch(points))).map(affineFromJacobian); + }, + async doubleAffine(point: CurveGPUG2AffinePoint): Promise { + return affineFromJacobian(await this.doubleJacobian(await this.affineToJacobian(point))); + }, + async doubleAffineBatch(points: readonly CurveGPUG2AffinePoint[]): Promise { + return (await this.doubleJacobianBatch(await this.affineToJacobianBatch(points))).map(affineFromJacobian); + }, + async scalarMulAffineResult(base: CurveGPUG2AffinePoint, scalar: Uint8Array): Promise { + return affineFromJacobian(await this.scalarMulAffine(base, scalar)); + }, + async scalarMulAffineResultBatch(bases: readonly CurveGPUG2AffinePoint[], scalars: readonly Uint8Array[]): Promise { + return (await this.scalarMulAffineBatch(bases, scalars)).map(affineFromJacobian); + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/g2_msm_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/g2_msm_module.ts new file mode 100644 index 0000000000..6bc2328147 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/g2_msm_module.ts @@ -0,0 +1,225 @@ +import type { + CurveGPUContext, + CurveGPUElementBytes, + CurveGPUG2AffinePoint, + CurveGPUG2JacobianPoint, + CurveGPUMSMOptions, + FieldModule, + G2Module, + G2MSMModule, + SupportedCurveID, +} from "./api.js"; +import { splitBytesLEToU32 } from "./convert.js"; +import { bestPippengerWindow } from "./msm_shared.js"; +import { runSparseSignedPippengerMSM, type PippengerRuntime } from "./msm_pippenger.js"; +import { lazyAsync } from "./runtime_common.js"; + +function packScalarWords(scalars: readonly Uint8Array[]): Uint32Array { + const out = new Uint32Array(scalars.length * 8); + scalars.forEach((scalar, index) => { + out.set(splitBytesLEToU32(scalar), index * 8); + }); + return out; +} + +function packScalarWordsPacked(scalarsPacked: Uint8Array): Uint32Array { + if (scalarsPacked.byteLength % 32 !== 0) { + throw new Error(`packed scalars: expected a multiple of 32 bytes, got ${scalarsPacked.byteLength}`); + } + const count = scalarsPacked.byteLength / 32; + const out = new Uint32Array(count * 8); + const view = new DataView(scalarsPacked.buffer, scalarsPacked.byteOffset, scalarsPacked.byteLength); + for (let i = 0; i < out.length; i += 1) { + out[i] = view.getUint32(i * 4, true); + } + return out; +} + +function ensurePackedScalars(scalarsPacked: Uint8Array, count: number, label: string): void { + const expected = count * 32; + if (scalarsPacked.byteLength !== expected) { + throw new Error(`${label}: expected ${expected} scalar bytes, got ${scalarsPacked.byteLength}`); + } +} + +function packAffinesToJacobianPacked( + bases: readonly CurveGPUG2AffinePoint[], + componentBytes: number, + pointBytes: number, + montOne: Uint8Array, +): Uint8Array { + const out = new Uint8Array(bases.length * pointBytes); + for (let i = 0; i < bases.length; i += 1) { + const base = i * pointBytes; + const b = bases[i]; + const isInfinity = + b.x.c0.every((byte) => byte === 0) && + b.x.c1.every((byte) => byte === 0) && + b.y.c0.every((byte) => byte === 0) && + b.y.c1.every((byte) => byte === 0); + if (!isInfinity) { + out.set(b.x.c0, base); + out.set(b.x.c1, base + componentBytes); + out.set(b.y.c0, base + 2 * componentBytes); + out.set(b.y.c1, base + 3 * componentBytes); + // z = fp2_one: c0 = mont_one, c1 = zero + out.set(montOne, base + 4 * componentBytes); + // c1 of z remains zero (already zero from new Uint8Array) + } + // infinity: all zeros already + } + return out; +} + +function unpackJacobianPoints( + bytes: Uint8Array, + count: number, + componentBytes: number, + pointBytes: number, +): CurveGPUG2JacobianPoint[] { + const out: CurveGPUG2JacobianPoint[] = []; + for (let i = 0; i < count; i += 1) { + const base = i * pointBytes; + out.push({ + x: { + c0: new Uint8Array(bytes.slice(base, base + componentBytes)), + c1: new Uint8Array(bytes.slice(base + componentBytes, base + 2 * componentBytes)), + }, + y: { + c0: new Uint8Array(bytes.slice(base + 2 * componentBytes, base + 3 * componentBytes)), + c1: new Uint8Array(bytes.slice(base + 3 * componentBytes, base + 4 * componentBytes)), + }, + z: { + c0: new Uint8Array(bytes.slice(base + 4 * componentBytes, base + 5 * componentBytes)), + c1: new Uint8Array(bytes.slice(base + 5 * componentBytes, base + 6 * componentBytes)), + }, + }); + } + return out; +} + +export function createG2MSMModule( + context: CurveGPUContext, + options: { + curve: SupportedCurveID; + componentBytes: number; + pointBytes: number; + runtime: PippengerRuntime; + }, + g2: G2Module, + fp: FieldModule, +): G2MSMModule { + const { curve, componentBytes, pointBytes, runtime } = options; + const label = `${curve}-g2-msm`; + + const getOneMontgomery = lazyAsync(async () => fp.montOne()); + + async function runBatch( + bases: readonly CurveGPUG2AffinePoint[], + scalars: readonly CurveGPUElementBytes[], + msmOptions: CurveGPUMSMOptions = {}, + ): Promise { + if (bases.length !== scalars.length) { + throw new Error(`${label}: bases and scalars length mismatch`); + } + const count = msmOptions.count ?? 1; + const termsPerInstance = msmOptions.termsPerInstance ?? (count === 1 ? bases.length : 0); + if (!Number.isInteger(count) || count <= 0) { + throw new Error(`${label}: count must be a positive integer`); + } + if (!Number.isInteger(termsPerInstance) || termsPerInstance <= 0) { + throw new Error(`${label}: termsPerInstance must be a positive integer`); + } + if (bases.length !== count * termsPerInstance) { + throw new Error(`${label}: expected ${count * termsPerInstance} bases/scalars for count=${count} termsPerInstance=${termsPerInstance}`); + } + + const montOne = await getOneMontgomery(); + const window = msmOptions.window ?? bestPippengerWindow(termsPerInstance); + const basesBytes = packAffinesToJacobianPacked(bases, componentBytes, pointBytes, montOne); + const scalarWords = packScalarWords(scalars as Uint8Array[]); + const outputBytes = await runSparseSignedPippengerMSM({ + device: context.device, + pool: context.bufferPool, + runtime, + basesBytes, + pointBytes, + uniformBytes: 32, + zeroPointBytes: new Uint8Array(pointBytes), + scalarWords, + count, + termsPerInstance, + window, + maxChunkSize: msmOptions.maxChunkSize, + labelPrefix: label, + debug: context.debug, + }); + return unpackJacobianPoints(outputBytes, count, componentBytes, pointBytes); + } + + return { + context, + curve, + group: "g2", + bestWindow(termCount: number): number { + return bestPippengerWindow(termCount); + }, + async pippengerAffine( + bases: readonly CurveGPUG2AffinePoint[], + scalars: readonly CurveGPUElementBytes[], + msmOptions: CurveGPUMSMOptions = {}, + ): Promise { + return (await runBatch(bases, scalars, { ...msmOptions, count: msmOptions.count ?? 1 }))[0]; + }, + async pippengerAffineResult( + bases: readonly CurveGPUG2AffinePoint[], + scalars: readonly CurveGPUElementBytes[], + msmOptions: CurveGPUMSMOptions = {}, + ): Promise { + return g2.jacobianToAffine(await this.pippengerAffine(bases, scalars, msmOptions)); + }, + async pippengerAffineBatch( + bases: readonly CurveGPUG2AffinePoint[], + scalars: readonly CurveGPUElementBytes[], + msmOptions: CurveGPUMSMOptions, + ): Promise { + return runBatch(bases, scalars, msmOptions); + }, + async pippengerPackedJacobianBases( + basesPacked: Uint8Array, + scalarsPacked: Uint8Array, + msmOptions: CurveGPUMSMOptions, + ): Promise { + const count = msmOptions.count ?? 1; + const termsPerInstance = msmOptions.termsPerInstance ?? 0; + if (!Number.isInteger(count) || count <= 0) { + throw new Error(`${label}: count must be a positive integer`); + } + if (!Number.isInteger(termsPerInstance) || termsPerInstance <= 0) { + throw new Error(`${label}: termsPerInstance must be a positive integer`); + } + const expectedPointBytes = count * termsPerInstance * pointBytes; + if (basesPacked.byteLength !== expectedPointBytes) { + throw new Error(`${label}: expected ${expectedPointBytes} base bytes, got ${basesPacked.byteLength}`); + } + ensurePackedScalars(scalarsPacked, count * termsPerInstance, `${label}.scalarsPacked`); + const window = msmOptions.window ?? bestPippengerWindow(termsPerInstance); + return runSparseSignedPippengerMSM({ + device: context.device, + pool: context.bufferPool, + runtime, + basesBytes: basesPacked, + pointBytes, + uniformBytes: 32, + zeroPointBytes: new Uint8Array(pointBytes), + scalarWords: packScalarWordsPacked(scalarsPacked), + count, + termsPerInstance, + window, + maxChunkSize: msmOptions.maxChunkSize, + labelPrefix: label, + debug: context.debug, + }); + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/groth16_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/groth16_module.ts new file mode 100644 index 0000000000..a632dc48aa --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/groth16_module.ts @@ -0,0 +1,330 @@ +import type { + CurveGPUContext, + G1Module, + G1MSMModule, + G2Module, + G2MSMModule, + Groth16ConstraintSystem, + Groth16Handle, + Groth16Module, + Groth16ProvingKey, + Groth16ProvingKeyFormat, + Groth16QuotientModule, + Groth16RuntimeKind, + Groth16RuntimeOptions, + Groth16VerificationKey, + SupportedCurveID, +} from "./api.js"; +import { installGroth16WebGPUBridge } from "./groth16_webgpu_bridge.js"; + +type GoInstance = { + importObject: WebAssembly.Imports; + run(instance: WebAssembly.Instance): Promise; +}; + +type GoConstructor = new () => GoInstance; + +type RuntimeGlobal = { + readConstraintSystem(curve: SupportedCurveID, bytes: Uint8Array): Promise<{ handle: string; constraints: number }>; + readProvingKey(curve: SupportedCurveID, bytes: Uint8Array, format: Groth16ProvingKeyFormat): Promise<{ handle: string }>; + readVerificationKey(curve: SupportedCurveID, bytes: Uint8Array): Promise<{ handle: string }>; + prepareProvingKey(handle: string): Promise; + prove(ccsHandle: string, pkHandle: string, witness: Uint8Array): Promise; + verify(proof: Uint8Array, vkHandle: string, publicWitness: Uint8Array): Promise; + release(handle: string): Promise; +}; + +type Groth16ModuleConfig = { + context: CurveGPUContext; + curve: SupportedCurveID; + modulusHex: string; + frBytes: number; + quotient: Groth16QuotientModule; + g1: G1Module; + g2: G2Module; + g1msm: G1MSMModule; + g2msm: G2MSMModule; +}; + +export const defaultGroth16RuntimeURLs = Object.freeze({ + wasmExecURL: new URL("../../assets/wasm_exec.js", import.meta.url).toString(), + webgpuWasmURL: new URL("../../assets/groth16-webgpu.wasm", import.meta.url).toString(), + nativeWasmURL: new URL("../../assets/groth16-native.wasm", import.meta.url).toString(), +}); + +const runtimeGlobals: Record = { + webgpu: "gnarkGroth16RuntimeWebGPU", + native: "gnarkGroth16RuntimeNative", +}; + +const loadedScripts = new Map>(); +const loadedRuntimes = new Map>(); + +function cloneBytes(bytes: Uint8Array): Uint8Array { + return new Uint8Array(bytes); +} + +function getGlobalObject(name: string): T | undefined { + return (globalThis as typeof globalThis & Record)[name]; +} + +function setGlobalObject(name: string, value: T | undefined): void { + (globalThis as typeof globalThis & Record)[name] = value; +} + +function getGoConstructor(): GoConstructor { + const Go = getGlobalObject("Go"); + if (typeof Go !== "function") { + throw new Error("Go WASM runtime is not available after loading wasm_exec.js"); + } + return Go; +} + +async function loadScript(url: string): Promise { + if (typeof document === "undefined") { + throw new Error("Groth16 WASM runtime loading requires a browser document"); + } + let promise = loadedScripts.get(url); + if (!promise) { + promise = new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = url; + script.onload = () => resolve(); + script.onerror = () => reject(new Error(`failed to load ${url}`)); + document.head.appendChild(script); + }); + loadedScripts.set(url, promise); + } + await promise; +} + +async function ensureWasmExec(url: string): Promise { + if (typeof getGlobalObject("Go") === "function") { + return; + } + await loadScript(url); +} + +async function waitForRuntimeGlobal(name: string): Promise { + const deadline = performance.now() + 10_000; + while (performance.now() < deadline) { + const runtime = getGlobalObject(name); + if (runtime) { + return runtime; + } + await new Promise((resolve) => setTimeout(resolve, 0)); + } + throw new Error(`Groth16 WASM runtime ${name} did not initialize`); +} + +async function loadGoRuntime( + kind: Groth16RuntimeKind, + options: Required, + beforeStart: () => void, +): Promise { + const wasmURL = kind === "native" ? options.nativeWasmURL : options.webgpuWasmURL; + const cacheKey = `${kind}\n${options.wasmExecURL}\n${wasmURL}`; + let promise = loadedRuntimes.get(cacheKey); + if (!promise) { + promise = (async () => { + beforeStart(); + await ensureWasmExec(options.wasmExecURL); + const response = await fetch(wasmURL); + if (!response.ok) { + throw new Error(`failed to fetch ${wasmURL}: ${response.status}`); + } + const bytes = await response.arrayBuffer(); + const go = new (getGoConstructor())(); + const { instance } = await WebAssembly.instantiate(bytes, go.importObject); + setGlobalObject(runtimeGlobals[kind], undefined); + void go.run(instance).catch((error: unknown) => { + console.error(`Groth16 ${kind} WASM runtime exited`, error); + }); + return waitForRuntimeGlobal(runtimeGlobals[kind]); + })(); + loadedRuntimes.set(cacheKey, promise); + } else { + beforeStart(); + } + return promise; +} + +function normalizeRuntimeOptions(options?: Groth16RuntimeOptions): Required { + return { + wasmExecURL: options?.wasmExecURL ?? defaultGroth16RuntimeURLs.wasmExecURL, + webgpuWasmURL: options?.webgpuWasmURL ?? defaultGroth16RuntimeURLs.webgpuWasmURL, + nativeWasmURL: options?.nativeWasmURL ?? defaultGroth16RuntimeURLs.nativeWasmURL, + }; +} + +class RuntimeHandle implements Groth16Handle { + #disposed = false; + + constructor( + readonly runtime: RuntimeGlobal, + readonly kind: Groth16RuntimeKind, + readonly curve: SupportedCurveID, + readonly type: "ccs" | "pk" | "vk", + readonly handle: string, + ) {} + + async dispose(): Promise { + if (this.#disposed) { + return; + } + this.#disposed = true; + await this.runtime.release(this.handle); + } + + assertUsable(expectedType: RuntimeHandle["type"]): void { + if (this.#disposed) { + throw new Error(`Groth16 ${this.type} handle has been disposed`); + } + if (this.type !== expectedType) { + throw new Error(`expected Groth16 ${expectedType} handle, got ${this.type}`); + } + } +} + +class ConstraintSystemHandle extends RuntimeHandle implements Groth16ConstraintSystem { + constructor(runtime: RuntimeGlobal, kind: Groth16RuntimeKind, curve: SupportedCurveID, handle: string, readonly constraints: number) { + super(runtime, kind, curve, "ccs", handle); + } +} + +class ProvingKeyHandle extends RuntimeHandle implements Groth16ProvingKey { + constructor(runtime: RuntimeGlobal, kind: Groth16RuntimeKind, curve: SupportedCurveID, handle: string) { + super(runtime, kind, curve, "pk", handle); + } +} + +class VerificationKeyHandle extends RuntimeHandle implements Groth16VerificationKey { + constructor(runtime: RuntimeGlobal, kind: Groth16RuntimeKind, curve: SupportedCurveID, handle: string) { + super(runtime, kind, curve, "vk", handle); + } +} + +function runtimeHandle(handle: Groth16Handle, type: RuntimeHandle["type"]): RuntimeHandle { + if (!(handle instanceof RuntimeHandle)) { + throw new Error("Groth16 handle was not created by this module"); + } + handle.assertUsable(type); + return handle; +} + +function assertSameRuntime(a: RuntimeHandle, b: RuntimeHandle): void { + if (a.runtime !== b.runtime || a.kind !== b.kind) { + throw new Error("Groth16 handles belong to different runtimes"); + } + if (a.curve !== b.curve) { + throw new Error(`Groth16 handles belong to different curves: ${a.curve} and ${b.curve}`); + } +} + +function writeUint32BE(out: Uint8Array, offset: number, value: number): void { + out[offset] = (value >>> 24) & 0xff; + out[offset + 1] = (value >>> 16) & 0xff; + out[offset + 2] = (value >>> 8) & 0xff; + out[offset + 3] = value & 0xff; +} + +function writeBigIntBE(out: Uint8Array, offset: number, byteSize: number, value: bigint): void { + let remaining = value; + for (let i = byteSize - 1; i >= 0; i--) { + out[offset + i] = Number(remaining & 0xffn); + remaining >>= 8n; + } +} + +export function createGroth16Module(config: Groth16ModuleConfig): Groth16Module { + const modulus = BigInt(config.modulusHex); + let currentRuntime: Promise | null = null; + let currentKind: Groth16RuntimeKind = "webgpu"; + + function installBridge(): void { + installGroth16WebGPUBridge({ + context: config.context, + curve: config.curve, + g1: config.g1, + g2: config.g2, + g1msm: config.g1msm, + g2msm: config.g2msm, + quotient: config.quotient, + }); + } + + async function loadRuntime(options?: Groth16RuntimeOptions & { kind?: Groth16RuntimeKind }): Promise { + currentKind = options?.kind ?? "webgpu"; + const runtimeOptions = normalizeRuntimeOptions(options); + currentRuntime = loadGoRuntime(currentKind, runtimeOptions, currentKind === "webgpu" ? installBridge : () => {}); + await currentRuntime; + } + + async function getRuntime(): Promise<{ runtime: RuntimeGlobal; kind: Groth16RuntimeKind }> { + if (!currentRuntime) { + await loadRuntime(); + } + return { runtime: await currentRuntime!, kind: currentKind }; + } + + return { + context: config.context, + curve: config.curve, + computeGroth16QuotientPackedRegular(a: Uint8Array, b: Uint8Array, c: Uint8Array): Promise { + return config.quotient.computeGroth16QuotientPackedRegular(a, b, c); + }, + computeGroth16QuotientPackedMont(a: Uint8Array, b: Uint8Array, c: Uint8Array): Promise { + return config.quotient.computeGroth16QuotientPackedMont(a, b, c); + }, + prewarmGroth16QuotientDomain(size: number): Promise { + return config.quotient.prewarmGroth16QuotientDomain(size); + }, + loadRuntime, + async readConstraintSystem(bytes: Uint8Array): Promise { + const { runtime, kind } = await getRuntime(); + const result = await runtime.readConstraintSystem(config.curve, cloneBytes(bytes)); + return new ConstraintSystemHandle(runtime, kind, config.curve, result.handle, result.constraints); + }, + async readProvingKey(bytes: Uint8Array, options?: { format?: Groth16ProvingKeyFormat }): Promise { + const { runtime, kind } = await getRuntime(); + const result = await runtime.readProvingKey(config.curve, cloneBytes(bytes), options?.format ?? "serialized"); + return new ProvingKeyHandle(runtime, kind, config.curve, result.handle); + }, + async readVerificationKey(bytes: Uint8Array): Promise { + const { runtime, kind } = await getRuntime(); + const result = await runtime.readVerificationKey(config.curve, cloneBytes(bytes)); + return new VerificationKeyHandle(runtime, kind, config.curve, result.handle); + }, + async prepareProvingKey(pk: Groth16ProvingKey): Promise { + const pkHandle = runtimeHandle(pk, "pk"); + await pkHandle.runtime.prepareProvingKey(pkHandle.handle); + }, + async prove(ccs: Groth16ConstraintSystem, pk: Groth16ProvingKey, witness: Uint8Array): Promise { + const ccsHandle = runtimeHandle(ccs, "ccs"); + const pkHandle = runtimeHandle(pk, "pk"); + assertSameRuntime(ccsHandle, pkHandle); + return ccsHandle.runtime.prove(ccsHandle.handle, pkHandle.handle, cloneBytes(witness)); + }, + async verify(proof: Uint8Array, vk: Groth16VerificationKey, publicWitness: Uint8Array): Promise { + const vkHandle = runtimeHandle(vk, "vk"); + return vkHandle.runtime.verify(cloneBytes(proof), vkHandle.handle, cloneBytes(publicWitness)); + }, + encodeWitness(values: readonly bigint[], options: { publicCount: number }): Uint8Array { + if (!Number.isInteger(options.publicCount) || options.publicCount < 0 || options.publicCount > values.length) { + throw new Error(`invalid publicCount ${options.publicCount}`); + } + const out = new Uint8Array(12 + values.length * config.frBytes); + writeUint32BE(out, 0, options.publicCount); + writeUint32BE(out, 4, values.length - options.publicCount); + writeUint32BE(out, 8, values.length); + for (let i = 0; i < values.length; i++) { + const value = values[i]; + if (value < 0n || value >= modulus) { + throw new Error(`witness value at index ${i} is outside the scalar field`); + } + writeBigIntBE(out, 12 + i * config.frBytes, config.frBytes, value); + } + return out; + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/groth16_webgpu_bridge.ts b/backend/accelerated/webgpu/web/src/curvegpu/groth16_webgpu_bridge.ts new file mode 100644 index 0000000000..6035194d0e --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/groth16_webgpu_bridge.ts @@ -0,0 +1,252 @@ +import type { + CurveGPUContext, + G1Module, + G1MSMModule, + G2Module, + G2MSMModule, + Groth16QuotientModule, + SupportedCurveID, +} from "./api.js"; + +const CURVE_CONFIG: Record = { + bn254: { + g1CoordinateBytes: 32, + g1PointBytes: 96, + g2ComponentBytes: 32, + g2PointBytes: 192, + }, + bls12_381: { + g1CoordinateBytes: 48, + g1PointBytes: 144, + g2ComponentBytes: 48, + g2PointBytes: 288, + }, + bls12_377: { + g1CoordinateBytes: 48, + g1PointBytes: 144, + g2ComponentBytes: 48, + g2PointBytes: 288, + }, +}; + +type BridgeDependencies = { + context: CurveGPUContext; + curve: SupportedCurveID; + g1: G1Module; + g2: G2Module; + g1msm: G1MSMModule; + g2msm: G2MSMModule; + quotient: Groth16QuotientModule; +}; + +type CachedKey = { + curve: SupportedCurveID; + g1A: Uint8Array; + g1ACount: number; + g1B: Uint8Array; + g1BCount: number; + g1K: Uint8Array; + g1KCount: number; + g1Z: Uint8Array; + g1ZCount: number; + g2B: Uint8Array; + g2BCount: number; + commitmentCount: number; + [name: string]: SupportedCurveID | Uint8Array | number; +}; + +let activeBridge: BridgeDependencies | null = null; +let nextHandle = 1; +const keyCache = new Map(); + +function cloneBytes(bytes: Uint8Array): Uint8Array { + return new Uint8Array(bytes); +} + +function assertBridge(curve: string): BridgeDependencies { + if (!activeBridge) { + throw new Error("Groth16 WebGPU bridge is not initialized"); + } + if (curve !== activeBridge.curve) { + throw new Error(`Groth16 WebGPU bridge is bound to ${activeBridge.curve}, got ${curve}`); + } + return activeBridge; +} + +function unpackG1JacobianPoint(curve: SupportedCurveID, packedPoint: Uint8Array) { + const coordinateBytes = CURVE_CONFIG[curve].g1CoordinateBytes; + return { + x: cloneBytes(packedPoint.slice(0, coordinateBytes)), + y: cloneBytes(packedPoint.slice(coordinateBytes, 2 * coordinateBytes)), + z: cloneBytes(packedPoint.slice(2 * coordinateBytes, 3 * coordinateBytes)), + }; +} + +function unpackG2JacobianPoint(curve: SupportedCurveID, packedPoint: Uint8Array) { + const componentBytes = CURVE_CONFIG[curve].g2ComponentBytes; + return { + x: { + c0: cloneBytes(packedPoint.slice(0, componentBytes)), + c1: cloneBytes(packedPoint.slice(componentBytes, 2 * componentBytes)), + }, + y: { + c0: cloneBytes(packedPoint.slice(2 * componentBytes, 3 * componentBytes)), + c1: cloneBytes(packedPoint.slice(3 * componentBytes, 4 * componentBytes)), + }, + z: { + c0: cloneBytes(packedPoint.slice(4 * componentBytes, 5 * componentBytes)), + c1: cloneBytes(packedPoint.slice(5 * componentBytes, 6 * componentBytes)), + }, + }; +} + +function getKey(handle: string): CachedKey { + const entry = keyCache.get(handle); + if (!entry) { + throw new Error(`unknown Groth16 key handle ${handle}`); + } + return entry; +} + +async function init(curve: SupportedCurveID) { + const bridge = assertBridge(curve); + return { + curve, + adapter: { + vendor: bridge.context.diagnostics.vendor ?? "", + architecture: bridge.context.diagnostics.architecture ?? "", + description: bridge.context.diagnostics.description ?? "", + }, + }; +} + +async function prepareKey(curve: SupportedCurveID, payload: Record) { + assertBridge(curve); + const handle = `${curve}:${nextHandle++}`; + const commitmentCount = Number(payload.commitmentCount ?? 0); + const entry: CachedKey = { + curve, + g1A: cloneBytes(payload.g1A as Uint8Array), + g1ACount: Number(payload.g1ACount), + g1B: cloneBytes(payload.g1B as Uint8Array), + g1BCount: Number(payload.g1BCount), + g1K: cloneBytes(payload.g1K as Uint8Array), + g1KCount: Number(payload.g1KCount), + g1Z: cloneBytes(payload.g1Z as Uint8Array), + g1ZCount: Number(payload.g1ZCount), + g2B: cloneBytes(payload.g2B as Uint8Array), + g2BCount: Number(payload.g2BCount), + commitmentCount, + }; + for (let i = 0; i < commitmentCount; i++) { + const basisName = `commitmentBasis${i}`; + const basisExpSigmaName = `commitmentBasisExpSigma${i}`; + entry[basisName] = cloneBytes(payload[basisName] as Uint8Array); + entry[`${basisName}Count`] = Number(payload[`${basisName}Count`]); + entry[basisExpSigmaName] = cloneBytes(payload[basisExpSigmaName] as Uint8Array); + entry[`${basisExpSigmaName}Count`] = Number(payload[`${basisExpSigmaName}Count`]); + } + keyCache.set(handle, entry); + return { handle }; +} + +async function msmG1Cached(entry: CachedKey, vectorName: string, scalarsPacked: Uint8Array): Promise { + const bridge = assertBridge(entry.curve); + const config = CURVE_CONFIG[entry.curve]; + const basesPacked = entry[vectorName]; + const count = entry[`${vectorName}Count`]; + if (!(basesPacked instanceof Uint8Array) || typeof count !== "number") { + throw new Error(`missing cached G1 vector ${vectorName}`); + } + const resultPacked = await bridge.g1msm.pippengerPackedJacobianBases(basesPacked, scalarsPacked, { + count: 1, + termsPerInstance: count, + window: bridge.g1msm.bestWindow(count), + }); + const jacobian = unpackG1JacobianPoint(entry.curve, resultPacked.slice(0, config.g1PointBytes)); + const affine = await bridge.g1.jacobianToAffine(jacobian); + const out = new Uint8Array(2 * config.g1CoordinateBytes); + out.set(affine.x, 0); + out.set(affine.y, config.g1CoordinateBytes); + return out; +} + +async function msmG2Cached(entry: CachedKey, vectorName: string, scalarsPacked: Uint8Array): Promise { + const bridge = assertBridge(entry.curve); + const config = CURVE_CONFIG[entry.curve]; + const basesPacked = entry[vectorName]; + const count = entry[`${vectorName}Count`]; + if (!(basesPacked instanceof Uint8Array) || typeof count !== "number") { + throw new Error(`missing cached G2 vector ${vectorName}`); + } + const resultPacked = await bridge.g2msm.pippengerPackedJacobianBases(basesPacked, cloneBytes(scalarsPacked), { + count: 1, + termsPerInstance: count, + window: bridge.g2msm.bestWindow(count), + }); + const jacobian = unpackG2JacobianPoint(entry.curve, resultPacked.slice(0, config.g2PointBytes)); + const affine = await bridge.g2.jacobianToAffine(jacobian); + const out = new Uint8Array(4 * config.g2ComponentBytes); + out.set(affine.x.c0, 0); + out.set(affine.x.c1, config.g2ComponentBytes); + out.set(affine.y.c0, 2 * config.g2ComponentBytes); + out.set(affine.y.c1, 3 * config.g2ComponentBytes); + return out; +} + +async function msmG1(handle: string, vectorName: string, scalarsPacked: Uint8Array) { + return msmG1Cached(getKey(handle), vectorName, cloneBytes(scalarsPacked)); +} + +async function msmBatch(handle: string, payload: Record) { + const entry = getKey(handle); + const points: Record = {}; + + if (payload.g1A) { + points.g1A = await msmG1Cached(entry, "g1A", payload.g1A); + } + if (payload.g1B) { + points.g1B = await msmG1Cached(entry, "g1B", payload.g1B); + points.g2B = await msmG2Cached(entry, "g2B", payload.g1B); + } + if (payload.g1K) { + points.g1K = await msmG1Cached(entry, "g1K", payload.g1K); + } + + return points; +} + +async function computeHZMSMG1(handle: string, aPacked: Uint8Array, bPacked: Uint8Array, cPacked: Uint8Array) { + const entry = getKey(handle); + const bridge = assertBridge(entry.curve); + const quotient = await bridge.quotient.computeGroth16QuotientPackedMont( + cloneBytes(aPacked), + cloneBytes(bPacked), + cloneBytes(cPacked), + ); + const zCount = Number(entry.g1ZCount); + const scalars = quotient.subarray(0, zCount * 32); + return msmG1Cached(entry, "g1Z", scalars); +} + +async function prewarmQuotientDomain(curve: SupportedCurveID, size: number) { + const bridge = assertBridge(curve); + await bridge.quotient.prewarmGroth16QuotientDomain(Number(size)); +} + +export function installGroth16WebGPUBridge(dependencies: BridgeDependencies): void { + activeBridge = dependencies; + (globalThis as typeof globalThis & { gnarkGroth16WebGPU?: unknown }).gnarkGroth16WebGPU = { + init, + prepareKey, + msmG1, + msmBatch, + computeHZMSMG1, + prewarmQuotientDomain, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/kernels.ts b/backend/accelerated/webgpu/web/src/curvegpu/kernels.ts new file mode 100644 index 0000000000..8feb5f13a6 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/kernels.ts @@ -0,0 +1,21 @@ +import type { CurveID, FieldID, FieldShape } from "./types.js"; +import { shapeFor } from "./types.js"; +import { fetchShaderText } from "./shaders.js"; + +export interface KernelDescriptor { + curve: CurveID; + field: FieldID; + shaderPath: string; + shape: FieldShape; +} + +export async function loadFieldKernel(curve: CurveID, field: FieldID): Promise { + const shaderPath = `/shaders/curves/${curve}/${field}_arith.wgsl`; + await fetchShaderText(shaderPath); + return { + curve, + field, + shaderPath, + shape: shapeFor(curve, field), + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/msm_bench_sources.ts b/backend/accelerated/webgpu/web/src/curvegpu/msm_bench_sources.ts new file mode 100644 index 0000000000..71b0d607a0 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/msm_bench_sources.ts @@ -0,0 +1,176 @@ +import { fetchBytes, fetchJSON } from "./browser_utils.js"; + +export type BaseLoadResult = { + bases: TBases; + prepMs: number; +}; + +export type BaseSourceInitResult = { + context: TContext; + postMetricLines?: string[]; +}; + +export type BaseSourceProvider = { + init: () => Promise>; + loadBases: (args: { context: TContext; size: number }) => Promise>; +}; + +export type FixtureMetadata = { + count: number; + point_bytes: number; + format: string; +}; + +export type PreferredByteBaseSource = "fixture" | "generated"; + +export type PreferredByteBaseSourceContext = { + baseSource: PreferredByteBaseSource; + baseFixture: Uint8Array | null; + fixtureMeta: FixtureMetadata | null; +}; + +function slicePointByteFixture(fixture: Uint8Array, pointBytes: number, count: number): Uint8Array { + const byteLength = count * pointBytes; + if (fixture.byteLength < byteLength) { + throw new Error(`fixture has ${Math.floor(fixture.byteLength / pointBytes)} points, need ${count}`); + } + return fixture.slice(0, byteLength); +} + +function isFetchFailure(error: unknown): boolean { + return error instanceof Error; +} + +export function createPreferredByteBaseSource(options: { + locationSearch: string; + pointBytes: number; + fixtureJSONPath?: string; + fixtureBinPath?: string; + generatedLoadBases?: (size: number) => Promise; + generateHint?: (size: number) => string; + fixtureLabel?: string; +}): BaseSourceProvider { + const params = new URLSearchParams(options.locationSearch); + const explicitSourceRaw = params.get("base-source") ?? params.get("baseSource"); + const explicitSource: PreferredByteBaseSource | null = + explicitSourceRaw === "fixture" || explicitSourceRaw === "generated" ? explicitSourceRaw : null; + + async function tryLoadFixture(): Promise<{ fixtureMeta: FixtureMetadata; baseFixture: Uint8Array; fixtureLoadMs: number } | null> { + if (!options.fixtureJSONPath || !options.fixtureBinPath) { + return null; + } + const start = performance.now(); + const [fixtureMeta, baseFixture] = await Promise.all([ + fetchJSON(options.fixtureJSONPath), + fetchBytes(options.fixtureBinPath), + ]); + const fixtureLoadMs = performance.now() - start; + if (fixtureMeta.point_bytes !== options.pointBytes) { + throw new Error(`unexpected fixture point size: ${fixtureMeta.point_bytes}`); + } + if (baseFixture.byteLength !== fixtureMeta.count * fixtureMeta.point_bytes) { + throw new Error( + `fixture length mismatch: got ${baseFixture.byteLength}, want ${fixtureMeta.count * fixtureMeta.point_bytes}`, + ); + } + return { fixtureMeta, baseFixture, fixtureLoadMs }; + } + + function missingFixtureMessage(size: number): string { + const noun = options.fixtureLabel ?? "base"; + const base = `no local ${noun} fixture is available`; + if (!options.generateHint) { + return base; + } + const hintSize = size > 0 ? size : 1 << 19; + return `${base}; generate one with \`${options.generateHint(hintSize)}\``; + } + + function smallFixtureMessage(pointCount: number, size: number): string { + const base = `fixture has ${pointCount} points, need ${size}`; + if (!options.generateHint) { + return base; + } + return `${base}; generate a larger one with \`${options.generateHint(size)}\``; + } + + return { + init: async () => { + let fixtureMeta: FixtureMetadata | null = null; + let baseFixture: Uint8Array | null = null; + let fixtureLoadMs: number | null = null; + + if (explicitSource !== "generated") { + try { + const loaded = await tryLoadFixture(); + if (loaded) { + fixtureMeta = loaded.fixtureMeta; + baseFixture = loaded.baseFixture; + fixtureLoadMs = loaded.fixtureLoadMs; + } + } catch (error) { + if (explicitSource === "fixture" || !isFetchFailure(error)) { + throw error; + } + } + } + + let baseSource: PreferredByteBaseSource; + if (explicitSource === "fixture") { + if (!baseFixture || !fixtureMeta) { + throw new Error(missingFixtureMessage(0)); + } + baseSource = "fixture"; + } else if (explicitSource === "generated") { + if (!options.generatedLoadBases) { + throw new Error("generated base source is not configured"); + } + baseSource = "generated"; + } else if (baseFixture && fixtureMeta) { + baseSource = "fixture"; + } else if (options.generatedLoadBases) { + baseSource = "generated"; + } else { + throw new Error(missingFixtureMessage(0)); + } + + const postMetricLines: string[] = [`base_source = ${baseSource}`]; + if (fixtureLoadMs !== null) { + postMetricLines.push(`fixture_load_ms = ${fixtureLoadMs.toFixed(3)}`); + } + if (fixtureMeta) { + postMetricLines.push(`fixture_points = ${fixtureMeta.count}`); + } + + return { + context: { + baseSource, + baseFixture, + fixtureMeta, + }, + postMetricLines, + }; + }, + loadBases: async ({ context, size }) => { + const prepStart = performance.now(); + + if (context.baseSource === "fixture") { + if (!context.baseFixture || !context.fixtureMeta) { + throw new Error(missingFixtureMessage(size)); + } + if (context.fixtureMeta.count < size) { + throw new Error(smallFixtureMessage(context.fixtureMeta.count, size)); + } + return { + bases: slicePointByteFixture(context.baseFixture, options.pointBytes, size), + prepMs: performance.now() - prepStart, + }; + } + + if (!options.generatedLoadBases) { + throw new Error("generated base source is not configured"); + } + return { bases: await options.generatedLoadBases(size), prepMs: performance.now() - prepStart }; + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/msm_gpu_runtime.ts b/backend/accelerated/webgpu/web/src/curvegpu/msm_gpu_runtime.ts new file mode 100644 index 0000000000..a4752b2c1e --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/msm_gpu_runtime.ts @@ -0,0 +1,219 @@ +export type Kernel = { + pipeline: GPUComputePipeline; + bindGroupLayout: GPUBindGroupLayout; +}; + +function logComputePipelineCreation(label: string, entryPoint: string, debug: boolean): void { + if (!debug) { + return; + } + const key = "__curvegpuComputePipelineCreateCount"; + const state = globalThis as typeof globalThis & { [key: string]: number | undefined }; + const count = (state[key] ?? 0) + 1; + state[key] = count; + console.debug(`[curvegpu] createComputePipeline #${count}: ${label} entry=${entryPoint}`); +} + +declare const GPUShaderStage: { COMPUTE: number }; +declare const GPUBufferUsage: { + STORAGE: number; + COPY_DST: number; + COPY_SRC: number; + MAP_READ: number; + UNIFORM: number; +}; +declare const GPUMapMode: { READ: number }; + +export async function createMSMKernelSetAsync>( + device: GPUDevice, + shaderCode: string, + labelPrefix: string, + entryPoints: T, + debug = false, +): Promise<{ [K in keyof T]: Kernel }> { + const shaderModule = device.createShaderModule({ + label: `${labelPrefix}-shader`, + code: shaderCode, + }); + const bindGroupLayout = device.createBindGroupLayout({ + label: `${labelPrefix}-bgl`, + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, + { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + ], + }); + const pipelineLayout = device.createPipelineLayout({ + label: `${labelPrefix}-pl`, + bindGroupLayouts: [bindGroupLayout], + }); + const out = {} as { [K in keyof T]: Kernel }; + await Promise.all( + Object.entries(entryPoints).map(async ([name, entryPoint]) => { + logComputePipelineCreation(`${labelPrefix}-${entryPoint}`, entryPoint, debug); + const pipeline = await device.createComputePipelineAsync({ + label: `${labelPrefix}-${entryPoint}`, + layout: pipelineLayout, + compute: { module: shaderModule, entryPoint }, + }); + out[name as keyof T] = { pipeline, bindGroupLayout }; + }), + ); + return out; +} + +export function createStorageBufferFromBytes( + device: GPUDevice, + label: string, + bytes: Uint8Array, + size: number, +): GPUBuffer { + const buffer = device.createBuffer({ + label, + size, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + if (bytes.byteLength > 0) { + device.queue.writeBuffer(buffer, 0, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)); + } + return buffer; +} + +export function createU32StorageBuffer( + device: GPUDevice, + label: string, + values: Uint32Array, +): GPUBuffer { + const buffer = device.createBuffer({ + label, + size: Math.max(4, values.byteLength), + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + if (values.byteLength > 0) { + device.queue.writeBuffer(buffer, 0, values.buffer.slice(values.byteOffset, values.byteOffset + values.byteLength)); + } + return buffer; +} + +export function createEmptyPointStorageBuffer( + device: GPUDevice, + label: string, + count: number, + pointBytes: number, +): GPUBuffer { + return device.createBuffer({ + label, + size: Math.max(1, count) * pointBytes, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); +} + +export function createParamsBuffer( + device: GPUDevice, + label: string, + uniformBytes: number, + values: { + count: number; + opcode?: number; + termsPerInstance?: number; + window?: number; + numWindows?: number; + bucketCount?: number; + rowWidth?: number; + }, +): GPUBuffer { + const buffer = device.createBuffer({ + label, + size: uniformBytes, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + const params = new Uint32Array(uniformBytes / 4); + params[0] = values.count; + params[1] = values.opcode ?? 0; + params[2] = values.termsPerInstance ?? 0; + params[3] = values.window ?? 0; + params[4] = values.numWindows ?? 0; + params[5] = values.bucketCount ?? 0; + params[6] = values.rowWidth ?? 0; + device.queue.writeBuffer(buffer, 0, params.buffer); + return buffer; +} + +export function createBindGroupForBuffers( + device: GPUDevice, + kernel: Kernel, + label: string, + inputA: GPUBuffer, + inputB: GPUBuffer, + output: GPUBuffer, + params: GPUBuffer, + meta0?: GPUBuffer, + meta1?: GPUBuffer, + meta2?: GPUBuffer, +): GPUBindGroup { + const metaA = meta0 ?? inputB; + const metaB = meta1 ?? inputB; + const metaC = meta2 ?? inputB; + return device.createBindGroup({ + label, + layout: kernel.bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: inputA } }, + { binding: 1, resource: { buffer: inputB } }, + { binding: 2, resource: { buffer: output } }, + { binding: 3, resource: { buffer: params } }, + { binding: 4, resource: { buffer: metaA } }, + { binding: 5, resource: { buffer: metaB } }, + { binding: 6, resource: { buffer: metaC } }, + ], + }); +} + +export async function submitKernel( + device: GPUDevice, + kernel: Kernel, + bindGroup: GPUBindGroup, + count: number, + label: string, + workgroupSize = 64, + debug = false, +): Promise { + if (debug) { + console.debug(`[curvegpu] submitKernel start: ${label} count=${count}`); + } + const encoder = device.createCommandEncoder({ label: `${label}-encoder` }); + const pass = encoder.beginComputePass({ label: `${label}-pass` }); + pass.setPipeline(kernel.pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(Math.ceil(count / workgroupSize)); + pass.end(); + device.queue.submit([encoder.finish()]); + await device.queue.onSubmittedWorkDone(); + if (debug) { + console.debug(`[curvegpu] submitKernel done: ${label}`); + } +} + +export async function readbackBuffer( + device: GPUDevice, + buffer: GPUBuffer, + size: number, +): Promise { + const staging = device.createBuffer({ + label: "g1-readback-staging", + size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + const encoder = device.createCommandEncoder({ label: "g1-readback-encoder" }); + encoder.copyBufferToBuffer(buffer, 0, staging, 0, size); + device.queue.submit([encoder.finish()]); + await staging.mapAsync(GPUMapMode.READ); + const bytes = new Uint8Array(staging.getMappedRange()).slice(); + staging.unmap(); + staging.destroy(); + return bytes; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/msm_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/msm_module.ts new file mode 100644 index 0000000000..e6af6a791f --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/msm_module.ts @@ -0,0 +1,208 @@ +import type { + CurveGPUAffinePoint, + CurveGPUContext, + CurveGPUElementBytes, + CurveGPUJacobianPoint, + CurveGPUPackedPointLayout, + FieldModule, + G1Module, + G1MSMModule, + SupportedCurveID, + CurveGPUMSMOptions, +} from "./api.js"; +import { splitBytesLEToU32 } from "./convert.js"; +import { bestPippengerWindow } from "./msm_shared.js"; +import { runSparseSignedPippengerMSM } from "./msm_pippenger.js"; +import type { PippengerRuntime } from "./msm_pippenger.js"; +import { cloneBytes, ensureByteLength, lazyAsync } from "./runtime_common.js"; + +function clonePoint(point: CurveGPUJacobianPoint): CurveGPUJacobianPoint { + return { x: cloneBytes(point.x), y: cloneBytes(point.y), z: cloneBytes(point.z) }; +} + +function unpackJacobianPoints(bytes: Uint8Array, count: number, coordinateBytes: number, pointBytes: number): CurveGPUJacobianPoint[] { + const out: CurveGPUJacobianPoint[] = []; + for (let i = 0; i < count; i += 1) { + const base = i * pointBytes; + out.push({ + x: cloneBytes(bytes.slice(base, base + coordinateBytes)), + y: cloneBytes(bytes.slice(base + coordinateBytes, base + 2 * coordinateBytes)), + z: cloneBytes(bytes.slice(base + 2 * coordinateBytes, base + 3 * coordinateBytes)), + }); + } + return out; +} + +function packAffineBases( + bases: readonly CurveGPUAffinePoint[], + coordinateBytes: number, + pointBytes: number, + oneMontZ: Uint8Array, +): Uint8Array { + const out = new Uint8Array(bases.length * pointBytes); + bases.forEach((base, index) => { + ensureByteLength(base.x, coordinateBytes, `bases[${index}].x`); + ensureByteLength(base.y, coordinateBytes, `bases[${index}].y`); + const isInfinity = base.x.every((byte) => byte === 0) && base.y.every((byte) => byte === 0); + const offset = index * pointBytes; + out.set(base.x, offset); + out.set(base.y, offset + coordinateBytes); + out.set(isInfinity ? new Uint8Array(coordinateBytes) : oneMontZ, offset + 2 * coordinateBytes); + }); + return out; +} + +function packScalarWords(scalars: readonly Uint8Array[]): Uint32Array { + const out = new Uint32Array(scalars.length * 8); + scalars.forEach((scalar, index) => { + ensureByteLength(scalar, 32, `scalars[${index}]`); + out.set(splitBytesLEToU32(scalar), index * 8); + }); + return out; +} + +function ensurePackedScalars(scalarsPacked: Uint8Array, count: number, label: string): void { + const expected = count * 32; + if (scalarsPacked.byteLength !== expected) { + throw new Error(`${label}: expected ${expected} scalar bytes, got ${scalarsPacked.byteLength}`); + } +} + +function packScalarWordsPacked(scalarsPacked: Uint8Array): Uint32Array { + if (scalarsPacked.byteLength % 32 !== 0) { + throw new Error(`packed scalars: expected a multiple of 32 bytes, got ${scalarsPacked.byteLength}`); + } + const count = scalarsPacked.byteLength / 32; + const out = new Uint32Array(count * 8); + const view = new DataView(scalarsPacked.buffer, scalarsPacked.byteOffset, scalarsPacked.byteLength); + for (let i = 0; i < out.length; i += 1) { + out[i] = view.getUint32(i * 4, true); + } + return out; +} + +export function createG1MSMModule( + context: CurveGPUContext, + options: { + curve: SupportedCurveID; + coordinateBytes: number; + pointBytes: number; + runtime: PippengerRuntime; + }, + fp: FieldModule, + g1: G1Module, +): G1MSMModule { + const { curve, coordinateBytes, pointBytes, runtime } = options; + const label = `${curve}-g1-msm`; + + const getOneMontgomery = lazyAsync(async () => fp.montOne()); + + async function runBatch( + bases: readonly CurveGPUAffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options: CurveGPUMSMOptions = {}, + ): Promise { + if (bases.length !== scalars.length) { + throw new Error(`${label}: bases and scalars length mismatch`); + } + const count = options.count ?? 1; + const termsPerInstance = options.termsPerInstance ?? (count === 1 ? bases.length : 0); + if (!Number.isInteger(count) || count <= 0) { + throw new Error(`${label}: count must be a positive integer`); + } + if (!Number.isInteger(termsPerInstance) || termsPerInstance <= 0) { + throw new Error(`${label}: termsPerInstance must be a positive integer`); + } + if (bases.length !== count * termsPerInstance) { + throw new Error(`${label}: expected ${count * termsPerInstance} bases/scalars for count=${count} termsPerInstance=${termsPerInstance}`); + } + + const oneMontZ = await getOneMontgomery(); + const window = options.window ?? bestPippengerWindow(termsPerInstance); + const output = await runSparseSignedPippengerMSM({ + device: context.device, + runtime, + basesBytes: packAffineBases(bases, coordinateBytes, pointBytes, oneMontZ), + pointBytes, + uniformBytes: 32, + zeroPointBytes: new Uint8Array(pointBytes), + scalarWords: packScalarWords(scalars), + count, + termsPerInstance, + window, + maxChunkSize: options.maxChunkSize, + labelPrefix: label, + debug: context.debug, + }); + return unpackJacobianPoints(output, count, coordinateBytes, pointBytes).map(clonePoint); + } + + return { + context, + curve, + group: "g1", + bestWindow(termCount: number): number { + return bestPippengerWindow(termCount); + }, + async pippengerAffine( + bases: readonly CurveGPUAffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options: CurveGPUMSMOptions = {}, + ): Promise { + return (await runBatch(bases, scalars, { ...options, count: options.count ?? 1 }))[0]; + }, + async pippengerAffineResult( + bases: readonly CurveGPUAffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options: CurveGPUMSMOptions = {}, + ): Promise { + return g1.jacobianToAffine(await this.pippengerAffine(bases, scalars, options)); + }, + async pippengerAffineBatch( + bases: readonly CurveGPUAffinePoint[], + scalars: readonly CurveGPUElementBytes[], + options: CurveGPUMSMOptions, + ): Promise { + return runBatch(bases, scalars, options); + }, + async pippengerPackedJacobianBases( + basesPacked: Uint8Array, + scalarsPacked: Uint8Array, + options: CurveGPUMSMOptions & { layout?: CurveGPUPackedPointLayout }, + ): Promise { + const count = options.count ?? 1; + const termsPerInstance = options.termsPerInstance ?? 0; + if (!Number.isInteger(count) || count <= 0) { + throw new Error(`${label}: count must be a positive integer`); + } + if (!Number.isInteger(termsPerInstance) || termsPerInstance <= 0) { + throw new Error(`${label}: termsPerInstance must be a positive integer`); + } + if ((options.layout ?? "jacobian_x_y_z_le") !== "jacobian_x_y_z_le") { + throw new Error(`${label}: unsupported packed point layout ${options.layout}`); + } + const expectedPointBytes = count * termsPerInstance * pointBytes; + if (basesPacked.byteLength !== expectedPointBytes) { + throw new Error(`${label}: expected ${expectedPointBytes} base bytes, got ${basesPacked.byteLength}`); + } + ensurePackedScalars(scalarsPacked, count * termsPerInstance, `${label}.scalarsPacked`); + const window = options.window ?? bestPippengerWindow(termsPerInstance); + return runSparseSignedPippengerMSM({ + device: context.device, + pool: context.bufferPool, + runtime, + basesBytes: basesPacked, + pointBytes, + uniformBytes: 32, + zeroPointBytes: new Uint8Array(pointBytes), + scalarWords: packScalarWordsPacked(scalarsPacked), + count, + termsPerInstance, + window, + maxChunkSize: options.maxChunkSize, + labelPrefix: label, + debug: context.debug, + }); + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/msm_pippenger.ts b/backend/accelerated/webgpu/web/src/curvegpu/msm_pippenger.ts new file mode 100644 index 0000000000..9a7168d426 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/msm_pippenger.ts @@ -0,0 +1,264 @@ +import { buildSparseSignedBucketMetadataWords } from "./msm_shared.js"; +import { + createBindGroupForBuffers, + createEmptyPointStorageBuffer, + createParamsBuffer, + createStorageBufferFromBytes, + createU32StorageBuffer, + type Kernel, + readbackBuffer, + submitKernel, +} from "./msm_gpu_runtime.js"; +import type { BufferPool } from "./buffer_pool.js"; + +declare const GPUBufferUsage: { STORAGE: number; COPY_SRC: number; COPY_DST: number }; + +type SparseSignedBucketMetadata = ReturnType; + +export type WindowReductionOptions = { + device: GPUDevice; + pool?: BufferPool; + pointBytes: number; + uniformBytes: number; + zeroInput: GPUBuffer; + bucketOutput: GPUBuffer; + bucketCountOut: number; + bucketValuesInput: GPUBuffer; + windowStartsInput: GPUBuffer; + windowCountsInput: GPUBuffer; + metadata: SparseSignedBucketMetadata; + count: number; + labelPrefix: string; +}; + +export type WindowReductionResult = { + windowOutput: GPUBuffer; + cleanupBuffers: GPUBuffer[]; +}; + +export type PippengerRuntime = { + bucket: Kernel; + bucketWorkgroupSize?: number; + combine: Kernel; + reduceWindows(options: WindowReductionOptions): Promise; +}; + +export function buildJacPippengerRuntime( + kernels: { bucket: Kernel; weightJac: Kernel; subsumJac: Kernel; combine: Kernel }, + workgroupSize = 64, + debug = false, +): PippengerRuntime { + return { + bucket: kernels.bucket, + bucketWorkgroupSize: workgroupSize, + combine: kernels.combine, + async reduceWindows(options: WindowReductionOptions): Promise { + const { + device, + pool, + pointBytes, + uniformBytes, + zeroInput, + bucketOutput, + bucketCountOut, + bucketValuesInput, + windowStartsInput, + windowCountsInput, + metadata, + count, + labelPrefix, + } = options; + const weightedBucketOutput = createEmptyPointStorageBuffer(device, `${labelPrefix}-weighted-out`, bucketCountOut, pointBytes); + const weightParams = createParamsBuffer(device, `${labelPrefix}-weight-params`, uniformBytes, { count: bucketCountOut }); + const weightBindGroup = createBindGroupForBuffers(device, kernels.weightJac, `${labelPrefix}-weight-bg`, + bucketOutput, zeroInput, weightedBucketOutput, weightParams, bucketValuesInput); + await submitKernel(device, kernels.weightJac, weightBindGroup, bucketCountOut, `${labelPrefix}-weight`, workgroupSize, debug); + + const windowSize = Math.max(1, count * metadata.numWindows) * pointBytes; + const windowUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST; + const windowOutput = pool + ? pool.acquire(windowSize, windowUsage, `${labelPrefix}-window-out`) + : createEmptyPointStorageBuffer(device, `${labelPrefix}-window-out`, count * metadata.numWindows, pointBytes); + + const windowParams = createParamsBuffer(device, `${labelPrefix}-window-params`, uniformBytes, { count: count * metadata.numWindows }); + const windowBindGroup = createBindGroupForBuffers(device, kernels.subsumJac, `${labelPrefix}-window-bg`, + weightedBucketOutput, zeroInput, windowOutput, windowParams, bucketValuesInput, windowStartsInput, windowCountsInput); + await submitKernel(device, kernels.subsumJac, windowBindGroup, count * metadata.numWindows * workgroupSize, + `${labelPrefix}-window`, workgroupSize, debug); + return { windowOutput, cleanupBuffers: [weightedBucketOutput, weightParams, windowParams] }; + }, + }; +} + +export async function runSparseSignedPippengerMSM(options: { + device: GPUDevice; + pool?: BufferPool; + runtime: PippengerRuntime; + basesBytes: Uint8Array; + pointBytes: number; + uniformBytes: number; + zeroPointBytes: Uint8Array; + scalarWords: Uint32Array; + count: number; + termsPerInstance: number; + window: number; + maxChunkSize?: number; + labelPrefix: string; + debug?: boolean; +}): Promise { + const { + device, + pool, + runtime, + basesBytes, + pointBytes, + uniformBytes, + zeroPointBytes, + scalarWords, + count, + termsPerInstance, + window, + maxChunkSize = 256, + labelPrefix, + debug = false, + } = options; + + const metadata = buildSparseSignedBucketMetadataWords(scalarWords, count, termsPerInstance, window, maxChunkSize); + if (debug) { + console.debug("[curvegpu] msm metadata", { + labelPrefix, + count, + termsPerInstance, + window, + pointBytes, + numWindows: metadata.numWindows, + bucketCount: metadata.bucketCount, + bucketCountOut: metadata.bucketPointers.length, + baseIndicesLen: metadata.baseIndices.length, + bucketPointersLen: metadata.bucketPointers.length, + bucketSizesLen: metadata.bucketSizes.length, + bucketValuesLen: metadata.bucketValues.length, + windowStartsLen: metadata.windowStarts.length, + windowCountsLen: metadata.windowCounts.length, + bucketSizesHead: Array.from(metadata.bucketSizes.slice(0, 16)), + bucketValuesHead: Array.from(metadata.bucketValues.slice(0, 16)), + windowCountsHead: Array.from(metadata.windowCounts.slice(0, 16)), + }); + } + const storageInUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST; + const storagePointUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST; + + const zeroSize = Math.max(4, pointBytes); + const zeroInput = pool + ? pool.acquire(zeroSize, storageInUsage, `${labelPrefix}-zero`) + : createStorageBufferFromBytes(device, `${labelPrefix}-zero`, zeroPointBytes, pointBytes); + if (pool) { + device.queue.writeBuffer(zeroInput, 0, zeroPointBytes.buffer, zeroPointBytes.byteOffset, zeroPointBytes.byteLength); + } + + const basesSize = Math.max(1, termsPerInstance * count) * pointBytes; + const basesInput = pool + ? pool.acquire(basesSize, storageInUsage, `${labelPrefix}-bases`) + : createStorageBufferFromBytes(device, `${labelPrefix}-bases`, basesBytes, basesSize); + if (pool) { + device.queue.writeBuffer(basesInput, 0, basesBytes.buffer, basesBytes.byteOffset, basesBytes.byteLength); + } + const baseIndicesInput = createU32StorageBuffer(device, `${labelPrefix}-base-indices`, metadata.baseIndices); + const bucketPointersInput = createU32StorageBuffer(device, `${labelPrefix}-bucket-pointers`, metadata.bucketPointers); + const bucketSizesInput = createU32StorageBuffer(device, `${labelPrefix}-bucket-sizes`, metadata.bucketSizes); + + const bucketCountOut = metadata.bucketPointers.length; + const bucketSize = Math.max(1, bucketCountOut) * pointBytes; + const bucketOutput = pool + ? pool.acquire(bucketSize, storagePointUsage, `${labelPrefix}-bucket-out`) + : createEmptyPointStorageBuffer(device, `${labelPrefix}-bucket-out`, bucketCountOut, pointBytes); + const bucketParams = createParamsBuffer(device, `${labelPrefix}-bucket-params`, uniformBytes, { + count: bucketCountOut, + termsPerInstance, + window, + numWindows: metadata.numWindows, + bucketCount: metadata.bucketCount, + }); + const bucketBindGroup = createBindGroupForBuffers( + device, + runtime.bucket, + `${labelPrefix}-bucket-bg`, + basesInput, + zeroInput, + bucketOutput, + bucketParams, + baseIndicesInput, + bucketPointersInput, + bucketSizesInput, + ); + await submitKernel(device, runtime.bucket, bucketBindGroup, bucketCountOut, `${labelPrefix}-bucket`, runtime.bucketWorkgroupSize ?? 64, debug); + + const bucketValuesInput = createU32StorageBuffer(device, `${labelPrefix}-bucket-values`, metadata.bucketValues); + const windowStartsInput = createU32StorageBuffer(device, `${labelPrefix}-window-starts`, metadata.windowStarts); + const windowCountsInput = createU32StorageBuffer(device, `${labelPrefix}-window-counts`, metadata.windowCounts); + + const { windowOutput, cleanupBuffers: windowReductionCleanup } = await runtime.reduceWindows({ + device, + pool, + pointBytes, + uniformBytes, + zeroInput, + bucketOutput, + bucketCountOut, + bucketValuesInput, + windowStartsInput, + windowCountsInput, + metadata, + count, + labelPrefix, + }); + + const finalSize = Math.max(1, count) * pointBytes; + const finalOutput = pool + ? pool.acquire(finalSize, storagePointUsage, `${labelPrefix}-final-out`) + : createEmptyPointStorageBuffer(device, `${labelPrefix}-final-out`, count, pointBytes); + const finalParams = createParamsBuffer(device, `${labelPrefix}-final-params`, uniformBytes, { + count, + termsPerInstance, + window, + numWindows: metadata.numWindows, + bucketCount: metadata.bucketCount, + }); + const finalBindGroup = createBindGroupForBuffers( + device, + runtime.combine, + `${labelPrefix}-final-bg`, + windowOutput, + zeroInput, + finalOutput, + finalParams, + ); + await submitKernel(device, runtime.combine, finalBindGroup, count, `${labelPrefix}-final`, 64, debug); + + const result = await readbackBuffer(device, finalOutput, Math.max(1, count) * pointBytes); + + if (pool) { + pool.release(zeroInput); + pool.release(basesInput); + pool.release(bucketOutput); + pool.release(windowOutput); + pool.release(finalOutput); + } else { + zeroInput.destroy(); + basesInput.destroy(); + bucketOutput.destroy(); + windowOutput.destroy(); + finalOutput.destroy(); + } + baseIndicesInput.destroy(); + bucketPointersInput.destroy(); + bucketSizesInput.destroy(); + bucketValuesInput.destroy(); + windowStartsInput.destroy(); + windowCountsInput.destroy(); + windowReductionCleanup.forEach((buffer) => buffer.destroy()); + bucketParams.destroy(); + finalParams.destroy(); + + return result; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/msm_shared.ts b/backend/accelerated/webgpu/web/src/curvegpu/msm_shared.ts new file mode 100644 index 0000000000..0dfac8b2a1 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/msm_shared.ts @@ -0,0 +1,204 @@ +import { bytesToHex } from "./browser_utils.js"; + +export type ScalarBatch = { + hexes: string[]; + words: Uint32Array; +}; + +export type SparseSignedBucketMetadata = { + baseIndices: Uint32Array; + bucketPointers: Uint32Array; + bucketSizes: Uint32Array; + bucketValues: Uint32Array; + windowStarts: Uint32Array; + windowCounts: Uint32Array; + numWindows: number; + bucketCount: number; +}; + +export const INDEX_SIGN_BIT = 0x80000000; + +export function bestPippengerWindow(count: number): number { + const windows = [4, 5, 6, 7, 8, 9, 10, 11, 12]; + let best = windows[0]; + let bestCost = Number.POSITIVE_INFINITY; + for (const window of windows) { + const cost = Math.ceil(255 / window) * (count + (1 << window)); + if (cost < bestCost) { + bestCost = cost; + best = window; + } + } + return best; +} + +export function hexesToScalarWords(hexes: readonly string[]): Uint32Array { + const words = new Uint32Array(hexes.length * 8); + for (let i = 0; i < hexes.length; i += 1) { + const hex = hexes[i]; + for (let byteIndex = 0; byteIndex < 32; byteIndex += 1) { + const value = Number.parseInt(hex.slice(byteIndex * 2, byteIndex * 2 + 2), 16); + words[i * 8 + (byteIndex >>> 2)] |= value << ((byteIndex & 3) * 8); + } + } + return words; +} + +export function makeRandomScalarBatch(count: number, salt = 0x9e3779b9): ScalarBatch { + const hexes = new Array(count); + const words = new Uint32Array(count * 8); + for (let index = 0; index < count; index += 1) { + const scalar = makeRandomScalarData((salt ^ count ^ index) >>> 0); + hexes[index] = scalar.hex; + words.set(scalar.words, index * 8); + } + return { hexes, words }; +} + +export function buildSparseSignedBucketMetadataWords( + scalarWords: Uint32Array, + count: number, + termsPerInstance: number, + window: number, + maxChunkSize = 256, +): SparseSignedBucketMetadata { + const numWindows = Math.ceil(256 / window) + 1; + const bucketCount = 1 << (window - 1); + const totalWindows = count * numWindows; + const logicalBucketSizes = new Uint32Array(totalWindows * bucketCount); + const half = 1 << (window - 1); + const full = 1 << window; + + for (let instance = 0; instance < count; instance += 1) { + const baseOffset = instance * termsPerInstance; + for (let term = 0; term < termsPerInstance; term += 1) { + const idx = baseOffset + term; + const scalarBase = idx * 8; + let carry = 0; + for (let win = 0; win < numWindows; win += 1) { + const unsigned = win < numWindows - 1 ? extractWindowDigitWords(scalarWords, scalarBase, win * window, window) : 0; + let value = unsigned + carry; + carry = 0; + if (value >= half) { + value = full - value; + if (value !== 0) { + const slot = (instance * numWindows + win) * bucketCount + (value - 1); + logicalBucketSizes[slot] += 1; + } + carry = 1; + } else if (value !== 0) { + const slot = (instance * numWindows + win) * bucketCount + (value - 1); + logicalBucketSizes[slot] += 1; + } + } + } + } + + const logicalBucketPointers = new Uint32Array(totalWindows * bucketCount); + let totalEntries = 0; + for (let i = 0; i < logicalBucketSizes.length; i += 1) { + logicalBucketPointers[i] = totalEntries; + totalEntries += logicalBucketSizes[i]; + } + const baseIndices = new Uint32Array(totalEntries); + const writeOffsets = logicalBucketPointers.slice(); + + for (let instance = 0; instance < count; instance += 1) { + const baseOffset = instance * termsPerInstance; + for (let term = 0; term < termsPerInstance; term += 1) { + const idx = baseOffset + term; + const scalarBase = idx * 8; + let carry = 0; + for (let win = 0; win < numWindows; win += 1) { + const unsigned = win < numWindows - 1 ? extractWindowDigitWords(scalarWords, scalarBase, win * window, window) : 0; + let value = unsigned + carry; + carry = 0; + let neg = false; + if (value >= half) { + value = full - value; + neg = value !== 0; + carry = 1; + } + if (value === 0) { + continue; + } + const slot = (instance * numWindows + win) * bucketCount + (value - 1); + const raw = neg ? ((idx | INDEX_SIGN_BIT) >>> 0) : idx; + baseIndices[writeOffsets[slot]] = raw; + writeOffsets[slot] += 1; + } + } + } + + const bucketPointers: number[] = []; + const bucketSizes: number[] = []; + const bucketValues: number[] = []; + const windowStarts = new Uint32Array(totalWindows); + const windowCounts = new Uint32Array(totalWindows); + for (let windowSlot = 0; windowSlot < totalWindows; windowSlot += 1) { + windowStarts[windowSlot] = bucketPointers.length; + let dispatchedInWindow = 0; + const bucketBase = windowSlot * bucketCount; + for (let value = 1; value <= bucketCount; value += 1) { + const slot = bucketBase + (value - 1); + const size = logicalBucketSizes[slot]; + if (size === 0) { + continue; + } + const ptr = logicalBucketPointers[slot]; + for (let offset = 0; offset < size; offset += maxChunkSize) { + bucketPointers.push(ptr + offset); + bucketSizes.push(Math.min(size - offset, maxChunkSize)); + bucketValues.push(value); + dispatchedInWindow += 1; + } + } + windowCounts[windowSlot] = dispatchedInWindow; + } + + return { + baseIndices, + bucketPointers: Uint32Array.from(bucketPointers), + bucketSizes: Uint32Array.from(bucketSizes), + bucketValues: Uint32Array.from(bucketValues), + windowStarts, + windowCounts, + numWindows, + bucketCount, + }; +} + +function extractWindowDigitWords(words: Uint32Array, scalarBase: number, bitOffset: number, window: number): number { + if (window <= 0) { + return 0; + } + const word = Math.floor(bitOffset / 32); + const shift = bitOffset % 32; + const mask = (1 << window) - 1; + if (word >= 8) { + return 0; + } + const lo = words[scalarBase + word] >>> shift; + if (shift + window <= 32 || word + 1 >= 8) { + return lo & mask; + } + const highWidth = shift + window - 32; + const hiMask = (1 << highWidth) - 1; + const hi = words[scalarBase + word + 1] & hiMask; + return (lo | (hi << (32 - shift))) & mask; +} + +function makeRandomScalarData(seed: number): { hex: string; words: Uint32Array } { + const bytes = new Uint8Array(32); + const words = new Uint32Array(8); + let state = seed >>> 0; + for (let i = 0; i < bytes.length; i += 1) { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + const value = state & 0xff; + bytes[i] = value; + words[i >>> 2] |= value << ((i & 3) * 8); + } + return { hex: bytesToHex(bytes), words }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/ntt_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/ntt_module.ts new file mode 100644 index 0000000000..b2bc9a4286 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/ntt_module.ts @@ -0,0 +1,633 @@ +import type { CurveGPUContext, CurveGPUElementBytes, FieldModule, Groth16QuotientModule, NTTModule, SupportedCurveID } from "./api.js"; +import type { SimpleKernel } from "./runtime_common.js"; +import { + createSimpleBindGroup, + createSimpleStorageBuffer, + createSimpleStorageBufferFromBytes, + createSimpleUniformBuffer, + ensureByteLength, + lazyAsync, + packElementBatch, + readbackSimpleBuffer, + runSimpleKernel, + submitSimpleKernel, + unpackElementBatch, +} from "./runtime_common.js"; +import { fetchJSON } from "./browser_utils.js"; +import { hexToBytesLE } from "./encoding.js"; + +declare const GPUBufferUsage: { + STORAGE: number; + COPY_DST: number; + COPY_SRC: number; +}; + +const VECTOR_OP_MUL_FACTORS = 3; +const VECTOR_OP_BIT_REVERSE_COPY = 4; +const FIELD_OP_SUB = 4; +const FIELD_OP_MUL = 9; +const FIELD_OP_TO_MONT = 11; +const FIELD_OP_FROM_MONT = 12; +type DomainMetadata = { + log_n: number; + size: number; + omega_hex: string; + omega_inv_hex: string; + cardinality_inv_hex: string; + coset_gen_hex: string; + coset_gen_inv_hex: string; + coset_den_inv_hex: string; +}; + +type DomainMetadataFile = { + domains: DomainMetadata[]; +}; + +type PreparedDomain = { + forwardStageMont: Uint8Array[]; + inverseStageMont: Uint8Array[]; + inverseScaleMont: Uint8Array; + inverseScaleFactorsPackedMont: Uint8Array; + cosetPowersPackedMont: Uint8Array; + inverseCosetPowersPackedMont: Uint8Array; + cosetDenInvMont: Uint8Array; + cosetDenInvFactorsPackedMont: Uint8Array; +}; + +function hexToBigInt(hex: string): bigint { + return BigInt(`0x${hex}`); +} + +function modPow(base: bigint, exp: bigint, mod: bigint): bigint { + let result = 1n; + let acc = base % mod; + let power = exp; + while (power > 0n) { + if ((power & 1n) === 1n) { + result = (result * acc) % mod; + } + acc = (acc * acc) % mod; + power >>= 1n; + } + return result; +} + +function bigIntToBytesLE(value: bigint, byteSize: number): Uint8Array { + const out = new Uint8Array(byteSize); + let x = value; + for (let i = 0; i < byteSize; i += 1) { + out[i] = Number(x & 0xffn); + x >>= 8n; + } + return out; +} + +function ensurePackedElements(bytes: Uint8Array, elementBytes: number, label: string): number { + if (bytes.byteLength % elementBytes !== 0) { + throw new Error(`${label}: expected a multiple of ${elementBytes} bytes, got ${bytes.byteLength}`); + } + return bytes.byteLength / elementBytes; +} + +function repeatPackedElement(value: Uint8Array, count: number): Uint8Array { + const out = new Uint8Array(value.byteLength * count); + for (let i = 0; i < count; i += 1) { + out.set(value, i * value.byteLength); + } + return out; +} + +function repeatPackedVector(value: Uint8Array, count: number): Uint8Array { + const out = new Uint8Array(value.byteLength * count); + for (let i = 0; i < count; i += 1) { + out.set(value, i * value.byteLength); + } + return out; +} + +function buildPowerVectorPackedRegular(base: bigint, count: number, modulus: bigint, elementBytes: number): Uint8Array { + const out = new Uint8Array(count * elementBytes); + let acc = 1n; + for (let i = 0; i < count; i += 1) { + out.set(bigIntToBytesLE(acc, elementBytes), i * elementBytes); + acc = (acc * base) % modulus; + } + return out; +} + +function buildRegularStageElements(domain: DomainMetadata, inverse: boolean, modulus: bigint, elementBytes: number): Uint8Array[][] { + const logN = domain.log_n; + const omega = hexToBigInt(inverse ? domain.omega_inv_hex : domain.omega_hex); + const stages: Uint8Array[][] = []; + for (let stage = 1; stage <= logN; stage += 1) { + const m = 1 << (stage - 1); + const exponentShift = BigInt(logN - stage); + const step = modPow(omega, 1n << exponentShift, modulus); + const stageElements: Uint8Array[] = []; + let acc = 1n; + for (let i = 0; i < m; i += 1) { + stageElements.push(bigIntToBytesLE(acc, elementBytes)); + acc = (acc * step) % modulus; + } + stages.push(stageElements); + } + return stages; +} + +export function createNTTModule( + context: CurveGPUContext, + options: { + curve: SupportedCurveID; + vectorKernel: SimpleKernel; + fieldKernel: SimpleKernel; + nttKernel: SimpleKernel; + domainPath: string; + modulusHex: string; + }, + fr: FieldModule, +): NTTModule & Groth16QuotientModule { + const { curve, vectorKernel, fieldKernel, nttKernel, domainPath, modulusHex } = options; + const label = `${curve}-fr-ntt`; + const elementBytes = fr.byteSize; + + const getVectorKernel = lazyAsync(async () => vectorKernel); + const getFieldKernel = lazyAsync(async () => fieldKernel); + const getNTTKernel = lazyAsync(async () => nttKernel); + const getDomains = lazyAsync(async () => fetchJSON(domainPath)); + const domainCache = new Map>(); + const modulus = BigInt(modulusHex); + + async function prepareDomain(size: number): Promise { + const cached = domainCache.get(size); + if (cached) { + return cached; + } + const promise = (async (): Promise => { + const file = await getDomains(); + const domain = file.domains.find((item) => item.size === size); + if (!domain) { + throw new Error(`${label}: missing domain metadata for size ${size}`); + } + const forwardStageRegular = buildRegularStageElements(domain, false, modulus, elementBytes); + const inverseStageRegular = buildRegularStageElements(domain, true, modulus, elementBytes); + const forwardStageMont = await Promise.all( + forwardStageRegular.map(async (stage) => + fr.toMontgomeryPacked(packElementBatch(stage, elementBytes, `${label}.forwardStageRegular`)), + ), + ); + const inverseStageMont = await Promise.all( + inverseStageRegular.map(async (stage) => + fr.toMontgomeryPacked(packElementBatch(stage, elementBytes, `${label}.inverseStageRegular`)), + ), + ); + const inverseScaleMont = await fr.toMontgomery(hexToBytesLE(domain.cardinality_inv_hex, elementBytes)); + const cosetPowersPackedMont = await fr.toMontgomeryPacked( + buildPowerVectorPackedRegular(hexToBigInt(domain.coset_gen_hex), size, modulus, elementBytes), + ); + const inverseCosetPowersPackedMont = await fr.toMontgomeryPacked( + buildPowerVectorPackedRegular(hexToBigInt(domain.coset_gen_inv_hex), size, modulus, elementBytes), + ); + const cosetDenInvMont = await fr.toMontgomery(hexToBytesLE(domain.coset_den_inv_hex, elementBytes)); + return { + forwardStageMont, + inverseStageMont, + inverseScaleMont, + inverseScaleFactorsPackedMont: repeatPackedElement(inverseScaleMont, size), + cosetPowersPackedMont, + inverseCosetPowersPackedMont, + cosetDenInvMont, + cosetDenInvFactorsPackedMont: repeatPackedElement(cosetDenInvMont, size), + }; + })(); + domainCache.set(size, promise); + return promise; + } + + async function runVectorOpPacked(opcode: number, valuesPacked: Uint8Array, factorsPacked?: Uint8Array, logCount = 0): Promise { + const count = ensurePackedElements(valuesPacked, elementBytes, `${label}.valuesPacked`); + const kernel = await getVectorKernel(); + const factorBytes = factorsPacked ?? new Uint8Array(valuesPacked.byteLength); + if (factorBytes.byteLength !== valuesPacked.byteLength) { + throw new Error(`${label}.factorsPacked: expected ${valuesPacked.byteLength} bytes, got ${factorBytes.byteLength}`); + } + return runSimpleKernel({ + device: context.device, + pool: context.bufferPool, + kernel, + label: `${label}-vector-packed-${opcode}`, + inputA: valuesPacked, + inputB: factorBytes, + outputBytes: count * elementBytes, + uniformWords: Uint32Array.from([count, opcode, logCount, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + } + + async function runFieldOpPacked(opcode: number, inputA: Uint8Array, inputB: Uint8Array): Promise { + const count = ensurePackedElements(inputA, elementBytes, `${label}.fieldPackedA`); + if (inputB.byteLength !== inputA.byteLength) { + throw new Error(`${label}.fieldPackedB: expected ${inputA.byteLength} bytes, got ${inputB.byteLength}`); + } + const kernel = await getFieldKernel(); + return runSimpleKernel({ + device: context.device, + pool: context.bufferPool, + kernel, + label: `${label}-field-packed-${opcode}`, + inputA, + inputB, + outputBytes: count * elementBytes, + uniformWords: Uint32Array.from([count, opcode, 0, 0, 0, 0, 0, 0]), + workgroups: Math.ceil(count / kernel.workgroupSize), + }); + } + + async function runPipelinePackedBatch(options: { + values: Uint8Array; + vectorSize: number; + vectorCount: number; + inverse: boolean; + inputRegular: boolean; + outputRegular: boolean; + inputBitReversed?: boolean; + inverseCoset?: boolean; + }): Promise { + const { + values, + vectorSize, + vectorCount, + inverse, + inputRegular, + outputRegular, + inputBitReversed = false, + inverseCoset = false, + } = options; + if (inverseCoset && !inverse) { + throw new Error(`${label}: inverseCoset requires inverse NTT`); + } + if (!Number.isInteger(vectorSize) || vectorSize <= 0 || (vectorSize & (vectorSize - 1)) !== 0) { + throw new Error(`${label}: NTT input length must be a non-zero power of two`); + } + if (!Number.isInteger(vectorCount) || vectorCount <= 0) { + throw new Error(`${label}: NTT vector count must be positive`); + } + const totalCount = ensurePackedElements(values, elementBytes, `${label}.pipeline.values`); + const expectedCount = vectorSize * vectorCount; + if (totalCount !== expectedCount) { + throw new Error(`${label}: expected ${expectedCount} packed elements, got ${totalCount}`); + } + + const totalBytes = totalCount * elementBytes; + const domain = await prepareDomain(vectorSize); + const [fieldKernel, vectorKernel, nttKernel] = await Promise.all([getFieldKernel(), getVectorKernel(), getNTTKernel()]); + const zeroAux = createSimpleStorageBufferFromBytes( + context.device, + `${label}-zero-aux`, + new Uint8Array(4), + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + ); + let current = createSimpleStorageBufferFromBytes( + context.device, + `${label}-state-a`, + values, + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, + ); + let next = createSimpleStorageBuffer( + context.device, + `${label}-state-b`, + totalBytes, + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, + ); + + const dispatch = async ( + kernel: SimpleKernel, + inputA: GPUBuffer, + inputB: GPUBuffer, + output: GPUBuffer, + uniformWords: Uint32Array, + opLabel: string, + ): Promise => { + const uniform = createSimpleUniformBuffer(context.device, `${opLabel}-params`, uniformWords); + try { + const bindGroup = createSimpleBindGroup(context.device, kernel, `${opLabel}-bg`, inputA, inputB, output, uniform); + await submitSimpleKernel(context.device, kernel, bindGroup, Math.ceil(totalCount / kernel.workgroupSize), opLabel); + } finally { + uniform.destroy(); + } + }; + + const dispatchNTTStage = async ( + inputA: GPUBuffer, + twiddles: GPUBuffer, + output: GPUBuffer, + uniformWords: Uint32Array, + opLabel: string, + ): Promise => { + const uniform = createSimpleUniformBuffer(context.device, `${opLabel}-params`, uniformWords); + try { + const bindGroup = createSimpleBindGroup(context.device, nttKernel, `${opLabel}-bg`, inputA, twiddles, output, uniform); + const encoder = context.device.createCommandEncoder({ label: `${opLabel}-encoder` }); + const pass = encoder.beginComputePass({ label: `${opLabel}-pass` }); + pass.setPipeline(nttKernel.pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(Math.ceil((vectorSize / 2) / nttKernel.workgroupSize), vectorCount, 1); + pass.end(); + context.device.queue.submit([encoder.finish()]); + await context.device.queue.onSubmittedWorkDone(); + } finally { + uniform.destroy(); + } + }; + + const swap = (): void => { + const tmp = current; + current = next; + next = tmp; + }; + + try { + if (inputRegular) { + await dispatch( + fieldKernel, + current, + zeroAux, + next, + Uint32Array.from([totalCount, FIELD_OP_TO_MONT, 0, 0, 0, 0, 0, 0]), + `${label}-to-mont`, + ); + swap(); + } + + if (!inputBitReversed) { + await dispatch( + vectorKernel, + current, + zeroAux, + next, + Uint32Array.from([totalCount, VECTOR_OP_BIT_REVERSE_COPY, Math.round(Math.log2(vectorSize)), vectorSize, 0, 0, 0, 0]), + `${label}-bit-reverse`, + ); + swap(); + } + + const stages = inverse ? domain.inverseStageMont : domain.forwardStageMont; + for (let stage = 0; stage < stages.length; stage += 1) { + const twiddleBuffer = createSimpleStorageBufferFromBytes( + context.device, + `${label}-twiddles-${stage}`, + stages[stage], + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + ); + try { + await dispatchNTTStage( + current, + twiddleBuffer, + next, + Uint32Array.from([vectorSize, 1 << stage, vectorCount, inverse ? 1 : 0, 0, 0, 0, 0]), + `${label}-stage-${stage}-${inverse ? "inv" : "fwd"}`, + ); + } finally { + twiddleBuffer.destroy(); + } + swap(); + } + + if (inverse) { + const inverseScaleFactorsPackedMont = + vectorCount === 1 ? domain.inverseScaleFactorsPackedMont : repeatPackedVector(domain.inverseScaleFactorsPackedMont, vectorCount); + const factorBuffer = createSimpleStorageBufferFromBytes( + context.device, + `${label}-inverse-scale`, + inverseScaleFactorsPackedMont, + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + ); + try { + await dispatch( + vectorKernel, + current, + factorBuffer, + next, + Uint32Array.from([totalCount, VECTOR_OP_MUL_FACTORS, 0, 0, 0, 0, 0, 0]), + `${label}-inverse-scale`, + ); + } finally { + factorBuffer.destroy(); + } + swap(); + } + + if (inverseCoset) { + const inverseCosetPowersPackedMont = + vectorCount === 1 ? domain.inverseCosetPowersPackedMont : repeatPackedVector(domain.inverseCosetPowersPackedMont, vectorCount); + const factorBuffer = createSimpleStorageBufferFromBytes( + context.device, + `${label}-inverse-coset-scale`, + inverseCosetPowersPackedMont, + GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + ); + try { + await dispatch( + vectorKernel, + current, + factorBuffer, + next, + Uint32Array.from([totalCount, VECTOR_OP_MUL_FACTORS, 0, 0, 0, 0, 0, 0]), + `${label}-inverse-coset-scale`, + ); + } finally { + factorBuffer.destroy(); + } + swap(); + } + + if (outputRegular) { + await dispatch( + fieldKernel, + current, + zeroAux, + next, + Uint32Array.from([totalCount, FIELD_OP_FROM_MONT, 0, 0, 0, 0, 0, 0]), + `${label}-from-mont`, + ); + swap(); + } + + return await readbackSimpleBuffer(context.device, current, totalBytes, `${label}-pipeline`); + } finally { + zeroAux.destroy(); + current.destroy(); + next.destroy(); + } + } + + async function runPipelinePacked(options: { + values: Uint8Array; + inverse: boolean; + inputRegular: boolean; + outputRegular: boolean; + inputBitReversed?: boolean; + inverseCoset?: boolean; + }): Promise { + const count = ensurePackedElements(options.values, elementBytes, `${label}.pipeline.values`); + return runPipelinePackedBatch({ + ...options, + vectorSize: count, + vectorCount: 1, + }); + } + + async function computeGroth16QuotientPacked( + a: Uint8Array, + b: Uint8Array, + c: Uint8Array, + inputMontgomery: boolean, + ): Promise { + const count = ensurePackedElements(a, elementBytes, `${label}.groth16.a`); + if (b.byteLength !== a.byteLength || c.byteLength !== a.byteLength) { + throw new Error(`${label}: Groth16 quotient inputs must have identical packed lengths`); + } + if (count === 0 || (count & (count - 1)) !== 0) { + throw new Error(`${label}: Groth16 quotient input length must be a non-zero power of two`); + } + + const domain = await prepareDomain(count); + const [aMont, bMont, cMont] = inputMontgomery + ? [a, b, c] + : await Promise.all([ + fr.toMontgomeryPacked(a), + fr.toMontgomeryPacked(b), + fr.toMontgomeryPacked(c), + ]); + const [aCoeffMont, bCoeffMont, cCoeffMont] = await Promise.all([ + runPipelinePacked({ values: aMont, inverse: true, inputRegular: false, outputRegular: false }), + runPipelinePacked({ values: bMont, inverse: true, inputRegular: false, outputRegular: false }), + runPipelinePacked({ values: cMont, inverse: true, inputRegular: false, outputRegular: false }), + ]); + const [aCosetInputMont, bCosetInputMont, cCosetInputMont] = await Promise.all([ + runVectorOpPacked(VECTOR_OP_MUL_FACTORS, aCoeffMont, domain.cosetPowersPackedMont), + runVectorOpPacked(VECTOR_OP_MUL_FACTORS, bCoeffMont, domain.cosetPowersPackedMont), + runVectorOpPacked(VECTOR_OP_MUL_FACTORS, cCoeffMont, domain.cosetPowersPackedMont), + ]); + const [aCosetMont, bCosetMont, cCosetMont] = await Promise.all([ + runPipelinePacked({ values: aCosetInputMont, inverse: false, inputRegular: false, outputRegular: false }), + runPipelinePacked({ values: bCosetInputMont, inverse: false, inputRegular: false, outputRegular: false }), + runPipelinePacked({ values: cCosetInputMont, inverse: false, inputRegular: false, outputRegular: false }), + ]); + const abCosetMont = await runFieldOpPacked(FIELD_OP_MUL, aCosetMont, bCosetMont); + const numeratorCosetMont = await runFieldOpPacked(FIELD_OP_SUB, abCosetMont, cCosetMont); + const scaledCosetMont = await runVectorOpPacked( + VECTOR_OP_MUL_FACTORS, + numeratorCosetMont, + domain.cosetDenInvFactorsPackedMont, + ); + const hShiftedCoeffMont = await runPipelinePacked({ + values: scaledCosetMont, + inverse: true, + inputRegular: false, + outputRegular: false, + }); + const hCoeffMont = await runVectorOpPacked( + VECTOR_OP_MUL_FACTORS, + hShiftedCoeffMont, + domain.inverseCosetPowersPackedMont, + ); + const hCoeffRegular = await fr.fromMontgomeryPacked(hCoeffMont); + return runVectorOpPacked(VECTOR_OP_BIT_REVERSE_COPY, hCoeffRegular, undefined, Math.round(Math.log2(count))); + } + + async function prewarmGroth16QuotientDomain(size: number): Promise { + await prepareDomain(size); + } + + return { + context, + curve, + field: "fr", + async supportedSizes(): Promise { + const file = await getDomains(); + return file.domains.map((domain) => domain.size).sort((a, b) => a - b); + }, + async forward(values: readonly CurveGPUElementBytes[]): Promise { + values.forEach((value, index) => ensureByteLength(value, elementBytes, `${label}.forward[${index}]`)); + const size = values.length; + if (size === 0 || (size & (size - 1)) !== 0) { + throw new Error(`${label}: NTT input length must be a non-zero power of two`); + } + const output = await runPipelinePacked({ + values: packElementBatch(values, elementBytes, `${label}.forward.values`), + inverse: false, + inputRegular: false, + outputRegular: false, + }); + return unpackElementBatch(output, elementBytes, size); + }, + async inverse(values: readonly CurveGPUElementBytes[]): Promise { + values.forEach((value, index) => ensureByteLength(value, elementBytes, `${label}.inverse[${index}]`)); + const size = values.length; + if (size === 0 || (size & (size - 1)) !== 0) { + throw new Error(`${label}: NTT input length must be a non-zero power of two`); + } + const output = await runPipelinePacked({ + values: packElementBatch(values, elementBytes, `${label}.inverse.values`), + inverse: true, + inputRegular: false, + outputRegular: false, + }); + return unpackElementBatch(output, elementBytes, size); + }, + async forwardPackedMont(values: Uint8Array): Promise { + return runPipelinePacked({ values, inverse: false, inputRegular: false, outputRegular: false }); + }, + async inversePackedMont(values: Uint8Array): Promise { + return runPipelinePacked({ values, inverse: true, inputRegular: false, outputRegular: false }); + }, + async forwardPackedMontBatch(values: Uint8Array, vectorSize: number, vectorCount: number): Promise { + return runPipelinePackedBatch({ values, vectorSize, vectorCount, inverse: false, inputRegular: false, outputRegular: false }); + }, + async inversePackedMontBatch(values: Uint8Array, vectorSize: number, vectorCount: number): Promise { + return runPipelinePackedBatch({ values, vectorSize, vectorCount, inverse: true, inputRegular: false, outputRegular: false }); + }, + async inverseBitReversePackedRegular(values: Uint8Array): Promise { + return runPipelinePacked({ + values, + inverse: true, + inputRegular: true, + outputRegular: true, + inputBitReversed: true, + }); + }, + async inverseCosetPackedRegular(values: Uint8Array): Promise { + return runPipelinePacked({ + values, + inverse: true, + inputRegular: true, + outputRegular: true, + inverseCoset: true, + }); + }, + async inverseCosetBitReversePackedRegular(values: Uint8Array): Promise { + return runPipelinePacked({ + values, + inverse: true, + inputRegular: true, + outputRegular: true, + inputBitReversed: true, + inverseCoset: true, + }); + }, + async forwardPackedRegular(values: Uint8Array): Promise { + return runPipelinePacked({ values, inverse: false, inputRegular: true, outputRegular: true }); + }, + async inversePackedRegular(values: Uint8Array): Promise { + return runPipelinePacked({ values, inverse: true, inputRegular: true, outputRegular: true }); + }, + prewarmDomain: prewarmGroth16QuotientDomain, + prewarmGroth16QuotientDomain, + async computeGroth16QuotientPackedRegular(a: Uint8Array, b: Uint8Array, c: Uint8Array): Promise { + return computeGroth16QuotientPacked(a, b, c, false); + }, + async computeGroth16QuotientPackedMont(a: Uint8Array, b: Uint8Array, c: Uint8Array): Promise { + return computeGroth16QuotientPacked(a, b, c, true); + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/pipeline_registry.ts b/backend/accelerated/webgpu/web/src/curvegpu/pipeline_registry.ts new file mode 100644 index 0000000000..609666abf5 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/pipeline_registry.ts @@ -0,0 +1,138 @@ +import type { Kernel } from "./msm_gpu_runtime.js"; +import type { SimpleKernel } from "./runtime_common.js"; +import { loadShaderParts } from "./runtime_common.js"; + +declare const GPUShaderStage: { COMPUTE: number }; + +export interface PipelineRegistry { + getOpsKernel(entryPoint: string): SimpleKernel; + getMSMKernel(entryPoint: string): Kernel; +} + +export type OpsShaderSpec = { + shaderParts: readonly string[]; + entryPoint: string; + /** Pass WORKGROUP_SIZE override constant at pipeline creation time. Only valid for shaders that declare `override WORKGROUP_SIZE`. */ + useWorkgroupOverride?: boolean; +}; + +export type MSMShaderSpec = { + shaderParts: readonly string[]; + entryPoints: readonly string[]; +}; + +export async function buildPipelineRegistry(options: { + device: GPUDevice; + opsShaders: OpsShaderSpec[]; + msmShaders: MSMShaderSpec[]; + /** Workgroup size to use for ops kernels that declare `override WORKGROUP_SIZE`. Defaults to 64. */ + opsWorkgroupSize?: number; + debug?: boolean; +}): Promise { + const { device, opsShaders, msmShaders, opsWorkgroupSize = 64, debug = false } = options; + + // Shared bind group layout for ops kernels (4-binding: read-only-storage×2, storage, uniform) + const opsLayout = device.createBindGroupLayout({ + label: "curvegpu-ops-bgl", + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, + ], + }); + + // Shared bind group layout for MSM kernels (7-binding: same 4 + read-only-storage×3) + const msmLayout = device.createBindGroupLayout({ + label: "curvegpu-msm-bgl", + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, + { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + ], + }); + + const opsPipelineLayout = device.createPipelineLayout({ + label: "curvegpu-ops-pl", + bindGroupLayouts: [opsLayout], + }); + + const msmPipelineLayout = device.createPipelineLayout({ + label: "curvegpu-msm-pl", + bindGroupLayouts: [msmLayout], + }); + + // Load all shader texts in parallel + const [opsShaderTexts, msmShaderTexts] = await Promise.all([ + Promise.all(opsShaders.map((spec) => loadShaderParts(spec.shaderParts))), + Promise.all(msmShaders.map((spec) => loadShaderParts(spec.shaderParts))), + ]); + + const opsKernels = new Map(); + const msmKernels = new Map(); + + // Create all pipelines in parallel + await Promise.all([ + ...opsShaders.map(async (spec, i) => { + const shaderCode = opsShaderTexts[i]; + const shaderModule = device.createShaderModule({ + label: `curvegpu-ops-${spec.entryPoint}-shader`, + code: shaderCode, + }); + if (debug) { + console.debug(`[curvegpu] createComputePipelineAsync: ${spec.entryPoint}`); + } + const effectiveWorkgroupSize = spec.useWorkgroupOverride ? opsWorkgroupSize : 64; + const computeDesc: GPUProgrammableStage = spec.useWorkgroupOverride + ? { module: shaderModule, entryPoint: spec.entryPoint, constants: { WORKGROUP_SIZE: effectiveWorkgroupSize } } + : { module: shaderModule, entryPoint: spec.entryPoint }; + const pipeline = await device.createComputePipelineAsync({ + label: `curvegpu-ops-${spec.entryPoint}`, + layout: opsPipelineLayout, + compute: computeDesc, + }); + opsKernels.set(spec.entryPoint, { pipeline, bindGroupLayout: opsLayout, workgroupSize: effectiveWorkgroupSize }); + }), + ...msmShaders.map(async (spec, i) => { + const shaderCode = msmShaderTexts[i]; + const shaderModule = device.createShaderModule({ + label: `curvegpu-msm-${spec.entryPoints[0]}-shader`, + code: shaderCode, + }); + await Promise.all( + spec.entryPoints.map(async (entryPoint) => { + if (debug) { + console.debug(`[curvegpu] createComputePipelineAsync: ${entryPoint}`); + } + const pipeline = await device.createComputePipelineAsync({ + label: `curvegpu-msm-${entryPoint}`, + layout: msmPipelineLayout, + compute: { module: shaderModule, entryPoint }, + }); + msmKernels.set(entryPoint, { pipeline, bindGroupLayout: msmLayout }); + }), + ); + }), + ]); + + return { + getOpsKernel(entryPoint: string): SimpleKernel { + const kernel = opsKernels.get(entryPoint); + if (!kernel) { + throw new Error(`[curvegpu] ops kernel not found: ${entryPoint}`); + } + return kernel; + }, + getMSMKernel(entryPoint: string): Kernel { + const kernel = msmKernels.get(entryPoint); + if (!kernel) { + throw new Error(`[curvegpu] MSM kernel not found: ${entryPoint}`); + } + return kernel; + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/plonk_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/plonk_module.ts new file mode 100644 index 0000000000..34b13f1234 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/plonk_module.ts @@ -0,0 +1,327 @@ +import type { + CurveGPUContext, + FieldModule, + G1Module, + G1MSMModule, + NTTModule, + PlonkConstraintSystem, + PlonkHandle, + PlonkModule, + PlonkProvingKey, + PlonkProvingKeyFormat, + PlonkRuntimeKind, + PlonkRuntimeOptions, + PlonkVerificationKey, + SupportedCurveID, +} from "./api.js"; +import type { PlonkQuotientModule } from "./plonk_quotient_module.js"; +import { installPlonkWebGPUBridge } from "./plonk_webgpu_bridge.js"; + +type GoInstance = { + importObject: WebAssembly.Imports; + run(instance: WebAssembly.Instance): Promise; +}; + +type GoConstructor = new () => GoInstance; + +type RuntimeGlobal = { + readConstraintSystem(curve: SupportedCurveID, bytes: Uint8Array): Promise<{ handle: string; constraints: number }>; + readProvingKey(curve: SupportedCurveID, bytes: Uint8Array, format: PlonkProvingKeyFormat): Promise<{ handle: string }>; + readVerificationKey(curve: SupportedCurveID, bytes: Uint8Array): Promise<{ handle: string }>; + prepareProvingKey(handle: string, ccsHandle?: string): Promise; + prove(ccsHandle: string, pkHandle: string, witness: Uint8Array): Promise; + verify(proof: Uint8Array, vkHandle: string, publicWitness: Uint8Array): Promise; + release(handle: string): Promise; +}; + +type PlonkModuleConfig = { + context: CurveGPUContext; + curve: SupportedCurveID; + modulusHex: string; + frBytes: number; + fr: FieldModule; + ntt: NTTModule; + quotient: PlonkQuotientModule; + g1: G1Module; + g1msm: G1MSMModule; +}; + +export const defaultPlonkRuntimeURLs = Object.freeze({ + wasmExecURL: new URL("../../assets/wasm_exec.js", import.meta.url).toString(), + webgpuWasmURL: new URL("../../assets/plonk-webgpu.wasm", import.meta.url).toString(), + nativeWasmURL: new URL("../../assets/plonk-native.wasm", import.meta.url).toString(), +}); + +const runtimeGlobals: Record = { + webgpu: "gnarkPlonkRuntimeWebGPU", + native: "gnarkPlonkRuntimeNative", +}; + +const loadedScripts = new Map>(); +const loadedRuntimes = new Map>(); + +function cloneBytes(bytes: Uint8Array): Uint8Array { + return new Uint8Array(bytes); +} + +function getGlobalObject(name: string): T | undefined { + return (globalThis as typeof globalThis & Record)[name]; +} + +function setGlobalObject(name: string, value: T | undefined): void { + (globalThis as typeof globalThis & Record)[name] = value; +} + +function getGoConstructor(): GoConstructor { + const Go = getGlobalObject("Go"); + if (typeof Go !== "function") { + throw new Error("Go WASM runtime is not available after loading wasm_exec.js"); + } + return Go; +} + +async function loadScript(url: string): Promise { + if (typeof document === "undefined") { + throw new Error("PLONK WASM runtime loading requires a browser document"); + } + let promise = loadedScripts.get(url); + if (!promise) { + promise = new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = url; + script.onload = () => resolve(); + script.onerror = () => reject(new Error(`failed to load ${url}`)); + document.head.appendChild(script); + }); + loadedScripts.set(url, promise); + } + await promise; +} + +async function ensureWasmExec(url: string): Promise { + if (typeof getGlobalObject("Go") === "function") { + return; + } + await loadScript(url); +} + +async function waitForRuntimeGlobal(name: string): Promise { + const deadline = performance.now() + 10_000; + while (performance.now() < deadline) { + const runtime = getGlobalObject(name); + if (runtime) { + return runtime; + } + await new Promise((resolve) => setTimeout(resolve, 0)); + } + throw new Error(`PLONK WASM runtime ${name} did not initialize`); +} + +async function loadGoRuntime( + kind: PlonkRuntimeKind, + options: Required, + beforeStart: () => void, +): Promise { + const wasmURL = kind === "native" ? options.nativeWasmURL : options.webgpuWasmURL; + const cacheKey = `${kind}\n${options.wasmExecURL}\n${wasmURL}`; + let promise = loadedRuntimes.get(cacheKey); + if (!promise) { + promise = (async () => { + beforeStart(); + await ensureWasmExec(options.wasmExecURL); + const response = await fetch(wasmURL); + if (!response.ok) { + throw new Error(`failed to fetch ${wasmURL}: ${response.status}`); + } + const bytes = await response.arrayBuffer(); + const go = new (getGoConstructor())(); + const { instance } = await WebAssembly.instantiate(bytes, go.importObject); + setGlobalObject(runtimeGlobals[kind], undefined); + void go.run(instance).catch((error: unknown) => { + console.error(`PLONK ${kind} WASM runtime exited`, error); + }); + return waitForRuntimeGlobal(runtimeGlobals[kind]); + })(); + loadedRuntimes.set(cacheKey, promise); + } else { + beforeStart(); + } + return promise; +} + +function normalizeRuntimeOptions(options?: PlonkRuntimeOptions): Required { + return { + wasmExecURL: options?.wasmExecURL ?? defaultPlonkRuntimeURLs.wasmExecURL, + webgpuWasmURL: options?.webgpuWasmURL ?? defaultPlonkRuntimeURLs.webgpuWasmURL, + nativeWasmURL: options?.nativeWasmURL ?? defaultPlonkRuntimeURLs.nativeWasmURL, + }; +} + +class RuntimeHandle implements PlonkHandle { + #disposed = false; + + constructor( + readonly runtime: RuntimeGlobal, + readonly kind: PlonkRuntimeKind, + readonly curve: SupportedCurveID, + readonly type: "ccs" | "pk" | "vk", + readonly handle: string, + ) {} + + async dispose(): Promise { + if (this.#disposed) { + return; + } + this.#disposed = true; + await this.runtime.release(this.handle); + } + + assertUsable(expectedType: RuntimeHandle["type"]): void { + if (this.#disposed) { + throw new Error(`PLONK ${this.type} handle has been disposed`); + } + if (this.type !== expectedType) { + throw new Error(`expected PLONK ${expectedType} handle, got ${this.type}`); + } + } +} + +class ConstraintSystemHandle extends RuntimeHandle implements PlonkConstraintSystem { + constructor(runtime: RuntimeGlobal, kind: PlonkRuntimeKind, curve: SupportedCurveID, handle: string, readonly constraints: number) { + super(runtime, kind, curve, "ccs", handle); + } +} + +class ProvingKeyHandle extends RuntimeHandle implements PlonkProvingKey { + constructor(runtime: RuntimeGlobal, kind: PlonkRuntimeKind, curve: SupportedCurveID, handle: string) { + super(runtime, kind, curve, "pk", handle); + } +} + +class VerificationKeyHandle extends RuntimeHandle implements PlonkVerificationKey { + constructor(runtime: RuntimeGlobal, kind: PlonkRuntimeKind, curve: SupportedCurveID, handle: string) { + super(runtime, kind, curve, "vk", handle); + } +} + +function runtimeHandle(handle: PlonkHandle, type: RuntimeHandle["type"]): RuntimeHandle { + if (!(handle instanceof RuntimeHandle)) { + throw new Error("PLONK handle was not created by this module"); + } + handle.assertUsable(type); + return handle; +} + +function assertSameRuntime(a: RuntimeHandle, b: RuntimeHandle): void { + if (a.runtime !== b.runtime || a.kind !== b.kind) { + throw new Error("PLONK handles belong to different runtimes"); + } + if (a.curve !== b.curve) { + throw new Error(`PLONK handles belong to different curves: ${a.curve} and ${b.curve}`); + } +} + +function writeUint32BE(out: Uint8Array, offset: number, value: number): void { + out[offset] = (value >>> 24) & 0xff; + out[offset + 1] = (value >>> 16) & 0xff; + out[offset + 2] = (value >>> 8) & 0xff; + out[offset + 3] = value & 0xff; +} + +function writeBigIntBE(out: Uint8Array, offset: number, byteSize: number, value: bigint): void { + let remaining = value; + for (let i = byteSize - 1; i >= 0; i--) { + out[offset + i] = Number(remaining & 0xffn); + remaining >>= 8n; + } +} + +export function createPlonkModule(config: PlonkModuleConfig): PlonkModule { + const modulus = BigInt(config.modulusHex); + let currentRuntime: Promise | null = null; + let currentKind: PlonkRuntimeKind = "webgpu"; + + function installBridge(): void { + installPlonkWebGPUBridge({ + context: config.context, + curve: config.curve, + fr: config.fr, + ntt: config.ntt, + quotient: config.quotient, + g1: config.g1, + g1msm: config.g1msm, + }); + } + + async function loadRuntime(options?: PlonkRuntimeOptions & { kind?: PlonkRuntimeKind }): Promise { + currentKind = options?.kind ?? "webgpu"; + const runtimeOptions = normalizeRuntimeOptions(options); + currentRuntime = loadGoRuntime(currentKind, runtimeOptions, currentKind === "webgpu" ? installBridge : () => {}); + await currentRuntime; + } + + async function getRuntime(): Promise<{ runtime: RuntimeGlobal; kind: PlonkRuntimeKind }> { + if (!currentRuntime) { + await loadRuntime(); + } + return { runtime: await currentRuntime!, kind: currentKind }; + } + + return { + context: config.context, + curve: config.curve, + loadRuntime, + async readConstraintSystem(bytes: Uint8Array): Promise { + const { runtime, kind } = await getRuntime(); + const result = await runtime.readConstraintSystem(config.curve, cloneBytes(bytes)); + return new ConstraintSystemHandle(runtime, kind, config.curve, result.handle, result.constraints); + }, + async readProvingKey(bytes: Uint8Array, options?: { format?: PlonkProvingKeyFormat }): Promise { + const { runtime, kind } = await getRuntime(); + const result = await runtime.readProvingKey(config.curve, cloneBytes(bytes), options?.format ?? "serialized"); + return new ProvingKeyHandle(runtime, kind, config.curve, result.handle); + }, + async readVerificationKey(bytes: Uint8Array): Promise { + const { runtime, kind } = await getRuntime(); + const result = await runtime.readVerificationKey(config.curve, cloneBytes(bytes)); + return new VerificationKeyHandle(runtime, kind, config.curve, result.handle); + }, + async prepareProvingKey(pk: PlonkProvingKey, ccs?: PlonkConstraintSystem): Promise { + const pkHandle = runtimeHandle(pk, "pk"); + if (ccs) { + const ccsHandle = runtimeHandle(ccs, "ccs"); + assertSameRuntime(ccsHandle, pkHandle); + await pkHandle.runtime.prepareProvingKey(pkHandle.handle, ccsHandle.handle); + return; + } + await pkHandle.runtime.prepareProvingKey(pkHandle.handle); + }, + async prove(ccs: PlonkConstraintSystem, pk: PlonkProvingKey, witness: Uint8Array): Promise { + const ccsHandle = runtimeHandle(ccs, "ccs"); + const pkHandle = runtimeHandle(pk, "pk"); + assertSameRuntime(ccsHandle, pkHandle); + return ccsHandle.runtime.prove(ccsHandle.handle, pkHandle.handle, cloneBytes(witness)); + }, + async verify(proof: Uint8Array, vk: PlonkVerificationKey, publicWitness: Uint8Array): Promise { + const vkHandle = runtimeHandle(vk, "vk"); + return vkHandle.runtime.verify(cloneBytes(proof), vkHandle.handle, cloneBytes(publicWitness)); + }, + encodeWitness(values: readonly bigint[], options: { publicCount: number }): Uint8Array { + if (!Number.isInteger(options.publicCount) || options.publicCount < 0 || options.publicCount > values.length) { + throw new Error(`invalid publicCount ${options.publicCount}`); + } + const out = new Uint8Array(12 + values.length * config.frBytes); + writeUint32BE(out, 0, options.publicCount); + writeUint32BE(out, 4, values.length - options.publicCount); + writeUint32BE(out, 8, values.length); + for (let i = 0; i < values.length; i++) { + const value = values[i]; + if (value < 0n || value >= modulus) { + throw new Error(`witness value at index ${i} is outside the scalar field`); + } + writeBigIntBE(out, 12 + i * config.frBytes, config.frBytes, value); + } + return out; + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/plonk_quotient_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/plonk_quotient_module.ts new file mode 100644 index 0000000000..b04c505613 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/plonk_quotient_module.ts @@ -0,0 +1,650 @@ +import type { CurveGPUContext, FieldModule, NTTModule, SupportedCurveID } from "./api.js"; +import { + createSimpleStorageBuffer, + createSimpleStorageBufferFromBytes, + createSimpleUniformBuffer, + loadShaderParts, + readbackSimpleBuffer, +} from "./runtime_common.js"; + +declare const GPUShaderStage: { COMPUTE: number }; + +const PLONK_QUOTIENT_BASE_DYNAMIC_VECTOR_COUNT = 5; +const PLONK_QUOTIENT_BASE_STATIC_VECTOR_COUNT = 7; +const PLONK_QUOTIENT_BLIND_COUNT = 4; +const PLONK_QUOTIENT_SCALAR_COUNT = 7; +const PLONK_QUOTIENT_WORKGROUP_SIZE = 64; + +const PLONK_QUOTIENT_SHADER_PARTS: Record = { + bn254: [ + "/shaders/curves/bn254/fr_arith.wgsl#section=fr_types", + "/shaders/curves/bn254/fr_arith.wgsl#section=fr_constants", + "/shaders/curves/bn254/fr_arith.wgsl#section=fr_core", + "/shaders/curves/bn254/fr_plonk_quotient.wgsl", + ], + bls12_381: [ + "/shaders/curves/bls12_381/fr_arith.wgsl#section=fr_types", + "/shaders/curves/bls12_381/fr_arith.wgsl#section=fr_constants", + "/shaders/curves/bls12_381/fr_arith.wgsl#section=fr_core", + "/shaders/curves/bls12_381/fr_plonk_quotient.wgsl", + ], + bls12_377: [ + "/shaders/curves/bls12_377/fr_arith.wgsl#section=fr_types", + "/shaders/curves/bls12_377/fr_arith.wgsl#section=fr_constants", + "/shaders/curves/bls12_377/fr_arith.wgsl#section=fr_core", + "/shaders/curves/bls12_377/fr_plonk_quotient.wgsl", + ], +}; + +type PlonkQuotientKernel = { + device: GPUDevice; + pipeline: GPUComputePipeline; + bindGroupLayout: GPUBindGroupLayout; + workgroupSize: number; +}; + +export type PlonkTransformAndEvaluateQuotientCosetInput = { + dynamicValuesPacked: Uint8Array; + scalingPacked: Uint8Array; + staticValuesPacked: Uint8Array; + twiddlesPacked: Uint8Array; + denominatorsPacked: Uint8Array; + blindsPacked: Uint8Array; + scalarsPacked: Uint8Array; + elementCount: number; + blindCoeffCount: number; + commitmentCount: number; + dynamicTransformCacheKey?: number; + staticMontCacheKey?: number; +}; + +export type PlonkTransformAndEvaluateQuotientCosetsInput = PlonkTransformAndEvaluateQuotientCosetInput & { + staticMontCacheKeysPacked: Uint8Array; + cosetCount: number; + auxMontCacheKey?: number; +}; + +export type PlonkPreloadQuotientStaticAndAuxInput = { + staticValuesPacked: Uint8Array; + staticMontCacheKeysPacked: Uint8Array; + scalingPacked: Uint8Array; + twiddlesPacked: Uint8Array; + denominatorsPacked: Uint8Array; + elementCount: number; + staticVectorCount: number; + cosetCount: number; + auxMontCacheKey: number; +}; + +export type PlonkQuotientModule = { + readonly context: CurveGPUContext; + readonly curve: SupportedCurveID; + transformAndEvaluateQuotientCoset(input: PlonkTransformAndEvaluateQuotientCosetInput): Promise; + transformAndEvaluateQuotientCosets(input: PlonkTransformAndEvaluateQuotientCosetsInput): Promise; + preloadQuotientStaticAndAux(input: PlonkPreloadQuotientStaticAndAuxInput): Promise; + prewarmPlonkQuotientEvaluateKernel(commitmentCount?: number): Promise; +}; + +function cloneBytes(bytes: Uint8Array): Uint8Array { + return new Uint8Array(bytes); +} + +function repeatPackedVector(value: Uint8Array, count: number): Uint8Array { + const out = new Uint8Array(value.byteLength * count); + for (let i = 0; i < count; i += 1) { + out.set(value, i * value.byteLength); + } + return out; +} + +function repeatEachPackedVector(values: Uint8Array, vectorBytes: number, repeatCount: number): Uint8Array { + const vectorCount = values.byteLength / vectorBytes; + const out = new Uint8Array(values.byteLength * repeatCount); + for (let i = 0; i < vectorCount; i += 1) { + const vector = values.subarray(i * vectorBytes, (i + 1) * vectorBytes); + for (let j = 0; j < repeatCount; j += 1) { + out.set(vector, (i * repeatCount + j) * vectorBytes); + } + } + return out; +} + +function unpackU32LE(bytes: Uint8Array, count: number, label: string): number[] { + if (bytes.byteLength !== count * 4) { + throw new Error(`${label}: expected ${count * 4} bytes, got ${bytes.byteLength}`); + } + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + return Array.from({ length: count }, (_, i) => view.getUint32(i * 4, true)); +} + +export function createPlonkQuotientModule(config: { + context: CurveGPUContext; + curve: SupportedCurveID; + fr: FieldModule; + ntt: NTTModule; +}): PlonkQuotientModule { + const { context, curve, fr, ntt } = config; + const quotientKernels = new Map(); + let dynamicTransformCache: + | { + key: number; + elementCount: number; + dynamicVectorCount: number; + coeffMont: Uint8Array; + } + | null = null; + const staticMontCache = new Map< + number, + { + elementCount: number; + staticVectorCount: number; + mont: Uint8Array; + } + >(); + const auxMontCache = new Map< + number, + { + elementCount: number; + cosetCount: number; + scalingMont: Uint8Array; + twiddlesMont: Uint8Array; + denominatorsMont: Uint8Array; + } + >(); + + async function getQuotientKernel(commitmentCount: number): Promise { + if (!Number.isInteger(commitmentCount) || commitmentCount < 0) { + throw new Error(`invalid PLONK quotient commitment count ${commitmentCount}`); + } + const device = context.device; + const cached = quotientKernels.get(commitmentCount); + if (cached?.device === device) { + return cached; + } + + const bindGroupLayout = device.createBindGroupLayout({ + label: `plonk-${curve}-quotient-bgl`, + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, + { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, + { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }, + ], + }); + const pipelineLayout = device.createPipelineLayout({ + label: `plonk-${curve}-quotient-pl`, + bindGroupLayouts: [bindGroupLayout], + }); + const code = await loadShaderParts(PLONK_QUOTIENT_SHADER_PARTS[curve]); + const shader = device.createShaderModule({ + label: `plonk-${curve}-quotient-shader`, + code, + }); + const pipeline = await device.createComputePipelineAsync({ + label: `plonk-${curve}-quotient-c${commitmentCount}`, + layout: pipelineLayout, + compute: { + module: shader, + entryPoint: "fr_plonk_quotient_main", + constants: { + WORKGROUP_SIZE: PLONK_QUOTIENT_WORKGROUP_SIZE, + COMMITMENT_COUNT: commitmentCount, + }, + }, + }); + const kernel = { + device, + pipeline, + bindGroupLayout, + workgroupSize: PLONK_QUOTIENT_WORKGROUP_SIZE, + }; + quotientKernels.set(commitmentCount, kernel); + return kernel; + } + + async function runQuotientKernelMont( + vectorsMontPacked: Uint8Array, + blindsMontPacked: Uint8Array, + scalarsMontPacked: Uint8Array, + elementCount: number, + blindCoeffCount: number, + commitmentCount: number, + cosetCount = 1, + ) { + const elementBytes = fr.byteSize; + const vectorBytes = elementCount * elementBytes; + const outputBytes = cosetCount * vectorBytes; + const device = context.device; + const kernel = await getQuotientKernel(commitmentCount); + const vectorsBuffer = createSimpleStorageBufferFromBytes(device, "plonk-quotient-vectors", vectorsMontPacked); + const blindsBuffer = createSimpleStorageBufferFromBytes(device, "plonk-quotient-blinds", blindsMontPacked); + const scalarsBuffer = createSimpleStorageBufferFromBytes(device, "plonk-quotient-scalars", scalarsMontPacked); + const outputBuffer = createSimpleStorageBuffer(device, "plonk-quotient-output", outputBytes); + const paramsBuffer = createSimpleUniformBuffer( + device, + "plonk-quotient-params", + new Uint32Array([elementCount, blindCoeffCount, cosetCount, 0]), + ); + + try { + const bindGroup = device.createBindGroup({ + label: "plonk-quotient-bg", + layout: kernel.bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: vectorsBuffer } }, + { binding: 1, resource: { buffer: blindsBuffer } }, + { binding: 2, resource: { buffer: scalarsBuffer } }, + { binding: 3, resource: { buffer: outputBuffer } }, + { binding: 4, resource: { buffer: paramsBuffer } }, + ], + }); + const encoder = device.createCommandEncoder({ label: "plonk-quotient-encoder" }); + const pass = encoder.beginComputePass({ label: "plonk-quotient-pass" }); + pass.setPipeline(kernel.pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(Math.ceil(elementCount / kernel.workgroupSize), cosetCount, 1); + pass.end(); + device.queue.submit([encoder.finish()]); + await device.queue.onSubmittedWorkDone(); + return await readbackSimpleBuffer(device, outputBuffer, outputBytes, "plonk-quotient"); + } finally { + vectorsBuffer.destroy(); + blindsBuffer.destroy(); + scalarsBuffer.destroy(); + outputBuffer.destroy(); + paramsBuffer.destroy(); + } + } + + async function transformAndEvaluateQuotientCoset(input: PlonkTransformAndEvaluateQuotientCosetInput): Promise { + const { + dynamicValuesPacked, + scalingPacked, + staticValuesPacked, + twiddlesPacked, + denominatorsPacked, + blindsPacked, + scalarsPacked, + elementCount, + blindCoeffCount, + commitmentCount, + dynamicTransformCacheKey = 0, + staticMontCacheKey = 0, + } = input; + const elementBytes = fr.byteSize; + const vectorBytes = elementCount * elementBytes; + if (!Number.isInteger(elementCount) || elementCount <= 0 || (elementCount & (elementCount - 1)) !== 0) { + throw new Error(`invalid PLONK quotient evaluate element count ${elementCount}`); + } + if (!Number.isInteger(blindCoeffCount) || blindCoeffCount < 0) { + throw new Error(`invalid PLONK quotient blind coefficient count ${blindCoeffCount}`); + } + if (!Number.isInteger(commitmentCount) || commitmentCount < 0) { + throw new Error(`invalid PLONK quotient commitment count ${commitmentCount}`); + } + + const dynamicVectorCount = PLONK_QUOTIENT_BASE_DYNAMIC_VECTOR_COUNT + commitmentCount; + const staticVectorCount = PLONK_QUOTIENT_BASE_STATIC_VECTOR_COUNT + commitmentCount; + const vectorCount = dynamicVectorCount + staticVectorCount + 2; + const expectedDynamicBytes = dynamicVectorCount * vectorBytes; + const canReuseDynamicCache = + dynamicTransformCacheKey > 0 && + dynamicTransformCache?.key === dynamicTransformCacheKey && + dynamicTransformCache.elementCount === elementCount && + dynamicTransformCache.dynamicVectorCount === dynamicVectorCount; + const cachedDynamicCoeffMont = canReuseDynamicCache ? dynamicTransformCache?.coeffMont : undefined; + if (dynamicValuesPacked.byteLength !== expectedDynamicBytes && !(dynamicValuesPacked.byteLength === 0 && canReuseDynamicCache)) { + throw new Error( + `PLONK quotient transform/evaluate expected ${expectedDynamicBytes} dynamic bytes, got ${dynamicValuesPacked.byteLength}`, + ); + } + if (scalingPacked.byteLength !== vectorBytes) { + throw new Error(`PLONK quotient transform/evaluate expected ${vectorBytes} scaling bytes, got ${scalingPacked.byteLength}`); + } + const expectedStaticBytes = staticVectorCount * vectorBytes; + const cachedStatic = staticMontCacheKey > 0 ? staticMontCache.get(staticMontCacheKey) : undefined; + const canReuseStaticCache = + staticMontCacheKey > 0 && + cachedStatic?.elementCount === elementCount && + cachedStatic.staticVectorCount === staticVectorCount; + if (staticValuesPacked.byteLength !== expectedStaticBytes && !(staticValuesPacked.byteLength === 0 && canReuseStaticCache)) { + throw new Error( + `PLONK quotient transform/evaluate expected ${expectedStaticBytes} static bytes, got ${staticValuesPacked.byteLength}`, + ); + } + if (twiddlesPacked.byteLength !== vectorBytes) { + throw new Error(`PLONK quotient transform/evaluate expected ${vectorBytes} twiddle bytes, got ${twiddlesPacked.byteLength}`); + } + if (denominatorsPacked.byteLength !== vectorBytes) { + throw new Error(`PLONK quotient transform/evaluate expected ${vectorBytes} denominator bytes, got ${denominatorsPacked.byteLength}`); + } + const blindBytes = PLONK_QUOTIENT_BLIND_COUNT * blindCoeffCount * elementBytes; + if (blindsPacked.byteLength !== blindBytes) { + throw new Error(`PLONK quotient transform/evaluate expected ${blindBytes} blinding bytes, got ${blindsPacked.byteLength}`); + } + const scalarBytes = PLONK_QUOTIENT_SCALAR_COUNT * elementBytes; + if (scalarsPacked.byteLength !== scalarBytes) { + throw new Error(`PLONK quotient transform/evaluate expected ${scalarBytes} scalar bytes, got ${scalarsPacked.byteLength}`); + } + + const vectorsMontPacked = new Uint8Array(vectorCount * vectorBytes); + const dynamicCoeffMont = + cachedDynamicCoeffMont + ? cachedDynamicCoeffMont + : await (async (): Promise => { + const dynamicMont = await fr.toMontgomeryPacked(cloneBytes(dynamicValuesPacked)); + const coeffMont = await ntt.inversePackedMontBatch(dynamicMont, elementCount, dynamicVectorCount); + if (dynamicTransformCacheKey > 0) { + dynamicTransformCache = { + key: dynamicTransformCacheKey, + elementCount, + dynamicVectorCount, + coeffMont, + }; + } + return coeffMont; + })(); + const scalingMont = await fr.toMontgomeryPacked(cloneBytes(scalingPacked)); + const scalingMontBatch = repeatPackedVector(scalingMont, dynamicVectorCount); + const shiftedCoeffMont = await fr.mulPackedMont(dynamicCoeffMont, scalingMontBatch); + vectorsMontPacked.set(await ntt.forwardPackedMontBatch(shiftedCoeffMont, elementCount, dynamicVectorCount)); + + const cachedStaticMont = canReuseStaticCache ? cachedStatic?.mont : undefined; + const staticMontPromise = cachedStaticMont + ? Promise.resolve(cachedStaticMont) + : fr.toMontgomeryPacked(cloneBytes(staticValuesPacked)).then((mont) => { + if (staticMontCacheKey > 0) { + staticMontCache.set(staticMontCacheKey, { + elementCount, + staticVectorCount, + mont, + }); + } + return mont; + }); + const [staticMont, twiddlesMont, denominatorsMont, blindsMont, scalarsMont] = await Promise.all([ + staticMontPromise, + fr.toMontgomeryPacked(cloneBytes(twiddlesPacked)), + fr.toMontgomeryPacked(cloneBytes(denominatorsPacked)), + fr.toMontgomeryPacked(cloneBytes(blindsPacked)), + fr.toMontgomeryPacked(cloneBytes(scalarsPacked)), + ]); + + vectorsMontPacked.set(staticMont, dynamicVectorCount * vectorBytes); + vectorsMontPacked.set(twiddlesMont, (dynamicVectorCount + staticVectorCount) * vectorBytes); + vectorsMontPacked.set(denominatorsMont, (dynamicVectorCount + staticVectorCount + 1) * vectorBytes); + return runQuotientKernelMont(vectorsMontPacked, blindsMont, scalarsMont, elementCount, blindCoeffCount, commitmentCount); + } + + async function transformAndEvaluateQuotientCosets(input: PlonkTransformAndEvaluateQuotientCosetsInput): Promise { + const { + dynamicValuesPacked, + scalingPacked, + staticValuesPacked, + staticMontCacheKeysPacked, + twiddlesPacked, + denominatorsPacked, + blindsPacked, + scalarsPacked, + elementCount, + blindCoeffCount, + commitmentCount, + dynamicTransformCacheKey = 0, + cosetCount, + auxMontCacheKey = 0, + } = input; + const elementBytes = fr.byteSize; + const vectorBytes = elementCount * elementBytes; + if (!Number.isInteger(elementCount) || elementCount <= 0 || (elementCount & (elementCount - 1)) !== 0) { + throw new Error(`invalid PLONK quotient evaluate element count ${elementCount}`); + } + if (!Number.isInteger(blindCoeffCount) || blindCoeffCount < 0) { + throw new Error(`invalid PLONK quotient blind coefficient count ${blindCoeffCount}`); + } + if (!Number.isInteger(commitmentCount) || commitmentCount < 0) { + throw new Error(`invalid PLONK quotient commitment count ${commitmentCount}`); + } + if (!Number.isInteger(cosetCount) || cosetCount <= 0) { + throw new Error(`invalid PLONK quotient coset count ${cosetCount}`); + } + if (!Number.isInteger(auxMontCacheKey) || auxMontCacheKey < 0) { + throw new Error(`invalid PLONK quotient aux cache key ${auxMontCacheKey}`); + } + + const dynamicVectorCount = PLONK_QUOTIENT_BASE_DYNAMIC_VECTOR_COUNT + commitmentCount; + const staticVectorCount = PLONK_QUOTIENT_BASE_STATIC_VECTOR_COUNT + commitmentCount; + const vectorCount = dynamicVectorCount + staticVectorCount + 2; + const expectedDynamicBytes = dynamicVectorCount * vectorBytes; + if (dynamicValuesPacked.byteLength !== expectedDynamicBytes) { + throw new Error( + `PLONK quotient cosets expected ${expectedDynamicBytes} dynamic bytes, got ${dynamicValuesPacked.byteLength}`, + ); + } + const cachedAux = auxMontCacheKey > 0 ? auxMontCache.get(auxMontCacheKey) : undefined; + const canReuseAuxCache = + scalingPacked.byteLength === 0 && + twiddlesPacked.byteLength === 0 && + denominatorsPacked.byteLength === 0 && + cachedAux !== undefined && + cachedAux.elementCount === elementCount && + cachedAux.cosetCount === cosetCount; + if (scalingPacked.byteLength !== cosetCount * vectorBytes && !canReuseAuxCache) { + throw new Error(`PLONK quotient cosets expected ${cosetCount * vectorBytes} scaling bytes, got ${scalingPacked.byteLength}`); + } + const expectedStaticBytes = cosetCount * staticVectorCount * vectorBytes; + const staticMontCacheKeys = unpackU32LE(staticMontCacheKeysPacked, cosetCount, "PLONK quotient static cache keys"); + const canReuseStaticCache = + staticValuesPacked.byteLength === 0 && + staticMontCacheKeys.every((key) => { + const cached = key > 0 ? staticMontCache.get(key) : undefined; + return cached?.elementCount === elementCount && cached.staticVectorCount === staticVectorCount; + }); + if (staticValuesPacked.byteLength !== expectedStaticBytes && !canReuseStaticCache) { + throw new Error( + `PLONK quotient cosets expected ${expectedStaticBytes} static bytes, got ${staticValuesPacked.byteLength}`, + ); + } + if (twiddlesPacked.byteLength !== vectorBytes && !canReuseAuxCache) { + throw new Error(`PLONK quotient cosets expected ${vectorBytes} twiddle bytes, got ${twiddlesPacked.byteLength}`); + } + if (denominatorsPacked.byteLength !== cosetCount * vectorBytes && !canReuseAuxCache) { + throw new Error( + `PLONK quotient cosets expected ${cosetCount * vectorBytes} denominator bytes, got ${denominatorsPacked.byteLength}`, + ); + } + const blindBytes = PLONK_QUOTIENT_BLIND_COUNT * blindCoeffCount * elementBytes; + if (blindsPacked.byteLength !== cosetCount * blindBytes) { + throw new Error(`PLONK quotient cosets expected ${cosetCount * blindBytes} blinding bytes, got ${blindsPacked.byteLength}`); + } + const scalarBytes = PLONK_QUOTIENT_SCALAR_COUNT * elementBytes; + if (scalarsPacked.byteLength !== cosetCount * scalarBytes) { + throw new Error(`PLONK quotient cosets expected ${cosetCount * scalarBytes} scalar bytes, got ${scalarsPacked.byteLength}`); + } + + const dynamicMont = await fr.toMontgomeryPacked(cloneBytes(dynamicValuesPacked)); + const dynamicCoeffMont = await ntt.inversePackedMontBatch(dynamicMont, elementCount, dynamicVectorCount); + if (dynamicTransformCacheKey > 0) { + dynamicTransformCache = { + key: dynamicTransformCacheKey, + elementCount, + dynamicVectorCount, + coeffMont: dynamicCoeffMont, + }; + } + let scalingMont: Uint8Array; + let twiddlesMont: Uint8Array; + let denominatorsMont: Uint8Array; + if (canReuseAuxCache) { + if (!cachedAux) { + throw new Error(`PLONK quotient missing aux cache key ${auxMontCacheKey}`); + } + ({ scalingMont, twiddlesMont, denominatorsMont } = cachedAux); + } else { + [scalingMont, twiddlesMont, denominatorsMont] = await Promise.all([ + fr.toMontgomeryPacked(cloneBytes(scalingPacked)), + fr.toMontgomeryPacked(cloneBytes(twiddlesPacked)), + fr.toMontgomeryPacked(cloneBytes(denominatorsPacked)), + ]); + if (auxMontCacheKey > 0) { + auxMontCache.set(auxMontCacheKey, { + elementCount, + cosetCount, + scalingMont: cloneBytes(scalingMont), + twiddlesMont: cloneBytes(twiddlesMont), + denominatorsMont: cloneBytes(denominatorsMont), + }); + } + } + const shiftedCoeffMont = await fr.mulPackedMont( + repeatPackedVector(dynamicCoeffMont, cosetCount), + repeatEachPackedVector(scalingMont, vectorBytes, dynamicVectorCount), + ); + const dynamicCosetsMont = await ntt.forwardPackedMontBatch(shiftedCoeffMont, elementCount, dynamicVectorCount * cosetCount); + + const staticMont = canReuseStaticCache + ? (() => { + const out = new Uint8Array(cosetCount * staticVectorCount * vectorBytes); + for (let i = 0; i < cosetCount; i += 1) { + const cached = staticMontCache.get(staticMontCacheKeys[i]); + if (!cached) { + throw new Error(`PLONK quotient missing static cache key ${staticMontCacheKeys[i]}`); + } + out.set(cached.mont, i * staticVectorCount * vectorBytes); + } + return out; + })() + : await fr.toMontgomeryPacked(cloneBytes(staticValuesPacked)); + if (!canReuseStaticCache) { + for (let i = 0; i < cosetCount; i += 1) { + const key = staticMontCacheKeys[i]; + if (key > 0) { + const start = i * staticVectorCount * vectorBytes; + staticMontCache.set(key, { + elementCount, + staticVectorCount, + mont: cloneBytes(staticMont.subarray(start, start + staticVectorCount * vectorBytes)), + }); + } + } + } + + const [blindsMont, scalarsMont] = await Promise.all([ + fr.toMontgomeryPacked(cloneBytes(blindsPacked)), + fr.toMontgomeryPacked(cloneBytes(scalarsPacked)), + ]); + + const vectorsMontPacked = new Uint8Array(cosetCount * vectorCount * vectorBytes); + for (let i = 0; i < cosetCount; i += 1) { + const vectorsStart = i * vectorCount * vectorBytes; + const dynamicStart = i * dynamicVectorCount * vectorBytes; + vectorsMontPacked.set( + dynamicCosetsMont.subarray(dynamicStart, dynamicStart + dynamicVectorCount * vectorBytes), + vectorsStart, + ); + const staticStart = i * staticVectorCount * vectorBytes; + vectorsMontPacked.set( + staticMont.subarray(staticStart, staticStart + staticVectorCount * vectorBytes), + vectorsStart + dynamicVectorCount * vectorBytes, + ); + vectorsMontPacked.set(twiddlesMont, vectorsStart + (dynamicVectorCount + staticVectorCount) * vectorBytes); + const denominatorStart = i * vectorBytes; + vectorsMontPacked.set( + denominatorsMont.subarray(denominatorStart, denominatorStart + vectorBytes), + vectorsStart + (dynamicVectorCount + staticVectorCount + 1) * vectorBytes, + ); + } + return runQuotientKernelMont( + vectorsMontPacked, + blindsMont, + scalarsMont, + elementCount, + blindCoeffCount, + commitmentCount, + cosetCount, + ); + } + + async function preloadQuotientStaticAndAux(input: PlonkPreloadQuotientStaticAndAuxInput): Promise { + const { + staticValuesPacked, + staticMontCacheKeysPacked, + scalingPacked, + twiddlesPacked, + denominatorsPacked, + elementCount, + staticVectorCount, + cosetCount, + auxMontCacheKey, + } = input; + const elementBytes = fr.byteSize; + const vectorBytes = elementCount * elementBytes; + if (!Number.isInteger(elementCount) || elementCount <= 0 || (elementCount & (elementCount - 1)) !== 0) { + throw new Error(`invalid PLONK quotient preload element count ${elementCount}`); + } + if (!Number.isInteger(staticVectorCount) || staticVectorCount <= 0) { + throw new Error(`invalid PLONK quotient preload static vector count ${staticVectorCount}`); + } + if (!Number.isInteger(cosetCount) || cosetCount <= 0) { + throw new Error(`invalid PLONK quotient preload coset count ${cosetCount}`); + } + if (!Number.isInteger(auxMontCacheKey) || auxMontCacheKey <= 0) { + throw new Error(`invalid PLONK quotient preload aux cache key ${auxMontCacheKey}`); + } + + const expectedStaticBytes = cosetCount * staticVectorCount * vectorBytes; + if (staticValuesPacked.byteLength !== expectedStaticBytes) { + throw new Error(`PLONK quotient preload expected ${expectedStaticBytes} static bytes, got ${staticValuesPacked.byteLength}`); + } + if (scalingPacked.byteLength !== cosetCount * vectorBytes) { + throw new Error(`PLONK quotient preload expected ${cosetCount * vectorBytes} scaling bytes, got ${scalingPacked.byteLength}`); + } + if (twiddlesPacked.byteLength !== vectorBytes) { + throw new Error(`PLONK quotient preload expected ${vectorBytes} twiddle bytes, got ${twiddlesPacked.byteLength}`); + } + if (denominatorsPacked.byteLength !== cosetCount * vectorBytes) { + throw new Error( + `PLONK quotient preload expected ${cosetCount * vectorBytes} denominator bytes, got ${denominatorsPacked.byteLength}`, + ); + } + + const staticMontCacheKeys = unpackU32LE(staticMontCacheKeysPacked, cosetCount, "PLONK quotient preload static cache keys"); + const [staticMont, scalingMont, twiddlesMont, denominatorsMont] = await Promise.all([ + fr.toMontgomeryPacked(cloneBytes(staticValuesPacked)), + fr.toMontgomeryPacked(cloneBytes(scalingPacked)), + fr.toMontgomeryPacked(cloneBytes(twiddlesPacked)), + fr.toMontgomeryPacked(cloneBytes(denominatorsPacked)), + ]); + + for (let i = 0; i < cosetCount; i += 1) { + const key = staticMontCacheKeys[i]; + if (key <= 0) { + throw new Error(`invalid PLONK quotient preload static cache key ${key}`); + } + const start = i * staticVectorCount * vectorBytes; + staticMontCache.set(key, { + elementCount, + staticVectorCount, + mont: cloneBytes(staticMont.subarray(start, start + staticVectorCount * vectorBytes)), + }); + } + auxMontCache.set(auxMontCacheKey, { + elementCount, + cosetCount, + scalingMont: cloneBytes(scalingMont), + twiddlesMont: cloneBytes(twiddlesMont), + denominatorsMont: cloneBytes(denominatorsMont), + }); + } + + return { + context, + curve, + transformAndEvaluateQuotientCoset, + transformAndEvaluateQuotientCosets, + preloadQuotientStaticAndAux, + async prewarmPlonkQuotientEvaluateKernel(commitmentCount = 0): Promise { + await getQuotientKernel(commitmentCount); + }, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/plonk_webgpu_bridge.ts b/backend/accelerated/webgpu/web/src/curvegpu/plonk_webgpu_bridge.ts new file mode 100644 index 0000000000..c1a5a7d099 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/plonk_webgpu_bridge.ts @@ -0,0 +1,487 @@ +import type { CurveGPUContext, FieldModule, G1Module, G1MSMModule, NTTModule, SupportedCurveID } from "./api.js"; +import type { PlonkQuotientModule } from "./plonk_quotient_module.js"; + +const CURVE_CONFIG: Record = { + bn254: { + g1CoordinateBytes: 32, + g1PointBytes: 96, + }, + bls12_381: { + g1CoordinateBytes: 48, + g1PointBytes: 144, + }, + bls12_377: { + g1CoordinateBytes: 48, + g1PointBytes: 144, + }, +}; + +type BridgeDependencies = { + context: CurveGPUContext; + curve: SupportedCurveID; + fr: FieldModule; + ntt: NTTModule; + g1: G1Module; + g1msm: G1MSMModule; + quotient: PlonkQuotientModule; +}; + +type CachedKey = { + curve: SupportedCurveID; + kzg: Uint8Array; + kzgCount: number; + kzgLagrange: Uint8Array; + kzgLagrangeCount: number; +}; + +let activeBridge: BridgeDependencies | null = null; +let nextHandle = 1; +const keyCache = new Map(); + +function cloneBytes(bytes: Uint8Array): Uint8Array { + return new Uint8Array(bytes); +} + +function assertBridge(curve: string): BridgeDependencies { + if (!activeBridge) { + throw new Error("PLONK WebGPU bridge is not initialized"); + } + if (curve !== activeBridge.curve) { + throw new Error(`PLONK WebGPU bridge is bound to ${activeBridge.curve}, got ${curve}`); + } + return activeBridge; +} + +function getKey(handle: string): CachedKey { + const entry = keyCache.get(handle); + if (!entry) { + throw new Error(`unknown PLONK key handle ${handle}`); + } + return entry; +} + +function unpackG1JacobianPoint(curve: SupportedCurveID, packedPoint: Uint8Array) { + const coordinateBytes = CURVE_CONFIG[curve].g1CoordinateBytes; + return { + x: cloneBytes(packedPoint.slice(0, coordinateBytes)), + y: cloneBytes(packedPoint.slice(coordinateBytes, 2 * coordinateBytes)), + z: cloneBytes(packedPoint.slice(2 * coordinateBytes, 3 * coordinateBytes)), + }; +} + +function unpackG1JacobianPoints(curve: SupportedCurveID, packedPoints: Uint8Array, count: number) { + const pointBytes = CURVE_CONFIG[curve].g1PointBytes; + if (packedPoints.byteLength !== count * pointBytes) { + throw new Error(`expected ${count * pointBytes} packed G1 Jacobian bytes, got ${packedPoints.byteLength}`); + } + return Array.from({ length: count }, (_, i) => { + const start = i * pointBytes; + return unpackG1JacobianPoint(curve, packedPoints.slice(start, start + pointBytes)); + }); +} + +function packG1AffinePoints(curve: SupportedCurveID, points: readonly { x: Uint8Array; y: Uint8Array }[]) { + const coordinateBytes = CURVE_CONFIG[curve].g1CoordinateBytes; + const out = new Uint8Array(points.length * 2 * coordinateBytes); + for (const [i, point] of points.entries()) { + const start = i * 2 * coordinateBytes; + out.set(point.x, start); + out.set(point.y, start + coordinateBytes); + } + return out; +} + +async function init(curve: SupportedCurveID) { + const bridge = assertBridge(curve); + return { + curve, + adapter: { + vendor: bridge.context.diagnostics.vendor ?? "", + architecture: bridge.context.diagnostics.architecture ?? "", + description: bridge.context.diagnostics.description ?? "", + }, + }; +} + +async function prepareKey(curve: SupportedCurveID, payload: Record) { + assertBridge(curve); + const kzg = payload.kzg; + const kzgCount = Number(payload.kzgCount); + const kzgLagrange = payload.kzgLagrange; + const kzgLagrangeCount = Number(payload.kzgLagrangeCount); + if (!(kzg instanceof Uint8Array)) { + throw new Error("PLONK key payload is missing kzg"); + } + if (!(kzgLagrange instanceof Uint8Array)) { + throw new Error("PLONK key payload is missing kzgLagrange"); + } + if (!Number.isInteger(kzgCount) || kzgCount <= 0) { + throw new Error(`invalid PLONK kzgCount ${payload.kzgCount}`); + } + if (!Number.isInteger(kzgLagrangeCount) || kzgLagrangeCount <= 0) { + throw new Error(`invalid PLONK kzgLagrangeCount ${payload.kzgLagrangeCount}`); + } + const kzgExpectedBytes = kzgCount * CURVE_CONFIG[curve].g1PointBytes; + if (kzg.byteLength !== kzgExpectedBytes) { + throw new Error(`PLONK kzg expected ${kzgExpectedBytes} bytes, got ${kzg.byteLength}`); + } + const kzgLagrangeExpectedBytes = kzgLagrangeCount * CURVE_CONFIG[curve].g1PointBytes; + if (kzgLagrange.byteLength !== kzgLagrangeExpectedBytes) { + throw new Error(`PLONK kzgLagrange expected ${kzgLagrangeExpectedBytes} bytes, got ${kzgLagrange.byteLength}`); + } + const handle = `${curve}:${nextHandle++}`; + const entry: CachedKey = { + curve, + kzg: cloneBytes(kzg), + kzgCount, + kzgLagrange: cloneBytes(kzgLagrange), + kzgLagrangeCount, + }; + keyCache.set(handle, entry); + return { handle }; +} + +async function msmG1(handle: string, vectorName: string, scalarsPacked: Uint8Array, start = 0, count?: number) { + const entry = getKey(handle); + const bridge = assertBridge(entry.curve); + const config = CURVE_CONFIG[entry.curve]; + if (vectorName !== "kzg" && vectorName !== "kzgLagrange") { + throw new Error(`missing cached PLONK G1 vector ${vectorName}`); + } + const vector = entry[vectorName]; + const vectorCount = entry[`${vectorName}Count`]; + const termCount = count ?? vectorCount - start; + if (!Number.isInteger(start) || start < 0 || !Number.isInteger(termCount) || termCount <= 0) { + throw new Error(`invalid PLONK MSM range start=${start} count=${termCount}`); + } + if (start + termCount > vectorCount) { + throw new Error(`PLONK MSM range exceeds ${vectorName}: start=${start} count=${termCount}`); + } + const baseStart = start * config.g1PointBytes; + const baseEnd = (start + termCount) * config.g1PointBytes; + const basesPacked = vector.subarray(baseStart, baseEnd); + const resultPacked = await bridge.g1msm.pippengerPackedJacobianBases(basesPacked, cloneBytes(scalarsPacked), { + count: 1, + termsPerInstance: termCount, + window: bridge.g1msm.bestWindow(termCount), + }); + const jacobian = unpackG1JacobianPoint(entry.curve, resultPacked.slice(0, config.g1PointBytes)); + const affine = await bridge.g1.jacobianToAffine(jacobian); + const out = new Uint8Array(2 * config.g1CoordinateBytes); + out.set(affine.x, 0); + out.set(affine.y, config.g1CoordinateBytes); + return out; +} + +async function msmG1Batch( + handle: string, + vectorName: string, + scalarsPacked: Uint8Array, + start = 0, + termsPerInstance?: number, + count?: number, +) { + const entry = getKey(handle); + const bridge = assertBridge(entry.curve); + const config = CURVE_CONFIG[entry.curve]; + if (vectorName !== "kzg" && vectorName !== "kzgLagrange") { + throw new Error(`missing cached PLONK G1 vector ${vectorName}`); + } + const vector = entry[vectorName]; + const vectorCount = entry[`${vectorName}Count`]; + const instanceCount = count ?? 0; + const termCount = termsPerInstance ?? 0; + if (!Number.isInteger(start) || start < 0 || !Number.isInteger(termCount) || termCount <= 0) { + throw new Error(`invalid PLONK MSM batch range start=${start} termsPerInstance=${termCount}`); + } + if (!Number.isInteger(instanceCount) || instanceCount <= 0) { + throw new Error(`invalid PLONK MSM batch count ${instanceCount}`); + } + if (start + termCount > vectorCount) { + throw new Error(`PLONK MSM batch range exceeds ${vectorName}: start=${start} termsPerInstance=${termCount}`); + } + + const scalarBytes = bridge.fr.byteSize * termCount * instanceCount; + if (scalarsPacked.byteLength !== scalarBytes) { + throw new Error(`PLONK MSM batch expected ${scalarBytes} scalar bytes, got ${scalarsPacked.byteLength}`); + } + + const baseStart = start * config.g1PointBytes; + const baseEnd = (start + termCount) * config.g1PointBytes; + const bases = vector.subarray(baseStart, baseEnd); + const basesPacked = new Uint8Array(bases.byteLength * instanceCount); + for (let i = 0; i < instanceCount; i++) { + basesPacked.set(bases, i * bases.byteLength); + } + + const resultPacked = await bridge.g1msm.pippengerPackedJacobianBases(basesPacked, cloneBytes(scalarsPacked), { + count: instanceCount, + termsPerInstance: termCount, + window: bridge.g1msm.bestWindow(termCount), + }); + const jacobians = unpackG1JacobianPoints(entry.curve, resultPacked, instanceCount); + const affines = await bridge.g1.jacobianToAffineBatch(jacobians); + return packG1AffinePoints(entry.curve, affines); +} + +async function transformQuotientCoset( + curve: SupportedCurveID, + valuesPacked: Uint8Array, + scalingPacked: Uint8Array, + vectorCount: number, + elementCount: number, +) { + const bridge = assertBridge(curve); + const elementBytes = bridge.fr.byteSize; + const vectorBytes = elementCount * elementBytes; + if (!Number.isInteger(vectorCount) || vectorCount <= 0) { + throw new Error(`invalid PLONK quotient vector count ${vectorCount}`); + } + if (!Number.isInteger(elementCount) || elementCount <= 0 || (elementCount & (elementCount - 1)) !== 0) { + throw new Error(`invalid PLONK quotient element count ${elementCount}`); + } + if (valuesPacked.byteLength !== vectorCount * vectorBytes) { + throw new Error(`PLONK quotient transform expected ${vectorCount * vectorBytes} value bytes, got ${valuesPacked.byteLength}`); + } + if (scalingPacked.byteLength !== vectorBytes) { + throw new Error(`PLONK quotient transform expected ${vectorBytes} scaling bytes, got ${scalingPacked.byteLength}`); + } + + const scalingMont = await bridge.fr.toMontgomeryPacked(cloneBytes(scalingPacked)); + const out = new Uint8Array(valuesPacked.byteLength); + await Promise.all( + Array.from({ length: vectorCount }, async (_, i) => { + const start = i * vectorBytes; + const end = start + vectorBytes; + const valuesMont = await bridge.fr.toMontgomeryPacked(cloneBytes(valuesPacked.subarray(start, end))); + const coeffMont = await bridge.ntt.inversePackedMont(valuesMont); + const shiftedCoeffMont = await bridge.fr.mulPackedMont(coeffMont, scalingMont); + const shiftedEvalMont = await bridge.ntt.forwardPackedMont(shiftedCoeffMont); + const shiftedEvalRegular = await bridge.fr.fromMontgomeryPacked(shiftedEvalMont); + out.set(shiftedEvalRegular, start); + }), + ); + return out; +} + +async function canonicalizeQuotientFromCoset(curve: SupportedCurveID, valuesPacked: Uint8Array, elementCount: number) { + return canonicalizeQuotientVectors(curve, valuesPacked, 1, elementCount, true, true); +} + +async function lagrangeQuotientVectors( + curve: SupportedCurveID, + valuesPacked: Uint8Array, + vectorCount: number, + elementCount: number, +) { + const bridge = assertBridge(curve); + const vectorBytes = elementCount * bridge.fr.byteSize; + if (!Number.isInteger(vectorCount) || vectorCount <= 0) { + throw new Error(`invalid PLONK quotient lagrange vector count ${vectorCount}`); + } + if (!Number.isInteger(elementCount) || elementCount <= 0 || (elementCount & (elementCount - 1)) !== 0) { + throw new Error(`invalid PLONK quotient lagrange element count ${elementCount}`); + } + if (valuesPacked.byteLength !== vectorCount * vectorBytes) { + throw new Error(`PLONK quotient lagrange expected ${vectorCount * vectorBytes} value bytes, got ${valuesPacked.byteLength}`); + } + + const out = new Uint8Array(valuesPacked.byteLength); + await Promise.all( + Array.from({ length: vectorCount }, async (_, i) => { + const start = i * vectorBytes; + const end = start + vectorBytes; + const values = cloneBytes(valuesPacked.subarray(start, end)); + out.set(await bridge.ntt.forwardPackedRegular(values), start); + }), + ); + return out; +} + +async function canonicalizeQuotientVectors( + curve: SupportedCurveID, + valuesPacked: Uint8Array, + vectorCount: number, + elementCount: number, + inputBitReversed: boolean, + inverseCoset: boolean, +) { + const bridge = assertBridge(curve); + const vectorBytes = elementCount * bridge.fr.byteSize; + if (!Number.isInteger(vectorCount) || vectorCount <= 0) { + throw new Error(`invalid PLONK quotient canonicalize vector count ${vectorCount}`); + } + if (!Number.isInteger(elementCount) || elementCount <= 0 || (elementCount & (elementCount - 1)) !== 0) { + throw new Error(`invalid PLONK quotient canonicalize element count ${elementCount}`); + } + if (valuesPacked.byteLength !== vectorCount * vectorBytes) { + throw new Error(`PLONK quotient canonicalize expected ${vectorCount * vectorBytes} value bytes, got ${valuesPacked.byteLength}`); + } + + const out = new Uint8Array(valuesPacked.byteLength); + await Promise.all( + Array.from({ length: vectorCount }, async (_, i) => { + const start = i * vectorBytes; + const end = start + vectorBytes; + const input = cloneBytes(valuesPacked.subarray(start, end)); + let canonical: Uint8Array; + if (inverseCoset) { + canonical = inputBitReversed + ? await bridge.ntt.inverseCosetBitReversePackedRegular(input) + : await bridge.ntt.inverseCosetPackedRegular(input); + } else { + canonical = inputBitReversed + ? await bridge.ntt.inverseBitReversePackedRegular(input) + : await bridge.ntt.inversePackedRegular(input); + } + out.set(canonical, start); + }), + ); + return out; +} + +async function transformAndEvaluateQuotientCoset( + curve: SupportedCurveID, + dynamicValuesPacked: Uint8Array, + scalingPacked: Uint8Array, + staticValuesPacked: Uint8Array, + twiddlesPacked: Uint8Array, + denominatorsPacked: Uint8Array, + blindsPacked: Uint8Array, + scalarsPacked: Uint8Array, + elementCount: number, + blindCoeffCount: number, + commitmentCount: number, + dynamicTransformCacheKey: number, + staticMontCacheKey: number, +) { + const bridge = assertBridge(curve); + return bridge.quotient.transformAndEvaluateQuotientCoset({ + dynamicValuesPacked, + scalingPacked, + staticValuesPacked, + twiddlesPacked, + denominatorsPacked, + blindsPacked, + scalarsPacked, + elementCount, + blindCoeffCount, + commitmentCount, + dynamicTransformCacheKey, + staticMontCacheKey, + }); +} + +async function transformAndEvaluateQuotientCosets( + curve: SupportedCurveID, + dynamicValuesPacked: Uint8Array, + scalingPacked: Uint8Array, + staticValuesPacked: Uint8Array, + staticMontCacheKeysPacked: Uint8Array, + twiddlesPacked: Uint8Array, + denominatorsPacked: Uint8Array, + blindsPacked: Uint8Array, + scalarsPacked: Uint8Array, + elementCount: number, + blindCoeffCount: number, + commitmentCount: number, + dynamicTransformCacheKey: number, + cosetCount: number, + auxMontCacheKey: number, +) { + const bridge = assertBridge(curve); + return bridge.quotient.transformAndEvaluateQuotientCosets({ + dynamicValuesPacked, + scalingPacked, + staticValuesPacked, + staticMontCacheKeysPacked, + twiddlesPacked, + denominatorsPacked, + blindsPacked, + scalarsPacked, + elementCount, + blindCoeffCount, + commitmentCount, + dynamicTransformCacheKey, + cosetCount, + auxMontCacheKey, + }); +} + +async function preloadQuotientStaticAndAux( + curve: SupportedCurveID, + staticValuesPacked: Uint8Array, + staticMontCacheKeysPacked: Uint8Array, + scalingPacked: Uint8Array, + twiddlesPacked: Uint8Array, + denominatorsPacked: Uint8Array, + elementCount: number, + staticVectorCount: number, + cosetCount: number, + auxMontCacheKey: number, +) { + const bridge = assertBridge(curve); + return bridge.quotient.preloadQuotientStaticAndAux({ + staticValuesPacked, + staticMontCacheKeysPacked, + scalingPacked, + twiddlesPacked, + denominatorsPacked, + elementCount, + staticVectorCount, + cosetCount, + auxMontCacheKey, + }); +} + +async function prewarmQuotientTransformDomain(curve: SupportedCurveID, elementCount: number) { + const bridge = assertBridge(curve); + if (!Number.isInteger(elementCount) || elementCount <= 0 || (elementCount & (elementCount - 1)) !== 0) { + throw new Error(`invalid PLONK quotient prewarm element count ${elementCount}`); + } + await bridge.ntt.prewarmDomain(elementCount); + + // Trigger the exact packed transform path once so first prove does not pay + // shader/domain lazy initialization in quotient_num_coset_0. + const zeroVector = new Uint8Array(elementCount * bridge.fr.byteSize); + await transformQuotientCoset(curve, zeroVector, zeroVector, 1, elementCount); +} + +async function prewarmQuotientCanonicalizeDomain(curve: SupportedCurveID, elementCount: number) { + const bridge = assertBridge(curve); + if (!Number.isInteger(elementCount) || elementCount <= 0 || (elementCount & (elementCount - 1)) !== 0) { + throw new Error(`invalid PLONK quotient canonicalize prewarm element count ${elementCount}`); + } + await bridge.ntt.prewarmDomain(elementCount); + + const zeroVector = new Uint8Array(elementCount * bridge.fr.byteSize); + await canonicalizeQuotientFromCoset(curve, zeroVector, elementCount); +} + +async function prewarmQuotientEvaluateKernel(curve: SupportedCurveID, commitmentCount = 0) { + const bridge = assertBridge(curve); + await bridge.quotient.prewarmPlonkQuotientEvaluateKernel(commitmentCount); +} + +export function installPlonkWebGPUBridge(dependencies: BridgeDependencies): void { + activeBridge = dependencies; + (globalThis as typeof globalThis & { gnarkPlonkWebGPU?: unknown }).gnarkPlonkWebGPU = { + init, + prepareKey, + msmG1, + msmG1Batch, + transformQuotientCoset, + transformAndEvaluateQuotientCoset, + transformAndEvaluateQuotientCosets, + preloadQuotientStaticAndAux, + canonicalizeQuotientFromCoset, + lagrangeQuotientVectors, + canonicalizeQuotientVectors, + prewarmQuotientTransformDomain, + prewarmQuotientCanonicalizeDomain, + prewarmQuotientEvaluateKernel, + }; +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/runtime_common.ts b/backend/accelerated/webgpu/web/src/curvegpu/runtime_common.ts new file mode 100644 index 0000000000..3aaac53e30 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/runtime_common.ts @@ -0,0 +1,233 @@ +import { fetchText } from "./browser_utils.js"; +import { fetchShaderParts } from "./shaders.js"; +import type { BufferPool } from "./buffer_pool.js"; + +declare const GPUBufferUsage: { + STORAGE: number; + COPY_DST: number; + COPY_SRC: number; + MAP_READ: number; + UNIFORM: number; +}; +declare const GPUMapMode: { READ: number }; + +export type SimpleKernel = { + pipeline: GPUComputePipeline; + bindGroupLayout: GPUBindGroupLayout; + workgroupSize: number; +}; + +export function lazyAsync(factory: () => Promise): () => Promise { + let promise: Promise | null = null; + return (): Promise => { + if (!promise) { + promise = factory(); + } + return promise; + }; +} + +export function cloneBytes(bytes: Uint8Array): Uint8Array { + return new Uint8Array(bytes); +} + +export function ensureByteLength(bytes: Uint8Array, expected: number, label: string): void { + if (bytes.byteLength !== expected) { + throw new Error(`${label}: expected ${expected} bytes, got ${bytes.byteLength}`); + } +} + +export function packElementBatch(values: readonly Uint8Array[], elementBytes: number, label: string): Uint8Array { + const out = new Uint8Array(values.length * elementBytes); + values.forEach((value, index) => { + ensureByteLength(value, elementBytes, `${label}[${index}]`); + out.set(value, index * elementBytes); + }); + return out; +} + +export function unpackElementBatch(bytes: Uint8Array, elementBytes: number, count: number): Uint8Array[] { + const out: Uint8Array[] = []; + for (let i = 0; i < count; i += 1) { + out.push(cloneBytes(bytes.slice(i * elementBytes, (i + 1) * elementBytes))); + } + return out; +} + +export async function loadShaderText(path: string): Promise { + return fetchText(path); +} + +export async function loadShaderParts(parts: readonly string[]): Promise { + return fetchShaderParts(parts); +} + +export function createSimpleStorageBuffer( + device: GPUDevice, + label: string, + size: number, + usage: GPUBufferUsageFlags = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, +): GPUBuffer { + return device.createBuffer({ + label, + size: Math.max(4, size), + usage, + }); +} + +export function createSimpleStorageBufferFromBytes( + device: GPUDevice, + label: string, + bytes: Uint8Array, + usage: GPUBufferUsageFlags = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, +): GPUBuffer { + const buffer = createSimpleStorageBuffer(device, label, bytes.byteLength, usage); + if (bytes.byteLength > 0) { + device.queue.writeBuffer(buffer, 0, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)); + } + return buffer; +} + +export function createSimpleUniformBuffer( + device: GPUDevice, + label: string, + uniformWords: Uint32Array, +): GPUBuffer { + const buffer = createSimpleStorageBuffer( + device, + label, + uniformWords.byteLength, + GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + ); + device.queue.writeBuffer(buffer, 0, uniformWords.buffer, uniformWords.byteOffset, uniformWords.byteLength); + return buffer; +} + +export function createSimpleBindGroup( + device: GPUDevice, + kernel: SimpleKernel, + label: string, + inputA: GPUBuffer, + inputB: GPUBuffer, + output: GPUBuffer, + uniform: GPUBuffer, +): GPUBindGroup { + return device.createBindGroup({ + label, + layout: kernel.bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: inputA } }, + { binding: 1, resource: { buffer: inputB } }, + { binding: 2, resource: { buffer: output } }, + { binding: 3, resource: { buffer: uniform } }, + ], + }); +} + +export async function submitSimpleKernel( + device: GPUDevice, + kernel: SimpleKernel, + bindGroup: GPUBindGroup, + workgroups: number, + label: string, +): Promise { + const encoder = device.createCommandEncoder({ label: `${label}-encoder` }); + const pass = encoder.beginComputePass({ label: `${label}-pass` }); + pass.setPipeline(kernel.pipeline); + pass.setBindGroup(0, bindGroup); + pass.dispatchWorkgroups(workgroups, 1, 1); + pass.end(); + device.queue.submit([encoder.finish()]); + await device.queue.onSubmittedWorkDone(); +} + +export async function readbackSimpleBuffer( + device: GPUDevice, + buffer: GPUBuffer, + outputBytes: number, + label: string, +): Promise { + let mapped = false; + const readbackBuffer = createSimpleStorageBuffer( + device, + `${label}-readback`, + outputBytes, + GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + ); + try { + const encoder = device.createCommandEncoder({ label: `${label}-readback-encoder` }); + encoder.copyBufferToBuffer(buffer, 0, readbackBuffer, 0, Math.max(4, outputBytes)); + device.queue.submit([encoder.finish()]); + await device.queue.onSubmittedWorkDone(); + await readbackBuffer.mapAsync(GPUMapMode.READ); + mapped = true; + const range = readbackBuffer.getMappedRange(); + const out = new Uint8Array(range.slice(0, outputBytes)); + readbackBuffer.unmap(); + mapped = false; + return out; + } finally { + if (mapped) { + readbackBuffer.unmap(); + } + readbackBuffer.destroy(); + } +} + +export async function runSimpleKernel(options: { + device: GPUDevice; + pool?: BufferPool; + kernel: SimpleKernel; + label: string; + inputA: Uint8Array; + inputB: Uint8Array; + outputBytes: number; + uniformWords: Uint32Array; + workgroups: number; +}): Promise { + const { device, pool, kernel, label, inputA, inputB, outputBytes, uniformWords, workgroups } = options; + const storageInUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST; + const storageOutUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC; + const uniformUsage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST; + + const inputABuffer = pool + ? pool.acquire(inputA.byteLength, storageInUsage, `${label}-input-a`) + : createSimpleStorageBuffer(device, `${label}-input-a`, inputA.byteLength, storageInUsage); + if (inputA.byteLength > 0) { + device.queue.writeBuffer(inputABuffer, 0, inputA.buffer, inputA.byteOffset, inputA.byteLength); + } + + const inputBBuffer = pool + ? pool.acquire(inputB.byteLength, storageInUsage, `${label}-input-b`) + : createSimpleStorageBuffer(device, `${label}-input-b`, inputB.byteLength, storageInUsage); + if (inputB.byteLength > 0) { + device.queue.writeBuffer(inputBBuffer, 0, inputB.buffer, inputB.byteOffset, inputB.byteLength); + } + + const outputBuffer = pool + ? pool.acquire(outputBytes, storageOutUsage, `${label}-output`) + : createSimpleStorageBuffer(device, `${label}-output`, outputBytes, storageOutUsage); + + const uniformBuffer = pool + ? pool.acquire(uniformWords.byteLength, uniformUsage, `${label}-params`) + : createSimpleStorageBuffer(device, `${label}-params`, uniformWords.byteLength, uniformUsage); + device.queue.writeBuffer(uniformBuffer, 0, uniformWords.buffer, uniformWords.byteOffset, uniformWords.byteLength); + + try { + const bindGroup = createSimpleBindGroup(device, kernel, `${label}-bg`, inputABuffer, inputBBuffer, outputBuffer, uniformBuffer); + await submitSimpleKernel(device, kernel, bindGroup, workgroups, label); + return await readbackSimpleBuffer(device, outputBuffer, outputBytes, label); + } finally { + if (pool) { + pool.release(inputABuffer); + pool.release(inputBBuffer); + pool.release(outputBuffer); + pool.release(uniformBuffer); + } else { + inputABuffer.destroy(); + inputBBuffer.destroy(); + outputBuffer.destroy(); + uniformBuffer.destroy(); + } + } +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/shaders.ts b/backend/accelerated/webgpu/web/src/curvegpu/shaders.ts new file mode 100644 index 0000000000..abedfd3d42 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/shaders.ts @@ -0,0 +1,67 @@ +import { CurveGPUShaderError } from "./errors.js"; + +let bundledShaders: Record | null = null; + +/** + * Install a pre-built shader bundle so the library can operate without + * making `fetch()` requests for WGSL files. + * + * Typically called automatically by importing the generated + * `shader_bundle.generated.js` file (produced by `npm run build:shaders`). + * When a bundle is installed, `fetchShaderText` reads from it and only falls + * back to `fetch()` for paths not present in the bundle. + */ +export function setBundledShaders(bundle: Record): void { + bundledShaders = bundle; +} + +export async function fetchShaderText(path: string): Promise { + const filePath = path.split("#", 1)[0]; + if (bundledShaders) { + const content = bundledShaders[filePath]; + if (content !== undefined) { + return content; + } + } + const response = await fetch(filePath); + if (!response.ok) { + throw new CurveGPUShaderError(`failed to load shader ${path}: ${response.status} ${response.statusText}`); + } + return response.text(); +} + +export async function fetchShaderPart(spec: string): Promise { + const [path, fragment] = spec.split("#", 2); + const text = await fetchShaderText(path); + if (!fragment) { + return text; + } + const prefix = "section="; + if (!fragment.startsWith(prefix)) { + throw new CurveGPUShaderError(`unsupported shader fragment spec: ${spec}`); + } + const section = fragment.slice(prefix.length); + const begin = `// curvegpu:section ${section} begin`; + const end = `// curvegpu:section ${section} end`; + const start = text.indexOf(begin); + if (start < 0) { + throw new CurveGPUShaderError(`shader section ${section} begin marker not found in ${path}`); + } + let bodyStart = start + begin.length; + if (text[bodyStart] === "\r") { + bodyStart += 1; + } + if (text[bodyStart] === "\n") { + bodyStart += 1; + } + const stop = text.indexOf(end, bodyStart); + if (stop < 0) { + throw new CurveGPUShaderError(`shader section ${section} end marker not found in ${path}`); + } + return text.slice(bodyStart, stop); +} + +export async function fetchShaderParts(parts: readonly string[]): Promise { + const texts = await Promise.all(parts.map((part) => fetchShaderPart(part))); + return texts.map((text) => (text.endsWith("\n") ? text : `${text}\n`)).join(""); +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/types.ts b/backend/accelerated/webgpu/web/src/curvegpu/types.ts new file mode 100644 index 0000000000..cc568e80b1 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/curvegpu/types.ts @@ -0,0 +1,76 @@ +export type CurveID = "bn254" | "bls12_381" | "bls12_377"; +export type FieldID = "fr" | "fp"; + +export interface FieldShape { + curve: CurveID; + field: FieldID; + hostWords: 4 | 6; + gpuLimbs: 8 | 12; + byteSize: 32 | 48; +} + +export type U32x8 = Uint32Array & { length: 8 }; +export type U32x12 = Uint32Array & { length: 12 }; + +const GENERATED_SHAPES = { + bn254: { + fr: { + curve: "bn254", + field: "fr", + hostWords: 4, + gpuLimbs: 8, + byteSize: 32, + }, + fp: { + curve: "bn254", + field: "fp", + hostWords: 4, + gpuLimbs: 8, + byteSize: 32, + }, + }, + bls12_381: { + fr: { + curve: "bls12_381", + field: "fr", + hostWords: 4, + gpuLimbs: 8, + byteSize: 32, + }, + fp: { + curve: "bls12_381", + field: "fp", + hostWords: 6, + gpuLimbs: 12, + byteSize: 48, + }, + }, + bls12_377: { + fr: { + curve: "bls12_377", + field: "fr", + hostWords: 4, + gpuLimbs: 8, + byteSize: 32, + }, + fp: { + curve: "bls12_377", + field: "fp", + hostWords: 6, + gpuLimbs: 12, + byteSize: 48, + }, + }, +} as const; + +export function shapeFor(curve: CurveID, field: FieldID): FieldShape { + const fields = GENERATED_SHAPES[curve]; + if (!fields) { + throw new Error(`unsupported curve ${curve}`); + } + const shape = fields[field]; + if (!shape) { + throw new Error(`unsupported field ${field} for curve ${curve}`); + } + return shape; +} diff --git a/backend/accelerated/webgpu/web/src/index.ts b/backend/accelerated/webgpu/web/src/index.ts new file mode 100644 index 0000000000..272692e910 --- /dev/null +++ b/backend/accelerated/webgpu/web/src/index.ts @@ -0,0 +1,148 @@ +/** + * gnark-webgpu — WebGPU-accelerated elliptic curve arithmetic for BN254, BLS12-381, and BLS12-377. + * + * ## Quick start + * + * ```typescript + * import { createCurveGPUContext, createBN254 } from "gnark-webgpu"; + * + * const ctx = await createCurveGPUContext(); + * const curve = await createBN254(ctx); + * + * // G1 scalar multiplication + * const result = await curve.g1.scalarMul(base, scalar); + * + * // Multi-scalar multiplication (Pippenger) + * const msm = await curve.g1msm.pippengerPackedJacobianBases(bases, scalars, opts); + * + * ctx.close(); // release GPU resources + * ``` + * + * ## GPU context + * + * `createCurveGPUContext` requests a WebGPU device. Pass {@link CurveGPUContextOptions} + * to control power preference, required adapter limits, and debug logging. + * The returned {@link CurveGPUContext} must be closed with `close()` when no longer needed + * to release the underlying `GPUDevice` and buffer pool. + * + * ## Curve modules + * + * Use `createBN254`, `createBLS12381`, or `createBLS12377` (or the lower-level `createCurveModule`) to + * create a {@link CurveModule}. Each module contains sub-modules for field arithmetic + * ({@link FieldModule} `fr`, `fp`), curve arithmetic ({@link G1Module}, {@link G2Module}), + * NTT ({@link NTTModule}), and MSM ({@link G1MSMModule}, {@link G2MSMModule}). + * + * ## Coordinate conventions + * + * Public byte-oriented APIs use fixed-width little-endian `Uint8Array` values. + * Field arithmetic and point coordinates use Montgomery form unless a method + * explicitly says it accepts or returns regular little-endian values. + * + * - **MSM scalars** — regular little-endian scalar-field bytes. + * - **Affine/Jacobian coordinates** — Montgomery little-endian base-field bytes. + * - **Packed vectors** — concatenated fixed-width elements in the representation + * named by the method (`PackedRegular` or `PackedMont`). + * + * ## Shader bundling + * + * By default the library fetches WGSL shader sources at runtime. To eliminate the runtime + * `fetch()` dependency, import the generated bundle as a side-effect before creating any + * curve module: + * + * ```typescript + * import "gnark-webgpu/shader_bundle"; // sets bundled shaders via setBundledShaders() + * ``` + * + * Or call {@link setBundledShaders} directly with a `Record` of path → WGSL. + * + * ## Error handling + * + * All errors thrown by this library are instances of {@link CurveGPUError} or one of its + * subclasses: + * - {@link CurveGPUNotSupportedError} — WebGPU unavailable or required limits not met + * - {@link CurveGPUDeviceLostError} — GPU device was lost during operation + * - {@link CurveGPUShaderError} — shader fetch or compilation failure + * + * @module + */ +export type { + CurveGPUAffinePoint, + CurveGPUAdapterDiagnostics, + CurveGPUContext, + CurveGPUContextOptions, + CurveGPURequestedLimits, + CurveGPUElementBytes, + CurveGPUFp2Element, + CurveGPUG2AffinePoint, + CurveGPUG2JacobianPoint, + CurveGPUJacobianPoint, + CurveGPUPackedPointLayout, + CurveGPUMSMOptions, + CurveModule, + FieldModule, + G1Module, + G2Module, + G1MSMModule, + G2MSMModule, + Groth16ConstraintSystem, + Groth16Handle, + NTTModule, + Groth16Module, + Groth16ProvingKey, + Groth16ProvingKeyFormat, + Groth16QuotientModule, + Groth16RuntimeKind, + Groth16RuntimeOptions, + Groth16VerificationKey, + SupportedCurveID, + PlonkConstraintSystem, + PlonkHandle, + PlonkModule, + PlonkProvingKey, + PlonkProvingKeyFormat, + PlonkRuntimeKind, + PlonkRuntimeOptions, + PlonkVerificationKey, +} from "./curvegpu/api.js"; + +export { + CurveGPUError, + CurveGPUNotSupportedError, + CurveGPUDeviceLostError, + CurveGPUShaderError, +} from "./curvegpu/errors.js"; + +export { setBundledShaders } from "./curvegpu/shaders.js"; + +export { createCurveGPUContext } from "./curvegpu/context.js"; + +export { + createBLS12377, + createBLS12381, + createBN254, + createCurveModule, + curveDefinition, + supportedCurveIds, +} from "./curvegpu/curves.js"; + +export type { CurveDefinition } from "./curvegpu/curves.js"; + +export type { CurveID, FieldID, FieldShape } from "./curvegpu/types.js"; +export { shapeFor } from "./curvegpu/types.js"; +export { defaultGroth16RuntimeURLs } from "./curvegpu/groth16_module.js"; +export { defaultPlonkRuntimeURLs } from "./curvegpu/plonk_module.js"; + +export type { + MontgomeryLEBytes, + PackedMontgomeryLEBytes, + PackedRegularLEBytes, + RegularLEBytes, +} from "./curvegpu/encoding.js"; +export { hexToBytesLE } from "./curvegpu/encoding.js"; + +export { + joinU32LimbsToBigUint64, + joinU32LimbsToBytesLE, + splitBigUint64WordsToU32, + splitBytesLEToU32, +} from "./curvegpu/convert.js"; diff --git a/backend/accelerated/webgpu/web/tsconfig.json b/backend/accelerated/webgpu/web/tsconfig.json new file mode 100644 index 0000000000..c088359128 --- /dev/null +++ b/backend/accelerated/webgpu/web/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noEmitOnError": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": ".", + "lib": ["ES2022", "DOM"], + "types": ["@webgpu/types"] + }, + "include": ["index.ts", "src/**/*.ts"] +} From b52389b859a35bb74cdd510ecff2df0562fb961e Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Tue, 30 Jun 2026 14:50:40 +0200 Subject: [PATCH 2/8] fix: fetch shaders async --- backend/accelerated/webgpu/web/package.json | 2 +- backend/accelerated/webgpu/web/src/curvegpu/browser_utils.ts | 5 +++++ backend/accelerated/webgpu/web/tsconfig.json | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/accelerated/webgpu/web/package.json b/backend/accelerated/webgpu/web/package.json index 2df77a7d6d..7da05eab59 100644 --- a/backend/accelerated/webgpu/web/package.json +++ b/backend/accelerated/webgpu/web/package.json @@ -47,4 +47,4 @@ "typescript": "^5.8.0", "typescript-eslint": "^8.0.0" } -} \ No newline at end of file +} diff --git a/backend/accelerated/webgpu/web/src/curvegpu/browser_utils.ts b/backend/accelerated/webgpu/web/src/curvegpu/browser_utils.ts index baddf2893a..ff5133e273 100644 --- a/backend/accelerated/webgpu/web/src/curvegpu/browser_utils.ts +++ b/backend/accelerated/webgpu/web/src/curvegpu/browser_utils.ts @@ -1,3 +1,5 @@ +import { fetchShaderText } from "./shaders.js"; + export function mustElement(value: T | null, name: string): T { if (value === null) { throw new Error(`missing element: ${name}`); @@ -24,6 +26,9 @@ export function createPageUI(statusEl: HTMLElement | null, logEl: HTMLElement | } export async function fetchText(path: string): Promise { + if (path.startsWith("/shaders/")) { + return fetchShaderText(path); + } const response = await fetch(path); if (!response.ok) { throw new Error(`failed to load ${path}: ${response.status} ${response.statusText}`); diff --git a/backend/accelerated/webgpu/web/tsconfig.json b/backend/accelerated/webgpu/web/tsconfig.json index c088359128..c297fc9149 100644 --- a/backend/accelerated/webgpu/web/tsconfig.json +++ b/backend/accelerated/webgpu/web/tsconfig.json @@ -7,6 +7,8 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noEmitOnError": true, + "allowJs": true, + "checkJs": false, "declaration": true, "declarationMap": true, "outDir": "dist", From 42d333aebb7e8fecd1c9a5a3bf8af8dbf3134730 Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Tue, 30 Jun 2026 15:02:34 +0200 Subject: [PATCH 3/8] feat: generate NTT domains on demand --- .../webgpu/web/src/curvegpu/curves.ts | 15 ++-- .../webgpu/web/src/curvegpu/ntt_module.ts | 83 +++++++++++++++---- 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/backend/accelerated/webgpu/web/src/curvegpu/curves.ts b/backend/accelerated/webgpu/web/src/curvegpu/curves.ts index 2d6f2d7d86..7ef8148fcd 100644 --- a/backend/accelerated/webgpu/web/src/curvegpu/curves.ts +++ b/backend/accelerated/webgpu/web/src/curvegpu/curves.ts @@ -23,8 +23,9 @@ export interface CurveDefinition { readonly frArithShaderPath: string; readonly frVectorShaderPath: string; readonly frNTTShaderPath: string; - readonly frNTTDomainPath?: string; readonly frModulusHex?: string; + readonly frMultiplicativeGeneratorHex?: string; + readonly frCosetGeneratorHex?: string; readonly fpArithShaderPath: string; readonly g1ArithShaderParts: readonly string[]; readonly g1MSMShaderParts: readonly string[]; @@ -91,8 +92,9 @@ const CURVE_DEFINITIONS: Record = { frArithShaderPath: "/shaders/curves/bn254/fr_arith.wgsl", frVectorShaderPath: "/shaders/curves/bn254/fr_vector.wgsl", frNTTShaderPath: "/shaders/curves/bn254/fr_ntt.wgsl", - frNTTDomainPath: "/testdata/vectors/fr/bn254_ntt_domains.json", frModulusHex: "0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", + frMultiplicativeGeneratorHex: "5", + frCosetGeneratorHex: "5", fpArithShaderPath: "/shaders/curves/bn254/fp_arith.wgsl", g1ArithShaderParts: g1OpsShaderParts("/shaders/curves/bn254/fp_arith.wgsl", "/shaders/curves/bn254/g1_io.wgsl"), g1MSMShaderParts: g1MSMShaderParts("/shaders/curves/bn254/fp_arith.wgsl", "/shaders/curves/bn254/g1_io.wgsl"), @@ -109,8 +111,9 @@ const CURVE_DEFINITIONS: Record = { frArithShaderPath: "/shaders/curves/bls12_381/fr_arith.wgsl", frVectorShaderPath: "/shaders/curves/bls12_381/fr_vector.wgsl", frNTTShaderPath: "/shaders/curves/bls12_381/fr_ntt.wgsl", - frNTTDomainPath: "/testdata/vectors/fr/bls12_381_ntt_domains.json", frModulusHex: "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001", + frMultiplicativeGeneratorHex: "7", + frCosetGeneratorHex: "7", fpArithShaderPath: "/shaders/curves/bls12_381/fp_arith.wgsl", g1ArithShaderParts: g1OpsShaderParts("/shaders/curves/bls12_381/fp_arith.wgsl", "/shaders/curves/bls12_381/g1_io.wgsl"), g1MSMShaderParts: g1MSMShaderParts("/shaders/curves/bls12_381/fp_arith.wgsl", "/shaders/curves/bls12_381/g1_io.wgsl"), @@ -128,8 +131,9 @@ const CURVE_DEFINITIONS: Record = { frArithShaderPath: "/shaders/curves/bls12_377/fr_arith.wgsl", frVectorShaderPath: "/shaders/curves/bls12_377/fr_vector.wgsl", frNTTShaderPath: "/shaders/curves/bls12_377/fr_ntt.wgsl", - frNTTDomainPath: "/testdata/vectors/fr/bls12_377_ntt_domains.json", frModulusHex: "0x12ab655e9a2ca55660b44d1e5c37b00159aa76fed00000010a11800000000001", + frMultiplicativeGeneratorHex: "16", + frCosetGeneratorHex: "16", fpArithShaderPath: "/shaders/curves/bls12_377/fp_arith.wgsl", g1ArithShaderParts: g1OpsShaderParts("/shaders/curves/bls12_377/fp_arith.wgsl", "/shaders/curves/bls12_377/g1_io.wgsl"), g1MSMShaderParts: g1MSMShaderParts("/shaders/curves/bls12_377/fp_arith.wgsl", "/shaders/curves/bls12_377/g1_io.wgsl"), @@ -246,8 +250,9 @@ export async function createCurveModule(context: CurveGPUContext, curve: Support context, { curve: definition.id, - domainPath: definition.frNTTDomainPath ?? "", modulusHex: definition.frModulusHex ?? "", + multiplicativeGeneratorHex: definition.frMultiplicativeGeneratorHex ?? "", + cosetGeneratorHex: definition.frCosetGeneratorHex ?? "", vectorKernel: registry.getOpsKernel("fr_vector_main"), fieldKernel: registry.getOpsKernel("fr_ops_main"), nttKernel: registry.getOpsKernel("fr_ntt_stage_main"), diff --git a/backend/accelerated/webgpu/web/src/curvegpu/ntt_module.ts b/backend/accelerated/webgpu/web/src/curvegpu/ntt_module.ts index b2bc9a4286..7ab26f7987 100644 --- a/backend/accelerated/webgpu/web/src/curvegpu/ntt_module.ts +++ b/backend/accelerated/webgpu/web/src/curvegpu/ntt_module.ts @@ -13,7 +13,6 @@ import { submitSimpleKernel, unpackElementBatch, } from "./runtime_common.js"; -import { fetchJSON } from "./browser_utils.js"; import { hexToBytesLE } from "./encoding.js"; declare const GPUBufferUsage: { @@ -39,10 +38,6 @@ type DomainMetadata = { coset_den_inv_hex: string; }; -type DomainMetadataFile = { - domains: DomainMetadata[]; -}; - type PreparedDomain = { forwardStageMont: Uint8Array[]; inverseStageMont: Uint8Array[]; @@ -55,7 +50,7 @@ type PreparedDomain = { }; function hexToBigInt(hex: string): bigint { - return BigInt(`0x${hex}`); + return BigInt(hex.startsWith("0x") || hex.startsWith("0X") ? hex : `0x${hex}`); } function modPow(base: bigint, exp: bigint, mod: bigint): bigint { @@ -72,6 +67,34 @@ function modPow(base: bigint, exp: bigint, mod: bigint): bigint { return result; } +function modInv(value: bigint, mod: bigint): bigint { + if (value === 0n) { + throw new Error("cannot invert zero"); + } + return modPow(value, mod - 2n, mod); +} + +function twoAdicity(value: bigint): number { + let x = value; + let count = 0; + while ((x & 1n) === 0n) { + count += 1; + x >>= 1n; + } + return count; +} + +function assertPowerOfTwoSize(size: number, label: string): number { + if (!Number.isSafeInteger(size) || size <= 0) { + throw new Error(`${label}: NTT size must be a positive power of two, got ${size}`); + } + const logN = Math.log2(size); + if (!Number.isInteger(logN)) { + throw new Error(`${label}: NTT size must be a positive power of two, got ${size}`); + } + return logN; +} + function bigIntToBytesLE(value: bigint, byteSize: number): Uint8Array { const out = new Uint8Array(byteSize); let x = value; @@ -120,7 +143,7 @@ function buildRegularStageElements(domain: DomainMetadata, inverse: boolean, mod const omega = hexToBigInt(inverse ? domain.omega_inv_hex : domain.omega_hex); const stages: Uint8Array[][] = []; for (let stage = 1; stage <= logN; stage += 1) { - const m = 1 << (stage - 1); + const m = 2 ** (stage - 1); const exponentShift = BigInt(logN - stage); const step = modPow(omega, 1n << exponentShift, modulus); const stageElements: Uint8Array[] = []; @@ -141,21 +164,48 @@ export function createNTTModule( vectorKernel: SimpleKernel; fieldKernel: SimpleKernel; nttKernel: SimpleKernel; - domainPath: string; modulusHex: string; + multiplicativeGeneratorHex: string; + cosetGeneratorHex: string; }, fr: FieldModule, ): NTTModule & Groth16QuotientModule { - const { curve, vectorKernel, fieldKernel, nttKernel, domainPath, modulusHex } = options; + const { curve, vectorKernel, fieldKernel, nttKernel, modulusHex, multiplicativeGeneratorHex, cosetGeneratorHex } = options; const label = `${curve}-fr-ntt`; const elementBytes = fr.byteSize; const getVectorKernel = lazyAsync(async () => vectorKernel); const getFieldKernel = lazyAsync(async () => fieldKernel); const getNTTKernel = lazyAsync(async () => nttKernel); - const getDomains = lazyAsync(async () => fetchJSON(domainPath)); const domainCache = new Map>(); const modulus = BigInt(modulusHex); + const multiplicativeGenerator = hexToBigInt(multiplicativeGeneratorHex); + const cosetGenerator = hexToBigInt(cosetGeneratorHex); + const maxLogSize = twoAdicity(modulus - 1n); + + function buildDomainMetadata(size: number): DomainMetadata { + const logN = assertPowerOfTwoSize(size, label); + if (logN > maxLogSize) { + throw new Error(`${label}: NTT size ${size} exceeds scalar field two-adicity 2^${maxLogSize}`); + } + const sizeBig = BigInt(size); + const omega = modPow(multiplicativeGenerator, (modulus - 1n) / sizeBig, modulus); + const omegaInv = modInv(omega, modulus); + const cardinalityInv = modInv(sizeBig, modulus); + const cosetGenInv = modInv(cosetGenerator, modulus); + const cosetDen = (modPow(cosetGenerator, sizeBig, modulus) - 1n + modulus) % modulus; + const cosetDenInv = modInv(cosetDen, modulus); + return { + log_n: logN, + size, + omega_hex: omega.toString(16), + omega_inv_hex: omegaInv.toString(16), + cardinality_inv_hex: cardinalityInv.toString(16), + coset_gen_hex: cosetGenerator.toString(16), + coset_gen_inv_hex: cosetGenInv.toString(16), + coset_den_inv_hex: cosetDenInv.toString(16), + }; + } async function prepareDomain(size: number): Promise { const cached = domainCache.get(size); @@ -163,11 +213,7 @@ export function createNTTModule( return cached; } const promise = (async (): Promise => { - const file = await getDomains(); - const domain = file.domains.find((item) => item.size === size); - if (!domain) { - throw new Error(`${label}: missing domain metadata for size ${size}`); - } + const domain = buildDomainMetadata(size); const forwardStageRegular = buildRegularStageElements(domain, false, modulus, elementBytes); const inverseStageRegular = buildRegularStageElements(domain, true, modulus, elementBytes); const forwardStageMont = await Promise.all( @@ -544,8 +590,11 @@ export function createNTTModule( curve, field: "fr", async supportedSizes(): Promise { - const file = await getDomains(); - return file.domains.map((domain) => domain.size).sort((a, b) => a - b); + const sizes: number[] = []; + for (let logN = 3; logN <= maxLogSize; logN += 1) { + sizes.push(2 ** logN); + } + return sizes; }, async forward(values: readonly CurveGPUElementBytes[]): Promise { values.forEach((value, index) => ensureByteLength(value, elementBytes, `${label}.forward[${index}]`)); From 3a423d239a31b56ad8db91ed65a128d2ccaf9dbb Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Wed, 22 Jul 2026 01:24:09 +0200 Subject: [PATCH 4/8] test: add webgpu test suites --- .gitignore | 1 + .../webgpu/internal/generator/main.go | 117 ++++++ .../generator/templates/field_vectors.go.tmpl | 259 +++++++++++++ .../generator/templates/g1_bases.go.tmpl | 91 +++++ .../templates/g1_msm_vectors.go.tmpl | 78 ++++ .../templates/g1_ops_vectors.go.tmpl | 66 ++++ .../templates/g1_scalar_vectors.go.tmpl | 110 ++++++ .../templates/g2_msm_vectors.go.tmpl | 62 +++ .../templates/g2_ops_vectors.go.tmpl | 112 ++++++ .../generator/templates/helpers.go.tmpl | 160 ++++++++ .../generator/templates/ntt_vectors.go.tmpl | 104 +++++ .../generator/templates/testdata.go.tmpl | 18 + .../generator/templates/types.go.tmpl | 10 + .../webgpu/internal/testdata/generate/main.go | 365 ++++++++++++++++++ .../internal/testdata/groth16/common.go | 83 ++++ .../webgpu/internal/testdata/plonk/common.go | 121 ++++++ .../testgen/bls12-377/field_vectors.go | 266 +++++++++++++ .../testdata/testgen/bls12-377/g1_bases.go | 98 +++++ .../testgen/bls12-377/g1_msm_vectors.go | 85 ++++ .../testgen/bls12-377/g1_ops_vectors.go | 73 ++++ .../testgen/bls12-377/g1_scalar_vectors.go | 117 ++++++ .../testgen/bls12-377/g2_msm_vectors.go | 69 ++++ .../testgen/bls12-377/g2_ops_vectors.go | 119 ++++++ .../testdata/testgen/bls12-377/helpers.go | 167 ++++++++ .../testdata/testgen/bls12-377/ntt_vectors.go | 111 ++++++ .../testdata/testgen/bls12-377/testdata.go | 25 ++ .../testgen/bls12-381/field_vectors.go | 266 +++++++++++++ .../testdata/testgen/bls12-381/g1_bases.go | 98 +++++ .../testgen/bls12-381/g1_msm_vectors.go | 85 ++++ .../testgen/bls12-381/g1_ops_vectors.go | 73 ++++ .../testgen/bls12-381/g1_scalar_vectors.go | 117 ++++++ .../testgen/bls12-381/g2_msm_vectors.go | 69 ++++ .../testgen/bls12-381/g2_ops_vectors.go | 119 ++++++ .../testdata/testgen/bls12-381/helpers.go | 167 ++++++++ .../testdata/testgen/bls12-381/ntt_vectors.go | 111 ++++++ .../testdata/testgen/bls12-381/testdata.go | 25 ++ .../testdata/testgen/bn254/field_vectors.go | 266 +++++++++++++ .../testdata/testgen/bn254/g1_bases.go | 98 +++++ .../testdata/testgen/bn254/g1_msm_vectors.go | 85 ++++ .../testdata/testgen/bn254/g1_ops_vectors.go | 73 ++++ .../testgen/bn254/g1_scalar_vectors.go | 117 ++++++ .../testdata/testgen/bn254/g2_msm_vectors.go | 69 ++++ .../testdata/testgen/bn254/g2_ops_vectors.go | 119 ++++++ .../testdata/testgen/bn254/helpers.go | 167 ++++++++ .../testdata/testgen/bn254/ntt_vectors.go | 111 ++++++ .../testdata/testgen/bn254/testdata.go | 25 ++ .../webgpu/internal/testdata/testgen/types.go | 17 + .../accelerated/webgpu/web/eslint.config.js | 2 + backend/accelerated/webgpu/web/package.json | 7 + .../webgpu/web/tests/api/index.html | 58 +++ .../webgpu/web/tests/api/src/curvegpu_page.ts | 260 +++++++++++++ .../webgpu/web/tests/api/src/fp_ops_page.ts | 175 +++++++++ .../web/tests/api/src/fr_ntt_bench_page.ts | 156 ++++++++ .../webgpu/web/tests/api/src/fr_ntt_page.ts | 85 ++++ .../webgpu/web/tests/api/src/fr_ops_page.ts | 175 +++++++++ .../web/tests/api/src/fr_vector_bench_page.ts | 360 +++++++++++++++++ .../web/tests/api/src/fr_vector_ops_page.ts | 250 ++++++++++++ .../web/tests/api/src/g1_msm_bench_page.ts | 252 ++++++++++++ .../webgpu/web/tests/api/src/g1_msm_page.ts | 172 +++++++++ .../webgpu/web/tests/api/src/g1_ops_page.ts | 162 ++++++++ .../web/tests/api/src/g1_scalar_mul_page.ts | 121 ++++++ .../web/tests/api/src/g2_msm_bench_page.ts | 312 +++++++++++++++ .../webgpu/web/tests/api/src/g2_msm_page.ts | 255 ++++++++++++ .../webgpu/web/tests/api/src/g2_ops_page.ts | 172 +++++++++ .../web/tests/api/src/shared/bench_total.ts | 25 ++ .../web/tests/api/src/shared/page_library.ts | 47 +++ .../webgpu/web/tests/groth16/index.html | 82 ++++ .../webgpu/web/tests/groth16/main.js | 315 +++++++++++++++ .../accelerated/webgpu/web/tests/index.html | 31 ++ .../webgpu/web/tests/plonk/index.html | 82 ++++ .../webgpu/web/tests/plonk/main.js | 340 ++++++++++++++++ backend/accelerated/webgpu/web/tsconfig.json | 2 +- 72 files changed, 9061 insertions(+), 1 deletion(-) create mode 100644 backend/accelerated/webgpu/internal/generator/main.go create mode 100644 backend/accelerated/webgpu/internal/generator/templates/field_vectors.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/g1_bases.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/g1_msm_vectors.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/g1_ops_vectors.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/g1_scalar_vectors.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/g2_msm_vectors.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/g2_ops_vectors.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/helpers.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/ntt_vectors.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/testdata.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/generator/templates/types.go.tmpl create mode 100644 backend/accelerated/webgpu/internal/testdata/generate/main.go create mode 100644 backend/accelerated/webgpu/internal/testdata/groth16/common.go create mode 100644 backend/accelerated/webgpu/internal/testdata/plonk/common.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/field_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_bases.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_msm_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_ops_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_scalar_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g2_msm_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g2_ops_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/helpers.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/ntt_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/testdata.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/field_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_bases.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_msm_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_ops_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_scalar_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g2_msm_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g2_ops_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/helpers.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/ntt_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/testdata.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/field_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_bases.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_msm_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_ops_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_scalar_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/g2_msm_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/g2_ops_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/helpers.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/ntt_vectors.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/bn254/testdata.go create mode 100644 backend/accelerated/webgpu/internal/testdata/testgen/types.go create mode 100644 backend/accelerated/webgpu/web/tests/api/index.html create mode 100644 backend/accelerated/webgpu/web/tests/api/src/curvegpu_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/fp_ops_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/fr_ntt_bench_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/fr_ntt_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/fr_ops_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/fr_vector_bench_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/fr_vector_ops_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/g1_msm_bench_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/g1_msm_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/g1_ops_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/g1_scalar_mul_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/g2_msm_bench_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/g2_msm_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/g2_ops_page.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/shared/bench_total.ts create mode 100644 backend/accelerated/webgpu/web/tests/api/src/shared/page_library.ts create mode 100644 backend/accelerated/webgpu/web/tests/groth16/index.html create mode 100644 backend/accelerated/webgpu/web/tests/groth16/main.js create mode 100644 backend/accelerated/webgpu/web/tests/index.html create mode 100644 backend/accelerated/webgpu/web/tests/plonk/index.html create mode 100644 backend/accelerated/webgpu/web/tests/plonk/main.js diff --git a/.gitignore b/.gitignore index 7ccbfc09f3..43d9564651 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ examples/gbotrel/** backend/accelerated/webgpu/web/node_modules/ backend/accelerated/webgpu/web/dist/ backend/accelerated/webgpu/web/src/curvegpu/shader_bundle.generated.ts +backend/accelerated/webgpu/web/tests/fixtures/ diff --git a/backend/accelerated/webgpu/internal/generator/main.go b/backend/accelerated/webgpu/internal/generator/main.go new file mode 100644 index 0000000000..221498cf8b --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/consensys/bavard" +) + +type templateData struct { + CurveKey string + CurveDir string + GoPkg string + CurveImport string + FpImport string + FrImport string + FFTImport string + FpBytes int + ScalarBytes int + Limbs int + G1PointBytes int + G2PointBytes int + FpOpsSeed int64 + FrOpsSeed int64 + VectorSeed8 int64 + VectorSeed16 int64 + NTTSeed8 int64 + NTTSeed16 int64 +} + +//go:generate go run main.go +func main() { + _, currentFile, _, ok := runtime.Caller(0) + if !ok { + panic("resolve generator path") + } + generatorDir := filepath.Dir(currentFile) + testdataDir := filepath.Clean(filepath.Join(generatorDir, "../testdata/testgen")) + templatesDir := filepath.Join(generatorDir, "templates") + + data := []templateData{ + { + CurveKey: "bn254", CurveDir: "bn254", GoPkg: "bn254", + CurveImport: "github.com/consensys/gnark-crypto/ecc/bn254", + FpImport: "github.com/consensys/gnark-crypto/ecc/bn254/fp", + FrImport: "github.com/consensys/gnark-crypto/ecc/bn254/fr", + FFTImport: "github.com/consensys/gnark-crypto/ecc/bn254/fr/fft", + FpBytes: 32, ScalarBytes: 32, Limbs: 4, G1PointBytes: 96, G2PointBytes: 192, + FpOpsSeed: 20260402, FrOpsSeed: 20260403, VectorSeed8: 2026040201, VectorSeed16: 2026040202, NTTSeed8: 2026040203, NTTSeed16: 2026040204, + }, + { + CurveKey: "bls12_377", CurveDir: "bls12-377", GoPkg: "bls12377", + CurveImport: "github.com/consensys/gnark-crypto/ecc/bls12-377", + FpImport: "github.com/consensys/gnark-crypto/ecc/bls12-377/fp", + FrImport: "github.com/consensys/gnark-crypto/ecc/bls12-377/fr", + FFTImport: "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/fft", + FpBytes: 48, ScalarBytes: 32, Limbs: 6, G1PointBytes: 144, G2PointBytes: 288, + FpOpsSeed: 20260407, FrOpsSeed: 20260406, VectorSeed8: 2026040601, VectorSeed16: 2026040602, NTTSeed8: 2026040603, NTTSeed16: 2026040604, + }, + { + CurveKey: "bls12_381", CurveDir: "bls12-381", GoPkg: "bls12381", + CurveImport: "github.com/consensys/gnark-crypto/ecc/bls12-381", + FpImport: "github.com/consensys/gnark-crypto/ecc/bls12-381/fp", + FrImport: "github.com/consensys/gnark-crypto/ecc/bls12-381/fr", + FFTImport: "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/fft", + FpBytes: 48, ScalarBytes: 32, Limbs: 6, G1PointBytes: 144, G2PointBytes: 288, + FpOpsSeed: 20260405, FrOpsSeed: 20260404, VectorSeed8: 2026040401, VectorSeed16: 2026040402, NTTSeed8: 2026040403, NTTSeed16: 2026040404, + }, + } + + const copyrightHolder = "Consensys Software Inc." + bgen := bavard.NewBatchGenerator(copyrightHolder, 2026, "gnark") + + rootEntries := []bavard.Entry{ + {File: filepath.Join(testdataDir, "types.go"), Templates: []string{"types.go.tmpl"}}, + } + if err := bgen.Generate(struct{}{}, "testgen", templatesDir, rootEntries...); err != nil { + panic(err) + } + runCmd("gofmt", "-w", testdataDir) + runCmd("goimports", "-w", testdataDir) + + for _, d := range data { + entries := []bavard.Entry{ + {File: filepath.Join(testdataDir, d.CurveDir, "testdata.go"), Templates: []string{"testdata.go.tmpl"}}, + {File: filepath.Join(testdataDir, d.CurveDir, "field_vectors.go"), Templates: []string{"field_vectors.go.tmpl"}}, + {File: filepath.Join(testdataDir, d.CurveDir, "g1_bases.go"), Templates: []string{"g1_bases.go.tmpl"}}, + {File: filepath.Join(testdataDir, d.CurveDir, "g1_msm_vectors.go"), Templates: []string{"g1_msm_vectors.go.tmpl"}}, + {File: filepath.Join(testdataDir, d.CurveDir, "g1_ops_vectors.go"), Templates: []string{"g1_ops_vectors.go.tmpl"}}, + {File: filepath.Join(testdataDir, d.CurveDir, "g1_scalar_vectors.go"), Templates: []string{"g1_scalar_vectors.go.tmpl"}}, + {File: filepath.Join(testdataDir, d.CurveDir, "g2_msm_vectors.go"), Templates: []string{"g2_msm_vectors.go.tmpl"}}, + {File: filepath.Join(testdataDir, d.CurveDir, "g2_ops_vectors.go"), Templates: []string{"g2_ops_vectors.go.tmpl"}}, + {File: filepath.Join(testdataDir, d.CurveDir, "helpers.go"), Templates: []string{"helpers.go.tmpl"}}, + {File: filepath.Join(testdataDir, d.CurveDir, "ntt_vectors.go"), Templates: []string{"ntt_vectors.go.tmpl"}}, + } + if err := bgen.Generate(d, d.GoPkg, templatesDir, entries...); err != nil { + panic(err) + } + runCmd("gofmt", "-w", filepath.Join(testdataDir, d.CurveDir)) + runCmd("goimports", "-w", filepath.Join(testdataDir, d.CurveDir)) + } + +} + +func runCmd(name string, arg ...string) { + fmt.Println(name, strings.Join(arg, " ")) + cmd := exec.Command(name, arg...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + panic(err) + } +} diff --git a/backend/accelerated/webgpu/internal/generator/templates/field_vectors.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/field_vectors.go.tmpl new file mode 100644 index 0000000000..f5f66f4203 --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/field_vectors.go.tmpl @@ -0,0 +1,259 @@ +import ( + "fmt" + "math/big" + "math/bits" + "math/rand" + + fp "{{ .FpImport }}" + fr "{{ .FrImport }}" +) + +type FieldElementCaseJSON struct { + Name string `json:"name"` + ABytesLE string `json:"a_bytes_le"` + BBytesLE string `json:"b_bytes_le"` + EqualBytesLE string `json:"equal_bytes_le"` + AddBytesLE string `json:"add_bytes_le"` + SubBytesLE string `json:"sub_bytes_le"` + NegABytesLE string `json:"neg_a_bytes_le"` + DoubleABytesLE string `json:"double_a_bytes_le"` + MulBytesLE string `json:"mul_bytes_le"` + SquareABytesLE string `json:"square_a_bytes_le"` +} + +type NormalizeCaseJSON struct { + Name string `json:"name"` + InputBytesLE string `json:"input_bytes_le"` + ExpectedBytesLE string `json:"expected_bytes_le"` +} + +type ConvertCaseJSON struct { + Name string `json:"name"` + RegularBytes string `json:"regular_bytes_le"` + MontBytes string `json:"mont_bytes_le"` +} + +type FieldOpsVectorsJSON struct { + ElementCases []FieldElementCaseJSON `json:"element_cases"` + EdgeCases []FieldElementCaseJSON `json:"edge_cases"` + DifferentialCases []FieldElementCaseJSON `json:"differential_cases"` + NormalizeCases []NormalizeCaseJSON `json:"normalize_cases"` + ConvertCases []ConvertCaseJSON `json:"convert_cases"` +} + +type VectorCaseJSON struct { + Name string `json:"name"` + RegularInputs []string `json:"regular_inputs_le"` + MontInputs []string `json:"mont_inputs_le"` + MontFactors []string `json:"mont_factors_le"` + AddExpected []string `json:"add_expected_le"` + SubExpected []string `json:"sub_expected_le"` + MulExpected []string `json:"mul_expected_le"` + ToMontExpected []string `json:"to_mont_expected_le"` + FromMontExpected []string `json:"from_mont_expected_le"` + BitReverseExpected []string `json:"bit_reverse_expected_le"` +} + +type VectorOpsJSON struct { + VectorCases []VectorCaseJSON `json:"vector_cases"` +} + +func BuildFROpsVectors() FieldOpsVectorsJSON { + rng := rand.New(rand.NewSource({{ .FrOpsSeed }})) + return buildFieldOpsVectors( + buildFRElementCase, + buildFRConvertCase, + buildFRDifferentialCases(rng, 32), + frRegularHex, + frModulus, + frQMinusOne, + frQMinus, + frFloorHalfModulus, + frCeilHalfModulus, + ) +} + +func BuildFPOpsVectors() FieldOpsVectorsJSON { + rng := rand.New(rand.NewSource({{ .FpOpsSeed }})) + return buildFieldOpsVectors( + buildFPElementCase, + buildFPConvertCase, + buildFPDifferentialCases(rng, 32), + fpRegularHex, + fpModulus, + fpQMinusOne, + fpQMinus, + fpFloorHalfModulus, + fpCeilHalfModulus, + ) +} + +func buildFieldOpsVectors( + buildElementCase func(string, *big.Int, *big.Int) FieldElementCaseJSON, + buildConvertCase func(string, *big.Int) ConvertCaseJSON, + differentialCases []FieldElementCaseJSON, + regularHex func(*big.Int) string, + modulus func() *big.Int, + qMinusOne func() *big.Int, + qMinus func(uint64) *big.Int, + floorHalf func() *big.Int, + ceilHalf func() *big.Int, +) FieldOpsVectorsJSON { + return FieldOpsVectorsJSON{ + ElementCases: []FieldElementCaseJSON{ + buildElementCase("zero_zero", regularUint64(0), regularUint64(0)), + buildElementCase("zero_one", regularUint64(0), regularUint64(1)), + buildElementCase("one_one", regularUint64(1), regularUint64(1)), + buildElementCase("two_five", regularUint64(2), regularUint64(5)), + buildElementCase("neg_one_one", qMinusOne(), regularUint64(1)), + buildElementCase("seven_five", regularUint64(7), regularUint64(5)), + }, + EdgeCases: []FieldElementCaseJSON{ + buildElementCase("carry32_plus_one", pow2MinusOne(32), regularUint64(1)), + buildElementCase("carry64_plus_one", pow2MinusOne(64), regularUint64(1)), + buildElementCase("carry128_plus_one", pow2MinusOne(128), regularUint64(1)), + buildElementCase("carry192_plus_one", pow2MinusOne(192), regularUint64(1)), + buildElementCase("q_minus_two_plus_three", qMinus(2), regularUint64(3)), + buildElementCase("q_minus_one_q_minus_one", qMinusOne(), qMinusOne()), + buildElementCase("q_minus_two_q_minus_one", qMinus(2), qMinusOne()), + buildElementCase("half_q_floor_half_q_ceil", floorHalf(), ceilHalf()), + }, + DifferentialCases: differentialCases, + NormalizeCases: []NormalizeCaseJSON{ + {Name: "zero", InputBytesLE: regularHex(regularUint64(0)), ExpectedBytesLE: regularHex(regularUint64(0))}, + {Name: "one", InputBytesLE: regularHex(regularUint64(1)), ExpectedBytesLE: regularHex(regularUint64(1))}, + {Name: "q_minus_one", InputBytesLE: regularHex(qMinusOne()), ExpectedBytesLE: regularHex(qMinusOne())}, + {Name: "q", InputBytesLE: regularHex(modulus()), ExpectedBytesLE: regularHex(regularUint64(0))}, + {Name: "q_plus_one", InputBytesLE: regularHex(addBig(modulus(), regularUint64(1))), ExpectedBytesLE: regularHex(regularUint64(1))}, + {Name: "two_q_minus_one", InputBytesLE: regularHex(subBig(mulBig(modulus(), regularUint64(2)), regularUint64(1))), ExpectedBytesLE: regularHex(qMinusOne())}, + }, + ConvertCases: []ConvertCaseJSON{ + buildConvertCase("zero", regularUint64(0)), + buildConvertCase("one", regularUint64(1)), + buildConvertCase("two", regularUint64(2)), + buildConvertCase("five", regularUint64(5)), + buildConvertCase("seven", regularUint64(7)), + buildConvertCase("q_minus_one", qMinusOne()), + }, + } +} + +func BuildFRVectorOps() VectorOpsJSON { + return VectorOpsJSON{ + VectorCases: []VectorCaseJSON{ + buildFRVectorCase("n8_random", 8, rand.New(rand.NewSource({{ .VectorSeed8 }}))), + buildFRVectorCase("n16_random", 16, rand.New(rand.NewSource({{ .VectorSeed16 }}))), + }, + } +} + +func buildFRVectorCase(name string, size int, rng *rand.Rand) VectorCaseJSON { + out := VectorCaseJSON{ + Name: name, + RegularInputs: make([]string, size), + MontInputs: make([]string, size), + MontFactors: make([]string, size), + AddExpected: make([]string, size), + SubExpected: make([]string, size), + MulExpected: make([]string, size), + ToMontExpected: make([]string, size), + FromMontExpected: make([]string, size), + BitReverseExpected: make([]string, size), + } + for i := 0; i < size; i++ { + aRegular := randomFRFieldBigInt(rng) + bRegular := randomFRFieldBigInt(rng) + var aMont, bMont fr.Element + aMont.SetBigInt(aRegular) + bMont.SetBigInt(bRegular) + var add, sub, mul fr.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + mul.Mul(&aMont, &bMont) + out.RegularInputs[i] = frRegularHex(aRegular) + out.MontInputs[i] = frElementHex(aMont) + out.MontFactors[i] = frElementHex(bMont) + out.AddExpected[i] = frElementHex(add) + out.SubExpected[i] = frElementHex(sub) + out.MulExpected[i] = frElementHex(mul) + out.ToMontExpected[i] = frElementHex(aMont) + out.FromMontExpected[i] = frRegularHex(aRegular) + } + logCount := bits.Len(uint(size)) - 1 + for i := 0; i < size; i++ { + j := int(bits.Reverse64(uint64(i)) >> (64 - logCount)) + out.BitReverseExpected[i] = out.MontInputs[j] + } + return out +} + +func buildFRElementCase(name string, aRegular, bRegular *big.Int) FieldElementCaseJSON { + aMont := frToMont(aRegular) + bMont := frToMont(bRegular) + var add, sub, negA, dblA, mul, sqA fr.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + negA.Neg(&aMont) + dblA.Double(&aMont) + mul.Mul(&aMont, &bMont) + sqA.Square(&aMont) + equal := frZeroMont() + if aMont.Equal(&bMont) { + equal.SetUint64(1) + } + return FieldElementCaseJSON{Name: name, ABytesLE: frElementHex(aMont), BBytesLE: frElementHex(bMont), EqualBytesLE: frElementHex(equal), AddBytesLE: frElementHex(add), SubBytesLE: frElementHex(sub), NegABytesLE: frElementHex(negA), DoubleABytesLE: frElementHex(dblA), MulBytesLE: frElementHex(mul), SquareABytesLE: frElementHex(sqA)} +} + +func buildFPElementCase(name string, aRegular, bRegular *big.Int) FieldElementCaseJSON { + aMont := fpToMont(aRegular) + bMont := fpToMont(bRegular) + var add, sub, negA, dblA, mul, sqA fp.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + negA.Neg(&aMont) + dblA.Double(&aMont) + mul.Mul(&aMont, &bMont) + sqA.Square(&aMont) + equal := fpZeroMont() + if aMont.Equal(&bMont) { + equal.SetUint64(1) + } + return FieldElementCaseJSON{Name: name, ABytesLE: fpElementHex(aMont), BBytesLE: fpElementHex(bMont), EqualBytesLE: fpElementHex(equal), AddBytesLE: fpElementHex(add), SubBytesLE: fpElementHex(sub), NegABytesLE: fpElementHex(negA), DoubleABytesLE: fpElementHex(dblA), MulBytesLE: fpElementHex(mul), SquareABytesLE: fpElementHex(sqA)} +} + +func buildFRConvertCase(name string, regular *big.Int) ConvertCaseJSON { + return ConvertCaseJSON{Name: name, RegularBytes: frRegularHex(regular), MontBytes: frElementHex(frToMont(regular))} +} + +func buildFPConvertCase(name string, regular *big.Int) ConvertCaseJSON { + return ConvertCaseJSON{Name: name, RegularBytes: fpRegularHex(regular), MontBytes: fpElementHex(fpToMont(regular))} +} + +func buildFRDifferentialCases(rng *rand.Rand, count int) []FieldElementCaseJSON { + out := make([]FieldElementCaseJSON, count) + for i := 0; i < count; i++ { + a := randomFRFieldBigInt(rng) + b := randomFRFieldBigInt(rng) + if i%7 == 0 { + b = new(big.Int).Set(a) + } + out[i] = buildFRElementCase(fmt.Sprintf("random_%02d", i), a, b) + } + return out +} + +func buildFPDifferentialCases(rng *rand.Rand, count int) []FieldElementCaseJSON { + out := make([]FieldElementCaseJSON, count) + for i := 0; i < count; i++ { + a := randomFPFieldBigInt(rng) + b := randomFPFieldBigInt(rng) + if i%7 == 0 { + b = new(big.Int).Set(a) + } + out[i] = buildFPElementCase(fmt.Sprintf("random_%02d", i), a, b) + } + return out +} + +func frRegularHex(v *big.Int) string { return regularHex(v, {{ .ScalarBytes }}) } +func fpRegularHex(v *big.Int) string { return regularHex(v, {{ .FpBytes }}) } diff --git a/backend/accelerated/webgpu/internal/generator/templates/g1_bases.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/g1_bases.go.tmpl new file mode 100644 index 0000000000..23d926095e --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/g1_bases.go.tmpl @@ -0,0 +1,91 @@ +import ( + "encoding/json" + "math/rand" + + curve "{{ .CurveImport }}" + fp "{{ .FpImport }}" + fr "{{ .FrImport }}" + "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen" +) + +func BuildRandomG1Bases(count int, seed int64) ([]byte, error) { + _, _, genAff, _ := curve.Generators() + oneMontZ := montOne() + rng := rand.New(rand.NewSource(seed)) + scalars := make([]fr.Element, count) + for i := range scalars { + var raw [32]byte + for j := range raw { + raw[j] = byte(rng.Uint32()) + } + scalars[i].SetBytes(raw[:]) + if scalars[i].IsZero() { + scalars[i].SetUint64(1) + } + } + points := curve.BatchScalarMultiplicationG1(&genAff, scalars) + out := make([]byte, count*{{ .G1PointBytes }}) + for i := range points { + base := i * {{ .G1PointBytes }} + writeElementLE(out[base:base+{{ .FpBytes }}], points[i].X) + writeElementLE(out[base+{{ .FpBytes }}:base+2*{{ .FpBytes }}], points[i].Y) + writeElementLE(out[base+2*{{ .FpBytes }}:base+3*{{ .FpBytes }}], oneMontZ) + } + return out, nil +} + +func BuildSequentialG1Bases(count int) ([]byte, error) { + _, _, genAff, _ := curve.Generators() + oneMontZ := montOne() + scalars := make([]fr.Element, count) + for i := range scalars { + scalars[i].SetUint64(uint64(i + 1)) + } + points := curve.BatchScalarMultiplicationG1(&genAff, scalars) + out := make([]byte, count*{{ .G1PointBytes }}) + for i := range points { + base := i * {{ .G1PointBytes }} + writeElementLE(out[base:base+{{ .FpBytes }}], points[i].X) + writeElementLE(out[base+{{ .FpBytes }}:base+2*{{ .FpBytes }}], points[i].Y) + writeElementLE(out[base+2*{{ .FpBytes }}:base+3*{{ .FpBytes }}], oneMontZ) + } + return out, nil +} + +func BuildSequentialG2Bases(count int) ([]byte, error) { + _, _, _, genAff := curve.Generators() + oneMontZ := montOne() + zero := fp.Element{} + scalars := make([]fr.Element, count) + for i := range scalars { + scalars[i].SetUint64(uint64(i + 1)) + } + points := curve.BatchScalarMultiplicationG2(&genAff, scalars) + out := make([]byte, count*{{ .G2PointBytes }}) + for i := range points { + base := i * {{ .G2PointBytes }} + writeElementLE(out[base:base+{{ .FpBytes }}], points[i].X.A0) + writeElementLE(out[base+{{ .FpBytes }}:base+2*{{ .FpBytes }}], points[i].X.A1) + writeElementLE(out[base+2*{{ .FpBytes }}:base+3*{{ .FpBytes }}], points[i].Y.A0) + writeElementLE(out[base+3*{{ .FpBytes }}:base+4*{{ .FpBytes }}], points[i].Y.A1) + writeElementLE(out[base+4*{{ .FpBytes }}:base+5*{{ .FpBytes }}], oneMontZ) + writeElementLE(out[base+5*{{ .FpBytes }}:base+6*{{ .FpBytes }}], zero) + } + return out, nil +} + +func BuildG1BaseFixtureMetadata(count int) testgen.BaseFixtureMetadata { + return testgen.BaseFixtureMetadata{Count: count, PointBytes: {{ .G1PointBytes }}, Format: "jacobian_x_y_z_le"} +} + +func BuildG2BaseFixtureMetadata(count int) testgen.BaseFixtureMetadata { + return testgen.BaseFixtureMetadata{Count: count, PointBytes: {{ .G2PointBytes }}, Format: "jacobian_x_y_z_le"} +} + +func MarshalMetadataJSON(meta testgen.BaseFixtureMetadata) ([]byte, error) { + data, err := json.MarshalIndent(meta, "", " ") + if err != nil { + return nil, err + } + return append(data, '\n'), nil +} diff --git a/backend/accelerated/webgpu/internal/generator/templates/g1_msm_vectors.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/g1_msm_vectors.go.tmpl new file mode 100644 index 0000000000..ae0fb64cc6 --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/g1_msm_vectors.go.tmpl @@ -0,0 +1,78 @@ +import ( + "math/big" + + curve "{{ .CurveImport }}" + fr "{{ .FrImport }}" +) + +type G1MSMCaseJSON struct { + Name string `json:"name"` + BasesAffine []AffinePointJSON `json:"bases_affine"` + ScalarsBytesLE []string `json:"scalars_bytes_le"` + ExpectedAffine JacobianPointJSON `json:"expected_affine"` +} + +type G1MSMVectorsJSON struct { + TermsPerInstance int `json:"terms_per_instance"` + MSMCases []G1MSMCaseJSON `json:"msm_cases"` + OneMontZ string `json:"one_mont_z"` +} + +func BuildG1MSMVectors() G1MSMVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + five := scalarMulG1MSM(5) + seventeen := scalarMulG1MSM(17) + oneTwentyThree := scalarMulG1MSM(123) + twoHundredEleven := scalarMulG1MSM(211) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + cases := []struct { + name string + bases []*curve.G1Affine + scalars []fr.Element + }{ + {name: "single_generator", bases: []*curve.G1Affine{&genAff, infAff, infAff, infAff}, scalars: []fr.Element{newScalarUint64(1), newScalarUint64(0), newScalarUint64(0), newScalarUint64(0)}}, + {name: "simple_linear_combo", bases: []*curve.G1Affine{&genAff, five, seventeen, oneTwentyThree}, scalars: []fr.Element{newScalarUint64(3), newScalarUint64(4), newScalarUint64(5), newScalarUint64(6)}}, + {name: "includes_infinity_and_zero_scalar", bases: []*curve.G1Affine{infAff, five, infAff, seventeen}, scalars: []fr.Element{newScalarUint64(19), newScalarUint64(0), newScalarUint64(7), newScalarUint64(9)}}, + {name: "q_minus_one_mix", bases: []*curve.G1Affine{&genAff, five, twoHundredEleven, oneTwentyThree}, scalars: []fr.Element{newScalarBig(modMinusOne), newScalarUint64(2), newScalarUint64(0), newScalarUint64(13)}}, + } + out := G1MSMVectorsJSON{TermsPerInstance: 4, MSMCases: make([]G1MSMCaseJSON, len(cases)), OneMontZ: fpElementHex(montOne())} + for i, tc := range cases { + expected := naiveG1MSM(tc.bases, tc.scalars) + out.MSMCases[i] = G1MSMCaseJSON{Name: tc.name, BasesAffine: encodeAffineBatch(tc.bases), ScalarsBytesLE: encodeScalarBatch(tc.scalars), ExpectedAffine: affineOutputToJSON(expected)} + } + return out +} + +func scalarMulG1MSM(v uint64) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} + +func encodeAffineBatch(in []*curve.G1Affine) []AffinePointJSON { + out := make([]AffinePointJSON, len(in)) + for i, point := range in { + out[i] = affineToJSON(point) + } + return out +} + +func encodeScalarBatch(in []fr.Element) []string { + out := make([]string, len(in)) + for i, scalar := range in { + out[i] = scalarToHex(scalar) + } + return out +} + +func naiveG1MSM(bases []*curve.G1Affine, scalars []fr.Element) *curve.G1Affine { + sum := new(curve.G1Affine).SetInfinity() + for i := range bases { + var term curve.G1Affine + term.ScalarMultiplication(bases[i], scalarToBig(scalars[i])) + sum.Add(sum, &term) + } + return sum +} diff --git a/backend/accelerated/webgpu/internal/generator/templates/g1_ops_vectors.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/g1_ops_vectors.go.tmpl new file mode 100644 index 0000000000..5717ea8551 --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/g1_ops_vectors.go.tmpl @@ -0,0 +1,66 @@ +import ( + "math/big" + + curve "{{ .CurveImport }}" +) + +type G1OpsCaseJSON struct { + Name string `json:"name"` + PAffine AffinePointJSON `json:"p_affine"` + QAffine AffinePointJSON `json:"q_affine"` + PJacobian JacobianPointJSON `json:"p_jacobian"` + PAffineOutput JacobianPointJSON `json:"p_affine_output"` + NegPJacobian JacobianPointJSON `json:"neg_p_jacobian"` + DoublePJacobian JacobianPointJSON `json:"double_p_jacobian"` + AddMixedPPlusQJacob JacobianPointJSON `json:"add_mixed_p_plus_q_jacobian"` + AffineAddPPlusQ JacobianPointJSON `json:"affine_add_p_plus_q"` +} + +type G1OpsVectorsJSON struct { + PointCases []G1OpsCaseJSON `json:"point_cases"` +} + +func BuildG1OpsVectors() G1OpsVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + negGen := new(curve.G1Affine).Neg(&genAff) + five := scalarMulG1Ops(5) + seventeen := scalarMulG1Ops(17) + oneTwentyThree := scalarMulG1Ops(123) + cases := []struct { + name string + p *curve.G1Affine + q *curve.G1Affine + }{ + {name: "inf_plus_gen", p: infAff, q: &genAff}, + {name: "gen_plus_inf", p: &genAff, q: infAff}, + {name: "gen_plus_neg_gen", p: &genAff, q: negGen}, + {name: "gen_plus_gen", p: &genAff, q: &genAff}, + {name: "five_plus_seventeen", p: five, q: seventeen}, + {name: "one_twenty_three_self", p: oneTwentyThree, q: oneTwentyThree}, + } + out := G1OpsVectorsJSON{PointCases: make([]G1OpsCaseJSON, len(cases))} + for i, tc := range cases { + var pJac curve.G1Jac + pJac.FromAffine(tc.p) + var negP curve.G1Jac + negP.Neg(&pJac) + var doubleP curve.G1Jac + doubleP.Double(&pJac) + var addMixed curve.G1Jac + addMixed.Set(&pJac).AddMixed(tc.q) + var pAffineOut curve.G1Affine + pAffineOut.FromJacobian(&pJac) + var affineAdd curve.G1Affine + affineAdd.Add(tc.p, tc.q) + out.PointCases[i] = G1OpsCaseJSON{Name: tc.name, PAffine: affineToJSON(tc.p), QAffine: affineToJSON(tc.q), PJacobian: jacToJSON(&pJac), PAffineOutput: affineOutputToJSON(&pAffineOut), NegPJacobian: jacToJSON(&negP), DoublePJacobian: jacToJSON(&doubleP), AddMixedPPlusQJacob: jacToJSON(&addMixed), AffineAddPPlusQ: affineOutputToJSON(&affineAdd)} + } + return out +} + +func scalarMulG1Ops(v uint64) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} diff --git a/backend/accelerated/webgpu/internal/generator/templates/g1_scalar_vectors.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/g1_scalar_vectors.go.tmpl new file mode 100644 index 0000000000..5d3c58803b --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/g1_scalar_vectors.go.tmpl @@ -0,0 +1,110 @@ +import ( + "math/big" + + curve "{{ .CurveImport }}" + fp "{{ .FpImport }}" + fr "{{ .FrImport }}" +) + +type AffinePointJSON struct { + XBytesLE string `json:"x_bytes_le"` + YBytesLE string `json:"y_bytes_le"` +} + +type JacobianPointJSON struct { + XBytesLE string `json:"x_bytes_le"` + YBytesLE string `json:"y_bytes_le"` + ZBytesLE string `json:"z_bytes_le"` +} + +type G1ScalarMulCaseJSON struct { + Name string `json:"name"` + BaseAffine AffinePointJSON `json:"base_affine"` + ScalarBytesLE string `json:"scalar_bytes_le"` + ScalarMulAffine JacobianPointJSON `json:"scalar_mul_affine"` +} + +type G1ScalarMulBaseCaseJSON struct { + Name string `json:"name"` + ScalarBytesLE string `json:"scalar_bytes_le"` + ScalarMulBaseAffine JacobianPointJSON `json:"scalar_mul_base_affine"` +} + +type G1ScalarMulVectorsJSON struct { + GeneratorAffine AffinePointJSON `json:"generator_affine"` + OneMontZ string `json:"one_mont_z"` + ScalarCases []G1ScalarMulCaseJSON `json:"scalar_cases"` + BaseCases []G1ScalarMulBaseCaseJSON `json:"base_cases"` +} + +func BuildG1ScalarVectors() G1ScalarMulVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + fiveGen := scalarMulG1Generator(newScalarUint64(5)) + oneTwentyThreeGen := scalarMulG1Generator(newScalarUint64(123)) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + scalarCases := []struct { + name string + base *curve.G1Affine + scalar fr.Element + }{ + {name: "gen_times_zero", base: &genAff, scalar: newScalarUint64(0)}, + {name: "gen_times_one", base: &genAff, scalar: newScalarUint64(1)}, + {name: "gen_times_two", base: &genAff, scalar: newScalarUint64(2)}, + {name: "five_gen_times_seventeen", base: fiveGen, scalar: newScalarUint64(17)}, + {name: "infinity_times_one_twenty_three", base: infAff, scalar: newScalarUint64(123)}, + {name: "one_twenty_three_times_forty_two", base: oneTwentyThreeGen, scalar: newScalarUint64(42)}, + {name: "gen_times_q_minus_one", base: &genAff, scalar: newScalarBig(modMinusOne)}, + } + baseCases := []struct { + name string + scalar fr.Element + }{ + {name: "base_zero", scalar: newScalarUint64(0)}, + {name: "base_one", scalar: newScalarUint64(1)}, + {name: "base_two", scalar: newScalarUint64(2)}, + {name: "base_one_twenty_three", scalar: newScalarUint64(123)}, + {name: "base_q_minus_one", scalar: newScalarBig(modMinusOne)}, + } + out := G1ScalarMulVectorsJSON{ + GeneratorAffine: affineToJSON(&genAff), + OneMontZ: fpElementHex(montOne()), + ScalarCases: make([]G1ScalarMulCaseJSON, len(scalarCases)), + BaseCases: make([]G1ScalarMulBaseCaseJSON, len(baseCases)), + } + for i, tc := range scalarCases { + var expected curve.G1Affine + expected.ScalarMultiplication(tc.base, scalarToBig(tc.scalar)) + out.ScalarCases[i] = G1ScalarMulCaseJSON{Name: tc.name, BaseAffine: affineToJSON(tc.base), ScalarBytesLE: scalarToHex(tc.scalar), ScalarMulAffine: affineOutputToJSON(&expected)} + } + for i, tc := range baseCases { + var expected curve.G1Affine + expected.ScalarMultiplicationBase(scalarToBig(tc.scalar)) + out.BaseCases[i] = G1ScalarMulBaseCaseJSON{Name: tc.name, ScalarBytesLE: scalarToHex(tc.scalar), ScalarMulBaseAffine: affineOutputToJSON(&expected)} + } + return out +} + +func scalarMulG1Generator(s fr.Element) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, scalarToBig(s)) + return &out +} + +func affineToJSON(p *curve.G1Affine) AffinePointJSON { + return AffinePointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y)} +} + +func jacToJSON(p *curve.G1Jac) JacobianPointJSON { + return JacobianPointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y), ZBytesLE: fpElementHex(p.Z)} +} + +func affineOutputToJSON(p *curve.G1Affine) JacobianPointJSON { + if p.IsInfinity() { + return JacobianPointJSON{XBytesLE: zeroHex(), YBytesLE: zeroHex(), ZBytesLE: zeroHex()} + } + return JacobianPointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y), ZBytesLE: fpElementHex(montOne())} +} + +var _ fp.Element diff --git a/backend/accelerated/webgpu/internal/generator/templates/g2_msm_vectors.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/g2_msm_vectors.go.tmpl new file mode 100644 index 0000000000..6c54e6323c --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/g2_msm_vectors.go.tmpl @@ -0,0 +1,62 @@ +import ( + "math/big" + + curve "{{ .CurveImport }}" + fr "{{ .FrImport }}" +) + +type G2MSMCaseJSON struct { + Name string `json:"name"` + BasesAffine []G2AffinePointJSON `json:"bases_affine"` + ScalarsBytesLE []string `json:"scalars_bytes_le"` + ExpectedAffine G2JacobianPointJSON `json:"expected_affine"` +} + +type G2MSMVectorsJSON struct { + TermsPerInstance int `json:"terms_per_instance"` + MSMCases []G2MSMCaseJSON `json:"msm_cases"` +} + +func BuildG2MSMVectors() G2MSMVectorsJSON { + _, _, _, genAff := curve.Generators() + infAff := new(curve.G2Affine).SetInfinity() + five := scalarMulG2Ops(5) + seventeen := scalarMulG2Ops(17) + oneTwentyThree := scalarMulG2Ops(123) + twoHundredEleven := scalarMulG2Ops(211) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + cases := []struct { + name string + bases []*curve.G2Affine + scalars []fr.Element + }{ + {name: "single_generator", bases: []*curve.G2Affine{&genAff, infAff, infAff, infAff}, scalars: []fr.Element{newScalarUint64(1), newScalarUint64(0), newScalarUint64(0), newScalarUint64(0)}}, + {name: "simple_linear_combo", bases: []*curve.G2Affine{&genAff, five, seventeen, oneTwentyThree}, scalars: []fr.Element{newScalarUint64(3), newScalarUint64(4), newScalarUint64(5), newScalarUint64(6)}}, + {name: "includes_infinity_and_zero_scalar", bases: []*curve.G2Affine{infAff, five, infAff, seventeen}, scalars: []fr.Element{newScalarUint64(19), newScalarUint64(0), newScalarUint64(7), newScalarUint64(9)}}, + {name: "q_minus_one_mix", bases: []*curve.G2Affine{&genAff, five, twoHundredEleven, oneTwentyThree}, scalars: []fr.Element{newScalarBig(modMinusOne), newScalarUint64(2), newScalarUint64(0), newScalarUint64(13)}}, + } + out := G2MSMVectorsJSON{TermsPerInstance: 4, MSMCases: make([]G2MSMCaseJSON, len(cases))} + for i, tc := range cases { + expected := naiveG2MSM(tc.bases, tc.scalars) + out.MSMCases[i] = G2MSMCaseJSON{Name: tc.name, BasesAffine: encodeG2AffineBatch(tc.bases), ScalarsBytesLE: encodeScalarBatch(tc.scalars), ExpectedAffine: g2AffineOutputToJSON(expected)} + } + return out +} + +func encodeG2AffineBatch(in []*curve.G2Affine) []G2AffinePointJSON { + out := make([]G2AffinePointJSON, len(in)) + for i, point := range in { + out[i] = g2AffineToJSON(point) + } + return out +} + +func naiveG2MSM(bases []*curve.G2Affine, scalars []fr.Element) *curve.G2Affine { + sum := new(curve.G2Affine).SetInfinity() + for i := range bases { + var term curve.G2Affine + term.ScalarMultiplication(bases[i], scalarToBig(scalars[i])) + sum.Add(sum, &term) + } + return sum +} diff --git a/backend/accelerated/webgpu/internal/generator/templates/g2_ops_vectors.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/g2_ops_vectors.go.tmpl new file mode 100644 index 0000000000..0ad3bbdab1 --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/g2_ops_vectors.go.tmpl @@ -0,0 +1,112 @@ +import ( + "math/big" + + curve "{{ .CurveImport }}" + fp "{{ .FpImport }}" +) + +type Fp2PointJSON struct { + C0BytesLE string `json:"c0_bytes_le"` + C1BytesLE string `json:"c1_bytes_le"` +} + +type G2AffinePointJSON struct { + X Fp2PointJSON `json:"x"` + Y Fp2PointJSON `json:"y"` +} + +type G2JacobianPointJSON struct { + X Fp2PointJSON `json:"x"` + Y Fp2PointJSON `json:"y"` + Z Fp2PointJSON `json:"z"` +} + +type G2OpsCaseJSON struct { + Name string `json:"name"` + PAffine G2AffinePointJSON `json:"p_affine"` + QAffine G2AffinePointJSON `json:"q_affine"` + PJacobian G2JacobianPointJSON `json:"p_jacobian"` + PAffineOutput G2JacobianPointJSON `json:"p_affine_output"` + NegPJacobian G2JacobianPointJSON `json:"neg_p_jacobian"` + DoublePJacobian G2JacobianPointJSON `json:"double_p_jacobian"` + AddMixedPPlusQJacob G2JacobianPointJSON `json:"add_mixed_p_plus_q_jacobian"` + AffineAddPPlusQ G2JacobianPointJSON `json:"affine_add_p_plus_q"` +} + +type G2OpsVectorsJSON struct { + PointCases []G2OpsCaseJSON `json:"point_cases"` +} + +func BuildG2OpsVectors() G2OpsVectorsJSON { + _, _, _, genAff := curve.Generators() + infAff := new(curve.G2Affine).SetInfinity() + negGen := new(curve.G2Affine).Neg(&genAff) + five := scalarMulG2Ops(5) + seventeen := scalarMulG2Ops(17) + oneTwentyThree := scalarMulG2Ops(123) + cases := []struct { + name string + p *curve.G2Affine + q *curve.G2Affine + }{ + {name: "inf_plus_gen", p: infAff, q: &genAff}, + {name: "gen_plus_inf", p: &genAff, q: infAff}, + {name: "gen_plus_neg_gen", p: &genAff, q: negGen}, + {name: "gen_plus_gen", p: &genAff, q: &genAff}, + {name: "five_plus_seventeen", p: five, q: seventeen}, + {name: "one_twenty_three_self", p: oneTwentyThree, q: oneTwentyThree}, + } + out := G2OpsVectorsJSON{PointCases: make([]G2OpsCaseJSON, len(cases))} + for i, tc := range cases { + var pJac curve.G2Jac + pJac.FromAffine(tc.p) + var negP curve.G2Jac + negP.Neg(&pJac) + var doubleP curve.G2Jac + doubleP.Double(&pJac) + var addMixed curve.G2Jac + addMixed.Set(&pJac).AddMixed(tc.q) + var pAffineOut curve.G2Affine + pAffineOut.FromJacobian(&pJac) + var affineAdd curve.G2Affine + affineAdd.Add(tc.p, tc.q) + out.PointCases[i] = G2OpsCaseJSON{Name: tc.name, PAffine: g2AffineToJSON(tc.p), QAffine: g2AffineToJSON(tc.q), PJacobian: g2JacToJSON(&pJac), PAffineOutput: g2AffineOutputToJSON(&pAffineOut), NegPJacobian: g2JacToJSON(&negP), DoublePJacobian: g2JacToJSON(&doubleP), AddMixedPPlusQJacob: g2JacToJSON(&addMixed), AffineAddPPlusQ: g2AffineOutputToJSON(&affineAdd)} + } + return out +} + +func scalarMulG2Ops(v uint64) *curve.G2Affine { + _, _, _, genAff := curve.Generators() + var out curve.G2Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} + +func g2AffineToJSON(p *curve.G2Affine) G2AffinePointJSON { + return G2AffinePointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1)} +} + +func g2JacToJSON(p *curve.G2Jac) G2JacobianPointJSON { + return G2JacobianPointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1), Z: fp2ToJSON(p.Z.A0, p.Z.A1)} +} + +func g2AffineOutputToJSON(p *curve.G2Affine) G2JacobianPointJSON { + if p.IsInfinity() { + return G2JacobianPointJSON{X: zeroFp2JSON(), Y: zeroFp2JSON(), Z: zeroFp2JSON()} + } + return G2JacobianPointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1), Z: oneFp2JSON()} +} + +func fp2ToJSON(c0, c1 fp.Element) Fp2PointJSON { + return Fp2PointJSON{C0BytesLE: fpElementHex(c0), C1BytesLE: fpElementHex(c1)} +} + +func zeroFp2JSON() Fp2PointJSON { + var z fp.Element + return fp2ToJSON(z, z) +} + +func oneFp2JSON() Fp2PointJSON { + var zero fp.Element + return fp2ToJSON(montOne(), zero) +} diff --git a/backend/accelerated/webgpu/internal/generator/templates/helpers.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/helpers.go.tmpl new file mode 100644 index 0000000000..9c5e9fde0a --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/helpers.go.tmpl @@ -0,0 +1,160 @@ +import ( + "encoding/binary" + "encoding/hex" + "math/big" + + fp "{{ .FpImport }}" + fr "{{ .FrImport }}" +) + +func frToMont(regular *big.Int) fr.Element { + var z fr.Element + z.SetBigInt(regular) + return z +} + +func fpToMont(regular *big.Int) fp.Element { + var z fp.Element + z.SetBigInt(regular) + return z +} + +func frZeroMont() fr.Element { + var z fr.Element + z.SetZero() + return z +} + +func fpZeroMont() fp.Element { + var z fp.Element + z.SetZero() + return z +} + +func montOne() fp.Element { + var one fp.Element + one.SetOne() + return one +} + +func frElementHex(z fr.Element) string { + data := make([]byte, {{ .ScalarBytes }}) + for i := 0; i < len(z); i++ { + binary.LittleEndian.PutUint64(data[i*8:], z[i]) + } + return hex.EncodeToString(data) +} + +func fpElementHex(z fp.Element) string { + return hex.EncodeToString(wordsToBytes(z[:], {{ .FpBytes }})) +} + +func scalarToHex(v fr.Element) string { + bytesBE := v.Bytes() + return hex.EncodeToString(regularToLittleEndian(bytesBE[:])) +} + +func scalarToBig(v fr.Element) *big.Int { + var out big.Int + return v.BigInt(&out) +} + +func newScalarUint64(v uint64) fr.Element { + var out fr.Element + out.SetUint64(v) + return out +} + +func newScalarBig(v *big.Int) fr.Element { + var out fr.Element + out.SetBigInt(v) + return out +} + +func regularHex(v *big.Int, size int) string { + return hex.EncodeToString(regularToLittleEndian(v.FillBytes(make([]byte, size)))) +} + +func regularUint64(v uint64) *big.Int { + return new(big.Int).SetUint64(v) +} + +func addBig(a, b *big.Int) *big.Int { return new(big.Int).Add(a, b) } +func subBig(a, b *big.Int) *big.Int { return new(big.Int).Sub(a, b) } +func mulBig(a, b *big.Int) *big.Int { return new(big.Int).Mul(a, b) } + +func pow2MinusOne(bitsN uint) *big.Int { + return subBig(new(big.Int).Lsh(big.NewInt(1), bitsN), big.NewInt(1)) +} + +func frModulus() *big.Int { + return new(big.Int).Set(fr.Modulus()) +} + +func fpModulus() *big.Int { + return new(big.Int).Set(fp.Modulus()) +} + +func frQMinusOne() *big.Int { return new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) } +func fpQMinusOne() *big.Int { return new(big.Int).Sub(fp.Modulus(), big.NewInt(1)) } + +func frQMinus(delta uint64) *big.Int { + return new(big.Int).Sub(fr.Modulus(), new(big.Int).SetUint64(delta)) +} + +func fpQMinus(delta uint64) *big.Int { + return new(big.Int).Sub(fp.Modulus(), new(big.Int).SetUint64(delta)) +} + +func frFloorHalfModulus() *big.Int { return new(big.Int).Rsh(frQMinusOne(), 1) } +func frCeilHalfModulus() *big.Int { return new(big.Int).Sub(fr.Modulus(), frFloorHalfModulus()) } +func fpFloorHalfModulus() *big.Int { return new(big.Int).Rsh(fpQMinusOne(), 1) } +func fpCeilHalfModulus() *big.Int { return new(big.Int).Sub(fp.Modulus(), fpFloorHalfModulus()) } + +func randomFRFieldBigInt(rng interface{ Uint32() uint32 }) *big.Int { + buf := make([]byte, 48) + for i := range buf { + buf[i] = byte(rng.Uint32()) + } + return new(big.Int).Mod(new(big.Int).SetBytes(buf), fr.Modulus()) +} + +func randomFPFieldBigInt(rng interface{ Uint32() uint32 }) *big.Int { + buf := make([]byte, 64) + for i := range buf { + buf[i] = byte(rng.Uint32()) + } + return new(big.Int).Mod(new(big.Int).SetBytes(buf), fp.Modulus()) +} + +func wordsToBytes(words []uint64, size int) []byte { + out := make([]byte, size) + for i, word := range words { + base := i * 8 + out[base+0] = byte(word) + out[base+1] = byte(word >> 8) + out[base+2] = byte(word >> 16) + out[base+3] = byte(word >> 24) + out[base+4] = byte(word >> 32) + out[base+5] = byte(word >> 40) + out[base+6] = byte(word >> 48) + out[base+7] = byte(word >> 56) + } + return out +} + +func writeElementLE(dst []byte, v fp.Element) { + copy(dst, wordsToBytes(v[:], {{ .FpBytes }})) +} + +func regularToLittleEndian(in []byte) []byte { + out := make([]byte, len(in)) + for i := range in { + out[len(in)-1-i] = in[i] + } + return out +} + +func zeroHex() string { + return hex.EncodeToString(make([]byte, {{ .FpBytes }})) +} diff --git a/backend/accelerated/webgpu/internal/generator/templates/ntt_vectors.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/ntt_vectors.go.tmpl new file mode 100644 index 0000000000..080bb5e75f --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/ntt_vectors.go.tmpl @@ -0,0 +1,104 @@ +import ( + "math/big" + "math/bits" + "math/rand" + + fr "{{ .FrImport }}" + fft "{{ .FFTImport }}" + "github.com/consensys/gnark-crypto/utils" +) + +type NTTDomainFileJSON struct { + Domains []NTTDomainJSON `json:"domains"` +} + +type NTTDomainJSON struct { + LogN int `json:"log_n"` + Size int `json:"size"` + OmegaHex string `json:"omega_hex"` + OmegaInvHex string `json:"omega_inv_hex"` + CardinalityInvHex string `json:"cardinality_inv_hex"` + CosetGenHex string `json:"coset_gen_hex"` + CosetGenInvHex string `json:"coset_gen_inv_hex"` + CosetDenInvHex string `json:"coset_den_inv_hex"` +} + +type NTTVectorsJSON struct { + NTTCases []NTTCaseJSON `json:"ntt_cases"` +} + +type NTTCaseJSON struct { + Name string `json:"name"` + Size int `json:"size"` + InputMontLE []string `json:"input_mont_le"` + ForwardExpectedLE []string `json:"forward_expected_le"` + InverseExpectedLE []string `json:"inverse_expected_le"` + StageTwiddlesLE [][]string `json:"stage_twiddles_le"` + InverseStageTwiddlesLE [][]string `json:"inverse_stage_twiddles_le"` + InverseScaleLE string `json:"inverse_scale_le"` +} + +func BuildNTTDomainFile(minLog, maxLog int) NTTDomainFileJSON { + out := NTTDomainFileJSON{Domains: make([]NTTDomainJSON, 0, maxLog-minLog+1)} + for logN := minLog; logN <= maxLog; logN++ { + size := 1 << logN + domain := fft.NewDomain(uint64(size)) + var cosetDenInv, one fr.Element + one.SetOne() + cosetDenInv.Exp(domain.FrMultiplicativeGen, big.NewInt(int64(domain.Cardinality))) + cosetDenInv.Sub(&cosetDenInv, &one).Inverse(&cosetDenInv) + out.Domains = append(out.Domains, NTTDomainJSON{LogN: logN, Size: size, OmegaHex: domain.Generator.BigInt(new(big.Int)).Text(16), OmegaInvHex: domain.GeneratorInv.BigInt(new(big.Int)).Text(16), CardinalityInvHex: domain.CardinalityInv.BigInt(new(big.Int)).Text(16), CosetGenHex: domain.FrMultiplicativeGen.BigInt(new(big.Int)).Text(16), CosetGenInvHex: domain.FrMultiplicativeGenInv.BigInt(new(big.Int)).Text(16), CosetDenInvHex: cosetDenInv.BigInt(new(big.Int)).Text(16)}) + } + return out +} + +func BuildNTTVectors() NTTVectorsJSON { + return NTTVectorsJSON{NTTCases: []NTTCaseJSON{buildNTTCase("n8_random", 8, rand.New(rand.NewSource({{ .NTTSeed8 }}))), buildNTTCase("n16_random", 16, rand.New(rand.NewSource({{ .NTTSeed16 }})))}} +} + +func buildNTTCase(name string, size int, rng *rand.Rand) NTTCaseJSON { + domain := fft.NewDomain(uint64(size)) + twiddles, err := domain.Twiddles() + if err != nil { + panic(err) + } + twiddlesInv, err := domain.TwiddlesInv() + if err != nil { + panic(err) + } + input := make([]fr.Element, size) + for i := range input { + input[i].SetBigInt(randomFRFieldBigInt(rng)) + } + forward := make([]fr.Element, size) + copy(forward, input) + utils.BitReverse(forward) + domain.FFT(forward, fft.DIT) + inverse := make([]fr.Element, size) + copy(inverse, forward) + utils.BitReverse(inverse) + domain.FFTInverse(inverse, fft.DIT) + logN := bits.Len(uint(size)) - 1 + stageTwiddles := make([][]string, logN) + inverseStageTwiddles := make([][]string, logN) + for stage := 1; stage <= logN; stage++ { + m := 1 << (stage - 1) + src := twiddles[logN-stage] + srcInv := twiddlesInv[logN-stage] + stageTwiddles[stage-1] = make([]string, m) + inverseStageTwiddles[stage-1] = make([]string, m) + for i := 0; i < m; i++ { + stageTwiddles[stage-1][i] = frElementHex(src[i]) + inverseStageTwiddles[stage-1][i] = frElementHex(srcInv[i]) + } + } + return NTTCaseJSON{Name: name, Size: size, InputMontLE: encodeFRBatch(input), ForwardExpectedLE: encodeFRBatch(forward), InverseExpectedLE: encodeFRBatch(inverse), StageTwiddlesLE: stageTwiddles, InverseStageTwiddlesLE: inverseStageTwiddles, InverseScaleLE: frElementHex(domain.CardinalityInv)} +} + +func encodeFRBatch(in []fr.Element) []string { + out := make([]string, len(in)) + for i, value := range in { + out[i] = frElementHex(value) + } + return out +} diff --git a/backend/accelerated/webgpu/internal/generator/templates/testdata.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/testdata.go.tmpl new file mode 100644 index 0000000000..8121dbab39 --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/testdata.go.tmpl @@ -0,0 +1,18 @@ +import "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen" + +const CurveKey = "{{ .CurveKey }}" + +func JSONTargets(nttMaxLog int) []testgen.JSONTarget { + return []testgen.JSONTarget{ + {Path: "vectors/fr/{{ .CurveKey }}_fr_ops.json", Build: func() any { return BuildFROpsVectors() }}, + {Path: "vectors/fr/{{ .CurveKey }}_fr_vector_ops.json", Build: func() any { return BuildFRVectorOps() }}, + {Path: "vectors/fr/{{ .CurveKey }}_ntt_domains.json", Build: func() any { return BuildNTTDomainFile(3, nttMaxLog) }}, + {Path: "vectors/fr/{{ .CurveKey }}_fr_ntt.json", Build: func() any { return BuildNTTVectors() }}, + {Path: "vectors/fp/{{ .CurveKey }}_fp_ops.json", Build: func() any { return BuildFPOpsVectors() }}, + {Path: "vectors/g1/{{ .CurveKey }}_g1_ops.json", Build: func() any { return BuildG1OpsVectors() }}, + {Path: "vectors/g1/{{ .CurveKey }}_g1_scalar_mul.json", Build: func() any { return BuildG1ScalarVectors() }}, + {Path: "vectors/g1/{{ .CurveKey }}_g1_msm.json", Build: func() any { return BuildG1MSMVectors() }}, + {Path: "vectors/g2/{{ .CurveKey }}_g2_ops.json", Build: func() any { return BuildG2OpsVectors() }}, + {Path: "vectors/g2/{{ .CurveKey }}_g2_msm.json", Build: func() any { return BuildG2MSMVectors() }}, + } +} diff --git a/backend/accelerated/webgpu/internal/generator/templates/types.go.tmpl b/backend/accelerated/webgpu/internal/generator/templates/types.go.tmpl new file mode 100644 index 0000000000..da75e8c7b2 --- /dev/null +++ b/backend/accelerated/webgpu/internal/generator/templates/types.go.tmpl @@ -0,0 +1,10 @@ +type JSONTarget struct { + Path string + Build func() any +} + +type BaseFixtureMetadata struct { + Count int `json:"count"` + PointBytes int `json:"point_bytes"` + Format string `json:"format"` +} diff --git a/backend/accelerated/webgpu/internal/testdata/generate/main.go b/backend/accelerated/webgpu/internal/testdata/generate/main.go new file mode 100644 index 0000000000..1ac8f934ec --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/generate/main.go @@ -0,0 +1,365 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/consensys/gnark-crypto/ecc" + groth16circuit "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/groth16" + plonkcircuit "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/plonk" + "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen" + testgenbls12377 "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377" + testgenbls12381 "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381" + testgenbn254 "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen/bn254" + gnarkgroth16 "github.com/consensys/gnark/backend/groth16" + gnarkplonk "github.com/consensys/gnark/backend/plonk" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/test/unsafekzg" +) + +type curveTestdata struct { + id ecc.ID + key string + jsonTargets func(int) []testgen.JSONTarget + buildG1Bases func(int) ([]byte, error) + buildG2Bases func(int) ([]byte, error) + buildG1BaseMetadata func(int) testgen.BaseFixtureMetadata + buildG2BaseMetadata func(int) testgen.BaseFixtureMetadata +} + +func main() { + suite := flag.String("suite", "all", "fixture suite: api, groth16, plonk, or all") + out := flag.String("out", "tests/fixtures", "output fixture root") + curve := flag.String("curve", "all", "curve: bn254, bls12_377, bls12_381, or all") + logsCSV := flag.String("logs", "12,15,18", "comma-separated prover size logs") + commitmentsCSV := flag.String("commitments", "0,1,2", "comma-separated prover commitment counts") + apiG1FixtureCount := flag.Int("api-g1-fixture-count", 1<<12, "point count for API G1 MSM benchmark base fixtures") + apiG2FixtureCount := flag.Int("api-g2-fixture-count", 1<<12, "point count for API G2 MSM benchmark base fixtures") + apiNTTMaxLog := flag.Int("api-ntt-max-log", 13, "maximum log2 size for generated API NTT domain files") + flag.Parse() + + curves, err := selectCurves(*curve) + if err != nil { + exit(err) + } + logs, err := parsePositiveCSV(*logsCSV, "log") + if err != nil { + exit(err) + } + commitments, err := parseCommitments(*commitmentsCSV) + if err != nil { + exit(err) + } + outRoot, err := filepath.Abs(*out) + if err != nil { + exit(err) + } + + switch *suite { + case "all": + nttMaxLog := maxInt(*apiNTTMaxLog, maxIntSlice(logs)+1) + err = runAPI(filepath.Join(outRoot, "api"), curves, *apiG1FixtureCount, *apiG2FixtureCount, nttMaxLog) + if err == nil { + err = runGroth16(filepath.Join(outRoot, "groth16"), curves, logs, commitments) + } + if err == nil { + err = runPlonk(filepath.Join(outRoot, "plonk"), curves, logs, commitments) + } + case "api": + err = runAPI(filepath.Join(outRoot, "api"), curves, *apiG1FixtureCount, *apiG2FixtureCount, *apiNTTMaxLog) + case "groth16": + err = runGroth16(filepath.Join(outRoot, "groth16"), curves, logs, commitments) + case "plonk": + err = runPlonk(filepath.Join(outRoot, "plonk"), curves, logs, commitments) + default: + err = fmt.Errorf("unknown suite %q", *suite) + } + if err != nil { + exit(err) + } +} + +func runAPI(root string, curves []curveTestdata, g1FixtureCount, g2FixtureCount, nttMaxLog int) error { + for _, curve := range curves { + for _, target := range curve.jsonTargets(nttMaxLog) { + if err := writeJSON(filepath.Join(root, target.Path), target.Build()); err != nil { + return err + } + } + if err := writeBaseFixtures(root, curve, g1FixtureCount, g2FixtureCount); err != nil { + return err + } + } + return nil +} + +func writeBaseFixtures(root string, curve curveTestdata, g1Count, g2Count int) error { + g1, err := curve.buildG1Bases(g1Count) + if err != nil { + return err + } + if err := writeRaw(filepath.Join(root, "fixtures/g1", curve.key+"_bases_jacobian.bin"), g1); err != nil { + return err + } + if err := writeJSON(filepath.Join(root, "fixtures/g1", curve.key+"_bases_jacobian.json"), curve.buildG1BaseMetadata(g1Count)); err != nil { + return err + } + g2, err := curve.buildG2Bases(g2Count) + if err != nil { + return err + } + if err := writeRaw(filepath.Join(root, "fixtures/g2", curve.key+"_bases_jacobian.bin"), g2); err != nil { + return err + } + return writeJSON(filepath.Join(root, "fixtures/g2", curve.key+"_bases_jacobian.json"), curve.buildG2BaseMetadata(g2Count)) +} + +func runGroth16(root string, curves []curveTestdata, logs, commitments []int) error { + for _, curve := range curves { + for _, sizeLog := range logs { + for _, commitmentCount := range commitments { + depth := 1 << sizeLog + circuit := &groth16circuit.MulAddChainCircuit{Steps: depth, Commitments: commitmentCount} + ccs, err := frontend.Compile(curve.id.ScalarField(), r1cs.NewBuilder, circuit) + if err != nil { + return fmt.Errorf("compile groth16 %s 2^%d commit%d: %w", curve.key, sizeLog, commitmentCount, err) + } + pk, vk, err := gnarkgroth16.Setup(ccs) + if err != nil { + return fmt.Errorf("setup groth16 %s 2^%d commit%d: %w", curve.key, sizeLog, commitmentCount, err) + } + base := filepath.Join(root, curve.key, fmt.Sprintf("2pow%d", sizeLog), fmt.Sprintf("commit%d", commitmentCount)) + if err := writeWriterTo(filepath.Join(base, "ccs.bin"), ccs); err != nil { + return err + } + if err := writeDump(filepath.Join(base, "pk.dump"), pk); err != nil { + return err + } + if err := writeWriterTo(filepath.Join(base, "vk.bin"), vk); err != nil { + return err + } + } + } + } + return nil +} + +func runPlonk(root string, curves []curveTestdata, logs, commitments []int) error { + for _, curve := range curves { + for _, sizeLog := range logs { + for _, commitmentCount := range commitments { + steps := plonkcircuit.ChainStepsForTarget(sizeLog, commitmentCount) + circuit := &plonkcircuit.MulAddChainCircuit{Steps: steps, Commitments: commitmentCount} + ccs, err := frontend.Compile(curve.id.ScalarField(), scs.NewBuilder, circuit) + if err != nil { + return fmt.Errorf("compile plonk %s 2^%d commit%d: %w", curve.key, sizeLog, commitmentCount, err) + } + srs, srsLagrange, err := unsafekzg.NewSRS(ccs) + if err != nil { + return fmt.Errorf("srs plonk %s 2^%d commit%d: %w", curve.key, sizeLog, commitmentCount, err) + } + pk, vk, err := gnarkplonk.Setup(ccs, srs, srsLagrange) + if err != nil { + return fmt.Errorf("setup plonk %s 2^%d commit%d: %w", curve.key, sizeLog, commitmentCount, err) + } + base := filepath.Join(root, curve.key, fmt.Sprintf("2pow%d", sizeLog), fmt.Sprintf("commit%d", commitmentCount)) + if err := writeWriterTo(filepath.Join(base, "ccs.bin"), ccs); err != nil { + return err + } + if err := writeWriterTo(filepath.Join(base, "pk.bin"), pk); err != nil { + return err + } + if err := writeWriterTo(filepath.Join(base, "vk.bin"), vk); err != nil { + return err + } + } + } + } + return nil +} + +func selectCurves(curveName string) ([]curveTestdata, error) { + switch curveName { + case "all": + return []curveTestdata{bn254Testdata(), bls12377Testdata(), bls12381Testdata()}, nil + case "bn254": + return []curveTestdata{bn254Testdata()}, nil + case "bls12_377": + return []curveTestdata{bls12377Testdata()}, nil + case "bls12_381": + return []curveTestdata{bls12381Testdata()}, nil + default: + return nil, fmt.Errorf("unsupported curve %q", curveName) + } +} + +func bn254Testdata() curveTestdata { + return curveTestdata{ + id: ecc.BN254, + key: testgenbn254.CurveKey, + jsonTargets: testgenbn254.JSONTargets, + buildG1Bases: testgenbn254.BuildSequentialG1Bases, + buildG2Bases: testgenbn254.BuildSequentialG2Bases, + buildG1BaseMetadata: testgenbn254.BuildG1BaseFixtureMetadata, + buildG2BaseMetadata: testgenbn254.BuildG2BaseFixtureMetadata, + } +} + +func bls12377Testdata() curveTestdata { + return curveTestdata{ + id: ecc.BLS12_377, + key: testgenbls12377.CurveKey, + jsonTargets: testgenbls12377.JSONTargets, + buildG1Bases: testgenbls12377.BuildSequentialG1Bases, + buildG2Bases: testgenbls12377.BuildSequentialG2Bases, + buildG1BaseMetadata: testgenbls12377.BuildG1BaseFixtureMetadata, + buildG2BaseMetadata: testgenbls12377.BuildG2BaseFixtureMetadata, + } +} + +func bls12381Testdata() curveTestdata { + return curveTestdata{ + id: ecc.BLS12_381, + key: testgenbls12381.CurveKey, + jsonTargets: testgenbls12381.JSONTargets, + buildG1Bases: testgenbls12381.BuildSequentialG1Bases, + buildG2Bases: testgenbls12381.BuildSequentialG2Bases, + buildG1BaseMetadata: testgenbls12381.BuildG1BaseFixtureMetadata, + buildG2BaseMetadata: testgenbls12381.BuildG2BaseFixtureMetadata, + } +} + +func parsePositiveCSV(csv, label string) ([]int, error) { + parts := strings.Split(csv, ",") + out := make([]int, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + value, err := strconv.Atoi(part) + if err != nil || value <= 0 { + return nil, fmt.Errorf("invalid %s %q", label, part) + } + out = append(out, value) + } + if len(out) == 0 { + return nil, fmt.Errorf("no %ss provided", label) + } + return out, nil +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + +func maxIntSlice(values []int) int { + max := 0 + for _, value := range values { + if value > max { + max = value + } + } + return max +} + +func parseCommitments(csv string) ([]int, error) { + values, err := parsePositiveOrZeroCSV(csv, "commitment count") + if err != nil { + return nil, err + } + for _, value := range values { + if value > 2 { + return nil, fmt.Errorf("invalid commitment count %d", value) + } + } + return values, nil +} + +func parsePositiveOrZeroCSV(csv, label string) ([]int, error) { + parts := strings.Split(csv, ",") + out := make([]int, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + value, err := strconv.Atoi(part) + if err != nil || value < 0 { + return nil, fmt.Errorf("invalid %s %q", label, part) + } + out = append(out, value) + } + if len(out) == 0 { + return nil, fmt.Errorf("no %ss provided", label) + } + return out, nil +} + +func writeJSON(path string, value any) error { + data, err := json.MarshalIndent(value, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + return writeRaw(path, data) +} + +func writeRaw(path string, data []byte) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + if err := os.WriteFile(path, data, 0o644); err != nil { + return err + } + fmt.Printf("wrote %s\n", path) + return nil +} + +func writeWriterTo(path string, value io.WriterTo) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + _, err = value.WriteTo(f) + if err == nil { + fmt.Printf("wrote %s\n", path) + } + return err +} + +func writeDump(path string, value interface{ WriteDump(io.Writer) error }) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + if err := value.WriteDump(f); err != nil { + return err + } + fmt.Printf("wrote %s\n", path) + return nil +} + +func exit(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} diff --git a/backend/accelerated/webgpu/internal/testdata/groth16/common.go b/backend/accelerated/webgpu/internal/testdata/groth16/common.go new file mode 100644 index 0000000000..c05da2d7ce --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/groth16/common.go @@ -0,0 +1,83 @@ +package groth16 + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark/frontend" +) + +type MulAddChainCircuit struct { + X frontend.Variable + Y frontend.Variable + Out frontend.Variable `gnark:",public"` + Steps int `gnark:"-"` + Commitments int `gnark:"-"` +} + +func (c *MulAddChainCircuit) Define(api frontend.API) error { + acc := c.X + quarter := c.Steps / 4 + var commit0, commit1 []frontend.Variable + if c.Commitments >= 1 { + commit0 = make([]frontend.Variable, 0, quarter) + } + if c.Commitments >= 2 { + commit1 = make([]frontend.Variable, 0, quarter) + } + for i := 0; i < c.Steps; i++ { + acc = api.Add(api.Mul(acc, c.Y), 1) + switch { + case c.Commitments >= 1 && i < quarter: + commit0 = append(commit0, acc) + case c.Commitments >= 2 && i >= quarter && i < 2*quarter: + commit1 = append(commit1, acc) + } + } + switch c.Commitments { + case 0: + case 1: + if err := commitValues(api, commit0); err != nil { + return err + } + case 2: + if err := commitValues(api, commit0); err != nil { + return err + } + if err := commitValues(api, commit1); err != nil { + return err + } + default: + return fmt.Errorf("unsupported commitment count %d", c.Commitments) + } + api.AssertIsEqual(acc, c.Out) + return nil +} + +func commitValues(api frontend.API, values []frontend.Variable) error { + committer, ok := api.(frontend.Committer) + if !ok { + return fmt.Errorf("frontend does not support commitments") + } + commitment, err := committer.Commit(values...) + if err != nil { + return err + } + api.AssertIsDifferent(commitment, 0) + return nil +} + +func ComputeOutput(field *big.Int, x uint64, y uint64, depth int) *big.Int { + acc := new(big.Int).SetUint64(x) + mul := new(big.Int).SetUint64(y) + one := big.NewInt(1) + + for i := 0; i < depth; i++ { + acc.Mul(acc, mul) + acc.Mod(acc, field) + acc.Add(acc, one) + acc.Mod(acc, field) + } + + return acc +} diff --git a/backend/accelerated/webgpu/internal/testdata/plonk/common.go b/backend/accelerated/webgpu/internal/testdata/plonk/common.go new file mode 100644 index 0000000000..085a1fc353 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/plonk/common.go @@ -0,0 +1,121 @@ +package plonk + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark/frontend" +) + +type MulAddChainCircuit struct { + X frontend.Variable + Y frontend.Variable + Out frontend.Variable `gnark:",public"` + Steps int `gnark:"-"` + Commitments int `gnark:"-"` +} + +func TargetConstraints(sizeLog int) int { + return 1 << sizeLog +} + +func ChainStepsForTarget(sizeLog, commitments int) int { + limit := TargetConstraints(sizeLog) - 4 + if limit <= 0 { + return 1 + } + if commitments < 0 { + commitments = 0 + } + if commitments > 2 { + commitments = 2 + } + + steps := 4 + for { + next := steps + 4 + if estimatedConstraints(next, commitments) > limit { + return steps + } + steps = next + } +} + +func estimatedConstraints(steps, commitments int) int { + // Each chain step is four PLONK operations: mul, add, add, add-constant. + // Each commitment adds one commitment operation over a quarter of the chain, + // plus the small verifier challenge plumbing emitted by gnark. + return 4*steps + commitments*(steps/4+2) +} + +func (c *MulAddChainCircuit) Define(api frontend.API) error { + acc := c.X + quarter := c.Steps / 4 + var commit0, commit1 []frontend.Variable + if c.Commitments >= 1 { + commit0 = make([]frontend.Variable, 0, quarter) + } + if c.Commitments >= 2 { + commit1 = make([]frontend.Variable, 0, quarter) + } + for i := 0; i < c.Steps; i++ { + product := api.Mul(acc, c.Y) + sum := api.Add(product, acc) + sum = api.Add(sum, c.X) + acc = api.Add(sum, 1) + switch { + case c.Commitments >= 1 && i < quarter: + commit0 = append(commit0, acc) + case c.Commitments >= 2 && i >= quarter && i < 2*quarter: + commit1 = append(commit1, acc) + } + } + switch c.Commitments { + case 0: + case 1: + if err := commitValues(api, commit0); err != nil { + return err + } + case 2: + if err := commitValues(api, commit0); err != nil { + return err + } + if err := commitValues(api, commit1); err != nil { + return err + } + default: + return fmt.Errorf("unsupported commitment count %d", c.Commitments) + } + api.AssertIsEqual(acc, c.Out) + return nil +} + +func commitValues(api frontend.API, values []frontend.Variable) error { + committer, ok := api.(frontend.Committer) + if !ok { + return fmt.Errorf("frontend does not support commitments") + } + commitment, err := committer.Commit(values...) + if err != nil { + return err + } + api.AssertIsDifferent(commitment, 0) + return nil +} + +func ComputeOutput(field *big.Int, x uint64, y uint64, depth int) *big.Int { + acc := new(big.Int).SetUint64(x) + mul := new(big.Int).SetUint64(y) + xValue := new(big.Int).SetUint64(x) + one := big.NewInt(1) + + for i := 0; i < depth; i++ { + product := new(big.Int).Mul(acc, mul) + acc.Add(product, acc) + acc.Add(acc, xValue) + acc.Add(acc, one) + acc.Mod(acc, field) + } + + return acc +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/field_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/field_vectors.go new file mode 100644 index 0000000000..066975a280 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/field_vectors.go @@ -0,0 +1,266 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import ( + "fmt" + "math/big" + "math/bits" + "math/rand" + + fp "github.com/consensys/gnark-crypto/ecc/bls12-377/fp" + fr "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" +) + +type FieldElementCaseJSON struct { + Name string `json:"name"` + ABytesLE string `json:"a_bytes_le"` + BBytesLE string `json:"b_bytes_le"` + EqualBytesLE string `json:"equal_bytes_le"` + AddBytesLE string `json:"add_bytes_le"` + SubBytesLE string `json:"sub_bytes_le"` + NegABytesLE string `json:"neg_a_bytes_le"` + DoubleABytesLE string `json:"double_a_bytes_le"` + MulBytesLE string `json:"mul_bytes_le"` + SquareABytesLE string `json:"square_a_bytes_le"` +} + +type NormalizeCaseJSON struct { + Name string `json:"name"` + InputBytesLE string `json:"input_bytes_le"` + ExpectedBytesLE string `json:"expected_bytes_le"` +} + +type ConvertCaseJSON struct { + Name string `json:"name"` + RegularBytes string `json:"regular_bytes_le"` + MontBytes string `json:"mont_bytes_le"` +} + +type FieldOpsVectorsJSON struct { + ElementCases []FieldElementCaseJSON `json:"element_cases"` + EdgeCases []FieldElementCaseJSON `json:"edge_cases"` + DifferentialCases []FieldElementCaseJSON `json:"differential_cases"` + NormalizeCases []NormalizeCaseJSON `json:"normalize_cases"` + ConvertCases []ConvertCaseJSON `json:"convert_cases"` +} + +type VectorCaseJSON struct { + Name string `json:"name"` + RegularInputs []string `json:"regular_inputs_le"` + MontInputs []string `json:"mont_inputs_le"` + MontFactors []string `json:"mont_factors_le"` + AddExpected []string `json:"add_expected_le"` + SubExpected []string `json:"sub_expected_le"` + MulExpected []string `json:"mul_expected_le"` + ToMontExpected []string `json:"to_mont_expected_le"` + FromMontExpected []string `json:"from_mont_expected_le"` + BitReverseExpected []string `json:"bit_reverse_expected_le"` +} + +type VectorOpsJSON struct { + VectorCases []VectorCaseJSON `json:"vector_cases"` +} + +func BuildFROpsVectors() FieldOpsVectorsJSON { + rng := rand.New(rand.NewSource(20260406)) + return buildFieldOpsVectors( + buildFRElementCase, + buildFRConvertCase, + buildFRDifferentialCases(rng, 32), + frRegularHex, + frModulus, + frQMinusOne, + frQMinus, + frFloorHalfModulus, + frCeilHalfModulus, + ) +} + +func BuildFPOpsVectors() FieldOpsVectorsJSON { + rng := rand.New(rand.NewSource(20260407)) + return buildFieldOpsVectors( + buildFPElementCase, + buildFPConvertCase, + buildFPDifferentialCases(rng, 32), + fpRegularHex, + fpModulus, + fpQMinusOne, + fpQMinus, + fpFloorHalfModulus, + fpCeilHalfModulus, + ) +} + +func buildFieldOpsVectors( + buildElementCase func(string, *big.Int, *big.Int) FieldElementCaseJSON, + buildConvertCase func(string, *big.Int) ConvertCaseJSON, + differentialCases []FieldElementCaseJSON, + regularHex func(*big.Int) string, + modulus func() *big.Int, + qMinusOne func() *big.Int, + qMinus func(uint64) *big.Int, + floorHalf func() *big.Int, + ceilHalf func() *big.Int, +) FieldOpsVectorsJSON { + return FieldOpsVectorsJSON{ + ElementCases: []FieldElementCaseJSON{ + buildElementCase("zero_zero", regularUint64(0), regularUint64(0)), + buildElementCase("zero_one", regularUint64(0), regularUint64(1)), + buildElementCase("one_one", regularUint64(1), regularUint64(1)), + buildElementCase("two_five", regularUint64(2), regularUint64(5)), + buildElementCase("neg_one_one", qMinusOne(), regularUint64(1)), + buildElementCase("seven_five", regularUint64(7), regularUint64(5)), + }, + EdgeCases: []FieldElementCaseJSON{ + buildElementCase("carry32_plus_one", pow2MinusOne(32), regularUint64(1)), + buildElementCase("carry64_plus_one", pow2MinusOne(64), regularUint64(1)), + buildElementCase("carry128_plus_one", pow2MinusOne(128), regularUint64(1)), + buildElementCase("carry192_plus_one", pow2MinusOne(192), regularUint64(1)), + buildElementCase("q_minus_two_plus_three", qMinus(2), regularUint64(3)), + buildElementCase("q_minus_one_q_minus_one", qMinusOne(), qMinusOne()), + buildElementCase("q_minus_two_q_minus_one", qMinus(2), qMinusOne()), + buildElementCase("half_q_floor_half_q_ceil", floorHalf(), ceilHalf()), + }, + DifferentialCases: differentialCases, + NormalizeCases: []NormalizeCaseJSON{ + {Name: "zero", InputBytesLE: regularHex(regularUint64(0)), ExpectedBytesLE: regularHex(regularUint64(0))}, + {Name: "one", InputBytesLE: regularHex(regularUint64(1)), ExpectedBytesLE: regularHex(regularUint64(1))}, + {Name: "q_minus_one", InputBytesLE: regularHex(qMinusOne()), ExpectedBytesLE: regularHex(qMinusOne())}, + {Name: "q", InputBytesLE: regularHex(modulus()), ExpectedBytesLE: regularHex(regularUint64(0))}, + {Name: "q_plus_one", InputBytesLE: regularHex(addBig(modulus(), regularUint64(1))), ExpectedBytesLE: regularHex(regularUint64(1))}, + {Name: "two_q_minus_one", InputBytesLE: regularHex(subBig(mulBig(modulus(), regularUint64(2)), regularUint64(1))), ExpectedBytesLE: regularHex(qMinusOne())}, + }, + ConvertCases: []ConvertCaseJSON{ + buildConvertCase("zero", regularUint64(0)), + buildConvertCase("one", regularUint64(1)), + buildConvertCase("two", regularUint64(2)), + buildConvertCase("five", regularUint64(5)), + buildConvertCase("seven", regularUint64(7)), + buildConvertCase("q_minus_one", qMinusOne()), + }, + } +} + +func BuildFRVectorOps() VectorOpsJSON { + return VectorOpsJSON{ + VectorCases: []VectorCaseJSON{ + buildFRVectorCase("n8_random", 8, rand.New(rand.NewSource(2026040601))), + buildFRVectorCase("n16_random", 16, rand.New(rand.NewSource(2026040602))), + }, + } +} + +func buildFRVectorCase(name string, size int, rng *rand.Rand) VectorCaseJSON { + out := VectorCaseJSON{ + Name: name, + RegularInputs: make([]string, size), + MontInputs: make([]string, size), + MontFactors: make([]string, size), + AddExpected: make([]string, size), + SubExpected: make([]string, size), + MulExpected: make([]string, size), + ToMontExpected: make([]string, size), + FromMontExpected: make([]string, size), + BitReverseExpected: make([]string, size), + } + for i := 0; i < size; i++ { + aRegular := randomFRFieldBigInt(rng) + bRegular := randomFRFieldBigInt(rng) + var aMont, bMont fr.Element + aMont.SetBigInt(aRegular) + bMont.SetBigInt(bRegular) + var add, sub, mul fr.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + mul.Mul(&aMont, &bMont) + out.RegularInputs[i] = frRegularHex(aRegular) + out.MontInputs[i] = frElementHex(aMont) + out.MontFactors[i] = frElementHex(bMont) + out.AddExpected[i] = frElementHex(add) + out.SubExpected[i] = frElementHex(sub) + out.MulExpected[i] = frElementHex(mul) + out.ToMontExpected[i] = frElementHex(aMont) + out.FromMontExpected[i] = frRegularHex(aRegular) + } + logCount := bits.Len(uint(size)) - 1 + for i := 0; i < size; i++ { + j := int(bits.Reverse64(uint64(i)) >> (64 - logCount)) + out.BitReverseExpected[i] = out.MontInputs[j] + } + return out +} + +func buildFRElementCase(name string, aRegular, bRegular *big.Int) FieldElementCaseJSON { + aMont := frToMont(aRegular) + bMont := frToMont(bRegular) + var add, sub, negA, dblA, mul, sqA fr.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + negA.Neg(&aMont) + dblA.Double(&aMont) + mul.Mul(&aMont, &bMont) + sqA.Square(&aMont) + equal := frZeroMont() + if aMont.Equal(&bMont) { + equal.SetUint64(1) + } + return FieldElementCaseJSON{Name: name, ABytesLE: frElementHex(aMont), BBytesLE: frElementHex(bMont), EqualBytesLE: frElementHex(equal), AddBytesLE: frElementHex(add), SubBytesLE: frElementHex(sub), NegABytesLE: frElementHex(negA), DoubleABytesLE: frElementHex(dblA), MulBytesLE: frElementHex(mul), SquareABytesLE: frElementHex(sqA)} +} + +func buildFPElementCase(name string, aRegular, bRegular *big.Int) FieldElementCaseJSON { + aMont := fpToMont(aRegular) + bMont := fpToMont(bRegular) + var add, sub, negA, dblA, mul, sqA fp.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + negA.Neg(&aMont) + dblA.Double(&aMont) + mul.Mul(&aMont, &bMont) + sqA.Square(&aMont) + equal := fpZeroMont() + if aMont.Equal(&bMont) { + equal.SetUint64(1) + } + return FieldElementCaseJSON{Name: name, ABytesLE: fpElementHex(aMont), BBytesLE: fpElementHex(bMont), EqualBytesLE: fpElementHex(equal), AddBytesLE: fpElementHex(add), SubBytesLE: fpElementHex(sub), NegABytesLE: fpElementHex(negA), DoubleABytesLE: fpElementHex(dblA), MulBytesLE: fpElementHex(mul), SquareABytesLE: fpElementHex(sqA)} +} + +func buildFRConvertCase(name string, regular *big.Int) ConvertCaseJSON { + return ConvertCaseJSON{Name: name, RegularBytes: frRegularHex(regular), MontBytes: frElementHex(frToMont(regular))} +} + +func buildFPConvertCase(name string, regular *big.Int) ConvertCaseJSON { + return ConvertCaseJSON{Name: name, RegularBytes: fpRegularHex(regular), MontBytes: fpElementHex(fpToMont(regular))} +} + +func buildFRDifferentialCases(rng *rand.Rand, count int) []FieldElementCaseJSON { + out := make([]FieldElementCaseJSON, count) + for i := 0; i < count; i++ { + a := randomFRFieldBigInt(rng) + b := randomFRFieldBigInt(rng) + if i%7 == 0 { + b = new(big.Int).Set(a) + } + out[i] = buildFRElementCase(fmt.Sprintf("random_%02d", i), a, b) + } + return out +} + +func buildFPDifferentialCases(rng *rand.Rand, count int) []FieldElementCaseJSON { + out := make([]FieldElementCaseJSON, count) + for i := 0; i < count; i++ { + a := randomFPFieldBigInt(rng) + b := randomFPFieldBigInt(rng) + if i%7 == 0 { + b = new(big.Int).Set(a) + } + out[i] = buildFPElementCase(fmt.Sprintf("random_%02d", i), a, b) + } + return out +} + +func frRegularHex(v *big.Int) string { return regularHex(v, 32) } +func fpRegularHex(v *big.Int) string { return regularHex(v, 48) } diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_bases.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_bases.go new file mode 100644 index 0000000000..5248e052fb --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_bases.go @@ -0,0 +1,98 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import ( + "encoding/json" + "math/rand" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" + fp "github.com/consensys/gnark-crypto/ecc/bls12-377/fp" + fr "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen" +) + +func BuildRandomG1Bases(count int, seed int64) ([]byte, error) { + _, _, genAff, _ := curve.Generators() + oneMontZ := montOne() + rng := rand.New(rand.NewSource(seed)) + scalars := make([]fr.Element, count) + for i := range scalars { + var raw [32]byte + for j := range raw { + raw[j] = byte(rng.Uint32()) + } + scalars[i].SetBytes(raw[:]) + if scalars[i].IsZero() { + scalars[i].SetUint64(1) + } + } + points := curve.BatchScalarMultiplicationG1(&genAff, scalars) + out := make([]byte, count*144) + for i := range points { + base := i * 144 + writeElementLE(out[base:base+48], points[i].X) + writeElementLE(out[base+48:base+2*48], points[i].Y) + writeElementLE(out[base+2*48:base+3*48], oneMontZ) + } + return out, nil +} + +func BuildSequentialG1Bases(count int) ([]byte, error) { + _, _, genAff, _ := curve.Generators() + oneMontZ := montOne() + scalars := make([]fr.Element, count) + for i := range scalars { + scalars[i].SetUint64(uint64(i + 1)) + } + points := curve.BatchScalarMultiplicationG1(&genAff, scalars) + out := make([]byte, count*144) + for i := range points { + base := i * 144 + writeElementLE(out[base:base+48], points[i].X) + writeElementLE(out[base+48:base+2*48], points[i].Y) + writeElementLE(out[base+2*48:base+3*48], oneMontZ) + } + return out, nil +} + +func BuildSequentialG2Bases(count int) ([]byte, error) { + _, _, _, genAff := curve.Generators() + oneMontZ := montOne() + zero := fp.Element{} + scalars := make([]fr.Element, count) + for i := range scalars { + scalars[i].SetUint64(uint64(i + 1)) + } + points := curve.BatchScalarMultiplicationG2(&genAff, scalars) + out := make([]byte, count*288) + for i := range points { + base := i * 288 + writeElementLE(out[base:base+48], points[i].X.A0) + writeElementLE(out[base+48:base+2*48], points[i].X.A1) + writeElementLE(out[base+2*48:base+3*48], points[i].Y.A0) + writeElementLE(out[base+3*48:base+4*48], points[i].Y.A1) + writeElementLE(out[base+4*48:base+5*48], oneMontZ) + writeElementLE(out[base+5*48:base+6*48], zero) + } + return out, nil +} + +func BuildG1BaseFixtureMetadata(count int) testgen.BaseFixtureMetadata { + return testgen.BaseFixtureMetadata{Count: count, PointBytes: 144, Format: "jacobian_x_y_z_le"} +} + +func BuildG2BaseFixtureMetadata(count int) testgen.BaseFixtureMetadata { + return testgen.BaseFixtureMetadata{Count: count, PointBytes: 288, Format: "jacobian_x_y_z_le"} +} + +func MarshalMetadataJSON(meta testgen.BaseFixtureMetadata) ([]byte, error) { + data, err := json.MarshalIndent(meta, "", " ") + if err != nil { + return nil, err + } + return append(data, '\n'), nil +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_msm_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_msm_vectors.go new file mode 100644 index 0000000000..b918d85005 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_msm_vectors.go @@ -0,0 +1,85 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" + fr "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" +) + +type G1MSMCaseJSON struct { + Name string `json:"name"` + BasesAffine []AffinePointJSON `json:"bases_affine"` + ScalarsBytesLE []string `json:"scalars_bytes_le"` + ExpectedAffine JacobianPointJSON `json:"expected_affine"` +} + +type G1MSMVectorsJSON struct { + TermsPerInstance int `json:"terms_per_instance"` + MSMCases []G1MSMCaseJSON `json:"msm_cases"` + OneMontZ string `json:"one_mont_z"` +} + +func BuildG1MSMVectors() G1MSMVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + five := scalarMulG1MSM(5) + seventeen := scalarMulG1MSM(17) + oneTwentyThree := scalarMulG1MSM(123) + twoHundredEleven := scalarMulG1MSM(211) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + cases := []struct { + name string + bases []*curve.G1Affine + scalars []fr.Element + }{ + {name: "single_generator", bases: []*curve.G1Affine{&genAff, infAff, infAff, infAff}, scalars: []fr.Element{newScalarUint64(1), newScalarUint64(0), newScalarUint64(0), newScalarUint64(0)}}, + {name: "simple_linear_combo", bases: []*curve.G1Affine{&genAff, five, seventeen, oneTwentyThree}, scalars: []fr.Element{newScalarUint64(3), newScalarUint64(4), newScalarUint64(5), newScalarUint64(6)}}, + {name: "includes_infinity_and_zero_scalar", bases: []*curve.G1Affine{infAff, five, infAff, seventeen}, scalars: []fr.Element{newScalarUint64(19), newScalarUint64(0), newScalarUint64(7), newScalarUint64(9)}}, + {name: "q_minus_one_mix", bases: []*curve.G1Affine{&genAff, five, twoHundredEleven, oneTwentyThree}, scalars: []fr.Element{newScalarBig(modMinusOne), newScalarUint64(2), newScalarUint64(0), newScalarUint64(13)}}, + } + out := G1MSMVectorsJSON{TermsPerInstance: 4, MSMCases: make([]G1MSMCaseJSON, len(cases)), OneMontZ: fpElementHex(montOne())} + for i, tc := range cases { + expected := naiveG1MSM(tc.bases, tc.scalars) + out.MSMCases[i] = G1MSMCaseJSON{Name: tc.name, BasesAffine: encodeAffineBatch(tc.bases), ScalarsBytesLE: encodeScalarBatch(tc.scalars), ExpectedAffine: affineOutputToJSON(expected)} + } + return out +} + +func scalarMulG1MSM(v uint64) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} + +func encodeAffineBatch(in []*curve.G1Affine) []AffinePointJSON { + out := make([]AffinePointJSON, len(in)) + for i, point := range in { + out[i] = affineToJSON(point) + } + return out +} + +func encodeScalarBatch(in []fr.Element) []string { + out := make([]string, len(in)) + for i, scalar := range in { + out[i] = scalarToHex(scalar) + } + return out +} + +func naiveG1MSM(bases []*curve.G1Affine, scalars []fr.Element) *curve.G1Affine { + sum := new(curve.G1Affine).SetInfinity() + for i := range bases { + var term curve.G1Affine + term.ScalarMultiplication(bases[i], scalarToBig(scalars[i])) + sum.Add(sum, &term) + } + return sum +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_ops_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_ops_vectors.go new file mode 100644 index 0000000000..6d6289a293 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_ops_vectors.go @@ -0,0 +1,73 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" +) + +type G1OpsCaseJSON struct { + Name string `json:"name"` + PAffine AffinePointJSON `json:"p_affine"` + QAffine AffinePointJSON `json:"q_affine"` + PJacobian JacobianPointJSON `json:"p_jacobian"` + PAffineOutput JacobianPointJSON `json:"p_affine_output"` + NegPJacobian JacobianPointJSON `json:"neg_p_jacobian"` + DoublePJacobian JacobianPointJSON `json:"double_p_jacobian"` + AddMixedPPlusQJacob JacobianPointJSON `json:"add_mixed_p_plus_q_jacobian"` + AffineAddPPlusQ JacobianPointJSON `json:"affine_add_p_plus_q"` +} + +type G1OpsVectorsJSON struct { + PointCases []G1OpsCaseJSON `json:"point_cases"` +} + +func BuildG1OpsVectors() G1OpsVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + negGen := new(curve.G1Affine).Neg(&genAff) + five := scalarMulG1Ops(5) + seventeen := scalarMulG1Ops(17) + oneTwentyThree := scalarMulG1Ops(123) + cases := []struct { + name string + p *curve.G1Affine + q *curve.G1Affine + }{ + {name: "inf_plus_gen", p: infAff, q: &genAff}, + {name: "gen_plus_inf", p: &genAff, q: infAff}, + {name: "gen_plus_neg_gen", p: &genAff, q: negGen}, + {name: "gen_plus_gen", p: &genAff, q: &genAff}, + {name: "five_plus_seventeen", p: five, q: seventeen}, + {name: "one_twenty_three_self", p: oneTwentyThree, q: oneTwentyThree}, + } + out := G1OpsVectorsJSON{PointCases: make([]G1OpsCaseJSON, len(cases))} + for i, tc := range cases { + var pJac curve.G1Jac + pJac.FromAffine(tc.p) + var negP curve.G1Jac + negP.Neg(&pJac) + var doubleP curve.G1Jac + doubleP.Double(&pJac) + var addMixed curve.G1Jac + addMixed.Set(&pJac).AddMixed(tc.q) + var pAffineOut curve.G1Affine + pAffineOut.FromJacobian(&pJac) + var affineAdd curve.G1Affine + affineAdd.Add(tc.p, tc.q) + out.PointCases[i] = G1OpsCaseJSON{Name: tc.name, PAffine: affineToJSON(tc.p), QAffine: affineToJSON(tc.q), PJacobian: jacToJSON(&pJac), PAffineOutput: affineOutputToJSON(&pAffineOut), NegPJacobian: jacToJSON(&negP), DoublePJacobian: jacToJSON(&doubleP), AddMixedPPlusQJacob: jacToJSON(&addMixed), AffineAddPPlusQ: affineOutputToJSON(&affineAdd)} + } + return out +} + +func scalarMulG1Ops(v uint64) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_scalar_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_scalar_vectors.go new file mode 100644 index 0000000000..509ef14743 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g1_scalar_vectors.go @@ -0,0 +1,117 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" + fp "github.com/consensys/gnark-crypto/ecc/bls12-377/fp" + fr "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" +) + +type AffinePointJSON struct { + XBytesLE string `json:"x_bytes_le"` + YBytesLE string `json:"y_bytes_le"` +} + +type JacobianPointJSON struct { + XBytesLE string `json:"x_bytes_le"` + YBytesLE string `json:"y_bytes_le"` + ZBytesLE string `json:"z_bytes_le"` +} + +type G1ScalarMulCaseJSON struct { + Name string `json:"name"` + BaseAffine AffinePointJSON `json:"base_affine"` + ScalarBytesLE string `json:"scalar_bytes_le"` + ScalarMulAffine JacobianPointJSON `json:"scalar_mul_affine"` +} + +type G1ScalarMulBaseCaseJSON struct { + Name string `json:"name"` + ScalarBytesLE string `json:"scalar_bytes_le"` + ScalarMulBaseAffine JacobianPointJSON `json:"scalar_mul_base_affine"` +} + +type G1ScalarMulVectorsJSON struct { + GeneratorAffine AffinePointJSON `json:"generator_affine"` + OneMontZ string `json:"one_mont_z"` + ScalarCases []G1ScalarMulCaseJSON `json:"scalar_cases"` + BaseCases []G1ScalarMulBaseCaseJSON `json:"base_cases"` +} + +func BuildG1ScalarVectors() G1ScalarMulVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + fiveGen := scalarMulG1Generator(newScalarUint64(5)) + oneTwentyThreeGen := scalarMulG1Generator(newScalarUint64(123)) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + scalarCases := []struct { + name string + base *curve.G1Affine + scalar fr.Element + }{ + {name: "gen_times_zero", base: &genAff, scalar: newScalarUint64(0)}, + {name: "gen_times_one", base: &genAff, scalar: newScalarUint64(1)}, + {name: "gen_times_two", base: &genAff, scalar: newScalarUint64(2)}, + {name: "five_gen_times_seventeen", base: fiveGen, scalar: newScalarUint64(17)}, + {name: "infinity_times_one_twenty_three", base: infAff, scalar: newScalarUint64(123)}, + {name: "one_twenty_three_times_forty_two", base: oneTwentyThreeGen, scalar: newScalarUint64(42)}, + {name: "gen_times_q_minus_one", base: &genAff, scalar: newScalarBig(modMinusOne)}, + } + baseCases := []struct { + name string + scalar fr.Element + }{ + {name: "base_zero", scalar: newScalarUint64(0)}, + {name: "base_one", scalar: newScalarUint64(1)}, + {name: "base_two", scalar: newScalarUint64(2)}, + {name: "base_one_twenty_three", scalar: newScalarUint64(123)}, + {name: "base_q_minus_one", scalar: newScalarBig(modMinusOne)}, + } + out := G1ScalarMulVectorsJSON{ + GeneratorAffine: affineToJSON(&genAff), + OneMontZ: fpElementHex(montOne()), + ScalarCases: make([]G1ScalarMulCaseJSON, len(scalarCases)), + BaseCases: make([]G1ScalarMulBaseCaseJSON, len(baseCases)), + } + for i, tc := range scalarCases { + var expected curve.G1Affine + expected.ScalarMultiplication(tc.base, scalarToBig(tc.scalar)) + out.ScalarCases[i] = G1ScalarMulCaseJSON{Name: tc.name, BaseAffine: affineToJSON(tc.base), ScalarBytesLE: scalarToHex(tc.scalar), ScalarMulAffine: affineOutputToJSON(&expected)} + } + for i, tc := range baseCases { + var expected curve.G1Affine + expected.ScalarMultiplicationBase(scalarToBig(tc.scalar)) + out.BaseCases[i] = G1ScalarMulBaseCaseJSON{Name: tc.name, ScalarBytesLE: scalarToHex(tc.scalar), ScalarMulBaseAffine: affineOutputToJSON(&expected)} + } + return out +} + +func scalarMulG1Generator(s fr.Element) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, scalarToBig(s)) + return &out +} + +func affineToJSON(p *curve.G1Affine) AffinePointJSON { + return AffinePointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y)} +} + +func jacToJSON(p *curve.G1Jac) JacobianPointJSON { + return JacobianPointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y), ZBytesLE: fpElementHex(p.Z)} +} + +func affineOutputToJSON(p *curve.G1Affine) JacobianPointJSON { + if p.IsInfinity() { + return JacobianPointJSON{XBytesLE: zeroHex(), YBytesLE: zeroHex(), ZBytesLE: zeroHex()} + } + return JacobianPointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y), ZBytesLE: fpElementHex(montOne())} +} + +var _ fp.Element diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g2_msm_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g2_msm_vectors.go new file mode 100644 index 0000000000..f5f2de4e4d --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g2_msm_vectors.go @@ -0,0 +1,69 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" + fr "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" +) + +type G2MSMCaseJSON struct { + Name string `json:"name"` + BasesAffine []G2AffinePointJSON `json:"bases_affine"` + ScalarsBytesLE []string `json:"scalars_bytes_le"` + ExpectedAffine G2JacobianPointJSON `json:"expected_affine"` +} + +type G2MSMVectorsJSON struct { + TermsPerInstance int `json:"terms_per_instance"` + MSMCases []G2MSMCaseJSON `json:"msm_cases"` +} + +func BuildG2MSMVectors() G2MSMVectorsJSON { + _, _, _, genAff := curve.Generators() + infAff := new(curve.G2Affine).SetInfinity() + five := scalarMulG2Ops(5) + seventeen := scalarMulG2Ops(17) + oneTwentyThree := scalarMulG2Ops(123) + twoHundredEleven := scalarMulG2Ops(211) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + cases := []struct { + name string + bases []*curve.G2Affine + scalars []fr.Element + }{ + {name: "single_generator", bases: []*curve.G2Affine{&genAff, infAff, infAff, infAff}, scalars: []fr.Element{newScalarUint64(1), newScalarUint64(0), newScalarUint64(0), newScalarUint64(0)}}, + {name: "simple_linear_combo", bases: []*curve.G2Affine{&genAff, five, seventeen, oneTwentyThree}, scalars: []fr.Element{newScalarUint64(3), newScalarUint64(4), newScalarUint64(5), newScalarUint64(6)}}, + {name: "includes_infinity_and_zero_scalar", bases: []*curve.G2Affine{infAff, five, infAff, seventeen}, scalars: []fr.Element{newScalarUint64(19), newScalarUint64(0), newScalarUint64(7), newScalarUint64(9)}}, + {name: "q_minus_one_mix", bases: []*curve.G2Affine{&genAff, five, twoHundredEleven, oneTwentyThree}, scalars: []fr.Element{newScalarBig(modMinusOne), newScalarUint64(2), newScalarUint64(0), newScalarUint64(13)}}, + } + out := G2MSMVectorsJSON{TermsPerInstance: 4, MSMCases: make([]G2MSMCaseJSON, len(cases))} + for i, tc := range cases { + expected := naiveG2MSM(tc.bases, tc.scalars) + out.MSMCases[i] = G2MSMCaseJSON{Name: tc.name, BasesAffine: encodeG2AffineBatch(tc.bases), ScalarsBytesLE: encodeScalarBatch(tc.scalars), ExpectedAffine: g2AffineOutputToJSON(expected)} + } + return out +} + +func encodeG2AffineBatch(in []*curve.G2Affine) []G2AffinePointJSON { + out := make([]G2AffinePointJSON, len(in)) + for i, point := range in { + out[i] = g2AffineToJSON(point) + } + return out +} + +func naiveG2MSM(bases []*curve.G2Affine, scalars []fr.Element) *curve.G2Affine { + sum := new(curve.G2Affine).SetInfinity() + for i := range bases { + var term curve.G2Affine + term.ScalarMultiplication(bases[i], scalarToBig(scalars[i])) + sum.Add(sum, &term) + } + return sum +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g2_ops_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g2_ops_vectors.go new file mode 100644 index 0000000000..3abf5b8009 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/g2_ops_vectors.go @@ -0,0 +1,119 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" + fp "github.com/consensys/gnark-crypto/ecc/bls12-377/fp" +) + +type Fp2PointJSON struct { + C0BytesLE string `json:"c0_bytes_le"` + C1BytesLE string `json:"c1_bytes_le"` +} + +type G2AffinePointJSON struct { + X Fp2PointJSON `json:"x"` + Y Fp2PointJSON `json:"y"` +} + +type G2JacobianPointJSON struct { + X Fp2PointJSON `json:"x"` + Y Fp2PointJSON `json:"y"` + Z Fp2PointJSON `json:"z"` +} + +type G2OpsCaseJSON struct { + Name string `json:"name"` + PAffine G2AffinePointJSON `json:"p_affine"` + QAffine G2AffinePointJSON `json:"q_affine"` + PJacobian G2JacobianPointJSON `json:"p_jacobian"` + PAffineOutput G2JacobianPointJSON `json:"p_affine_output"` + NegPJacobian G2JacobianPointJSON `json:"neg_p_jacobian"` + DoublePJacobian G2JacobianPointJSON `json:"double_p_jacobian"` + AddMixedPPlusQJacob G2JacobianPointJSON `json:"add_mixed_p_plus_q_jacobian"` + AffineAddPPlusQ G2JacobianPointJSON `json:"affine_add_p_plus_q"` +} + +type G2OpsVectorsJSON struct { + PointCases []G2OpsCaseJSON `json:"point_cases"` +} + +func BuildG2OpsVectors() G2OpsVectorsJSON { + _, _, _, genAff := curve.Generators() + infAff := new(curve.G2Affine).SetInfinity() + negGen := new(curve.G2Affine).Neg(&genAff) + five := scalarMulG2Ops(5) + seventeen := scalarMulG2Ops(17) + oneTwentyThree := scalarMulG2Ops(123) + cases := []struct { + name string + p *curve.G2Affine + q *curve.G2Affine + }{ + {name: "inf_plus_gen", p: infAff, q: &genAff}, + {name: "gen_plus_inf", p: &genAff, q: infAff}, + {name: "gen_plus_neg_gen", p: &genAff, q: negGen}, + {name: "gen_plus_gen", p: &genAff, q: &genAff}, + {name: "five_plus_seventeen", p: five, q: seventeen}, + {name: "one_twenty_three_self", p: oneTwentyThree, q: oneTwentyThree}, + } + out := G2OpsVectorsJSON{PointCases: make([]G2OpsCaseJSON, len(cases))} + for i, tc := range cases { + var pJac curve.G2Jac + pJac.FromAffine(tc.p) + var negP curve.G2Jac + negP.Neg(&pJac) + var doubleP curve.G2Jac + doubleP.Double(&pJac) + var addMixed curve.G2Jac + addMixed.Set(&pJac).AddMixed(tc.q) + var pAffineOut curve.G2Affine + pAffineOut.FromJacobian(&pJac) + var affineAdd curve.G2Affine + affineAdd.Add(tc.p, tc.q) + out.PointCases[i] = G2OpsCaseJSON{Name: tc.name, PAffine: g2AffineToJSON(tc.p), QAffine: g2AffineToJSON(tc.q), PJacobian: g2JacToJSON(&pJac), PAffineOutput: g2AffineOutputToJSON(&pAffineOut), NegPJacobian: g2JacToJSON(&negP), DoublePJacobian: g2JacToJSON(&doubleP), AddMixedPPlusQJacob: g2JacToJSON(&addMixed), AffineAddPPlusQ: g2AffineOutputToJSON(&affineAdd)} + } + return out +} + +func scalarMulG2Ops(v uint64) *curve.G2Affine { + _, _, _, genAff := curve.Generators() + var out curve.G2Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} + +func g2AffineToJSON(p *curve.G2Affine) G2AffinePointJSON { + return G2AffinePointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1)} +} + +func g2JacToJSON(p *curve.G2Jac) G2JacobianPointJSON { + return G2JacobianPointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1), Z: fp2ToJSON(p.Z.A0, p.Z.A1)} +} + +func g2AffineOutputToJSON(p *curve.G2Affine) G2JacobianPointJSON { + if p.IsInfinity() { + return G2JacobianPointJSON{X: zeroFp2JSON(), Y: zeroFp2JSON(), Z: zeroFp2JSON()} + } + return G2JacobianPointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1), Z: oneFp2JSON()} +} + +func fp2ToJSON(c0, c1 fp.Element) Fp2PointJSON { + return Fp2PointJSON{C0BytesLE: fpElementHex(c0), C1BytesLE: fpElementHex(c1)} +} + +func zeroFp2JSON() Fp2PointJSON { + var z fp.Element + return fp2ToJSON(z, z) +} + +func oneFp2JSON() Fp2PointJSON { + var zero fp.Element + return fp2ToJSON(montOne(), zero) +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/helpers.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/helpers.go new file mode 100644 index 0000000000..2a6d3021ba --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/helpers.go @@ -0,0 +1,167 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import ( + "encoding/binary" + "encoding/hex" + "math/big" + + fp "github.com/consensys/gnark-crypto/ecc/bls12-377/fp" + fr "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" +) + +func frToMont(regular *big.Int) fr.Element { + var z fr.Element + z.SetBigInt(regular) + return z +} + +func fpToMont(regular *big.Int) fp.Element { + var z fp.Element + z.SetBigInt(regular) + return z +} + +func frZeroMont() fr.Element { + var z fr.Element + z.SetZero() + return z +} + +func fpZeroMont() fp.Element { + var z fp.Element + z.SetZero() + return z +} + +func montOne() fp.Element { + var one fp.Element + one.SetOne() + return one +} + +func frElementHex(z fr.Element) string { + data := make([]byte, 32) + for i := 0; i < len(z); i++ { + binary.LittleEndian.PutUint64(data[i*8:], z[i]) + } + return hex.EncodeToString(data) +} + +func fpElementHex(z fp.Element) string { + return hex.EncodeToString(wordsToBytes(z[:], 48)) +} + +func scalarToHex(v fr.Element) string { + bytesBE := v.Bytes() + return hex.EncodeToString(regularToLittleEndian(bytesBE[:])) +} + +func scalarToBig(v fr.Element) *big.Int { + var out big.Int + return v.BigInt(&out) +} + +func newScalarUint64(v uint64) fr.Element { + var out fr.Element + out.SetUint64(v) + return out +} + +func newScalarBig(v *big.Int) fr.Element { + var out fr.Element + out.SetBigInt(v) + return out +} + +func regularHex(v *big.Int, size int) string { + return hex.EncodeToString(regularToLittleEndian(v.FillBytes(make([]byte, size)))) +} + +func regularUint64(v uint64) *big.Int { + return new(big.Int).SetUint64(v) +} + +func addBig(a, b *big.Int) *big.Int { return new(big.Int).Add(a, b) } +func subBig(a, b *big.Int) *big.Int { return new(big.Int).Sub(a, b) } +func mulBig(a, b *big.Int) *big.Int { return new(big.Int).Mul(a, b) } + +func pow2MinusOne(bitsN uint) *big.Int { + return subBig(new(big.Int).Lsh(big.NewInt(1), bitsN), big.NewInt(1)) +} + +func frModulus() *big.Int { + return new(big.Int).Set(fr.Modulus()) +} + +func fpModulus() *big.Int { + return new(big.Int).Set(fp.Modulus()) +} + +func frQMinusOne() *big.Int { return new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) } +func fpQMinusOne() *big.Int { return new(big.Int).Sub(fp.Modulus(), big.NewInt(1)) } + +func frQMinus(delta uint64) *big.Int { + return new(big.Int).Sub(fr.Modulus(), new(big.Int).SetUint64(delta)) +} + +func fpQMinus(delta uint64) *big.Int { + return new(big.Int).Sub(fp.Modulus(), new(big.Int).SetUint64(delta)) +} + +func frFloorHalfModulus() *big.Int { return new(big.Int).Rsh(frQMinusOne(), 1) } +func frCeilHalfModulus() *big.Int { return new(big.Int).Sub(fr.Modulus(), frFloorHalfModulus()) } +func fpFloorHalfModulus() *big.Int { return new(big.Int).Rsh(fpQMinusOne(), 1) } +func fpCeilHalfModulus() *big.Int { return new(big.Int).Sub(fp.Modulus(), fpFloorHalfModulus()) } + +func randomFRFieldBigInt(rng interface{ Uint32() uint32 }) *big.Int { + buf := make([]byte, 48) + for i := range buf { + buf[i] = byte(rng.Uint32()) + } + return new(big.Int).Mod(new(big.Int).SetBytes(buf), fr.Modulus()) +} + +func randomFPFieldBigInt(rng interface{ Uint32() uint32 }) *big.Int { + buf := make([]byte, 64) + for i := range buf { + buf[i] = byte(rng.Uint32()) + } + return new(big.Int).Mod(new(big.Int).SetBytes(buf), fp.Modulus()) +} + +func wordsToBytes(words []uint64, size int) []byte { + out := make([]byte, size) + for i, word := range words { + base := i * 8 + out[base+0] = byte(word) + out[base+1] = byte(word >> 8) + out[base+2] = byte(word >> 16) + out[base+3] = byte(word >> 24) + out[base+4] = byte(word >> 32) + out[base+5] = byte(word >> 40) + out[base+6] = byte(word >> 48) + out[base+7] = byte(word >> 56) + } + return out +} + +func writeElementLE(dst []byte, v fp.Element) { + copy(dst, wordsToBytes(v[:], 48)) +} + +func regularToLittleEndian(in []byte) []byte { + out := make([]byte, len(in)) + for i := range in { + out[len(in)-1-i] = in[i] + } + return out +} + +func zeroHex() string { + return hex.EncodeToString(make([]byte, 48)) +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/ntt_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/ntt_vectors.go new file mode 100644 index 0000000000..a21080c1c7 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/ntt_vectors.go @@ -0,0 +1,111 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import ( + "math/big" + "math/bits" + "math/rand" + + fr "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + fft "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/fft" + "github.com/consensys/gnark-crypto/utils" +) + +type NTTDomainFileJSON struct { + Domains []NTTDomainJSON `json:"domains"` +} + +type NTTDomainJSON struct { + LogN int `json:"log_n"` + Size int `json:"size"` + OmegaHex string `json:"omega_hex"` + OmegaInvHex string `json:"omega_inv_hex"` + CardinalityInvHex string `json:"cardinality_inv_hex"` + CosetGenHex string `json:"coset_gen_hex"` + CosetGenInvHex string `json:"coset_gen_inv_hex"` + CosetDenInvHex string `json:"coset_den_inv_hex"` +} + +type NTTVectorsJSON struct { + NTTCases []NTTCaseJSON `json:"ntt_cases"` +} + +type NTTCaseJSON struct { + Name string `json:"name"` + Size int `json:"size"` + InputMontLE []string `json:"input_mont_le"` + ForwardExpectedLE []string `json:"forward_expected_le"` + InverseExpectedLE []string `json:"inverse_expected_le"` + StageTwiddlesLE [][]string `json:"stage_twiddles_le"` + InverseStageTwiddlesLE [][]string `json:"inverse_stage_twiddles_le"` + InverseScaleLE string `json:"inverse_scale_le"` +} + +func BuildNTTDomainFile(minLog, maxLog int) NTTDomainFileJSON { + out := NTTDomainFileJSON{Domains: make([]NTTDomainJSON, 0, maxLog-minLog+1)} + for logN := minLog; logN <= maxLog; logN++ { + size := 1 << logN + domain := fft.NewDomain(uint64(size)) + var cosetDenInv, one fr.Element + one.SetOne() + cosetDenInv.Exp(domain.FrMultiplicativeGen, big.NewInt(int64(domain.Cardinality))) + cosetDenInv.Sub(&cosetDenInv, &one).Inverse(&cosetDenInv) + out.Domains = append(out.Domains, NTTDomainJSON{LogN: logN, Size: size, OmegaHex: domain.Generator.BigInt(new(big.Int)).Text(16), OmegaInvHex: domain.GeneratorInv.BigInt(new(big.Int)).Text(16), CardinalityInvHex: domain.CardinalityInv.BigInt(new(big.Int)).Text(16), CosetGenHex: domain.FrMultiplicativeGen.BigInt(new(big.Int)).Text(16), CosetGenInvHex: domain.FrMultiplicativeGenInv.BigInt(new(big.Int)).Text(16), CosetDenInvHex: cosetDenInv.BigInt(new(big.Int)).Text(16)}) + } + return out +} + +func BuildNTTVectors() NTTVectorsJSON { + return NTTVectorsJSON{NTTCases: []NTTCaseJSON{buildNTTCase("n8_random", 8, rand.New(rand.NewSource(2026040603))), buildNTTCase("n16_random", 16, rand.New(rand.NewSource(2026040604)))}} +} + +func buildNTTCase(name string, size int, rng *rand.Rand) NTTCaseJSON { + domain := fft.NewDomain(uint64(size)) + twiddles, err := domain.Twiddles() + if err != nil { + panic(err) + } + twiddlesInv, err := domain.TwiddlesInv() + if err != nil { + panic(err) + } + input := make([]fr.Element, size) + for i := range input { + input[i].SetBigInt(randomFRFieldBigInt(rng)) + } + forward := make([]fr.Element, size) + copy(forward, input) + utils.BitReverse(forward) + domain.FFT(forward, fft.DIT) + inverse := make([]fr.Element, size) + copy(inverse, forward) + utils.BitReverse(inverse) + domain.FFTInverse(inverse, fft.DIT) + logN := bits.Len(uint(size)) - 1 + stageTwiddles := make([][]string, logN) + inverseStageTwiddles := make([][]string, logN) + for stage := 1; stage <= logN; stage++ { + m := 1 << (stage - 1) + src := twiddles[logN-stage] + srcInv := twiddlesInv[logN-stage] + stageTwiddles[stage-1] = make([]string, m) + inverseStageTwiddles[stage-1] = make([]string, m) + for i := 0; i < m; i++ { + stageTwiddles[stage-1][i] = frElementHex(src[i]) + inverseStageTwiddles[stage-1][i] = frElementHex(srcInv[i]) + } + } + return NTTCaseJSON{Name: name, Size: size, InputMontLE: encodeFRBatch(input), ForwardExpectedLE: encodeFRBatch(forward), InverseExpectedLE: encodeFRBatch(inverse), StageTwiddlesLE: stageTwiddles, InverseStageTwiddlesLE: inverseStageTwiddles, InverseScaleLE: frElementHex(domain.CardinalityInv)} +} + +func encodeFRBatch(in []fr.Element) []string { + out := make([]string, len(in)) + for i, value := range in { + out[i] = frElementHex(value) + } + return out +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/testdata.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/testdata.go new file mode 100644 index 0000000000..87ee6637a6 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-377/testdata.go @@ -0,0 +1,25 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12377 + +import "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen" + +const CurveKey = "bls12_377" + +func JSONTargets(nttMaxLog int) []testgen.JSONTarget { + return []testgen.JSONTarget{ + {Path: "vectors/fr/bls12_377_fr_ops.json", Build: func() any { return BuildFROpsVectors() }}, + {Path: "vectors/fr/bls12_377_fr_vector_ops.json", Build: func() any { return BuildFRVectorOps() }}, + {Path: "vectors/fr/bls12_377_ntt_domains.json", Build: func() any { return BuildNTTDomainFile(3, nttMaxLog) }}, + {Path: "vectors/fr/bls12_377_fr_ntt.json", Build: func() any { return BuildNTTVectors() }}, + {Path: "vectors/fp/bls12_377_fp_ops.json", Build: func() any { return BuildFPOpsVectors() }}, + {Path: "vectors/g1/bls12_377_g1_ops.json", Build: func() any { return BuildG1OpsVectors() }}, + {Path: "vectors/g1/bls12_377_g1_scalar_mul.json", Build: func() any { return BuildG1ScalarVectors() }}, + {Path: "vectors/g1/bls12_377_g1_msm.json", Build: func() any { return BuildG1MSMVectors() }}, + {Path: "vectors/g2/bls12_377_g2_ops.json", Build: func() any { return BuildG2OpsVectors() }}, + {Path: "vectors/g2/bls12_377_g2_msm.json", Build: func() any { return BuildG2MSMVectors() }}, + } +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/field_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/field_vectors.go new file mode 100644 index 0000000000..117d7daef2 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/field_vectors.go @@ -0,0 +1,266 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import ( + "fmt" + "math/big" + "math/bits" + "math/rand" + + fp "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" +) + +type FieldElementCaseJSON struct { + Name string `json:"name"` + ABytesLE string `json:"a_bytes_le"` + BBytesLE string `json:"b_bytes_le"` + EqualBytesLE string `json:"equal_bytes_le"` + AddBytesLE string `json:"add_bytes_le"` + SubBytesLE string `json:"sub_bytes_le"` + NegABytesLE string `json:"neg_a_bytes_le"` + DoubleABytesLE string `json:"double_a_bytes_le"` + MulBytesLE string `json:"mul_bytes_le"` + SquareABytesLE string `json:"square_a_bytes_le"` +} + +type NormalizeCaseJSON struct { + Name string `json:"name"` + InputBytesLE string `json:"input_bytes_le"` + ExpectedBytesLE string `json:"expected_bytes_le"` +} + +type ConvertCaseJSON struct { + Name string `json:"name"` + RegularBytes string `json:"regular_bytes_le"` + MontBytes string `json:"mont_bytes_le"` +} + +type FieldOpsVectorsJSON struct { + ElementCases []FieldElementCaseJSON `json:"element_cases"` + EdgeCases []FieldElementCaseJSON `json:"edge_cases"` + DifferentialCases []FieldElementCaseJSON `json:"differential_cases"` + NormalizeCases []NormalizeCaseJSON `json:"normalize_cases"` + ConvertCases []ConvertCaseJSON `json:"convert_cases"` +} + +type VectorCaseJSON struct { + Name string `json:"name"` + RegularInputs []string `json:"regular_inputs_le"` + MontInputs []string `json:"mont_inputs_le"` + MontFactors []string `json:"mont_factors_le"` + AddExpected []string `json:"add_expected_le"` + SubExpected []string `json:"sub_expected_le"` + MulExpected []string `json:"mul_expected_le"` + ToMontExpected []string `json:"to_mont_expected_le"` + FromMontExpected []string `json:"from_mont_expected_le"` + BitReverseExpected []string `json:"bit_reverse_expected_le"` +} + +type VectorOpsJSON struct { + VectorCases []VectorCaseJSON `json:"vector_cases"` +} + +func BuildFROpsVectors() FieldOpsVectorsJSON { + rng := rand.New(rand.NewSource(20260404)) + return buildFieldOpsVectors( + buildFRElementCase, + buildFRConvertCase, + buildFRDifferentialCases(rng, 32), + frRegularHex, + frModulus, + frQMinusOne, + frQMinus, + frFloorHalfModulus, + frCeilHalfModulus, + ) +} + +func BuildFPOpsVectors() FieldOpsVectorsJSON { + rng := rand.New(rand.NewSource(20260405)) + return buildFieldOpsVectors( + buildFPElementCase, + buildFPConvertCase, + buildFPDifferentialCases(rng, 32), + fpRegularHex, + fpModulus, + fpQMinusOne, + fpQMinus, + fpFloorHalfModulus, + fpCeilHalfModulus, + ) +} + +func buildFieldOpsVectors( + buildElementCase func(string, *big.Int, *big.Int) FieldElementCaseJSON, + buildConvertCase func(string, *big.Int) ConvertCaseJSON, + differentialCases []FieldElementCaseJSON, + regularHex func(*big.Int) string, + modulus func() *big.Int, + qMinusOne func() *big.Int, + qMinus func(uint64) *big.Int, + floorHalf func() *big.Int, + ceilHalf func() *big.Int, +) FieldOpsVectorsJSON { + return FieldOpsVectorsJSON{ + ElementCases: []FieldElementCaseJSON{ + buildElementCase("zero_zero", regularUint64(0), regularUint64(0)), + buildElementCase("zero_one", regularUint64(0), regularUint64(1)), + buildElementCase("one_one", regularUint64(1), regularUint64(1)), + buildElementCase("two_five", regularUint64(2), regularUint64(5)), + buildElementCase("neg_one_one", qMinusOne(), regularUint64(1)), + buildElementCase("seven_five", regularUint64(7), regularUint64(5)), + }, + EdgeCases: []FieldElementCaseJSON{ + buildElementCase("carry32_plus_one", pow2MinusOne(32), regularUint64(1)), + buildElementCase("carry64_plus_one", pow2MinusOne(64), regularUint64(1)), + buildElementCase("carry128_plus_one", pow2MinusOne(128), regularUint64(1)), + buildElementCase("carry192_plus_one", pow2MinusOne(192), regularUint64(1)), + buildElementCase("q_minus_two_plus_three", qMinus(2), regularUint64(3)), + buildElementCase("q_minus_one_q_minus_one", qMinusOne(), qMinusOne()), + buildElementCase("q_minus_two_q_minus_one", qMinus(2), qMinusOne()), + buildElementCase("half_q_floor_half_q_ceil", floorHalf(), ceilHalf()), + }, + DifferentialCases: differentialCases, + NormalizeCases: []NormalizeCaseJSON{ + {Name: "zero", InputBytesLE: regularHex(regularUint64(0)), ExpectedBytesLE: regularHex(regularUint64(0))}, + {Name: "one", InputBytesLE: regularHex(regularUint64(1)), ExpectedBytesLE: regularHex(regularUint64(1))}, + {Name: "q_minus_one", InputBytesLE: regularHex(qMinusOne()), ExpectedBytesLE: regularHex(qMinusOne())}, + {Name: "q", InputBytesLE: regularHex(modulus()), ExpectedBytesLE: regularHex(regularUint64(0))}, + {Name: "q_plus_one", InputBytesLE: regularHex(addBig(modulus(), regularUint64(1))), ExpectedBytesLE: regularHex(regularUint64(1))}, + {Name: "two_q_minus_one", InputBytesLE: regularHex(subBig(mulBig(modulus(), regularUint64(2)), regularUint64(1))), ExpectedBytesLE: regularHex(qMinusOne())}, + }, + ConvertCases: []ConvertCaseJSON{ + buildConvertCase("zero", regularUint64(0)), + buildConvertCase("one", regularUint64(1)), + buildConvertCase("two", regularUint64(2)), + buildConvertCase("five", regularUint64(5)), + buildConvertCase("seven", regularUint64(7)), + buildConvertCase("q_minus_one", qMinusOne()), + }, + } +} + +func BuildFRVectorOps() VectorOpsJSON { + return VectorOpsJSON{ + VectorCases: []VectorCaseJSON{ + buildFRVectorCase("n8_random", 8, rand.New(rand.NewSource(2026040401))), + buildFRVectorCase("n16_random", 16, rand.New(rand.NewSource(2026040402))), + }, + } +} + +func buildFRVectorCase(name string, size int, rng *rand.Rand) VectorCaseJSON { + out := VectorCaseJSON{ + Name: name, + RegularInputs: make([]string, size), + MontInputs: make([]string, size), + MontFactors: make([]string, size), + AddExpected: make([]string, size), + SubExpected: make([]string, size), + MulExpected: make([]string, size), + ToMontExpected: make([]string, size), + FromMontExpected: make([]string, size), + BitReverseExpected: make([]string, size), + } + for i := 0; i < size; i++ { + aRegular := randomFRFieldBigInt(rng) + bRegular := randomFRFieldBigInt(rng) + var aMont, bMont fr.Element + aMont.SetBigInt(aRegular) + bMont.SetBigInt(bRegular) + var add, sub, mul fr.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + mul.Mul(&aMont, &bMont) + out.RegularInputs[i] = frRegularHex(aRegular) + out.MontInputs[i] = frElementHex(aMont) + out.MontFactors[i] = frElementHex(bMont) + out.AddExpected[i] = frElementHex(add) + out.SubExpected[i] = frElementHex(sub) + out.MulExpected[i] = frElementHex(mul) + out.ToMontExpected[i] = frElementHex(aMont) + out.FromMontExpected[i] = frRegularHex(aRegular) + } + logCount := bits.Len(uint(size)) - 1 + for i := 0; i < size; i++ { + j := int(bits.Reverse64(uint64(i)) >> (64 - logCount)) + out.BitReverseExpected[i] = out.MontInputs[j] + } + return out +} + +func buildFRElementCase(name string, aRegular, bRegular *big.Int) FieldElementCaseJSON { + aMont := frToMont(aRegular) + bMont := frToMont(bRegular) + var add, sub, negA, dblA, mul, sqA fr.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + negA.Neg(&aMont) + dblA.Double(&aMont) + mul.Mul(&aMont, &bMont) + sqA.Square(&aMont) + equal := frZeroMont() + if aMont.Equal(&bMont) { + equal.SetUint64(1) + } + return FieldElementCaseJSON{Name: name, ABytesLE: frElementHex(aMont), BBytesLE: frElementHex(bMont), EqualBytesLE: frElementHex(equal), AddBytesLE: frElementHex(add), SubBytesLE: frElementHex(sub), NegABytesLE: frElementHex(negA), DoubleABytesLE: frElementHex(dblA), MulBytesLE: frElementHex(mul), SquareABytesLE: frElementHex(sqA)} +} + +func buildFPElementCase(name string, aRegular, bRegular *big.Int) FieldElementCaseJSON { + aMont := fpToMont(aRegular) + bMont := fpToMont(bRegular) + var add, sub, negA, dblA, mul, sqA fp.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + negA.Neg(&aMont) + dblA.Double(&aMont) + mul.Mul(&aMont, &bMont) + sqA.Square(&aMont) + equal := fpZeroMont() + if aMont.Equal(&bMont) { + equal.SetUint64(1) + } + return FieldElementCaseJSON{Name: name, ABytesLE: fpElementHex(aMont), BBytesLE: fpElementHex(bMont), EqualBytesLE: fpElementHex(equal), AddBytesLE: fpElementHex(add), SubBytesLE: fpElementHex(sub), NegABytesLE: fpElementHex(negA), DoubleABytesLE: fpElementHex(dblA), MulBytesLE: fpElementHex(mul), SquareABytesLE: fpElementHex(sqA)} +} + +func buildFRConvertCase(name string, regular *big.Int) ConvertCaseJSON { + return ConvertCaseJSON{Name: name, RegularBytes: frRegularHex(regular), MontBytes: frElementHex(frToMont(regular))} +} + +func buildFPConvertCase(name string, regular *big.Int) ConvertCaseJSON { + return ConvertCaseJSON{Name: name, RegularBytes: fpRegularHex(regular), MontBytes: fpElementHex(fpToMont(regular))} +} + +func buildFRDifferentialCases(rng *rand.Rand, count int) []FieldElementCaseJSON { + out := make([]FieldElementCaseJSON, count) + for i := 0; i < count; i++ { + a := randomFRFieldBigInt(rng) + b := randomFRFieldBigInt(rng) + if i%7 == 0 { + b = new(big.Int).Set(a) + } + out[i] = buildFRElementCase(fmt.Sprintf("random_%02d", i), a, b) + } + return out +} + +func buildFPDifferentialCases(rng *rand.Rand, count int) []FieldElementCaseJSON { + out := make([]FieldElementCaseJSON, count) + for i := 0; i < count; i++ { + a := randomFPFieldBigInt(rng) + b := randomFPFieldBigInt(rng) + if i%7 == 0 { + b = new(big.Int).Set(a) + } + out[i] = buildFPElementCase(fmt.Sprintf("random_%02d", i), a, b) + } + return out +} + +func frRegularHex(v *big.Int) string { return regularHex(v, 32) } +func fpRegularHex(v *big.Int) string { return regularHex(v, 48) } diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_bases.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_bases.go new file mode 100644 index 0000000000..cd82130d74 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_bases.go @@ -0,0 +1,98 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import ( + "encoding/json" + "math/rand" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" + fp "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen" +) + +func BuildRandomG1Bases(count int, seed int64) ([]byte, error) { + _, _, genAff, _ := curve.Generators() + oneMontZ := montOne() + rng := rand.New(rand.NewSource(seed)) + scalars := make([]fr.Element, count) + for i := range scalars { + var raw [32]byte + for j := range raw { + raw[j] = byte(rng.Uint32()) + } + scalars[i].SetBytes(raw[:]) + if scalars[i].IsZero() { + scalars[i].SetUint64(1) + } + } + points := curve.BatchScalarMultiplicationG1(&genAff, scalars) + out := make([]byte, count*144) + for i := range points { + base := i * 144 + writeElementLE(out[base:base+48], points[i].X) + writeElementLE(out[base+48:base+2*48], points[i].Y) + writeElementLE(out[base+2*48:base+3*48], oneMontZ) + } + return out, nil +} + +func BuildSequentialG1Bases(count int) ([]byte, error) { + _, _, genAff, _ := curve.Generators() + oneMontZ := montOne() + scalars := make([]fr.Element, count) + for i := range scalars { + scalars[i].SetUint64(uint64(i + 1)) + } + points := curve.BatchScalarMultiplicationG1(&genAff, scalars) + out := make([]byte, count*144) + for i := range points { + base := i * 144 + writeElementLE(out[base:base+48], points[i].X) + writeElementLE(out[base+48:base+2*48], points[i].Y) + writeElementLE(out[base+2*48:base+3*48], oneMontZ) + } + return out, nil +} + +func BuildSequentialG2Bases(count int) ([]byte, error) { + _, _, _, genAff := curve.Generators() + oneMontZ := montOne() + zero := fp.Element{} + scalars := make([]fr.Element, count) + for i := range scalars { + scalars[i].SetUint64(uint64(i + 1)) + } + points := curve.BatchScalarMultiplicationG2(&genAff, scalars) + out := make([]byte, count*288) + for i := range points { + base := i * 288 + writeElementLE(out[base:base+48], points[i].X.A0) + writeElementLE(out[base+48:base+2*48], points[i].X.A1) + writeElementLE(out[base+2*48:base+3*48], points[i].Y.A0) + writeElementLE(out[base+3*48:base+4*48], points[i].Y.A1) + writeElementLE(out[base+4*48:base+5*48], oneMontZ) + writeElementLE(out[base+5*48:base+6*48], zero) + } + return out, nil +} + +func BuildG1BaseFixtureMetadata(count int) testgen.BaseFixtureMetadata { + return testgen.BaseFixtureMetadata{Count: count, PointBytes: 144, Format: "jacobian_x_y_z_le"} +} + +func BuildG2BaseFixtureMetadata(count int) testgen.BaseFixtureMetadata { + return testgen.BaseFixtureMetadata{Count: count, PointBytes: 288, Format: "jacobian_x_y_z_le"} +} + +func MarshalMetadataJSON(meta testgen.BaseFixtureMetadata) ([]byte, error) { + data, err := json.MarshalIndent(meta, "", " ") + if err != nil { + return nil, err + } + return append(data, '\n'), nil +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_msm_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_msm_vectors.go new file mode 100644 index 0000000000..c78946f04f --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_msm_vectors.go @@ -0,0 +1,85 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" + fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" +) + +type G1MSMCaseJSON struct { + Name string `json:"name"` + BasesAffine []AffinePointJSON `json:"bases_affine"` + ScalarsBytesLE []string `json:"scalars_bytes_le"` + ExpectedAffine JacobianPointJSON `json:"expected_affine"` +} + +type G1MSMVectorsJSON struct { + TermsPerInstance int `json:"terms_per_instance"` + MSMCases []G1MSMCaseJSON `json:"msm_cases"` + OneMontZ string `json:"one_mont_z"` +} + +func BuildG1MSMVectors() G1MSMVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + five := scalarMulG1MSM(5) + seventeen := scalarMulG1MSM(17) + oneTwentyThree := scalarMulG1MSM(123) + twoHundredEleven := scalarMulG1MSM(211) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + cases := []struct { + name string + bases []*curve.G1Affine + scalars []fr.Element + }{ + {name: "single_generator", bases: []*curve.G1Affine{&genAff, infAff, infAff, infAff}, scalars: []fr.Element{newScalarUint64(1), newScalarUint64(0), newScalarUint64(0), newScalarUint64(0)}}, + {name: "simple_linear_combo", bases: []*curve.G1Affine{&genAff, five, seventeen, oneTwentyThree}, scalars: []fr.Element{newScalarUint64(3), newScalarUint64(4), newScalarUint64(5), newScalarUint64(6)}}, + {name: "includes_infinity_and_zero_scalar", bases: []*curve.G1Affine{infAff, five, infAff, seventeen}, scalars: []fr.Element{newScalarUint64(19), newScalarUint64(0), newScalarUint64(7), newScalarUint64(9)}}, + {name: "q_minus_one_mix", bases: []*curve.G1Affine{&genAff, five, twoHundredEleven, oneTwentyThree}, scalars: []fr.Element{newScalarBig(modMinusOne), newScalarUint64(2), newScalarUint64(0), newScalarUint64(13)}}, + } + out := G1MSMVectorsJSON{TermsPerInstance: 4, MSMCases: make([]G1MSMCaseJSON, len(cases)), OneMontZ: fpElementHex(montOne())} + for i, tc := range cases { + expected := naiveG1MSM(tc.bases, tc.scalars) + out.MSMCases[i] = G1MSMCaseJSON{Name: tc.name, BasesAffine: encodeAffineBatch(tc.bases), ScalarsBytesLE: encodeScalarBatch(tc.scalars), ExpectedAffine: affineOutputToJSON(expected)} + } + return out +} + +func scalarMulG1MSM(v uint64) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} + +func encodeAffineBatch(in []*curve.G1Affine) []AffinePointJSON { + out := make([]AffinePointJSON, len(in)) + for i, point := range in { + out[i] = affineToJSON(point) + } + return out +} + +func encodeScalarBatch(in []fr.Element) []string { + out := make([]string, len(in)) + for i, scalar := range in { + out[i] = scalarToHex(scalar) + } + return out +} + +func naiveG1MSM(bases []*curve.G1Affine, scalars []fr.Element) *curve.G1Affine { + sum := new(curve.G1Affine).SetInfinity() + for i := range bases { + var term curve.G1Affine + term.ScalarMultiplication(bases[i], scalarToBig(scalars[i])) + sum.Add(sum, &term) + } + return sum +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_ops_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_ops_vectors.go new file mode 100644 index 0000000000..574d821d31 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_ops_vectors.go @@ -0,0 +1,73 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" +) + +type G1OpsCaseJSON struct { + Name string `json:"name"` + PAffine AffinePointJSON `json:"p_affine"` + QAffine AffinePointJSON `json:"q_affine"` + PJacobian JacobianPointJSON `json:"p_jacobian"` + PAffineOutput JacobianPointJSON `json:"p_affine_output"` + NegPJacobian JacobianPointJSON `json:"neg_p_jacobian"` + DoublePJacobian JacobianPointJSON `json:"double_p_jacobian"` + AddMixedPPlusQJacob JacobianPointJSON `json:"add_mixed_p_plus_q_jacobian"` + AffineAddPPlusQ JacobianPointJSON `json:"affine_add_p_plus_q"` +} + +type G1OpsVectorsJSON struct { + PointCases []G1OpsCaseJSON `json:"point_cases"` +} + +func BuildG1OpsVectors() G1OpsVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + negGen := new(curve.G1Affine).Neg(&genAff) + five := scalarMulG1Ops(5) + seventeen := scalarMulG1Ops(17) + oneTwentyThree := scalarMulG1Ops(123) + cases := []struct { + name string + p *curve.G1Affine + q *curve.G1Affine + }{ + {name: "inf_plus_gen", p: infAff, q: &genAff}, + {name: "gen_plus_inf", p: &genAff, q: infAff}, + {name: "gen_plus_neg_gen", p: &genAff, q: negGen}, + {name: "gen_plus_gen", p: &genAff, q: &genAff}, + {name: "five_plus_seventeen", p: five, q: seventeen}, + {name: "one_twenty_three_self", p: oneTwentyThree, q: oneTwentyThree}, + } + out := G1OpsVectorsJSON{PointCases: make([]G1OpsCaseJSON, len(cases))} + for i, tc := range cases { + var pJac curve.G1Jac + pJac.FromAffine(tc.p) + var negP curve.G1Jac + negP.Neg(&pJac) + var doubleP curve.G1Jac + doubleP.Double(&pJac) + var addMixed curve.G1Jac + addMixed.Set(&pJac).AddMixed(tc.q) + var pAffineOut curve.G1Affine + pAffineOut.FromJacobian(&pJac) + var affineAdd curve.G1Affine + affineAdd.Add(tc.p, tc.q) + out.PointCases[i] = G1OpsCaseJSON{Name: tc.name, PAffine: affineToJSON(tc.p), QAffine: affineToJSON(tc.q), PJacobian: jacToJSON(&pJac), PAffineOutput: affineOutputToJSON(&pAffineOut), NegPJacobian: jacToJSON(&negP), DoublePJacobian: jacToJSON(&doubleP), AddMixedPPlusQJacob: jacToJSON(&addMixed), AffineAddPPlusQ: affineOutputToJSON(&affineAdd)} + } + return out +} + +func scalarMulG1Ops(v uint64) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_scalar_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_scalar_vectors.go new file mode 100644 index 0000000000..354d4aa37e --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g1_scalar_vectors.go @@ -0,0 +1,117 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" + fp "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" +) + +type AffinePointJSON struct { + XBytesLE string `json:"x_bytes_le"` + YBytesLE string `json:"y_bytes_le"` +} + +type JacobianPointJSON struct { + XBytesLE string `json:"x_bytes_le"` + YBytesLE string `json:"y_bytes_le"` + ZBytesLE string `json:"z_bytes_le"` +} + +type G1ScalarMulCaseJSON struct { + Name string `json:"name"` + BaseAffine AffinePointJSON `json:"base_affine"` + ScalarBytesLE string `json:"scalar_bytes_le"` + ScalarMulAffine JacobianPointJSON `json:"scalar_mul_affine"` +} + +type G1ScalarMulBaseCaseJSON struct { + Name string `json:"name"` + ScalarBytesLE string `json:"scalar_bytes_le"` + ScalarMulBaseAffine JacobianPointJSON `json:"scalar_mul_base_affine"` +} + +type G1ScalarMulVectorsJSON struct { + GeneratorAffine AffinePointJSON `json:"generator_affine"` + OneMontZ string `json:"one_mont_z"` + ScalarCases []G1ScalarMulCaseJSON `json:"scalar_cases"` + BaseCases []G1ScalarMulBaseCaseJSON `json:"base_cases"` +} + +func BuildG1ScalarVectors() G1ScalarMulVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + fiveGen := scalarMulG1Generator(newScalarUint64(5)) + oneTwentyThreeGen := scalarMulG1Generator(newScalarUint64(123)) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + scalarCases := []struct { + name string + base *curve.G1Affine + scalar fr.Element + }{ + {name: "gen_times_zero", base: &genAff, scalar: newScalarUint64(0)}, + {name: "gen_times_one", base: &genAff, scalar: newScalarUint64(1)}, + {name: "gen_times_two", base: &genAff, scalar: newScalarUint64(2)}, + {name: "five_gen_times_seventeen", base: fiveGen, scalar: newScalarUint64(17)}, + {name: "infinity_times_one_twenty_three", base: infAff, scalar: newScalarUint64(123)}, + {name: "one_twenty_three_times_forty_two", base: oneTwentyThreeGen, scalar: newScalarUint64(42)}, + {name: "gen_times_q_minus_one", base: &genAff, scalar: newScalarBig(modMinusOne)}, + } + baseCases := []struct { + name string + scalar fr.Element + }{ + {name: "base_zero", scalar: newScalarUint64(0)}, + {name: "base_one", scalar: newScalarUint64(1)}, + {name: "base_two", scalar: newScalarUint64(2)}, + {name: "base_one_twenty_three", scalar: newScalarUint64(123)}, + {name: "base_q_minus_one", scalar: newScalarBig(modMinusOne)}, + } + out := G1ScalarMulVectorsJSON{ + GeneratorAffine: affineToJSON(&genAff), + OneMontZ: fpElementHex(montOne()), + ScalarCases: make([]G1ScalarMulCaseJSON, len(scalarCases)), + BaseCases: make([]G1ScalarMulBaseCaseJSON, len(baseCases)), + } + for i, tc := range scalarCases { + var expected curve.G1Affine + expected.ScalarMultiplication(tc.base, scalarToBig(tc.scalar)) + out.ScalarCases[i] = G1ScalarMulCaseJSON{Name: tc.name, BaseAffine: affineToJSON(tc.base), ScalarBytesLE: scalarToHex(tc.scalar), ScalarMulAffine: affineOutputToJSON(&expected)} + } + for i, tc := range baseCases { + var expected curve.G1Affine + expected.ScalarMultiplicationBase(scalarToBig(tc.scalar)) + out.BaseCases[i] = G1ScalarMulBaseCaseJSON{Name: tc.name, ScalarBytesLE: scalarToHex(tc.scalar), ScalarMulBaseAffine: affineOutputToJSON(&expected)} + } + return out +} + +func scalarMulG1Generator(s fr.Element) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, scalarToBig(s)) + return &out +} + +func affineToJSON(p *curve.G1Affine) AffinePointJSON { + return AffinePointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y)} +} + +func jacToJSON(p *curve.G1Jac) JacobianPointJSON { + return JacobianPointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y), ZBytesLE: fpElementHex(p.Z)} +} + +func affineOutputToJSON(p *curve.G1Affine) JacobianPointJSON { + if p.IsInfinity() { + return JacobianPointJSON{XBytesLE: zeroHex(), YBytesLE: zeroHex(), ZBytesLE: zeroHex()} + } + return JacobianPointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y), ZBytesLE: fpElementHex(montOne())} +} + +var _ fp.Element diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g2_msm_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g2_msm_vectors.go new file mode 100644 index 0000000000..d9fbb6bf63 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g2_msm_vectors.go @@ -0,0 +1,69 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" + fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" +) + +type G2MSMCaseJSON struct { + Name string `json:"name"` + BasesAffine []G2AffinePointJSON `json:"bases_affine"` + ScalarsBytesLE []string `json:"scalars_bytes_le"` + ExpectedAffine G2JacobianPointJSON `json:"expected_affine"` +} + +type G2MSMVectorsJSON struct { + TermsPerInstance int `json:"terms_per_instance"` + MSMCases []G2MSMCaseJSON `json:"msm_cases"` +} + +func BuildG2MSMVectors() G2MSMVectorsJSON { + _, _, _, genAff := curve.Generators() + infAff := new(curve.G2Affine).SetInfinity() + five := scalarMulG2Ops(5) + seventeen := scalarMulG2Ops(17) + oneTwentyThree := scalarMulG2Ops(123) + twoHundredEleven := scalarMulG2Ops(211) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + cases := []struct { + name string + bases []*curve.G2Affine + scalars []fr.Element + }{ + {name: "single_generator", bases: []*curve.G2Affine{&genAff, infAff, infAff, infAff}, scalars: []fr.Element{newScalarUint64(1), newScalarUint64(0), newScalarUint64(0), newScalarUint64(0)}}, + {name: "simple_linear_combo", bases: []*curve.G2Affine{&genAff, five, seventeen, oneTwentyThree}, scalars: []fr.Element{newScalarUint64(3), newScalarUint64(4), newScalarUint64(5), newScalarUint64(6)}}, + {name: "includes_infinity_and_zero_scalar", bases: []*curve.G2Affine{infAff, five, infAff, seventeen}, scalars: []fr.Element{newScalarUint64(19), newScalarUint64(0), newScalarUint64(7), newScalarUint64(9)}}, + {name: "q_minus_one_mix", bases: []*curve.G2Affine{&genAff, five, twoHundredEleven, oneTwentyThree}, scalars: []fr.Element{newScalarBig(modMinusOne), newScalarUint64(2), newScalarUint64(0), newScalarUint64(13)}}, + } + out := G2MSMVectorsJSON{TermsPerInstance: 4, MSMCases: make([]G2MSMCaseJSON, len(cases))} + for i, tc := range cases { + expected := naiveG2MSM(tc.bases, tc.scalars) + out.MSMCases[i] = G2MSMCaseJSON{Name: tc.name, BasesAffine: encodeG2AffineBatch(tc.bases), ScalarsBytesLE: encodeScalarBatch(tc.scalars), ExpectedAffine: g2AffineOutputToJSON(expected)} + } + return out +} + +func encodeG2AffineBatch(in []*curve.G2Affine) []G2AffinePointJSON { + out := make([]G2AffinePointJSON, len(in)) + for i, point := range in { + out[i] = g2AffineToJSON(point) + } + return out +} + +func naiveG2MSM(bases []*curve.G2Affine, scalars []fr.Element) *curve.G2Affine { + sum := new(curve.G2Affine).SetInfinity() + for i := range bases { + var term curve.G2Affine + term.ScalarMultiplication(bases[i], scalarToBig(scalars[i])) + sum.Add(sum, &term) + } + return sum +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g2_ops_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g2_ops_vectors.go new file mode 100644 index 0000000000..c1da3b9248 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/g2_ops_vectors.go @@ -0,0 +1,119 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" + fp "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" +) + +type Fp2PointJSON struct { + C0BytesLE string `json:"c0_bytes_le"` + C1BytesLE string `json:"c1_bytes_le"` +} + +type G2AffinePointJSON struct { + X Fp2PointJSON `json:"x"` + Y Fp2PointJSON `json:"y"` +} + +type G2JacobianPointJSON struct { + X Fp2PointJSON `json:"x"` + Y Fp2PointJSON `json:"y"` + Z Fp2PointJSON `json:"z"` +} + +type G2OpsCaseJSON struct { + Name string `json:"name"` + PAffine G2AffinePointJSON `json:"p_affine"` + QAffine G2AffinePointJSON `json:"q_affine"` + PJacobian G2JacobianPointJSON `json:"p_jacobian"` + PAffineOutput G2JacobianPointJSON `json:"p_affine_output"` + NegPJacobian G2JacobianPointJSON `json:"neg_p_jacobian"` + DoublePJacobian G2JacobianPointJSON `json:"double_p_jacobian"` + AddMixedPPlusQJacob G2JacobianPointJSON `json:"add_mixed_p_plus_q_jacobian"` + AffineAddPPlusQ G2JacobianPointJSON `json:"affine_add_p_plus_q"` +} + +type G2OpsVectorsJSON struct { + PointCases []G2OpsCaseJSON `json:"point_cases"` +} + +func BuildG2OpsVectors() G2OpsVectorsJSON { + _, _, _, genAff := curve.Generators() + infAff := new(curve.G2Affine).SetInfinity() + negGen := new(curve.G2Affine).Neg(&genAff) + five := scalarMulG2Ops(5) + seventeen := scalarMulG2Ops(17) + oneTwentyThree := scalarMulG2Ops(123) + cases := []struct { + name string + p *curve.G2Affine + q *curve.G2Affine + }{ + {name: "inf_plus_gen", p: infAff, q: &genAff}, + {name: "gen_plus_inf", p: &genAff, q: infAff}, + {name: "gen_plus_neg_gen", p: &genAff, q: negGen}, + {name: "gen_plus_gen", p: &genAff, q: &genAff}, + {name: "five_plus_seventeen", p: five, q: seventeen}, + {name: "one_twenty_three_self", p: oneTwentyThree, q: oneTwentyThree}, + } + out := G2OpsVectorsJSON{PointCases: make([]G2OpsCaseJSON, len(cases))} + for i, tc := range cases { + var pJac curve.G2Jac + pJac.FromAffine(tc.p) + var negP curve.G2Jac + negP.Neg(&pJac) + var doubleP curve.G2Jac + doubleP.Double(&pJac) + var addMixed curve.G2Jac + addMixed.Set(&pJac).AddMixed(tc.q) + var pAffineOut curve.G2Affine + pAffineOut.FromJacobian(&pJac) + var affineAdd curve.G2Affine + affineAdd.Add(tc.p, tc.q) + out.PointCases[i] = G2OpsCaseJSON{Name: tc.name, PAffine: g2AffineToJSON(tc.p), QAffine: g2AffineToJSON(tc.q), PJacobian: g2JacToJSON(&pJac), PAffineOutput: g2AffineOutputToJSON(&pAffineOut), NegPJacobian: g2JacToJSON(&negP), DoublePJacobian: g2JacToJSON(&doubleP), AddMixedPPlusQJacob: g2JacToJSON(&addMixed), AffineAddPPlusQ: g2AffineOutputToJSON(&affineAdd)} + } + return out +} + +func scalarMulG2Ops(v uint64) *curve.G2Affine { + _, _, _, genAff := curve.Generators() + var out curve.G2Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} + +func g2AffineToJSON(p *curve.G2Affine) G2AffinePointJSON { + return G2AffinePointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1)} +} + +func g2JacToJSON(p *curve.G2Jac) G2JacobianPointJSON { + return G2JacobianPointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1), Z: fp2ToJSON(p.Z.A0, p.Z.A1)} +} + +func g2AffineOutputToJSON(p *curve.G2Affine) G2JacobianPointJSON { + if p.IsInfinity() { + return G2JacobianPointJSON{X: zeroFp2JSON(), Y: zeroFp2JSON(), Z: zeroFp2JSON()} + } + return G2JacobianPointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1), Z: oneFp2JSON()} +} + +func fp2ToJSON(c0, c1 fp.Element) Fp2PointJSON { + return Fp2PointJSON{C0BytesLE: fpElementHex(c0), C1BytesLE: fpElementHex(c1)} +} + +func zeroFp2JSON() Fp2PointJSON { + var z fp.Element + return fp2ToJSON(z, z) +} + +func oneFp2JSON() Fp2PointJSON { + var zero fp.Element + return fp2ToJSON(montOne(), zero) +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/helpers.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/helpers.go new file mode 100644 index 0000000000..950c0582d3 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/helpers.go @@ -0,0 +1,167 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import ( + "encoding/binary" + "encoding/hex" + "math/big" + + fp "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" +) + +func frToMont(regular *big.Int) fr.Element { + var z fr.Element + z.SetBigInt(regular) + return z +} + +func fpToMont(regular *big.Int) fp.Element { + var z fp.Element + z.SetBigInt(regular) + return z +} + +func frZeroMont() fr.Element { + var z fr.Element + z.SetZero() + return z +} + +func fpZeroMont() fp.Element { + var z fp.Element + z.SetZero() + return z +} + +func montOne() fp.Element { + var one fp.Element + one.SetOne() + return one +} + +func frElementHex(z fr.Element) string { + data := make([]byte, 32) + for i := 0; i < len(z); i++ { + binary.LittleEndian.PutUint64(data[i*8:], z[i]) + } + return hex.EncodeToString(data) +} + +func fpElementHex(z fp.Element) string { + return hex.EncodeToString(wordsToBytes(z[:], 48)) +} + +func scalarToHex(v fr.Element) string { + bytesBE := v.Bytes() + return hex.EncodeToString(regularToLittleEndian(bytesBE[:])) +} + +func scalarToBig(v fr.Element) *big.Int { + var out big.Int + return v.BigInt(&out) +} + +func newScalarUint64(v uint64) fr.Element { + var out fr.Element + out.SetUint64(v) + return out +} + +func newScalarBig(v *big.Int) fr.Element { + var out fr.Element + out.SetBigInt(v) + return out +} + +func regularHex(v *big.Int, size int) string { + return hex.EncodeToString(regularToLittleEndian(v.FillBytes(make([]byte, size)))) +} + +func regularUint64(v uint64) *big.Int { + return new(big.Int).SetUint64(v) +} + +func addBig(a, b *big.Int) *big.Int { return new(big.Int).Add(a, b) } +func subBig(a, b *big.Int) *big.Int { return new(big.Int).Sub(a, b) } +func mulBig(a, b *big.Int) *big.Int { return new(big.Int).Mul(a, b) } + +func pow2MinusOne(bitsN uint) *big.Int { + return subBig(new(big.Int).Lsh(big.NewInt(1), bitsN), big.NewInt(1)) +} + +func frModulus() *big.Int { + return new(big.Int).Set(fr.Modulus()) +} + +func fpModulus() *big.Int { + return new(big.Int).Set(fp.Modulus()) +} + +func frQMinusOne() *big.Int { return new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) } +func fpQMinusOne() *big.Int { return new(big.Int).Sub(fp.Modulus(), big.NewInt(1)) } + +func frQMinus(delta uint64) *big.Int { + return new(big.Int).Sub(fr.Modulus(), new(big.Int).SetUint64(delta)) +} + +func fpQMinus(delta uint64) *big.Int { + return new(big.Int).Sub(fp.Modulus(), new(big.Int).SetUint64(delta)) +} + +func frFloorHalfModulus() *big.Int { return new(big.Int).Rsh(frQMinusOne(), 1) } +func frCeilHalfModulus() *big.Int { return new(big.Int).Sub(fr.Modulus(), frFloorHalfModulus()) } +func fpFloorHalfModulus() *big.Int { return new(big.Int).Rsh(fpQMinusOne(), 1) } +func fpCeilHalfModulus() *big.Int { return new(big.Int).Sub(fp.Modulus(), fpFloorHalfModulus()) } + +func randomFRFieldBigInt(rng interface{ Uint32() uint32 }) *big.Int { + buf := make([]byte, 48) + for i := range buf { + buf[i] = byte(rng.Uint32()) + } + return new(big.Int).Mod(new(big.Int).SetBytes(buf), fr.Modulus()) +} + +func randomFPFieldBigInt(rng interface{ Uint32() uint32 }) *big.Int { + buf := make([]byte, 64) + for i := range buf { + buf[i] = byte(rng.Uint32()) + } + return new(big.Int).Mod(new(big.Int).SetBytes(buf), fp.Modulus()) +} + +func wordsToBytes(words []uint64, size int) []byte { + out := make([]byte, size) + for i, word := range words { + base := i * 8 + out[base+0] = byte(word) + out[base+1] = byte(word >> 8) + out[base+2] = byte(word >> 16) + out[base+3] = byte(word >> 24) + out[base+4] = byte(word >> 32) + out[base+5] = byte(word >> 40) + out[base+6] = byte(word >> 48) + out[base+7] = byte(word >> 56) + } + return out +} + +func writeElementLE(dst []byte, v fp.Element) { + copy(dst, wordsToBytes(v[:], 48)) +} + +func regularToLittleEndian(in []byte) []byte { + out := make([]byte, len(in)) + for i := range in { + out[len(in)-1-i] = in[i] + } + return out +} + +func zeroHex() string { + return hex.EncodeToString(make([]byte, 48)) +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/ntt_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/ntt_vectors.go new file mode 100644 index 0000000000..3ff2f7b5c8 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/ntt_vectors.go @@ -0,0 +1,111 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import ( + "math/big" + "math/bits" + "math/rand" + + fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + fft "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/fft" + "github.com/consensys/gnark-crypto/utils" +) + +type NTTDomainFileJSON struct { + Domains []NTTDomainJSON `json:"domains"` +} + +type NTTDomainJSON struct { + LogN int `json:"log_n"` + Size int `json:"size"` + OmegaHex string `json:"omega_hex"` + OmegaInvHex string `json:"omega_inv_hex"` + CardinalityInvHex string `json:"cardinality_inv_hex"` + CosetGenHex string `json:"coset_gen_hex"` + CosetGenInvHex string `json:"coset_gen_inv_hex"` + CosetDenInvHex string `json:"coset_den_inv_hex"` +} + +type NTTVectorsJSON struct { + NTTCases []NTTCaseJSON `json:"ntt_cases"` +} + +type NTTCaseJSON struct { + Name string `json:"name"` + Size int `json:"size"` + InputMontLE []string `json:"input_mont_le"` + ForwardExpectedLE []string `json:"forward_expected_le"` + InverseExpectedLE []string `json:"inverse_expected_le"` + StageTwiddlesLE [][]string `json:"stage_twiddles_le"` + InverseStageTwiddlesLE [][]string `json:"inverse_stage_twiddles_le"` + InverseScaleLE string `json:"inverse_scale_le"` +} + +func BuildNTTDomainFile(minLog, maxLog int) NTTDomainFileJSON { + out := NTTDomainFileJSON{Domains: make([]NTTDomainJSON, 0, maxLog-minLog+1)} + for logN := minLog; logN <= maxLog; logN++ { + size := 1 << logN + domain := fft.NewDomain(uint64(size)) + var cosetDenInv, one fr.Element + one.SetOne() + cosetDenInv.Exp(domain.FrMultiplicativeGen, big.NewInt(int64(domain.Cardinality))) + cosetDenInv.Sub(&cosetDenInv, &one).Inverse(&cosetDenInv) + out.Domains = append(out.Domains, NTTDomainJSON{LogN: logN, Size: size, OmegaHex: domain.Generator.BigInt(new(big.Int)).Text(16), OmegaInvHex: domain.GeneratorInv.BigInt(new(big.Int)).Text(16), CardinalityInvHex: domain.CardinalityInv.BigInt(new(big.Int)).Text(16), CosetGenHex: domain.FrMultiplicativeGen.BigInt(new(big.Int)).Text(16), CosetGenInvHex: domain.FrMultiplicativeGenInv.BigInt(new(big.Int)).Text(16), CosetDenInvHex: cosetDenInv.BigInt(new(big.Int)).Text(16)}) + } + return out +} + +func BuildNTTVectors() NTTVectorsJSON { + return NTTVectorsJSON{NTTCases: []NTTCaseJSON{buildNTTCase("n8_random", 8, rand.New(rand.NewSource(2026040403))), buildNTTCase("n16_random", 16, rand.New(rand.NewSource(2026040404)))}} +} + +func buildNTTCase(name string, size int, rng *rand.Rand) NTTCaseJSON { + domain := fft.NewDomain(uint64(size)) + twiddles, err := domain.Twiddles() + if err != nil { + panic(err) + } + twiddlesInv, err := domain.TwiddlesInv() + if err != nil { + panic(err) + } + input := make([]fr.Element, size) + for i := range input { + input[i].SetBigInt(randomFRFieldBigInt(rng)) + } + forward := make([]fr.Element, size) + copy(forward, input) + utils.BitReverse(forward) + domain.FFT(forward, fft.DIT) + inverse := make([]fr.Element, size) + copy(inverse, forward) + utils.BitReverse(inverse) + domain.FFTInverse(inverse, fft.DIT) + logN := bits.Len(uint(size)) - 1 + stageTwiddles := make([][]string, logN) + inverseStageTwiddles := make([][]string, logN) + for stage := 1; stage <= logN; stage++ { + m := 1 << (stage - 1) + src := twiddles[logN-stage] + srcInv := twiddlesInv[logN-stage] + stageTwiddles[stage-1] = make([]string, m) + inverseStageTwiddles[stage-1] = make([]string, m) + for i := 0; i < m; i++ { + stageTwiddles[stage-1][i] = frElementHex(src[i]) + inverseStageTwiddles[stage-1][i] = frElementHex(srcInv[i]) + } + } + return NTTCaseJSON{Name: name, Size: size, InputMontLE: encodeFRBatch(input), ForwardExpectedLE: encodeFRBatch(forward), InverseExpectedLE: encodeFRBatch(inverse), StageTwiddlesLE: stageTwiddles, InverseStageTwiddlesLE: inverseStageTwiddles, InverseScaleLE: frElementHex(domain.CardinalityInv)} +} + +func encodeFRBatch(in []fr.Element) []string { + out := make([]string, len(in)) + for i, value := range in { + out[i] = frElementHex(value) + } + return out +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/testdata.go b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/testdata.go new file mode 100644 index 0000000000..430539addf --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bls12-381/testdata.go @@ -0,0 +1,25 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bls12381 + +import "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen" + +const CurveKey = "bls12_381" + +func JSONTargets(nttMaxLog int) []testgen.JSONTarget { + return []testgen.JSONTarget{ + {Path: "vectors/fr/bls12_381_fr_ops.json", Build: func() any { return BuildFROpsVectors() }}, + {Path: "vectors/fr/bls12_381_fr_vector_ops.json", Build: func() any { return BuildFRVectorOps() }}, + {Path: "vectors/fr/bls12_381_ntt_domains.json", Build: func() any { return BuildNTTDomainFile(3, nttMaxLog) }}, + {Path: "vectors/fr/bls12_381_fr_ntt.json", Build: func() any { return BuildNTTVectors() }}, + {Path: "vectors/fp/bls12_381_fp_ops.json", Build: func() any { return BuildFPOpsVectors() }}, + {Path: "vectors/g1/bls12_381_g1_ops.json", Build: func() any { return BuildG1OpsVectors() }}, + {Path: "vectors/g1/bls12_381_g1_scalar_mul.json", Build: func() any { return BuildG1ScalarVectors() }}, + {Path: "vectors/g1/bls12_381_g1_msm.json", Build: func() any { return BuildG1MSMVectors() }}, + {Path: "vectors/g2/bls12_381_g2_ops.json", Build: func() any { return BuildG2OpsVectors() }}, + {Path: "vectors/g2/bls12_381_g2_msm.json", Build: func() any { return BuildG2MSMVectors() }}, + } +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/field_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/field_vectors.go new file mode 100644 index 0000000000..cc88ac1c84 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/field_vectors.go @@ -0,0 +1,266 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import ( + "fmt" + "math/big" + "math/bits" + "math/rand" + + fp "github.com/consensys/gnark-crypto/ecc/bn254/fp" + fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +type FieldElementCaseJSON struct { + Name string `json:"name"` + ABytesLE string `json:"a_bytes_le"` + BBytesLE string `json:"b_bytes_le"` + EqualBytesLE string `json:"equal_bytes_le"` + AddBytesLE string `json:"add_bytes_le"` + SubBytesLE string `json:"sub_bytes_le"` + NegABytesLE string `json:"neg_a_bytes_le"` + DoubleABytesLE string `json:"double_a_bytes_le"` + MulBytesLE string `json:"mul_bytes_le"` + SquareABytesLE string `json:"square_a_bytes_le"` +} + +type NormalizeCaseJSON struct { + Name string `json:"name"` + InputBytesLE string `json:"input_bytes_le"` + ExpectedBytesLE string `json:"expected_bytes_le"` +} + +type ConvertCaseJSON struct { + Name string `json:"name"` + RegularBytes string `json:"regular_bytes_le"` + MontBytes string `json:"mont_bytes_le"` +} + +type FieldOpsVectorsJSON struct { + ElementCases []FieldElementCaseJSON `json:"element_cases"` + EdgeCases []FieldElementCaseJSON `json:"edge_cases"` + DifferentialCases []FieldElementCaseJSON `json:"differential_cases"` + NormalizeCases []NormalizeCaseJSON `json:"normalize_cases"` + ConvertCases []ConvertCaseJSON `json:"convert_cases"` +} + +type VectorCaseJSON struct { + Name string `json:"name"` + RegularInputs []string `json:"regular_inputs_le"` + MontInputs []string `json:"mont_inputs_le"` + MontFactors []string `json:"mont_factors_le"` + AddExpected []string `json:"add_expected_le"` + SubExpected []string `json:"sub_expected_le"` + MulExpected []string `json:"mul_expected_le"` + ToMontExpected []string `json:"to_mont_expected_le"` + FromMontExpected []string `json:"from_mont_expected_le"` + BitReverseExpected []string `json:"bit_reverse_expected_le"` +} + +type VectorOpsJSON struct { + VectorCases []VectorCaseJSON `json:"vector_cases"` +} + +func BuildFROpsVectors() FieldOpsVectorsJSON { + rng := rand.New(rand.NewSource(20260403)) + return buildFieldOpsVectors( + buildFRElementCase, + buildFRConvertCase, + buildFRDifferentialCases(rng, 32), + frRegularHex, + frModulus, + frQMinusOne, + frQMinus, + frFloorHalfModulus, + frCeilHalfModulus, + ) +} + +func BuildFPOpsVectors() FieldOpsVectorsJSON { + rng := rand.New(rand.NewSource(20260402)) + return buildFieldOpsVectors( + buildFPElementCase, + buildFPConvertCase, + buildFPDifferentialCases(rng, 32), + fpRegularHex, + fpModulus, + fpQMinusOne, + fpQMinus, + fpFloorHalfModulus, + fpCeilHalfModulus, + ) +} + +func buildFieldOpsVectors( + buildElementCase func(string, *big.Int, *big.Int) FieldElementCaseJSON, + buildConvertCase func(string, *big.Int) ConvertCaseJSON, + differentialCases []FieldElementCaseJSON, + regularHex func(*big.Int) string, + modulus func() *big.Int, + qMinusOne func() *big.Int, + qMinus func(uint64) *big.Int, + floorHalf func() *big.Int, + ceilHalf func() *big.Int, +) FieldOpsVectorsJSON { + return FieldOpsVectorsJSON{ + ElementCases: []FieldElementCaseJSON{ + buildElementCase("zero_zero", regularUint64(0), regularUint64(0)), + buildElementCase("zero_one", regularUint64(0), regularUint64(1)), + buildElementCase("one_one", regularUint64(1), regularUint64(1)), + buildElementCase("two_five", regularUint64(2), regularUint64(5)), + buildElementCase("neg_one_one", qMinusOne(), regularUint64(1)), + buildElementCase("seven_five", regularUint64(7), regularUint64(5)), + }, + EdgeCases: []FieldElementCaseJSON{ + buildElementCase("carry32_plus_one", pow2MinusOne(32), regularUint64(1)), + buildElementCase("carry64_plus_one", pow2MinusOne(64), regularUint64(1)), + buildElementCase("carry128_plus_one", pow2MinusOne(128), regularUint64(1)), + buildElementCase("carry192_plus_one", pow2MinusOne(192), regularUint64(1)), + buildElementCase("q_minus_two_plus_three", qMinus(2), regularUint64(3)), + buildElementCase("q_minus_one_q_minus_one", qMinusOne(), qMinusOne()), + buildElementCase("q_minus_two_q_minus_one", qMinus(2), qMinusOne()), + buildElementCase("half_q_floor_half_q_ceil", floorHalf(), ceilHalf()), + }, + DifferentialCases: differentialCases, + NormalizeCases: []NormalizeCaseJSON{ + {Name: "zero", InputBytesLE: regularHex(regularUint64(0)), ExpectedBytesLE: regularHex(regularUint64(0))}, + {Name: "one", InputBytesLE: regularHex(regularUint64(1)), ExpectedBytesLE: regularHex(regularUint64(1))}, + {Name: "q_minus_one", InputBytesLE: regularHex(qMinusOne()), ExpectedBytesLE: regularHex(qMinusOne())}, + {Name: "q", InputBytesLE: regularHex(modulus()), ExpectedBytesLE: regularHex(regularUint64(0))}, + {Name: "q_plus_one", InputBytesLE: regularHex(addBig(modulus(), regularUint64(1))), ExpectedBytesLE: regularHex(regularUint64(1))}, + {Name: "two_q_minus_one", InputBytesLE: regularHex(subBig(mulBig(modulus(), regularUint64(2)), regularUint64(1))), ExpectedBytesLE: regularHex(qMinusOne())}, + }, + ConvertCases: []ConvertCaseJSON{ + buildConvertCase("zero", regularUint64(0)), + buildConvertCase("one", regularUint64(1)), + buildConvertCase("two", regularUint64(2)), + buildConvertCase("five", regularUint64(5)), + buildConvertCase("seven", regularUint64(7)), + buildConvertCase("q_minus_one", qMinusOne()), + }, + } +} + +func BuildFRVectorOps() VectorOpsJSON { + return VectorOpsJSON{ + VectorCases: []VectorCaseJSON{ + buildFRVectorCase("n8_random", 8, rand.New(rand.NewSource(2026040201))), + buildFRVectorCase("n16_random", 16, rand.New(rand.NewSource(2026040202))), + }, + } +} + +func buildFRVectorCase(name string, size int, rng *rand.Rand) VectorCaseJSON { + out := VectorCaseJSON{ + Name: name, + RegularInputs: make([]string, size), + MontInputs: make([]string, size), + MontFactors: make([]string, size), + AddExpected: make([]string, size), + SubExpected: make([]string, size), + MulExpected: make([]string, size), + ToMontExpected: make([]string, size), + FromMontExpected: make([]string, size), + BitReverseExpected: make([]string, size), + } + for i := 0; i < size; i++ { + aRegular := randomFRFieldBigInt(rng) + bRegular := randomFRFieldBigInt(rng) + var aMont, bMont fr.Element + aMont.SetBigInt(aRegular) + bMont.SetBigInt(bRegular) + var add, sub, mul fr.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + mul.Mul(&aMont, &bMont) + out.RegularInputs[i] = frRegularHex(aRegular) + out.MontInputs[i] = frElementHex(aMont) + out.MontFactors[i] = frElementHex(bMont) + out.AddExpected[i] = frElementHex(add) + out.SubExpected[i] = frElementHex(sub) + out.MulExpected[i] = frElementHex(mul) + out.ToMontExpected[i] = frElementHex(aMont) + out.FromMontExpected[i] = frRegularHex(aRegular) + } + logCount := bits.Len(uint(size)) - 1 + for i := 0; i < size; i++ { + j := int(bits.Reverse64(uint64(i)) >> (64 - logCount)) + out.BitReverseExpected[i] = out.MontInputs[j] + } + return out +} + +func buildFRElementCase(name string, aRegular, bRegular *big.Int) FieldElementCaseJSON { + aMont := frToMont(aRegular) + bMont := frToMont(bRegular) + var add, sub, negA, dblA, mul, sqA fr.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + negA.Neg(&aMont) + dblA.Double(&aMont) + mul.Mul(&aMont, &bMont) + sqA.Square(&aMont) + equal := frZeroMont() + if aMont.Equal(&bMont) { + equal.SetUint64(1) + } + return FieldElementCaseJSON{Name: name, ABytesLE: frElementHex(aMont), BBytesLE: frElementHex(bMont), EqualBytesLE: frElementHex(equal), AddBytesLE: frElementHex(add), SubBytesLE: frElementHex(sub), NegABytesLE: frElementHex(negA), DoubleABytesLE: frElementHex(dblA), MulBytesLE: frElementHex(mul), SquareABytesLE: frElementHex(sqA)} +} + +func buildFPElementCase(name string, aRegular, bRegular *big.Int) FieldElementCaseJSON { + aMont := fpToMont(aRegular) + bMont := fpToMont(bRegular) + var add, sub, negA, dblA, mul, sqA fp.Element + add.Add(&aMont, &bMont) + sub.Sub(&aMont, &bMont) + negA.Neg(&aMont) + dblA.Double(&aMont) + mul.Mul(&aMont, &bMont) + sqA.Square(&aMont) + equal := fpZeroMont() + if aMont.Equal(&bMont) { + equal.SetUint64(1) + } + return FieldElementCaseJSON{Name: name, ABytesLE: fpElementHex(aMont), BBytesLE: fpElementHex(bMont), EqualBytesLE: fpElementHex(equal), AddBytesLE: fpElementHex(add), SubBytesLE: fpElementHex(sub), NegABytesLE: fpElementHex(negA), DoubleABytesLE: fpElementHex(dblA), MulBytesLE: fpElementHex(mul), SquareABytesLE: fpElementHex(sqA)} +} + +func buildFRConvertCase(name string, regular *big.Int) ConvertCaseJSON { + return ConvertCaseJSON{Name: name, RegularBytes: frRegularHex(regular), MontBytes: frElementHex(frToMont(regular))} +} + +func buildFPConvertCase(name string, regular *big.Int) ConvertCaseJSON { + return ConvertCaseJSON{Name: name, RegularBytes: fpRegularHex(regular), MontBytes: fpElementHex(fpToMont(regular))} +} + +func buildFRDifferentialCases(rng *rand.Rand, count int) []FieldElementCaseJSON { + out := make([]FieldElementCaseJSON, count) + for i := 0; i < count; i++ { + a := randomFRFieldBigInt(rng) + b := randomFRFieldBigInt(rng) + if i%7 == 0 { + b = new(big.Int).Set(a) + } + out[i] = buildFRElementCase(fmt.Sprintf("random_%02d", i), a, b) + } + return out +} + +func buildFPDifferentialCases(rng *rand.Rand, count int) []FieldElementCaseJSON { + out := make([]FieldElementCaseJSON, count) + for i := 0; i < count; i++ { + a := randomFPFieldBigInt(rng) + b := randomFPFieldBigInt(rng) + if i%7 == 0 { + b = new(big.Int).Set(a) + } + out[i] = buildFPElementCase(fmt.Sprintf("random_%02d", i), a, b) + } + return out +} + +func frRegularHex(v *big.Int) string { return regularHex(v, 32) } +func fpRegularHex(v *big.Int) string { return regularHex(v, 32) } diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_bases.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_bases.go new file mode 100644 index 0000000000..4df406f694 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_bases.go @@ -0,0 +1,98 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import ( + "encoding/json" + "math/rand" + + curve "github.com/consensys/gnark-crypto/ecc/bn254" + fp "github.com/consensys/gnark-crypto/ecc/bn254/fp" + fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen" +) + +func BuildRandomG1Bases(count int, seed int64) ([]byte, error) { + _, _, genAff, _ := curve.Generators() + oneMontZ := montOne() + rng := rand.New(rand.NewSource(seed)) + scalars := make([]fr.Element, count) + for i := range scalars { + var raw [32]byte + for j := range raw { + raw[j] = byte(rng.Uint32()) + } + scalars[i].SetBytes(raw[:]) + if scalars[i].IsZero() { + scalars[i].SetUint64(1) + } + } + points := curve.BatchScalarMultiplicationG1(&genAff, scalars) + out := make([]byte, count*96) + for i := range points { + base := i * 96 + writeElementLE(out[base:base+32], points[i].X) + writeElementLE(out[base+32:base+2*32], points[i].Y) + writeElementLE(out[base+2*32:base+3*32], oneMontZ) + } + return out, nil +} + +func BuildSequentialG1Bases(count int) ([]byte, error) { + _, _, genAff, _ := curve.Generators() + oneMontZ := montOne() + scalars := make([]fr.Element, count) + for i := range scalars { + scalars[i].SetUint64(uint64(i + 1)) + } + points := curve.BatchScalarMultiplicationG1(&genAff, scalars) + out := make([]byte, count*96) + for i := range points { + base := i * 96 + writeElementLE(out[base:base+32], points[i].X) + writeElementLE(out[base+32:base+2*32], points[i].Y) + writeElementLE(out[base+2*32:base+3*32], oneMontZ) + } + return out, nil +} + +func BuildSequentialG2Bases(count int) ([]byte, error) { + _, _, _, genAff := curve.Generators() + oneMontZ := montOne() + zero := fp.Element{} + scalars := make([]fr.Element, count) + for i := range scalars { + scalars[i].SetUint64(uint64(i + 1)) + } + points := curve.BatchScalarMultiplicationG2(&genAff, scalars) + out := make([]byte, count*192) + for i := range points { + base := i * 192 + writeElementLE(out[base:base+32], points[i].X.A0) + writeElementLE(out[base+32:base+2*32], points[i].X.A1) + writeElementLE(out[base+2*32:base+3*32], points[i].Y.A0) + writeElementLE(out[base+3*32:base+4*32], points[i].Y.A1) + writeElementLE(out[base+4*32:base+5*32], oneMontZ) + writeElementLE(out[base+5*32:base+6*32], zero) + } + return out, nil +} + +func BuildG1BaseFixtureMetadata(count int) testgen.BaseFixtureMetadata { + return testgen.BaseFixtureMetadata{Count: count, PointBytes: 96, Format: "jacobian_x_y_z_le"} +} + +func BuildG2BaseFixtureMetadata(count int) testgen.BaseFixtureMetadata { + return testgen.BaseFixtureMetadata{Count: count, PointBytes: 192, Format: "jacobian_x_y_z_le"} +} + +func MarshalMetadataJSON(meta testgen.BaseFixtureMetadata) ([]byte, error) { + data, err := json.MarshalIndent(meta, "", " ") + if err != nil { + return nil, err + } + return append(data, '\n'), nil +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_msm_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_msm_vectors.go new file mode 100644 index 0000000000..2cdbc6cecd --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_msm_vectors.go @@ -0,0 +1,85 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bn254" + fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +type G1MSMCaseJSON struct { + Name string `json:"name"` + BasesAffine []AffinePointJSON `json:"bases_affine"` + ScalarsBytesLE []string `json:"scalars_bytes_le"` + ExpectedAffine JacobianPointJSON `json:"expected_affine"` +} + +type G1MSMVectorsJSON struct { + TermsPerInstance int `json:"terms_per_instance"` + MSMCases []G1MSMCaseJSON `json:"msm_cases"` + OneMontZ string `json:"one_mont_z"` +} + +func BuildG1MSMVectors() G1MSMVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + five := scalarMulG1MSM(5) + seventeen := scalarMulG1MSM(17) + oneTwentyThree := scalarMulG1MSM(123) + twoHundredEleven := scalarMulG1MSM(211) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + cases := []struct { + name string + bases []*curve.G1Affine + scalars []fr.Element + }{ + {name: "single_generator", bases: []*curve.G1Affine{&genAff, infAff, infAff, infAff}, scalars: []fr.Element{newScalarUint64(1), newScalarUint64(0), newScalarUint64(0), newScalarUint64(0)}}, + {name: "simple_linear_combo", bases: []*curve.G1Affine{&genAff, five, seventeen, oneTwentyThree}, scalars: []fr.Element{newScalarUint64(3), newScalarUint64(4), newScalarUint64(5), newScalarUint64(6)}}, + {name: "includes_infinity_and_zero_scalar", bases: []*curve.G1Affine{infAff, five, infAff, seventeen}, scalars: []fr.Element{newScalarUint64(19), newScalarUint64(0), newScalarUint64(7), newScalarUint64(9)}}, + {name: "q_minus_one_mix", bases: []*curve.G1Affine{&genAff, five, twoHundredEleven, oneTwentyThree}, scalars: []fr.Element{newScalarBig(modMinusOne), newScalarUint64(2), newScalarUint64(0), newScalarUint64(13)}}, + } + out := G1MSMVectorsJSON{TermsPerInstance: 4, MSMCases: make([]G1MSMCaseJSON, len(cases)), OneMontZ: fpElementHex(montOne())} + for i, tc := range cases { + expected := naiveG1MSM(tc.bases, tc.scalars) + out.MSMCases[i] = G1MSMCaseJSON{Name: tc.name, BasesAffine: encodeAffineBatch(tc.bases), ScalarsBytesLE: encodeScalarBatch(tc.scalars), ExpectedAffine: affineOutputToJSON(expected)} + } + return out +} + +func scalarMulG1MSM(v uint64) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} + +func encodeAffineBatch(in []*curve.G1Affine) []AffinePointJSON { + out := make([]AffinePointJSON, len(in)) + for i, point := range in { + out[i] = affineToJSON(point) + } + return out +} + +func encodeScalarBatch(in []fr.Element) []string { + out := make([]string, len(in)) + for i, scalar := range in { + out[i] = scalarToHex(scalar) + } + return out +} + +func naiveG1MSM(bases []*curve.G1Affine, scalars []fr.Element) *curve.G1Affine { + sum := new(curve.G1Affine).SetInfinity() + for i := range bases { + var term curve.G1Affine + term.ScalarMultiplication(bases[i], scalarToBig(scalars[i])) + sum.Add(sum, &term) + } + return sum +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_ops_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_ops_vectors.go new file mode 100644 index 0000000000..d6b240e0b2 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_ops_vectors.go @@ -0,0 +1,73 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bn254" +) + +type G1OpsCaseJSON struct { + Name string `json:"name"` + PAffine AffinePointJSON `json:"p_affine"` + QAffine AffinePointJSON `json:"q_affine"` + PJacobian JacobianPointJSON `json:"p_jacobian"` + PAffineOutput JacobianPointJSON `json:"p_affine_output"` + NegPJacobian JacobianPointJSON `json:"neg_p_jacobian"` + DoublePJacobian JacobianPointJSON `json:"double_p_jacobian"` + AddMixedPPlusQJacob JacobianPointJSON `json:"add_mixed_p_plus_q_jacobian"` + AffineAddPPlusQ JacobianPointJSON `json:"affine_add_p_plus_q"` +} + +type G1OpsVectorsJSON struct { + PointCases []G1OpsCaseJSON `json:"point_cases"` +} + +func BuildG1OpsVectors() G1OpsVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + negGen := new(curve.G1Affine).Neg(&genAff) + five := scalarMulG1Ops(5) + seventeen := scalarMulG1Ops(17) + oneTwentyThree := scalarMulG1Ops(123) + cases := []struct { + name string + p *curve.G1Affine + q *curve.G1Affine + }{ + {name: "inf_plus_gen", p: infAff, q: &genAff}, + {name: "gen_plus_inf", p: &genAff, q: infAff}, + {name: "gen_plus_neg_gen", p: &genAff, q: negGen}, + {name: "gen_plus_gen", p: &genAff, q: &genAff}, + {name: "five_plus_seventeen", p: five, q: seventeen}, + {name: "one_twenty_three_self", p: oneTwentyThree, q: oneTwentyThree}, + } + out := G1OpsVectorsJSON{PointCases: make([]G1OpsCaseJSON, len(cases))} + for i, tc := range cases { + var pJac curve.G1Jac + pJac.FromAffine(tc.p) + var negP curve.G1Jac + negP.Neg(&pJac) + var doubleP curve.G1Jac + doubleP.Double(&pJac) + var addMixed curve.G1Jac + addMixed.Set(&pJac).AddMixed(tc.q) + var pAffineOut curve.G1Affine + pAffineOut.FromJacobian(&pJac) + var affineAdd curve.G1Affine + affineAdd.Add(tc.p, tc.q) + out.PointCases[i] = G1OpsCaseJSON{Name: tc.name, PAffine: affineToJSON(tc.p), QAffine: affineToJSON(tc.q), PJacobian: jacToJSON(&pJac), PAffineOutput: affineOutputToJSON(&pAffineOut), NegPJacobian: jacToJSON(&negP), DoublePJacobian: jacToJSON(&doubleP), AddMixedPPlusQJacob: jacToJSON(&addMixed), AffineAddPPlusQ: affineOutputToJSON(&affineAdd)} + } + return out +} + +func scalarMulG1Ops(v uint64) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_scalar_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_scalar_vectors.go new file mode 100644 index 0000000000..11d409697e --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g1_scalar_vectors.go @@ -0,0 +1,117 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bn254" + fp "github.com/consensys/gnark-crypto/ecc/bn254/fp" + fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +type AffinePointJSON struct { + XBytesLE string `json:"x_bytes_le"` + YBytesLE string `json:"y_bytes_le"` +} + +type JacobianPointJSON struct { + XBytesLE string `json:"x_bytes_le"` + YBytesLE string `json:"y_bytes_le"` + ZBytesLE string `json:"z_bytes_le"` +} + +type G1ScalarMulCaseJSON struct { + Name string `json:"name"` + BaseAffine AffinePointJSON `json:"base_affine"` + ScalarBytesLE string `json:"scalar_bytes_le"` + ScalarMulAffine JacobianPointJSON `json:"scalar_mul_affine"` +} + +type G1ScalarMulBaseCaseJSON struct { + Name string `json:"name"` + ScalarBytesLE string `json:"scalar_bytes_le"` + ScalarMulBaseAffine JacobianPointJSON `json:"scalar_mul_base_affine"` +} + +type G1ScalarMulVectorsJSON struct { + GeneratorAffine AffinePointJSON `json:"generator_affine"` + OneMontZ string `json:"one_mont_z"` + ScalarCases []G1ScalarMulCaseJSON `json:"scalar_cases"` + BaseCases []G1ScalarMulBaseCaseJSON `json:"base_cases"` +} + +func BuildG1ScalarVectors() G1ScalarMulVectorsJSON { + _, _, genAff, _ := curve.Generators() + infAff := new(curve.G1Affine).SetInfinity() + fiveGen := scalarMulG1Generator(newScalarUint64(5)) + oneTwentyThreeGen := scalarMulG1Generator(newScalarUint64(123)) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + scalarCases := []struct { + name string + base *curve.G1Affine + scalar fr.Element + }{ + {name: "gen_times_zero", base: &genAff, scalar: newScalarUint64(0)}, + {name: "gen_times_one", base: &genAff, scalar: newScalarUint64(1)}, + {name: "gen_times_two", base: &genAff, scalar: newScalarUint64(2)}, + {name: "five_gen_times_seventeen", base: fiveGen, scalar: newScalarUint64(17)}, + {name: "infinity_times_one_twenty_three", base: infAff, scalar: newScalarUint64(123)}, + {name: "one_twenty_three_times_forty_two", base: oneTwentyThreeGen, scalar: newScalarUint64(42)}, + {name: "gen_times_q_minus_one", base: &genAff, scalar: newScalarBig(modMinusOne)}, + } + baseCases := []struct { + name string + scalar fr.Element + }{ + {name: "base_zero", scalar: newScalarUint64(0)}, + {name: "base_one", scalar: newScalarUint64(1)}, + {name: "base_two", scalar: newScalarUint64(2)}, + {name: "base_one_twenty_three", scalar: newScalarUint64(123)}, + {name: "base_q_minus_one", scalar: newScalarBig(modMinusOne)}, + } + out := G1ScalarMulVectorsJSON{ + GeneratorAffine: affineToJSON(&genAff), + OneMontZ: fpElementHex(montOne()), + ScalarCases: make([]G1ScalarMulCaseJSON, len(scalarCases)), + BaseCases: make([]G1ScalarMulBaseCaseJSON, len(baseCases)), + } + for i, tc := range scalarCases { + var expected curve.G1Affine + expected.ScalarMultiplication(tc.base, scalarToBig(tc.scalar)) + out.ScalarCases[i] = G1ScalarMulCaseJSON{Name: tc.name, BaseAffine: affineToJSON(tc.base), ScalarBytesLE: scalarToHex(tc.scalar), ScalarMulAffine: affineOutputToJSON(&expected)} + } + for i, tc := range baseCases { + var expected curve.G1Affine + expected.ScalarMultiplicationBase(scalarToBig(tc.scalar)) + out.BaseCases[i] = G1ScalarMulBaseCaseJSON{Name: tc.name, ScalarBytesLE: scalarToHex(tc.scalar), ScalarMulBaseAffine: affineOutputToJSON(&expected)} + } + return out +} + +func scalarMulG1Generator(s fr.Element) *curve.G1Affine { + _, _, genAff, _ := curve.Generators() + var out curve.G1Affine + out.ScalarMultiplication(&genAff, scalarToBig(s)) + return &out +} + +func affineToJSON(p *curve.G1Affine) AffinePointJSON { + return AffinePointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y)} +} + +func jacToJSON(p *curve.G1Jac) JacobianPointJSON { + return JacobianPointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y), ZBytesLE: fpElementHex(p.Z)} +} + +func affineOutputToJSON(p *curve.G1Affine) JacobianPointJSON { + if p.IsInfinity() { + return JacobianPointJSON{XBytesLE: zeroHex(), YBytesLE: zeroHex(), ZBytesLE: zeroHex()} + } + return JacobianPointJSON{XBytesLE: fpElementHex(p.X), YBytesLE: fpElementHex(p.Y), ZBytesLE: fpElementHex(montOne())} +} + +var _ fp.Element diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g2_msm_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g2_msm_vectors.go new file mode 100644 index 0000000000..8c13ce8691 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g2_msm_vectors.go @@ -0,0 +1,69 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bn254" + fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +type G2MSMCaseJSON struct { + Name string `json:"name"` + BasesAffine []G2AffinePointJSON `json:"bases_affine"` + ScalarsBytesLE []string `json:"scalars_bytes_le"` + ExpectedAffine G2JacobianPointJSON `json:"expected_affine"` +} + +type G2MSMVectorsJSON struct { + TermsPerInstance int `json:"terms_per_instance"` + MSMCases []G2MSMCaseJSON `json:"msm_cases"` +} + +func BuildG2MSMVectors() G2MSMVectorsJSON { + _, _, _, genAff := curve.Generators() + infAff := new(curve.G2Affine).SetInfinity() + five := scalarMulG2Ops(5) + seventeen := scalarMulG2Ops(17) + oneTwentyThree := scalarMulG2Ops(123) + twoHundredEleven := scalarMulG2Ops(211) + modMinusOne := new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) + cases := []struct { + name string + bases []*curve.G2Affine + scalars []fr.Element + }{ + {name: "single_generator", bases: []*curve.G2Affine{&genAff, infAff, infAff, infAff}, scalars: []fr.Element{newScalarUint64(1), newScalarUint64(0), newScalarUint64(0), newScalarUint64(0)}}, + {name: "simple_linear_combo", bases: []*curve.G2Affine{&genAff, five, seventeen, oneTwentyThree}, scalars: []fr.Element{newScalarUint64(3), newScalarUint64(4), newScalarUint64(5), newScalarUint64(6)}}, + {name: "includes_infinity_and_zero_scalar", bases: []*curve.G2Affine{infAff, five, infAff, seventeen}, scalars: []fr.Element{newScalarUint64(19), newScalarUint64(0), newScalarUint64(7), newScalarUint64(9)}}, + {name: "q_minus_one_mix", bases: []*curve.G2Affine{&genAff, five, twoHundredEleven, oneTwentyThree}, scalars: []fr.Element{newScalarBig(modMinusOne), newScalarUint64(2), newScalarUint64(0), newScalarUint64(13)}}, + } + out := G2MSMVectorsJSON{TermsPerInstance: 4, MSMCases: make([]G2MSMCaseJSON, len(cases))} + for i, tc := range cases { + expected := naiveG2MSM(tc.bases, tc.scalars) + out.MSMCases[i] = G2MSMCaseJSON{Name: tc.name, BasesAffine: encodeG2AffineBatch(tc.bases), ScalarsBytesLE: encodeScalarBatch(tc.scalars), ExpectedAffine: g2AffineOutputToJSON(expected)} + } + return out +} + +func encodeG2AffineBatch(in []*curve.G2Affine) []G2AffinePointJSON { + out := make([]G2AffinePointJSON, len(in)) + for i, point := range in { + out[i] = g2AffineToJSON(point) + } + return out +} + +func naiveG2MSM(bases []*curve.G2Affine, scalars []fr.Element) *curve.G2Affine { + sum := new(curve.G2Affine).SetInfinity() + for i := range bases { + var term curve.G2Affine + term.ScalarMultiplication(bases[i], scalarToBig(scalars[i])) + sum.Add(sum, &term) + } + return sum +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g2_ops_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g2_ops_vectors.go new file mode 100644 index 0000000000..812d400625 --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/g2_ops_vectors.go @@ -0,0 +1,119 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import ( + "math/big" + + curve "github.com/consensys/gnark-crypto/ecc/bn254" + fp "github.com/consensys/gnark-crypto/ecc/bn254/fp" +) + +type Fp2PointJSON struct { + C0BytesLE string `json:"c0_bytes_le"` + C1BytesLE string `json:"c1_bytes_le"` +} + +type G2AffinePointJSON struct { + X Fp2PointJSON `json:"x"` + Y Fp2PointJSON `json:"y"` +} + +type G2JacobianPointJSON struct { + X Fp2PointJSON `json:"x"` + Y Fp2PointJSON `json:"y"` + Z Fp2PointJSON `json:"z"` +} + +type G2OpsCaseJSON struct { + Name string `json:"name"` + PAffine G2AffinePointJSON `json:"p_affine"` + QAffine G2AffinePointJSON `json:"q_affine"` + PJacobian G2JacobianPointJSON `json:"p_jacobian"` + PAffineOutput G2JacobianPointJSON `json:"p_affine_output"` + NegPJacobian G2JacobianPointJSON `json:"neg_p_jacobian"` + DoublePJacobian G2JacobianPointJSON `json:"double_p_jacobian"` + AddMixedPPlusQJacob G2JacobianPointJSON `json:"add_mixed_p_plus_q_jacobian"` + AffineAddPPlusQ G2JacobianPointJSON `json:"affine_add_p_plus_q"` +} + +type G2OpsVectorsJSON struct { + PointCases []G2OpsCaseJSON `json:"point_cases"` +} + +func BuildG2OpsVectors() G2OpsVectorsJSON { + _, _, _, genAff := curve.Generators() + infAff := new(curve.G2Affine).SetInfinity() + negGen := new(curve.G2Affine).Neg(&genAff) + five := scalarMulG2Ops(5) + seventeen := scalarMulG2Ops(17) + oneTwentyThree := scalarMulG2Ops(123) + cases := []struct { + name string + p *curve.G2Affine + q *curve.G2Affine + }{ + {name: "inf_plus_gen", p: infAff, q: &genAff}, + {name: "gen_plus_inf", p: &genAff, q: infAff}, + {name: "gen_plus_neg_gen", p: &genAff, q: negGen}, + {name: "gen_plus_gen", p: &genAff, q: &genAff}, + {name: "five_plus_seventeen", p: five, q: seventeen}, + {name: "one_twenty_three_self", p: oneTwentyThree, q: oneTwentyThree}, + } + out := G2OpsVectorsJSON{PointCases: make([]G2OpsCaseJSON, len(cases))} + for i, tc := range cases { + var pJac curve.G2Jac + pJac.FromAffine(tc.p) + var negP curve.G2Jac + negP.Neg(&pJac) + var doubleP curve.G2Jac + doubleP.Double(&pJac) + var addMixed curve.G2Jac + addMixed.Set(&pJac).AddMixed(tc.q) + var pAffineOut curve.G2Affine + pAffineOut.FromJacobian(&pJac) + var affineAdd curve.G2Affine + affineAdd.Add(tc.p, tc.q) + out.PointCases[i] = G2OpsCaseJSON{Name: tc.name, PAffine: g2AffineToJSON(tc.p), QAffine: g2AffineToJSON(tc.q), PJacobian: g2JacToJSON(&pJac), PAffineOutput: g2AffineOutputToJSON(&pAffineOut), NegPJacobian: g2JacToJSON(&negP), DoublePJacobian: g2JacToJSON(&doubleP), AddMixedPPlusQJacob: g2JacToJSON(&addMixed), AffineAddPPlusQ: g2AffineOutputToJSON(&affineAdd)} + } + return out +} + +func scalarMulG2Ops(v uint64) *curve.G2Affine { + _, _, _, genAff := curve.Generators() + var out curve.G2Affine + out.ScalarMultiplication(&genAff, new(big.Int).SetUint64(v)) + return &out +} + +func g2AffineToJSON(p *curve.G2Affine) G2AffinePointJSON { + return G2AffinePointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1)} +} + +func g2JacToJSON(p *curve.G2Jac) G2JacobianPointJSON { + return G2JacobianPointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1), Z: fp2ToJSON(p.Z.A0, p.Z.A1)} +} + +func g2AffineOutputToJSON(p *curve.G2Affine) G2JacobianPointJSON { + if p.IsInfinity() { + return G2JacobianPointJSON{X: zeroFp2JSON(), Y: zeroFp2JSON(), Z: zeroFp2JSON()} + } + return G2JacobianPointJSON{X: fp2ToJSON(p.X.A0, p.X.A1), Y: fp2ToJSON(p.Y.A0, p.Y.A1), Z: oneFp2JSON()} +} + +func fp2ToJSON(c0, c1 fp.Element) Fp2PointJSON { + return Fp2PointJSON{C0BytesLE: fpElementHex(c0), C1BytesLE: fpElementHex(c1)} +} + +func zeroFp2JSON() Fp2PointJSON { + var z fp.Element + return fp2ToJSON(z, z) +} + +func oneFp2JSON() Fp2PointJSON { + var zero fp.Element + return fp2ToJSON(montOne(), zero) +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/helpers.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/helpers.go new file mode 100644 index 0000000000..c9b714bdeb --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/helpers.go @@ -0,0 +1,167 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import ( + "encoding/binary" + "encoding/hex" + "math/big" + + fp "github.com/consensys/gnark-crypto/ecc/bn254/fp" + fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +func frToMont(regular *big.Int) fr.Element { + var z fr.Element + z.SetBigInt(regular) + return z +} + +func fpToMont(regular *big.Int) fp.Element { + var z fp.Element + z.SetBigInt(regular) + return z +} + +func frZeroMont() fr.Element { + var z fr.Element + z.SetZero() + return z +} + +func fpZeroMont() fp.Element { + var z fp.Element + z.SetZero() + return z +} + +func montOne() fp.Element { + var one fp.Element + one.SetOne() + return one +} + +func frElementHex(z fr.Element) string { + data := make([]byte, 32) + for i := 0; i < len(z); i++ { + binary.LittleEndian.PutUint64(data[i*8:], z[i]) + } + return hex.EncodeToString(data) +} + +func fpElementHex(z fp.Element) string { + return hex.EncodeToString(wordsToBytes(z[:], 32)) +} + +func scalarToHex(v fr.Element) string { + bytesBE := v.Bytes() + return hex.EncodeToString(regularToLittleEndian(bytesBE[:])) +} + +func scalarToBig(v fr.Element) *big.Int { + var out big.Int + return v.BigInt(&out) +} + +func newScalarUint64(v uint64) fr.Element { + var out fr.Element + out.SetUint64(v) + return out +} + +func newScalarBig(v *big.Int) fr.Element { + var out fr.Element + out.SetBigInt(v) + return out +} + +func regularHex(v *big.Int, size int) string { + return hex.EncodeToString(regularToLittleEndian(v.FillBytes(make([]byte, size)))) +} + +func regularUint64(v uint64) *big.Int { + return new(big.Int).SetUint64(v) +} + +func addBig(a, b *big.Int) *big.Int { return new(big.Int).Add(a, b) } +func subBig(a, b *big.Int) *big.Int { return new(big.Int).Sub(a, b) } +func mulBig(a, b *big.Int) *big.Int { return new(big.Int).Mul(a, b) } + +func pow2MinusOne(bitsN uint) *big.Int { + return subBig(new(big.Int).Lsh(big.NewInt(1), bitsN), big.NewInt(1)) +} + +func frModulus() *big.Int { + return new(big.Int).Set(fr.Modulus()) +} + +func fpModulus() *big.Int { + return new(big.Int).Set(fp.Modulus()) +} + +func frQMinusOne() *big.Int { return new(big.Int).Sub(fr.Modulus(), big.NewInt(1)) } +func fpQMinusOne() *big.Int { return new(big.Int).Sub(fp.Modulus(), big.NewInt(1)) } + +func frQMinus(delta uint64) *big.Int { + return new(big.Int).Sub(fr.Modulus(), new(big.Int).SetUint64(delta)) +} + +func fpQMinus(delta uint64) *big.Int { + return new(big.Int).Sub(fp.Modulus(), new(big.Int).SetUint64(delta)) +} + +func frFloorHalfModulus() *big.Int { return new(big.Int).Rsh(frQMinusOne(), 1) } +func frCeilHalfModulus() *big.Int { return new(big.Int).Sub(fr.Modulus(), frFloorHalfModulus()) } +func fpFloorHalfModulus() *big.Int { return new(big.Int).Rsh(fpQMinusOne(), 1) } +func fpCeilHalfModulus() *big.Int { return new(big.Int).Sub(fp.Modulus(), fpFloorHalfModulus()) } + +func randomFRFieldBigInt(rng interface{ Uint32() uint32 }) *big.Int { + buf := make([]byte, 48) + for i := range buf { + buf[i] = byte(rng.Uint32()) + } + return new(big.Int).Mod(new(big.Int).SetBytes(buf), fr.Modulus()) +} + +func randomFPFieldBigInt(rng interface{ Uint32() uint32 }) *big.Int { + buf := make([]byte, 64) + for i := range buf { + buf[i] = byte(rng.Uint32()) + } + return new(big.Int).Mod(new(big.Int).SetBytes(buf), fp.Modulus()) +} + +func wordsToBytes(words []uint64, size int) []byte { + out := make([]byte, size) + for i, word := range words { + base := i * 8 + out[base+0] = byte(word) + out[base+1] = byte(word >> 8) + out[base+2] = byte(word >> 16) + out[base+3] = byte(word >> 24) + out[base+4] = byte(word >> 32) + out[base+5] = byte(word >> 40) + out[base+6] = byte(word >> 48) + out[base+7] = byte(word >> 56) + } + return out +} + +func writeElementLE(dst []byte, v fp.Element) { + copy(dst, wordsToBytes(v[:], 32)) +} + +func regularToLittleEndian(in []byte) []byte { + out := make([]byte, len(in)) + for i := range in { + out[len(in)-1-i] = in[i] + } + return out +} + +func zeroHex() string { + return hex.EncodeToString(make([]byte, 32)) +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/ntt_vectors.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/ntt_vectors.go new file mode 100644 index 0000000000..1a89e2885e --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/ntt_vectors.go @@ -0,0 +1,111 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import ( + "math/big" + "math/bits" + "math/rand" + + fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" + fft "github.com/consensys/gnark-crypto/ecc/bn254/fr/fft" + "github.com/consensys/gnark-crypto/utils" +) + +type NTTDomainFileJSON struct { + Domains []NTTDomainJSON `json:"domains"` +} + +type NTTDomainJSON struct { + LogN int `json:"log_n"` + Size int `json:"size"` + OmegaHex string `json:"omega_hex"` + OmegaInvHex string `json:"omega_inv_hex"` + CardinalityInvHex string `json:"cardinality_inv_hex"` + CosetGenHex string `json:"coset_gen_hex"` + CosetGenInvHex string `json:"coset_gen_inv_hex"` + CosetDenInvHex string `json:"coset_den_inv_hex"` +} + +type NTTVectorsJSON struct { + NTTCases []NTTCaseJSON `json:"ntt_cases"` +} + +type NTTCaseJSON struct { + Name string `json:"name"` + Size int `json:"size"` + InputMontLE []string `json:"input_mont_le"` + ForwardExpectedLE []string `json:"forward_expected_le"` + InverseExpectedLE []string `json:"inverse_expected_le"` + StageTwiddlesLE [][]string `json:"stage_twiddles_le"` + InverseStageTwiddlesLE [][]string `json:"inverse_stage_twiddles_le"` + InverseScaleLE string `json:"inverse_scale_le"` +} + +func BuildNTTDomainFile(minLog, maxLog int) NTTDomainFileJSON { + out := NTTDomainFileJSON{Domains: make([]NTTDomainJSON, 0, maxLog-minLog+1)} + for logN := minLog; logN <= maxLog; logN++ { + size := 1 << logN + domain := fft.NewDomain(uint64(size)) + var cosetDenInv, one fr.Element + one.SetOne() + cosetDenInv.Exp(domain.FrMultiplicativeGen, big.NewInt(int64(domain.Cardinality))) + cosetDenInv.Sub(&cosetDenInv, &one).Inverse(&cosetDenInv) + out.Domains = append(out.Domains, NTTDomainJSON{LogN: logN, Size: size, OmegaHex: domain.Generator.BigInt(new(big.Int)).Text(16), OmegaInvHex: domain.GeneratorInv.BigInt(new(big.Int)).Text(16), CardinalityInvHex: domain.CardinalityInv.BigInt(new(big.Int)).Text(16), CosetGenHex: domain.FrMultiplicativeGen.BigInt(new(big.Int)).Text(16), CosetGenInvHex: domain.FrMultiplicativeGenInv.BigInt(new(big.Int)).Text(16), CosetDenInvHex: cosetDenInv.BigInt(new(big.Int)).Text(16)}) + } + return out +} + +func BuildNTTVectors() NTTVectorsJSON { + return NTTVectorsJSON{NTTCases: []NTTCaseJSON{buildNTTCase("n8_random", 8, rand.New(rand.NewSource(2026040203))), buildNTTCase("n16_random", 16, rand.New(rand.NewSource(2026040204)))}} +} + +func buildNTTCase(name string, size int, rng *rand.Rand) NTTCaseJSON { + domain := fft.NewDomain(uint64(size)) + twiddles, err := domain.Twiddles() + if err != nil { + panic(err) + } + twiddlesInv, err := domain.TwiddlesInv() + if err != nil { + panic(err) + } + input := make([]fr.Element, size) + for i := range input { + input[i].SetBigInt(randomFRFieldBigInt(rng)) + } + forward := make([]fr.Element, size) + copy(forward, input) + utils.BitReverse(forward) + domain.FFT(forward, fft.DIT) + inverse := make([]fr.Element, size) + copy(inverse, forward) + utils.BitReverse(inverse) + domain.FFTInverse(inverse, fft.DIT) + logN := bits.Len(uint(size)) - 1 + stageTwiddles := make([][]string, logN) + inverseStageTwiddles := make([][]string, logN) + for stage := 1; stage <= logN; stage++ { + m := 1 << (stage - 1) + src := twiddles[logN-stage] + srcInv := twiddlesInv[logN-stage] + stageTwiddles[stage-1] = make([]string, m) + inverseStageTwiddles[stage-1] = make([]string, m) + for i := 0; i < m; i++ { + stageTwiddles[stage-1][i] = frElementHex(src[i]) + inverseStageTwiddles[stage-1][i] = frElementHex(srcInv[i]) + } + } + return NTTCaseJSON{Name: name, Size: size, InputMontLE: encodeFRBatch(input), ForwardExpectedLE: encodeFRBatch(forward), InverseExpectedLE: encodeFRBatch(inverse), StageTwiddlesLE: stageTwiddles, InverseStageTwiddlesLE: inverseStageTwiddles, InverseScaleLE: frElementHex(domain.CardinalityInv)} +} + +func encodeFRBatch(in []fr.Element) []string { + out := make([]string, len(in)) + for i, value := range in { + out[i] = frElementHex(value) + } + return out +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/bn254/testdata.go b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/testdata.go new file mode 100644 index 0000000000..1308fc4b3a --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/bn254/testdata.go @@ -0,0 +1,25 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package bn254 + +import "github.com/consensys/gnark/backend/accelerated/webgpu/internal/testdata/testgen" + +const CurveKey = "bn254" + +func JSONTargets(nttMaxLog int) []testgen.JSONTarget { + return []testgen.JSONTarget{ + {Path: "vectors/fr/bn254_fr_ops.json", Build: func() any { return BuildFROpsVectors() }}, + {Path: "vectors/fr/bn254_fr_vector_ops.json", Build: func() any { return BuildFRVectorOps() }}, + {Path: "vectors/fr/bn254_ntt_domains.json", Build: func() any { return BuildNTTDomainFile(3, nttMaxLog) }}, + {Path: "vectors/fr/bn254_fr_ntt.json", Build: func() any { return BuildNTTVectors() }}, + {Path: "vectors/fp/bn254_fp_ops.json", Build: func() any { return BuildFPOpsVectors() }}, + {Path: "vectors/g1/bn254_g1_ops.json", Build: func() any { return BuildG1OpsVectors() }}, + {Path: "vectors/g1/bn254_g1_scalar_mul.json", Build: func() any { return BuildG1ScalarVectors() }}, + {Path: "vectors/g1/bn254_g1_msm.json", Build: func() any { return BuildG1MSMVectors() }}, + {Path: "vectors/g2/bn254_g2_ops.json", Build: func() any { return BuildG2OpsVectors() }}, + {Path: "vectors/g2/bn254_g2_msm.json", Build: func() any { return BuildG2MSMVectors() }}, + } +} diff --git a/backend/accelerated/webgpu/internal/testdata/testgen/types.go b/backend/accelerated/webgpu/internal/testdata/testgen/types.go new file mode 100644 index 0000000000..5d84bce85a --- /dev/null +++ b/backend/accelerated/webgpu/internal/testdata/testgen/types.go @@ -0,0 +1,17 @@ +// Copyright 2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package testgen + +type JSONTarget struct { + Path string + Build func() any +} + +type BaseFixtureMetadata struct { + Count int `json:"count"` + PointBytes int `json:"point_bytes"` + Format string `json:"format"` +} diff --git a/backend/accelerated/webgpu/web/eslint.config.js b/backend/accelerated/webgpu/web/eslint.config.js index 9b1047eb7a..ceaae384d0 100644 --- a/backend/accelerated/webgpu/web/eslint.config.js +++ b/backend/accelerated/webgpu/web/eslint.config.js @@ -10,6 +10,8 @@ export default tseslint.config( ignores: [ "dist/**", "src/curvegpu/shader_bundle.generated.ts", + "tests/groth16/main.js", + "tests/plonk/main.js", ], }, js.configs.recommended, diff --git a/backend/accelerated/webgpu/web/package.json b/backend/accelerated/webgpu/web/package.json index 7da05eab59..a928d71f10 100644 --- a/backend/accelerated/webgpu/web/package.json +++ b/backend/accelerated/webgpu/web/package.json @@ -24,6 +24,9 @@ "dist/src/**/*.js", "dist/src/**/*.d.ts", "dist/src/**/*.d.ts.map", + "dist/tests/**/*.js", + "dist/tests/**/*.d.ts", + "dist/tests/**/*.d.ts.map", "dist/assets/**/*" ], "scripts": { @@ -38,6 +41,10 @@ "build:wasm:plonk": "npm run build:wasm:assets && npm run build:wasm:plonk:webgpu && npm run build:wasm:plonk:native", "build:wasm:plonk:native": "GOOS=js GOARCH=wasm go build -o dist/assets/plonk-native.wasm ../plonk/internal/wasmruntime/native", "build:wasm:plonk:webgpu": "GOOS=js GOARCH=wasm go build -o dist/assets/plonk-webgpu.wasm ../plonk/internal/wasmruntime/webgpu", + "build:test-fixtures:all": "go run ../internal/testdata/generate --suite all --out tests/fixtures", + "build:test-fixtures:api": "go run ../internal/testdata/generate --suite api --out tests/fixtures", + "build:test-fixtures:groth16": "go run ../internal/testdata/generate --suite groth16 --out tests/fixtures", + "build:test-fixtures:plonk": "go run ../internal/testdata/generate --suite plonk --out tests/fixtures", "lint": "eslint ." }, "devDependencies": { diff --git a/backend/accelerated/webgpu/web/tests/api/index.html b/backend/accelerated/webgpu/web/tests/api/index.html new file mode 100644 index 0000000000..a2c51d1ed5 --- /dev/null +++ b/backend/accelerated/webgpu/web/tests/api/index.html @@ -0,0 +1,58 @@ + + + + + + + gnark WebGPU API + + + + +

gnark WebGPU API Suite

+
+ + + + +
+ +
+ + Idle +
+

+  
+
+
+
\ No newline at end of file
diff --git a/backend/accelerated/webgpu/web/tests/api/src/curvegpu_page.ts b/backend/accelerated/webgpu/web/tests/api/src/curvegpu_page.ts
new file mode 100644
index 0000000000..b60e0aa897
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/curvegpu_page.ts
@@ -0,0 +1,260 @@
+export { };
+
+import "../../../src/curvegpu/shader_bundle.generated.js";
+import type { CurveModule, SupportedCurveID } from "../../../src/index.js";
+import { createCurveGPUContext, createCurveModule } from "../../../src/index.js";
+import { appendContextDiagnostics } from "./shared/page_library.js";
+
+type SuiteKind = "smoke" | "bench";
+
+type SuiteConfig = {
+  curve: SupportedCurveID;
+  id: string;
+  label: string;
+  kind: SuiteKind;
+  script: string;
+  defaultMinLog?: number;
+  defaultMaxLog?: number;
+  defaultIters?: number;
+};
+
+const BENCH_MIN_LOG = 10;
+const BENCH_MAX_LOG = 12;
+
+const CURVES: SupportedCurveID[] = ["bn254", "bls12_377", "bls12_381"];
+
+const SMOKE_SUITES = [
+  { id: "fr_ops", label: "fr ops", kind: "smoke", script: "/dist/tests/api/src/fr_ops_page.js" },
+  { id: "fr_vector_ops", label: "fr vector ops", kind: "smoke", script: "/dist/tests/api/src/fr_vector_ops_page.js" },
+  { id: "fr_ntt", label: "fr NTT", kind: "smoke", script: "/dist/tests/api/src/fr_ntt_page.js" },
+  { id: "fp_ops", label: "fp ops", kind: "smoke", script: "/dist/tests/api/src/fp_ops_page.js" },
+  { id: "g1_ops", label: "G1 ops", kind: "smoke", script: "/dist/tests/api/src/g1_ops_page.js" },
+  { id: "g1_scalar_mul", label: "G1 scalar mul", kind: "smoke", script: "/dist/tests/api/src/g1_scalar_mul_page.js" },
+  { id: "g1_msm", label: "G1 MSM", kind: "smoke", script: "/dist/tests/api/src/g1_msm_page.js" },
+  { id: "g2_ops", label: "G2 ops", kind: "smoke", script: "/dist/tests/api/src/g2_ops_page.js" },
+  { id: "g2_msm", label: "G2 MSM", kind: "smoke", script: "/dist/tests/api/src/g2_msm_page.js" },
+] as const;
+
+const BENCH_SUITES = [
+  { id: "fr_vector_bench", label: "fr vector bench", kind: "bench", script: "/dist/tests/api/src/fr_vector_bench_page.js", defaultIters: 3 },
+  { id: "fr_ntt_bench", label: "fr NTT bench", kind: "bench", script: "/dist/tests/api/src/fr_ntt_bench_page.js", defaultIters: 1 },
+  { id: "g1_msm_bench", label: "G1 MSM bench", kind: "bench", script: "/dist/tests/api/src/g1_msm_bench_page.js", defaultIters: 1 },
+  { id: "g2_msm_bench", label: "G2 MSM bench", kind: "bench", script: "/dist/tests/api/src/g2_msm_bench_page.js", defaultIters: 1 },
+] as const;
+
+const SUITES: SuiteConfig[] = CURVES.flatMap((curve) => [
+  ...SMOKE_SUITES.map((suite) => ({ curve, ...suite })),
+  ...BENCH_SUITES.map((suite) => ({
+    curve,
+    ...suite,
+    defaultMinLog: BENCH_MIN_LOG,
+    defaultMaxLog: BENCH_MAX_LOG,
+  })),
+]);
+
+type SuiteRunner = {
+  runSuite: (module: CurveModule, log: (msg: string) => void) => Promise<{ passed: number; failed: number }>;
+};
+
+function getById(id: string): T {
+  const el = document.getElementById(id);
+  if (!(el instanceof HTMLElement)) {
+    throw new Error(`missing element: ${id}`);
+  }
+  return el as T;
+}
+
+function makeLogger(logEl: HTMLPreElement): (msg: string) => void {
+  const lines: string[] = [];
+  return (msg: string) => {
+    lines.push(msg);
+    logEl.textContent = lines.join("\n");
+  };
+}
+
+async function buildModule(curve: SupportedCurveID, log: (msg: string) => void): Promise {
+  const context = await createCurveGPUContext();
+  const diagLines: string[] = [];
+  appendContextDiagnostics(diagLines, context);
+  for (const line of diagLines) {
+    log(line);
+  }
+  return createCurveModule(context, curve);
+}
+
+async function runAllSmoke(
+  module: CurveModule,
+  suites: SuiteConfig[],
+  log: (msg: string) => void,
+): Promise<{ passed: number; failed: number }> {
+  let passed = 0;
+  let failed = 0;
+  for (const suite of suites) {
+    try {
+      const mod = await import(suite.script) as SuiteRunner;
+      const result = await mod.runSuite(module, log);
+      passed += result.passed;
+      failed += result.failed;
+    } catch (error) {
+      log(`FAIL [${suite.id}]: ${error instanceof Error ? error.message : String(error)}`);
+      failed += 1;
+    }
+  }
+  return { passed, failed };
+}
+
+function populateSelectors(
+  curveSelect: HTMLSelectElement,
+  suiteSelect: HTMLSelectElement,
+  curve: string,
+  suiteId: string,
+): void {
+  const curves = [...new Set(SUITES.map((s) => s.curve))];
+  curveSelect.replaceChildren();
+  for (const c of curves) {
+    const opt = document.createElement("option");
+    opt.value = c;
+    opt.textContent = c;
+    if (c === curve) {
+      opt.selected = true;
+    }
+    curveSelect.appendChild(opt);
+  }
+
+  function updateSuiteOptions(selectedCurve: string): void {
+    suiteSelect.replaceChildren();
+    const addOpt = (value: string, text: string, selected: boolean): void => {
+      const opt = document.createElement("option");
+      opt.value = value;
+      opt.textContent = text;
+      if (selected) {
+        opt.selected = true;
+      }
+      suiteSelect.appendChild(opt);
+    };
+    addOpt("all", "all smoke", suiteId === "all");
+    for (const s of SUITES.filter((entry) => entry.curve === selectedCurve)) {
+      addOpt(s.id, s.label, s.id === suiteId);
+    }
+  }
+
+  updateSuiteOptions(curve);
+  curveSelect.addEventListener("change", () => {
+    updateSuiteOptions(curveSelect.value);
+  });
+}
+
+async function main(): Promise {
+  const params = new URLSearchParams(window.location.search);
+  const curve = (params.get("curve") ?? "bn254") as SupportedCurveID;
+  const suiteId = params.get("suite") ?? "fr_ops";
+
+  const logEl = getById("log");
+  const statusEl = getById("status");
+  const runButton = getById("run");
+  const curveSelect = getById("curve-select");
+  const suiteSelect = getById("suite-select");
+  const openButton = getById("open-suite");
+  const benchControls = getById("bench-controls");
+
+  populateSelectors(curveSelect, suiteSelect, curve, suiteId);
+
+  openButton.addEventListener("click", () => {
+    const newParams = new URLSearchParams(window.location.search);
+    newParams.set("curve", curveSelect.value);
+    newParams.set("suite", suiteSelect.value);
+    window.location.search = newParams.toString();
+  });
+
+  function setStatus(s: string): void {
+    statusEl.textContent = s;
+  }
+  function setPageState(s: "idle" | "running" | "pass" | "fail"): void {
+    document.body.setAttribute("data-status", s);
+  }
+
+  if (suiteId === "all") {
+    const smokeSuites = SUITES.filter((s) => s.curve === curve && s.kind === "smoke");
+
+    const runAll = async (): Promise => {
+      runButton.disabled = true;
+      setStatus("Running");
+      setPageState("running");
+      const log = makeLogger(logEl);
+      try {
+        const module = await buildModule(curve, log);
+        const result = await runAllSmoke(module, smokeSuites, log);
+        log("");
+        log(`Total: ${result.passed} passed, ${result.failed} failed`);
+        setStatus(result.failed === 0 ? "Pass" : "Fail");
+        setPageState(result.failed === 0 ? "pass" : "fail");
+      } catch (error) {
+        log(`FAIL: ${error instanceof Error ? error.message : String(error)}`);
+        setStatus("Fail");
+        setPageState("fail");
+      } finally {
+        runButton.disabled = false;
+      }
+    };
+
+    runButton.addEventListener("click", () => void runAll());
+    if (params.get("autorun") === "1") {
+      void runAll();
+    } else {
+      logEl.textContent = `Press Run to execute all ${curve} smoke suites.`;
+    }
+    return;
+  }
+
+  const selected = SUITES.find((s) => s.curve === curve && s.id === suiteId);
+  if (!selected) {
+    logEl.textContent = `Unknown suite: ${curve}:${suiteId}`;
+    return;
+  }
+
+  if (selected.kind === "bench") {
+    benchControls.hidden = false;
+    if (selected.defaultMinLog !== undefined) {
+      getById("min-log").value = `${selected.defaultMinLog}`;
+    }
+    if (selected.defaultMaxLog !== undefined) {
+      getById("max-log").value = `${selected.defaultMaxLog}`;
+    }
+    if (selected.defaultIters !== undefined) {
+      getById("iters").value = `${selected.defaultIters}`;
+    }
+    // Bench page registers its own Run button listener on import
+    await import(`${selected.script}`);
+    return;
+  }
+
+  // Smoke suite: orchestrator owns the Run button
+  const run = async (): Promise => {
+    runButton.disabled = true;
+    setStatus("Running");
+    setPageState("running");
+    const log = makeLogger(logEl);
+    try {
+      const module = await buildModule(curve, log);
+      const mod = await import(selected.script) as SuiteRunner;
+      await mod.runSuite(module, log);
+      setStatus("Pass");
+      setPageState("pass");
+    } catch (error) {
+      log(`FAIL: ${error instanceof Error ? error.message : String(error)}`);
+      setStatus("Fail");
+      setPageState("fail");
+    } finally {
+      runButton.disabled = false;
+    }
+  };
+
+  runButton.addEventListener("click", () => void run());
+  if (params.get("autorun") === "1") {
+    void run();
+  } else {
+    logEl.textContent = `Press Run to execute the ${curve} ${selected.id} suite.`;
+  }
+}
+
+void main();
diff --git a/backend/accelerated/webgpu/web/tests/api/src/fp_ops_page.ts b/backend/accelerated/webgpu/web/tests/api/src/fp_ops_page.ts
new file mode 100644
index 0000000000..1385209c39
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/fp_ops_page.ts
@@ -0,0 +1,175 @@
+export { };
+
+import { bytesToHex, fetchJSON, hexToBytes } from "../../../src/curvegpu/browser_utils.js";
+import type { CurveGPUElementBytes, CurveModule, FieldModule, SupportedCurveID } from "../../../src/index.js";
+import { curveDisplayName } from "./shared/page_library.js";
+
+type ElementCase = {
+  name: string;
+  a_bytes_le: string;
+  b_bytes_le: string;
+  equal_bytes_le: string;
+  add_bytes_le: string;
+  sub_bytes_le: string;
+  neg_a_bytes_le: string;
+  double_a_bytes_le: string;
+  mul_bytes_le: string;
+  square_a_bytes_le: string;
+};
+
+type NormalizeCase = {
+  name: string;
+  input_bytes_le: string;
+  expected_bytes_le: string;
+};
+
+type ConvertCase = {
+  name: string;
+  regular_bytes_le: string;
+  mont_bytes_le: string;
+};
+
+type FPOpsVectors = {
+  element_cases: ElementCase[];
+  edge_cases: ElementCase[];
+  differential_cases: ElementCase[];
+  normalize_cases: NormalizeCase[];
+  convert_cases: ConvertCase[];
+};
+
+type FpOpsConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  vectorPath: string;
+};
+
+const CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 fp Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fp/bn254_fp_ops.json",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 fp Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fp/bls12_377_fp_ops.json",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 fp Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fp/bls12_381_fp_ops.json",
+  },
+};
+
+function combineElementCases(vectors: FPOpsVectors): ElementCase[] {
+  return [...vectors.element_cases, ...vectors.edge_cases, ...vectors.differential_cases];
+}
+
+function zeroHex(byteSize: number): string {
+  return bytesToHex(new Uint8Array(byteSize));
+}
+
+function isNonZeroHex(hex: string): boolean {
+  return hexToBytes(hex).some((byte) => byte !== 0);
+}
+
+function bytesList(hexValues: readonly string[]): Uint8Array[] {
+  return hexValues.map(hexToBytes);
+}
+
+function expectHexBatch(name: string, got: readonly CurveGPUElementBytes[], wantHex: readonly string[], log: (msg: string) => void): void {
+  if (got.length !== wantHex.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${wantHex.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = bytesToHex(got[i]);
+    if (gotHex !== wantHex[i]) {
+      throw new Error(`${name}: mismatch at index ${i}: got=${gotHex} want=${wantHex[i]}`);
+    }
+  }
+  log(`${name}: OK`);
+}
+
+function expectBoolBatch(name: string, got: readonly boolean[], want: readonly boolean[], log: (msg: string) => void): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    if (got[i] !== want[i]) {
+      throw new Error(`${name}: mismatch at index ${i}: got=${String(got[i])} want=${String(want[i])}`);
+    }
+  }
+  log(`${name}: OK`);
+}
+
+function mustFindConvertCase(cases: readonly ConvertCase[], name: string): ConvertCase {
+  const found = cases.find((item) => item.name === name);
+  if (!found) {
+    throw new Error(`missing convert case ${name}`);
+  }
+  return found;
+}
+
+export async function runSuite(module: CurveModule, log: (msg: string) => void): Promise<{ passed: number; failed: number }> {
+  const config = CONFIGS[module.id];
+  if (!config) {
+    throw new Error(`fp ops vectors unavailable for curve ${module.id}`);
+  }
+  log(`=== ${config.title} ===`);
+  log("");
+  const vectors = await fetchJSON(config.vectorPath);
+  log(`cases.sanity = ${vectors.element_cases.length}`);
+  log(`cases.edge = ${vectors.edge_cases.length}`);
+  log(`cases.differential = ${vectors.differential_cases.length}`);
+  log(`cases.normalize = ${vectors.normalize_cases.length}`);
+  log(`cases.convert = ${vectors.convert_cases.length}`);
+
+  const fp: FieldModule = module.fp;
+  const elementCases = combineElementCases(vectors);
+  const aHex = elementCases.map((item) => item.a_bytes_le);
+  const bHex = elementCases.map((item) => item.b_bytes_le);
+  const aBytes = bytesList(aHex);
+  const bBytes = bytesList(bHex);
+  const zeroHexValue = zeroHex(fp.byteSize);
+  const oneMontHex = bytesToHex(await fp.montOne());
+
+  expectHexBatch("copy", await fp.copyBatch(aBytes), aHex, log);
+  expectBoolBatch("equal", await fp.equalBatch(aBytes, bBytes), elementCases.map((item) => isNonZeroHex(item.equal_bytes_le)), log);
+  expectHexBatch("zero", Array.from({ length: elementCases.length }, () => fp.zero()), Array.from({ length: elementCases.length }, () => zeroHexValue), log);
+  const oneBatch = Array.from({ length: elementCases.length }, () => hexToBytes(oneMontHex));
+  expectHexBatch("one", oneBatch, Array.from({ length: elementCases.length }, () => oneMontHex), log);
+  expectHexBatch("add", await fp.addBatch(aBytes, bBytes), elementCases.map((item) => item.add_bytes_le), log);
+  expectHexBatch("sub", await fp.subBatch(aBytes, bBytes), elementCases.map((item) => item.sub_bytes_le), log);
+  expectHexBatch("neg", await fp.negBatch(aBytes), elementCases.map((item) => item.neg_a_bytes_le), log);
+  expectHexBatch("double", await fp.doubleBatch(aBytes), elementCases.map((item) => item.double_a_bytes_le), log);
+  expectHexBatch("mul", await fp.mulBatch(aBytes, bBytes), elementCases.map((item) => item.mul_bytes_le), log);
+  expectHexBatch("square", await fp.squareBatch(aBytes), elementCases.map((item) => item.square_a_bytes_le), log);
+  expectHexBatch(
+    "to_mont",
+    await fp.toMontgomeryBatch(bytesList(vectors.convert_cases.map((item) => item.regular_bytes_le))),
+    vectors.convert_cases.map((item) => item.mont_bytes_le),
+    log,
+  );
+  expectHexBatch(
+    "from_mont",
+    await fp.fromMontgomeryBatch(bytesList(vectors.convert_cases.map((item) => item.mont_bytes_le))),
+    vectors.convert_cases.map((item) => item.regular_bytes_le),
+    log,
+  );
+  expectHexBatch(
+    "normalize",
+    await fp.normalizeMontBatch(bytesList(vectors.normalize_cases.map((item) => item.input_bytes_le))),
+    vectors.normalize_cases.map((item) => item.expected_bytes_le),
+    log,
+  );
+
+  const oneCase = mustFindConvertCase(vectors.convert_cases, "one");
+  const oneHexExpected = bytesToHex(await fp.montOne());
+  if (oneHexExpected !== oneCase.mont_bytes_le) {
+    throw new Error(`one: mismatch got=${oneHexExpected} want=${oneCase.mont_bytes_le}`);
+  }
+
+  log("");
+  log(`PASS: ${curveDisplayName(module.id)} fp browser smoke succeeded`);
+  return { passed: 1, failed: 0 };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/fr_ntt_bench_page.ts b/backend/accelerated/webgpu/web/tests/api/src/fr_ntt_bench_page.ts
new file mode 100644
index 0000000000..9b376ffd9c
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/fr_ntt_bench_page.ts
@@ -0,0 +1,156 @@
+export {};
+
+import { createPageUI, mustElement } from "../../../src/curvegpu/browser_utils.js";
+import { benchmarkTotalDuration } from "./shared/bench_total.js";
+import type { CurveGPUElementBytes, SupportedCurveID } from "../../../src/index.js";
+import { appendContextDiagnostics, createRequestedCurveModule, curveDisplayName, getRequestedCurveId } from "./shared/page_library.js";
+
+type NTTConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  successMessage: string;
+};
+
+const CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 fr NTT Browser Benchmark",
+    successMessage: "BN254 fr NTT browser benchmark completed",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 fr NTT Browser Benchmark",
+    successMessage: "BLS12-377 fr NTT browser benchmark completed",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 fr NTT Browser Benchmark",
+    successMessage: "BLS12-381 fr NTT browser benchmark completed",
+  },
+};
+
+const ELEMENT_BYTES = 32;
+
+const minLogEl = document.getElementById("min-log") as HTMLInputElement | null;
+const maxLogEl = document.getElementById("max-log") as HTMLInputElement | null;
+const itersEl = document.getElementById("iters") as HTMLInputElement | null;
+const runButton = document.getElementById("run") as HTMLButtonElement | null;
+const statusEl = document.getElementById("status") as HTMLElement | null;
+const logEl = document.getElementById("log") as HTMLElement | null;
+const { setStatus, setPageState, writeLog } = createPageUI(statusEl, logEl);
+
+function getConfig(): NTTConfig {
+  const curve = getRequestedCurveId();
+  const config = CONFIGS[curve];
+  if (!config) {
+    throw new Error(`fr NTT benchmark unavailable for curve ${curve}`);
+  }
+  return config;
+}
+
+function makeRegularBatch(count: number, seed: number): CurveGPUElementBytes[] {
+  const out: CurveGPUElementBytes[] = [];
+  let state = seed >>> 0;
+  for (let i = 0; i < count; i += 1) {
+    const value = new Uint8Array(ELEMENT_BYTES);
+    for (let byteIndex = 0; byteIndex < ELEMENT_BYTES; byteIndex += 1) {
+      state ^= state << 13;
+      state ^= state >>> 17;
+      state ^= state << 5;
+      value[byteIndex] = state & 0xff;
+    }
+    out.push(value);
+  }
+  return out;
+}
+
+async function runBenchmark(): Promise {
+  const config = getConfig();
+  const lines = [`=== ${config.title} ===`, ""];
+  writeLog(lines);
+  setStatus("Running");
+  setPageState("running");
+  mustElement(runButton, "run").disabled = true;
+
+  try {
+    const minLog = Number.parseInt(mustElement(minLogEl, "min-log").value, 10);
+    const maxLog = Number.parseInt(mustElement(maxLogEl, "max-log").value, 10);
+    const iters = Number.parseInt(mustElement(itersEl, "iters").value, 10);
+    if (!Number.isInteger(minLog) || !Number.isInteger(maxLog) || !Number.isInteger(iters) || minLog < 1 || maxLog < minLog || iters < 1) {
+      throw new Error("invalid benchmark controls");
+    }
+
+    const initStart = performance.now();
+    const curve = await createRequestedCurveModule(config.curve);
+    const initMs = performance.now() - initStart;
+
+    lines.push("1. Requesting adapter... OK");
+    appendContextDiagnostics(lines, curve.context);
+    lines.push("2. Requesting device... OK");
+    lines.push("3. Initializing curve module... OK");
+    lines.push(`init_ms = ${initMs.toFixed(3)}`);
+    lines.push("");
+    lines.push("size,op,init_ms,cold_total_ms,cold_with_init_ms,warm_total_ms");
+    writeLog(lines);
+
+    for (let logSize = minLog; logSize <= maxLog; logSize += 1) {
+      const size = 1 << logSize;
+      const regularValues = makeRegularBatch(size, 0x9e3779b9 ^ size);
+      const inputMont = await curve.fr.toMontgomeryBatch(regularValues);
+      const forwardBenchmark = await benchmarkTotalDuration(iters, async () => {
+        await curve.ntt.forward(inputMont);
+      });
+      lines.push(
+        [
+          `${size}`,
+          "forward_ntt",
+          initMs.toFixed(3),
+          forwardBenchmark.coldMs.toFixed(3),
+          (initMs + forwardBenchmark.coldMs).toFixed(3),
+          forwardBenchmark.warmMs.toFixed(3),
+        ].join(","),
+      );
+      writeLog(lines);
+
+      const forwardValues = await curve.ntt.forward(inputMont);
+      const inverseBenchmark = await benchmarkTotalDuration(iters, async () => {
+        await curve.ntt.inverse(forwardValues);
+      });
+      lines.push(
+        [
+          `${size}`,
+          "inverse_ntt",
+          initMs.toFixed(3),
+          inverseBenchmark.coldMs.toFixed(3),
+          (initMs + inverseBenchmark.coldMs).toFixed(3),
+          inverseBenchmark.warmMs.toFixed(3),
+        ].join(","),
+      );
+      writeLog(lines);
+    }
+
+    lines.push("");
+    lines.push(`PASS: ${config.successMessage}`);
+    writeLog(lines);
+    setStatus("Pass");
+    setPageState("pass");
+  } catch (error) {
+    lines.push(`FAIL: ${error instanceof Error ? error.message : String(error)}`);
+    writeLog(lines);
+    setStatus("Fail");
+    setPageState("fail");
+  } finally {
+    mustElement(runButton, "run").disabled = false;
+  }
+}
+
+mustElement(runButton, "run").addEventListener("click", () => {
+  void runBenchmark();
+});
+
+const config = getConfig();
+writeLog([
+  `=== ${config.title} ===`,
+  "",
+  `Press Run to benchmark ${curveDisplayName(config.curve)} fr NTT in browser WebGPU.`,
+]);
diff --git a/backend/accelerated/webgpu/web/tests/api/src/fr_ntt_page.ts b/backend/accelerated/webgpu/web/tests/api/src/fr_ntt_page.ts
new file mode 100644
index 0000000000..050150cb31
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/fr_ntt_page.ts
@@ -0,0 +1,85 @@
+export {};
+
+import { bytesToHex, fetchJSON, hexToBytes } from "../../../src/curvegpu/browser_utils.js";
+import type { CurveGPUElementBytes, CurveModule, SupportedCurveID } from "../../../src/index.js";
+import { curveDisplayName } from "./shared/page_library.js";
+
+type NTTCase = {
+  name: string;
+  size: number;
+  input_mont_le: string[];
+  forward_expected_le: string[];
+  inverse_expected_le: string[];
+  stage_twiddles_le: string[][];
+  inverse_stage_twiddles_le: string[][];
+  inverse_scale_le: string;
+};
+
+type FRNTTVectors = {
+  ntt_cases: NTTCase[];
+};
+
+type NTTConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  vectorPath: string;
+};
+
+const CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 fr NTT Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fr/bn254_fr_ntt.json",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 fr NTT Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fr/bls12_377_fr_ntt.json",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 fr NTT Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fr/bls12_381_fr_ntt.json",
+  },
+};
+
+function bytesList(hexValues: readonly string[]): CurveGPUElementBytes[] {
+  return hexValues.map(hexToBytes);
+}
+
+function expectHexBatch(name: string, got: readonly CurveGPUElementBytes[], want: readonly string[]): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = bytesToHex(got[i]);
+    if (gotHex !== want[i]) {
+      throw new Error(`${name}: mismatch at index ${i}: got=${gotHex} want=${want[i]}`);
+    }
+  }
+}
+
+export async function runSuite(module: CurveModule, log: (msg: string) => void): Promise<{ passed: number; failed: number }> {
+  const config = CONFIGS[module.id];
+  if (!config) {
+    throw new Error(`fr NTT vectors unavailable for curve ${module.id}`);
+  }
+  log(`=== ${config.title} ===`);
+  log("");
+  const vectors = await fetchJSON(config.vectorPath);
+  log(`cases.ntt = ${vectors.ntt_cases.length}`);
+
+  for (const item of vectors.ntt_cases) {
+    const input = bytesList(item.input_mont_le);
+    const forward = await module.ntt.forward(input);
+    expectHexBatch(`${item.name}: forward_ntt`, forward, item.forward_expected_le);
+    const inverse = await module.ntt.inverse(forward);
+    expectHexBatch(`${item.name}: inverse_ntt`, inverse, item.inverse_expected_le);
+  }
+
+  log("forward_ntt: OK");
+  log("inverse_ntt: OK");
+  log("");
+  log(`PASS: ${curveDisplayName(module.id)} fr NTT browser smoke succeeded`);
+  return { passed: 1, failed: 0 };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/fr_ops_page.ts b/backend/accelerated/webgpu/web/tests/api/src/fr_ops_page.ts
new file mode 100644
index 0000000000..31531b79fd
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/fr_ops_page.ts
@@ -0,0 +1,175 @@
+export { };
+
+import { bytesToHex, fetchJSON, hexToBytes } from "../../../src/curvegpu/browser_utils.js";
+import type { CurveGPUElementBytes, CurveModule, FieldModule, SupportedCurveID } from "../../../src/index.js";
+import { curveDisplayName } from "./shared/page_library.js";
+
+type ElementCase = {
+  name: string;
+  a_bytes_le: string;
+  b_bytes_le: string;
+  equal_bytes_le: string;
+  add_bytes_le: string;
+  sub_bytes_le: string;
+  neg_a_bytes_le: string;
+  double_a_bytes_le: string;
+  mul_bytes_le: string;
+  square_a_bytes_le: string;
+};
+
+type NormalizeCase = {
+  name: string;
+  input_bytes_le: string;
+  expected_bytes_le: string;
+};
+
+type ConvertCase = {
+  name: string;
+  regular_bytes_le: string;
+  mont_bytes_le: string;
+};
+
+type FROpsVectors = {
+  element_cases: ElementCase[];
+  edge_cases: ElementCase[];
+  differential_cases: ElementCase[];
+  normalize_cases: NormalizeCase[];
+  convert_cases: ConvertCase[];
+};
+
+type FrOpsConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  vectorPath: string;
+};
+
+const CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 fr Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fr/bn254_fr_ops.json",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 fr Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fr/bls12_377_fr_ops.json",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 fr Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fr/bls12_381_fr_ops.json",
+  },
+};
+
+function combineElementCases(vectors: FROpsVectors): ElementCase[] {
+  return [...vectors.element_cases, ...vectors.edge_cases, ...vectors.differential_cases];
+}
+
+function zeroHex(byteSize: number): string {
+  return bytesToHex(new Uint8Array(byteSize));
+}
+
+function isNonZeroHex(hex: string): boolean {
+  return hexToBytes(hex).some((byte) => byte !== 0);
+}
+
+function bytesList(hexValues: readonly string[]): Uint8Array[] {
+  return hexValues.map(hexToBytes);
+}
+
+function expectHexBatch(name: string, got: readonly CurveGPUElementBytes[], wantHex: readonly string[], log: (msg: string) => void): void {
+  if (got.length !== wantHex.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${wantHex.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = bytesToHex(got[i]);
+    if (gotHex !== wantHex[i]) {
+      throw new Error(`${name}: mismatch at index ${i}: got=${gotHex} want=${wantHex[i]}`);
+    }
+  }
+  log(`${name}: OK`);
+}
+
+function expectBoolBatch(name: string, got: readonly boolean[], want: readonly boolean[], log: (msg: string) => void): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    if (got[i] !== want[i]) {
+      throw new Error(`${name}: mismatch at index ${i}: got=${String(got[i])} want=${String(want[i])}`);
+    }
+  }
+  log(`${name}: OK`);
+}
+
+function mustFindConvertCase(cases: readonly ConvertCase[], name: string): ConvertCase {
+  const found = cases.find((item) => item.name === name);
+  if (!found) {
+    throw new Error(`missing convert case ${name}`);
+  }
+  return found;
+}
+
+export async function runSuite(module: CurveModule, log: (msg: string) => void): Promise<{ passed: number; failed: number }> {
+  const config = CONFIGS[module.id];
+  if (!config) {
+    throw new Error(`fr ops vectors unavailable for curve ${module.id}`);
+  }
+  log(`=== ${config.title} ===`);
+  log("");
+  const vectors = await fetchJSON(config.vectorPath);
+  log(`cases.sanity = ${vectors.element_cases.length}`);
+  log(`cases.edge = ${vectors.edge_cases.length}`);
+  log(`cases.differential = ${vectors.differential_cases.length}`);
+  log(`cases.normalize = ${vectors.normalize_cases.length}`);
+  log(`cases.convert = ${vectors.convert_cases.length}`);
+
+  const fr: FieldModule = module.fr;
+  const elementCases = combineElementCases(vectors);
+  const aHex = elementCases.map((item) => item.a_bytes_le);
+  const bHex = elementCases.map((item) => item.b_bytes_le);
+  const aBytes = bytesList(aHex);
+  const bBytes = bytesList(bHex);
+  const zeroHexValue = zeroHex(fr.byteSize);
+  const oneMontHex = bytesToHex(await fr.montOne());
+
+  expectHexBatch("copy", await fr.copyBatch(aBytes), aHex, log);
+  expectBoolBatch("equal", await fr.equalBatch(aBytes, bBytes), elementCases.map((item) => isNonZeroHex(item.equal_bytes_le)), log);
+  expectHexBatch("zero", Array.from({ length: elementCases.length }, () => fr.zero()), Array.from({ length: elementCases.length }, () => zeroHexValue), log);
+  const oneBatch = Array.from({ length: elementCases.length }, () => hexToBytes(oneMontHex));
+  expectHexBatch("one", oneBatch, Array.from({ length: elementCases.length }, () => oneMontHex), log);
+  expectHexBatch("add", await fr.addBatch(aBytes, bBytes), elementCases.map((item) => item.add_bytes_le), log);
+  expectHexBatch("sub", await fr.subBatch(aBytes, bBytes), elementCases.map((item) => item.sub_bytes_le), log);
+  expectHexBatch("neg", await fr.negBatch(aBytes), elementCases.map((item) => item.neg_a_bytes_le), log);
+  expectHexBatch("double", await fr.doubleBatch(aBytes), elementCases.map((item) => item.double_a_bytes_le), log);
+  expectHexBatch("mul", await fr.mulBatch(aBytes, bBytes), elementCases.map((item) => item.mul_bytes_le), log);
+  expectHexBatch("square", await fr.squareBatch(aBytes), elementCases.map((item) => item.square_a_bytes_le), log);
+  expectHexBatch(
+    "to_mont",
+    await fr.toMontgomeryBatch(bytesList(vectors.convert_cases.map((item) => item.regular_bytes_le))),
+    vectors.convert_cases.map((item) => item.mont_bytes_le),
+    log,
+  );
+  expectHexBatch(
+    "from_mont",
+    await fr.fromMontgomeryBatch(bytesList(vectors.convert_cases.map((item) => item.mont_bytes_le))),
+    vectors.convert_cases.map((item) => item.regular_bytes_le),
+    log,
+  );
+  expectHexBatch(
+    "normalize",
+    await fr.normalizeMontBatch(bytesList(vectors.normalize_cases.map((item) => item.input_bytes_le))),
+    vectors.normalize_cases.map((item) => item.expected_bytes_le),
+    log,
+  );
+
+  const oneCase = mustFindConvertCase(vectors.convert_cases, "one");
+  const oneHexExpected = bytesToHex(await fr.montOne());
+  if (oneHexExpected !== oneCase.mont_bytes_le) {
+    throw new Error(`one: mismatch got=${oneHexExpected} want=${oneCase.mont_bytes_le}`);
+  }
+
+  log("");
+  log(`PASS: ${curveDisplayName(module.id)} fr browser smoke succeeded`);
+  return { passed: 1, failed: 0 };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/fr_vector_bench_page.ts b/backend/accelerated/webgpu/web/tests/api/src/fr_vector_bench_page.ts
new file mode 100644
index 0000000000..55ebd646c5
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/fr_vector_bench_page.ts
@@ -0,0 +1,360 @@
+import {
+  appendAdapterDiagnostics,
+  createPageUI,
+  fetchText,
+  mustElement,
+} from "../../../src/curvegpu/browser_utils.js";
+
+const FR_OP_TO_MONT = 11;
+
+const FR_VECTOR_OP_ADD = 1;
+const FR_VECTOR_OP_SUB = 2;
+const FR_VECTOR_OP_MUL_FACTORS = 3;
+const FR_VECTOR_OP_BIT_REVERSE_COPY = 4;
+
+const ELEMENT_WORDS = 8;
+const ELEMENT_BYTES = 32;
+const UNIFORM_BYTES = 32;
+
+type BenchConfig = {
+  curve: string;
+  title: string;
+  arithShaderPath: string;
+  vectorShaderPath: string;
+  arithLabel: string;
+  vectorLabel: string;
+};
+
+type Kernel = {
+  pipeline: GPUComputePipeline;
+  bindGroupLayout: GPUBindGroupLayout;
+};
+
+type Profile = {
+  uploadMs: number;
+  kernelMs: number;
+  readbackMs: number;
+  totalMs: number;
+};
+
+declare const GPUShaderStage: {
+  COMPUTE: number;
+};
+
+declare const GPUBufferUsage: {
+  STORAGE: number;
+  COPY_DST: number;
+  COPY_SRC: number;
+  MAP_READ: number;
+  UNIFORM: number;
+};
+
+declare const GPUMapMode: {
+  READ: number;
+};
+
+const minLogEl = document.getElementById("min-log") as HTMLInputElement | null;
+const maxLogEl = document.getElementById("max-log") as HTMLInputElement | null;
+const itersEl = document.getElementById("iters") as HTMLInputElement | null;
+const runButton = document.getElementById("run") as HTMLButtonElement | null;
+const statusEl = document.getElementById("status") as HTMLElement | null;
+const logEl = document.getElementById("log") as HTMLElement | null;
+const { setStatus, setPageState, writeLog } = createPageUI(statusEl, logEl);
+
+const CONFIGS: Record = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 fr Vector Browser Benchmark",
+    arithShaderPath: "/shaders/curves/bn254/fr_arith.wgsl",
+    vectorShaderPath: "/shaders/curves/bn254/fr_vector.wgsl",
+    arithLabel: "bn254-fr",
+    vectorLabel: "bn254-fr-vector",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 fr Vector Browser Benchmark",
+    arithShaderPath: "/shaders/curves/bls12_377/fr_arith.wgsl",
+    vectorShaderPath: "/shaders/curves/bls12_377/fr_vector.wgsl",
+    arithLabel: "bls12-377-fr",
+    vectorLabel: "bls12-377-fr-vector",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 fr Vector Browser Benchmark",
+    arithShaderPath: "/shaders/curves/bls12_381/fr_arith.wgsl",
+    vectorShaderPath: "/shaders/curves/bls12_381/fr_vector.wgsl",
+    arithLabel: "bls12-381-fr",
+    vectorLabel: "bls12-381-fr-vector",
+  },
+};
+
+function getConfig(): BenchConfig {
+  const curve = new URLSearchParams(window.location.search).get("curve") ?? "bn254";
+  const config = CONFIGS[curve];
+  if (!config) {
+    throw new Error(`unsupported curve: ${curve}`);
+  }
+  return config;
+}
+
+function createKernel(device: GPUDevice, label: string, shaderCode: string, entryPoint: string): Kernel {
+  const shaderModule = device.createShaderModule({ label: `${label}-shader`, code: shaderCode });
+  const bindGroupLayout = device.createBindGroupLayout({
+    label: `${label}-bgl`,
+    entries: [
+      { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
+      { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
+      { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
+      { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
+    ],
+  });
+  const pipelineLayout = device.createPipelineLayout({
+    label: `${label}-pl`,
+    bindGroupLayouts: [bindGroupLayout],
+  });
+  const pipeline = device.createComputePipeline({
+    label: `${label}-pipeline`,
+    layout: pipelineLayout,
+    compute: { module: shaderModule, entryPoint },
+  });
+  return { pipeline, bindGroupLayout };
+}
+
+function makeRegularBatch(count: number, seed: number): Uint32Array {
+  const words = new Uint32Array(count * ELEMENT_WORDS);
+  let state = seed >>> 0;
+  for (let i = 0; i < count; i += 1) {
+    state ^= state << 13;
+    state ^= state >>> 17;
+    state ^= state << 5;
+    words[i * ELEMENT_WORDS + 0] = state >>> 0;
+    state ^= state << 13;
+    state ^= state >>> 17;
+    state ^= state << 5;
+    words[i * ELEMENT_WORDS + 1] = state >>> 0;
+  }
+  return words;
+}
+
+function makeZeroBatch(count: number): Uint32Array {
+  return new Uint32Array(count * ELEMENT_WORDS);
+}
+
+async function runFullPathBenchmark(
+  device: GPUDevice,
+  kernel: Kernel,
+  inputA: Uint32Array,
+  inputB: Uint32Array,
+  opcode: number,
+  logCount: number,
+): Promise<{ out: Uint32Array; profile: Profile }> {
+  const count = inputA.byteLength / ELEMENT_BYTES;
+  const dataBytes = inputA.byteLength;
+  const totalStart = performance.now();
+  const inputABuffer = device.createBuffer({
+    label: "input-a",
+    size: dataBytes,
+    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+  });
+  const inputBBuffer = device.createBuffer({
+    label: "input-b",
+    size: dataBytes,
+    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+  });
+  const outputBuffer = device.createBuffer({
+    label: "output",
+    size: dataBytes,
+    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+  });
+  const stagingBuffer = device.createBuffer({
+    label: "staging",
+    size: dataBytes,
+    usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+  });
+  const uniformBuffer = device.createBuffer({
+    label: "params",
+    size: UNIFORM_BYTES,
+    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+  });
+
+  const uploadStart = performance.now();
+  device.queue.writeBuffer(inputABuffer, 0, inputA.buffer, inputA.byteOffset, inputA.byteLength);
+  device.queue.writeBuffer(inputBBuffer, 0, inputB.buffer, inputB.byteOffset, inputB.byteLength);
+  const params = new Uint32Array(UNIFORM_BYTES / 4);
+  params[0] = count;
+  params[1] = opcode;
+  params[2] = logCount;
+  device.queue.writeBuffer(uniformBuffer, 0, params);
+
+  const bindGroup = device.createBindGroup({
+    label: "bind-group",
+    layout: kernel.bindGroupLayout,
+    entries: [
+      { binding: 0, resource: { buffer: inputABuffer } },
+      { binding: 1, resource: { buffer: inputBBuffer } },
+      { binding: 2, resource: { buffer: outputBuffer } },
+      { binding: 3, resource: { buffer: uniformBuffer } },
+    ],
+  });
+  const uploadMs = performance.now() - uploadStart;
+
+  const kernelStart = performance.now();
+  const encoder = device.createCommandEncoder({ label: "encoder" });
+  const pass = encoder.beginComputePass({ label: "pass" });
+  pass.setPipeline(kernel.pipeline);
+  pass.setBindGroup(0, bindGroup);
+  pass.dispatchWorkgroups(Math.ceil(count / 64));
+  pass.end();
+  encoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, dataBytes);
+  device.queue.submit([encoder.finish()]);
+  const kernelMs = performance.now() - kernelStart;
+
+  const readbackStart = performance.now();
+  await stagingBuffer.mapAsync(GPUMapMode.READ);
+  const out = new Uint32Array(stagingBuffer.getMappedRange().slice(0));
+  stagingBuffer.unmap();
+  const readbackMs = performance.now() - readbackStart;
+
+  inputABuffer.destroy();
+  inputBBuffer.destroy();
+  outputBuffer.destroy();
+  stagingBuffer.destroy();
+  uniformBuffer.destroy();
+
+  return {
+    out,
+    profile: {
+      uploadMs,
+      kernelMs,
+      readbackMs,
+      totalMs: performance.now() - totalStart,
+    },
+  };
+}
+
+async function toMontBatch(device: GPUDevice, arithKernel: Kernel, regularWords: Uint32Array): Promise {
+  const zeros = makeZeroBatch(regularWords.byteLength / ELEMENT_BYTES);
+  return (await runFullPathBenchmark(device, arithKernel, regularWords, zeros, FR_OP_TO_MONT, 0)).out;
+}
+
+async function benchOp(
+  device: GPUDevice,
+  kernel: Kernel,
+  inputA: Uint32Array,
+  inputB: Uint32Array,
+  opcode: number,
+  logCount: number,
+  iters: number,
+): Promise<{ cold: Profile; warm: Profile }> {
+  const cold = await runFullPathBenchmark(device, kernel, inputA, inputB, opcode, logCount);
+  if (iters === 1) {
+    return { cold: cold.profile, warm: cold.profile };
+  }
+  let uploadMs = 0;
+  let kernelMs = 0;
+  let readbackMs = 0;
+  let totalMs = 0;
+  for (let i = 0; i < iters; i += 1) {
+    const warm = await runFullPathBenchmark(device, kernel, inputA, inputB, opcode, logCount);
+    uploadMs += warm.profile.uploadMs;
+    kernelMs += warm.profile.kernelMs;
+    readbackMs += warm.profile.readbackMs;
+    totalMs += warm.profile.totalMs;
+  }
+  return {
+    cold: cold.profile,
+    warm: {
+      uploadMs: uploadMs / iters,
+      kernelMs: kernelMs / iters,
+      readbackMs: readbackMs / iters,
+      totalMs: totalMs / iters,
+    },
+  };
+}
+
+async function runBenchmark(): Promise {
+  const config = getConfig();
+  const lines = [`=== ${config.title} ===`, ""];
+  writeLog(lines);
+  setStatus("Running");
+  setPageState("running");
+  mustElement(runButton, "run").disabled = true;
+
+  try {
+    const minLog = Number.parseInt(mustElement(minLogEl, "min-log").value, 10);
+    const maxLog = Number.parseInt(mustElement(maxLogEl, "max-log").value, 10);
+    const iters = Number.parseInt(mustElement(itersEl, "iters").value, 10);
+    if (!Number.isInteger(minLog) || !Number.isInteger(maxLog) || !Number.isInteger(iters) || minLog < 1 || maxLog < minLog || iters < 1) {
+      throw new Error("invalid benchmark controls");
+    }
+    if (!navigator.gpu) {
+      throw new Error("WebGPU is not available in this browser");
+    }
+
+    const initStart = performance.now();
+    lines.push("1. Requesting adapter... OK");
+    const adapter = await navigator.gpu.requestAdapter();
+    if (!adapter) {
+      throw new Error("requestAdapter returned null");
+    }
+    await appendAdapterDiagnostics(adapter, lines);
+    lines.push("2. Requesting device... OK");
+    const device = await adapter.requestDevice();
+
+    const [arithShader, vectorShader] = await Promise.all([
+      fetchText(config.arithShaderPath),
+      fetchText(config.vectorShaderPath),
+    ]);
+    lines.push("3. Loading shaders... OK");
+
+    const arithKernel = createKernel(device, config.arithLabel, arithShader, "fr_ops_main");
+    const vectorKernel = createKernel(device, config.vectorLabel, vectorShader, "fr_vector_main");
+    const initElapsed = performance.now() - initStart;
+    lines.push("4. Creating pipelines... OK");
+    lines.push(`init_ms = ${initElapsed.toFixed(3)}`);
+    lines.push("");
+    lines.push("size,op,init_ms,cold_upload_ms,cold_kernel_ms,cold_readback_ms,cold_total_ms,cold_with_init_ms,warm_upload_ms,warm_kernel_ms,warm_readback_ms,warm_total_ms");
+
+    for (let logSize = minLog; logSize <= maxLog; logSize += 1) {
+      const size = 1 << logSize;
+      const leftRegular = makeRegularBatch(size, 0x12345678 ^ size);
+      const rightRegular = makeRegularBatch(size, 0x9e3779b9 ^ size);
+      const zeros = makeZeroBatch(size);
+
+      const leftMont = await toMontBatch(device, arithKernel, leftRegular);
+      const rightMont = await toMontBatch(device, arithKernel, rightRegular);
+
+      const addBench = await benchOp(device, vectorKernel, leftMont, rightMont, FR_VECTOR_OP_ADD, 0, iters);
+      lines.push(`${size},add,${initElapsed.toFixed(3)},${addBench.cold.uploadMs.toFixed(3)},${addBench.cold.kernelMs.toFixed(3)},${addBench.cold.readbackMs.toFixed(3)},${addBench.cold.totalMs.toFixed(3)},${(initElapsed + addBench.cold.totalMs).toFixed(3)},${addBench.warm.uploadMs.toFixed(3)},${addBench.warm.kernelMs.toFixed(3)},${addBench.warm.readbackMs.toFixed(3)},${addBench.warm.totalMs.toFixed(3)}`);
+
+      const subBench = await benchOp(device, vectorKernel, leftMont, rightMont, FR_VECTOR_OP_SUB, 0, iters);
+      lines.push(`${size},sub,${initElapsed.toFixed(3)},${subBench.cold.uploadMs.toFixed(3)},${subBench.cold.kernelMs.toFixed(3)},${subBench.cold.readbackMs.toFixed(3)},${subBench.cold.totalMs.toFixed(3)},${(initElapsed + subBench.cold.totalMs).toFixed(3)},${subBench.warm.uploadMs.toFixed(3)},${subBench.warm.kernelMs.toFixed(3)},${subBench.warm.readbackMs.toFixed(3)},${subBench.warm.totalMs.toFixed(3)}`);
+
+      const mulBench = await benchOp(device, vectorKernel, leftMont, rightMont, FR_VECTOR_OP_MUL_FACTORS, 0, iters);
+      lines.push(`${size},mul,${initElapsed.toFixed(3)},${mulBench.cold.uploadMs.toFixed(3)},${mulBench.cold.kernelMs.toFixed(3)},${mulBench.cold.readbackMs.toFixed(3)},${mulBench.cold.totalMs.toFixed(3)},${(initElapsed + mulBench.cold.totalMs).toFixed(3)},${mulBench.warm.uploadMs.toFixed(3)},${mulBench.warm.kernelMs.toFixed(3)},${mulBench.warm.readbackMs.toFixed(3)},${mulBench.warm.totalMs.toFixed(3)}`);
+
+      const bitReverseBench = await benchOp(device, vectorKernel, leftMont, zeros, FR_VECTOR_OP_BIT_REVERSE_COPY, logSize, iters);
+      lines.push(`${size},bit_reverse,${initElapsed.toFixed(3)},${bitReverseBench.cold.uploadMs.toFixed(3)},${bitReverseBench.cold.kernelMs.toFixed(3)},${bitReverseBench.cold.readbackMs.toFixed(3)},${bitReverseBench.cold.totalMs.toFixed(3)},${(initElapsed + bitReverseBench.cold.totalMs).toFixed(3)},${bitReverseBench.warm.uploadMs.toFixed(3)},${bitReverseBench.warm.kernelMs.toFixed(3)},${bitReverseBench.warm.readbackMs.toFixed(3)},${bitReverseBench.warm.totalMs.toFixed(3)}`);
+      writeLog(lines);
+    }
+
+    lines.push("");
+    lines.push(`PASS: ${config.curve} fr vector browser benchmark completed`);
+    writeLog(lines);
+    setStatus("Pass");
+    setPageState("pass");
+  } catch (error) {
+    lines.push(`FAIL: ${error instanceof Error ? error.message : String(error)}`);
+    writeLog(lines);
+    setStatus("Fail");
+    setPageState("fail");
+  } finally {
+    mustElement(runButton, "run").disabled = false;
+  }
+}
+
+mustElement(runButton, "run").addEventListener("click", () => {
+  void runBenchmark();
+});
+
+export {};
diff --git a/backend/accelerated/webgpu/web/tests/api/src/fr_vector_ops_page.ts b/backend/accelerated/webgpu/web/tests/api/src/fr_vector_ops_page.ts
new file mode 100644
index 0000000000..dd6d30265c
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/fr_vector_ops_page.ts
@@ -0,0 +1,250 @@
+export {};
+
+import {
+  bytesToHex,
+  fetchText,
+  hexToBytes,
+} from "../../../src/curvegpu/browser_utils.js";
+import type { CurveModule } from "../../../src/index.js";
+
+type VectorConfig = {
+  curve: string;
+  title: string;
+  vectorPath: string;
+  arithShaderPath: string;
+  vectorShaderPath: string;
+  arithLabel: string;
+  vectorLabel: string;
+};
+
+type VectorCase = {
+  name: string;
+  regular_inputs_le: string[];
+  mont_inputs_le: string[];
+  mont_factors_le: string[];
+  add_expected_le: string[];
+  sub_expected_le: string[];
+  mul_expected_le: string[];
+  to_mont_expected_le: string[];
+  from_mont_expected_le: string[];
+  bit_reverse_expected_le: string[];
+};
+
+type FRVectorOpsVectors = {
+  vector_cases: VectorCase[];
+};
+
+declare const GPUShaderStage: {
+  COMPUTE: number;
+};
+
+declare const GPUBufferUsage: {
+  STORAGE: number;
+  COPY_DST: number;
+  COPY_SRC: number;
+  MAP_READ: number;
+  UNIFORM: number;
+};
+
+declare const GPUMapMode: {
+  READ: number;
+};
+
+const FR_OP_ADD = 3;
+const FR_OP_SUB = 4;
+const FR_OP_MUL = 9;
+const FR_OP_TO_MONT = 11;
+const FR_OP_FROM_MONT = 12;
+
+const FR_VECTOR_OP_MUL_FACTORS = 3;
+const FR_VECTOR_OP_BIT_REVERSE_COPY = 4;
+
+const ELEMENT_BYTES = 32;
+const UNIFORM_BYTES = 32;
+
+const CONFIGS: Record = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 fr Vector Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fr/bn254_fr_vector_ops.json",
+    arithShaderPath: "/shaders/curves/bn254/fr_arith.wgsl",
+    vectorShaderPath: "/shaders/curves/bn254/fr_vector.wgsl",
+    arithLabel: "bn254-fr",
+    vectorLabel: "bn254-fr-vector",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 fr Vector Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fr/bls12_377_fr_vector_ops.json",
+    arithShaderPath: "/shaders/curves/bls12_377/fr_arith.wgsl",
+    vectorShaderPath: "/shaders/curves/bls12_377/fr_vector.wgsl",
+    arithLabel: "bls12-377-fr",
+    vectorLabel: "bls12-377-fr-vector",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 fr Vector Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/fr/bls12_381_fr_vector_ops.json",
+    arithShaderPath: "/shaders/curves/bls12_381/fr_arith.wgsl",
+    vectorShaderPath: "/shaders/curves/bls12_381/fr_vector.wgsl",
+    arithLabel: "bls12-381-fr",
+    vectorLabel: "bls12-381-fr-vector",
+  },
+};
+
+function packHexBatch(hexValues: readonly string[]): Uint8Array {
+  const out = new Uint8Array(hexValues.length * ELEMENT_BYTES);
+  hexValues.forEach((hex, index) => {
+    out.set(hexToBytes(hex), index * ELEMENT_BYTES);
+  });
+  return out;
+}
+
+function createStorageBuffer(device: GPUDevice, label: string, size: number, usage: GPUBufferUsageFlags): GPUBuffer {
+  return device.createBuffer({ label, size, usage });
+}
+
+function createKernel(device: GPUDevice, label: string, shaderCode: string, entryPoint: string): {
+  pipeline: GPUComputePipeline;
+  bindGroupLayout: GPUBindGroupLayout;
+} {
+  const shaderModule = device.createShaderModule({ label: `${label}-shader`, code: shaderCode });
+  const bindGroupLayout = device.createBindGroupLayout({
+    label: `${label}-bgl`,
+    entries: [
+      { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
+      { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
+      { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
+      { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
+    ],
+  });
+  const pipelineLayout = device.createPipelineLayout({
+    label: `${label}-pl`,
+    bindGroupLayouts: [bindGroupLayout],
+  });
+  const pipeline = device.createComputePipeline({
+    label: `${label}-pipeline`,
+    layout: pipelineLayout,
+    compute: { module: shaderModule, entryPoint },
+  });
+  return { pipeline, bindGroupLayout };
+}
+
+async function runKernel(
+  device: GPUDevice,
+  kernel: { pipeline: GPUComputePipeline; bindGroupLayout: GPUBindGroupLayout },
+  aHex: readonly string[],
+  bHex: readonly string[],
+  opcode: number,
+  logCount: number,
+): Promise {
+  const count = aHex.length;
+  const dataBytes = count * ELEMENT_BYTES;
+  const inputA = createStorageBuffer(device, "input-a", dataBytes, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
+  const inputB = createStorageBuffer(device, "input-b", dataBytes, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
+  const output = createStorageBuffer(device, "output", dataBytes, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
+  const staging = createStorageBuffer(device, "staging", dataBytes, GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ);
+  const uniform = device.createBuffer({
+    label: "params",
+    size: UNIFORM_BYTES,
+    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+  });
+
+  const aBytes = packHexBatch(aHex);
+  const bBytes = packHexBatch(bHex);
+  device.queue.writeBuffer(inputA, 0, aBytes.buffer.slice(aBytes.byteOffset, aBytes.byteOffset + aBytes.byteLength));
+  device.queue.writeBuffer(inputB, 0, bBytes.buffer.slice(bBytes.byteOffset, bBytes.byteOffset + bBytes.byteLength));
+  const params = new Uint32Array(UNIFORM_BYTES / 4);
+  params[0] = count;
+  params[1] = opcode;
+  params[2] = logCount;
+  device.queue.writeBuffer(uniform, 0, params.buffer);
+
+  const bindGroup = device.createBindGroup({
+    label: "bind-group",
+    layout: kernel.bindGroupLayout,
+    entries: [
+      { binding: 0, resource: { buffer: inputA } },
+      { binding: 1, resource: { buffer: inputB } },
+      { binding: 2, resource: { buffer: output } },
+      { binding: 3, resource: { buffer: uniform } },
+    ],
+  });
+
+  const encoder = device.createCommandEncoder({ label: "encoder" });
+  const pass = encoder.beginComputePass({ label: "pass" });
+  pass.setPipeline(kernel.pipeline);
+  pass.setBindGroup(0, bindGroup);
+  pass.dispatchWorkgroups(Math.ceil(count / 64));
+  pass.end();
+  encoder.copyBufferToBuffer(output, 0, staging, 0, dataBytes);
+  device.queue.submit([encoder.finish()]);
+
+  await staging.mapAsync(GPUMapMode.READ);
+  const view = new Uint8Array(staging.getMappedRange()).slice();
+  staging.unmap();
+
+  inputA.destroy();
+  inputB.destroy();
+  output.destroy();
+  staging.destroy();
+  uniform.destroy();
+
+  const out: string[] = [];
+  for (let i = 0; i < count; i += 1) {
+    out.push(bytesToHex(view.slice(i * ELEMENT_BYTES, (i + 1) * ELEMENT_BYTES)));
+  }
+  return out;
+}
+
+function expectBatch(name: string, got: readonly string[], want: readonly string[]): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    if (got[i] !== want[i]) {
+      throw new Error(`${name}: mismatch at index ${i}: got=${got[i]} want=${want[i]}`);
+    }
+  }
+}
+
+export async function runSuite(module: CurveModule, log: (msg: string) => void): Promise<{ passed: number; failed: number }> {
+  const config = CONFIGS[module.id];
+  const device = module.context.device;
+  log(`=== ${config.title} ===`);
+  log("");
+
+  const [arithShader, vectorShader] = await Promise.all([
+    fetchText(config.arithShaderPath),
+    fetchText(config.vectorShaderPath),
+  ]);
+  const vectorsText = await fetchText(config.vectorPath);
+  const vectors = JSON.parse(vectorsText) as FRVectorOpsVectors;
+  log(`cases.vector = ${vectors.vector_cases.length}`);
+
+  const arithKernel = createKernel(device, config.arithLabel, arithShader, "fr_ops_main");
+  const vectorKernel = createKernel(device, config.vectorLabel, vectorShader, "fr_vector_main");
+
+  for (const vectorCase of vectors.vector_cases) {
+    const zeros = vectorCase.mont_inputs_le.map(() => "0000000000000000000000000000000000000000000000000000000000000000");
+    expectBatch(`${vectorCase.name}:add`, await runKernel(device, arithKernel, vectorCase.mont_inputs_le, vectorCase.mont_factors_le, FR_OP_ADD, 0), vectorCase.add_expected_le);
+    expectBatch(`${vectorCase.name}:sub`, await runKernel(device, arithKernel, vectorCase.mont_inputs_le, vectorCase.mont_factors_le, FR_OP_SUB, 0), vectorCase.sub_expected_le);
+    expectBatch(`${vectorCase.name}:mul`, await runKernel(device, arithKernel, vectorCase.mont_inputs_le, vectorCase.mont_factors_le, FR_OP_MUL, 0), vectorCase.mul_expected_le);
+    expectBatch(`${vectorCase.name}:to_mont`, await runKernel(device, arithKernel, vectorCase.regular_inputs_le, zeros, FR_OP_TO_MONT, 0), vectorCase.to_mont_expected_le);
+    expectBatch(`${vectorCase.name}:from_mont`, await runKernel(device, arithKernel, vectorCase.mont_inputs_le, zeros, FR_OP_FROM_MONT, 0), vectorCase.from_mont_expected_le);
+    expectBatch(`${vectorCase.name}:mul_factors`, await runKernel(device, vectorKernel, vectorCase.mont_inputs_le, vectorCase.mont_factors_le, FR_VECTOR_OP_MUL_FACTORS, 0), vectorCase.mul_expected_le);
+    const logCount = Math.round(Math.log2(vectorCase.mont_inputs_le.length));
+    expectBatch(`${vectorCase.name}:bit_reverse_copy`, await runKernel(device, vectorKernel, vectorCase.mont_inputs_le, zeros, FR_VECTOR_OP_BIT_REVERSE_COPY, logCount), vectorCase.bit_reverse_expected_le);
+  }
+
+  log("add: OK");
+  log("sub: OK");
+  log("mul: OK");
+  log("to_mont: OK");
+  log("from_mont: OK");
+  log("mul_factors: OK");
+  log("bit_reverse_copy: OK");
+  log("");
+  log(`PASS: ${config.title} succeeded`);
+  return { passed: 1, failed: 0 };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/g1_msm_bench_page.ts b/backend/accelerated/webgpu/web/tests/api/src/g1_msm_bench_page.ts
new file mode 100644
index 0000000000..7f48c7a187
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/g1_msm_bench_page.ts
@@ -0,0 +1,252 @@
+export { };
+
+import {
+  bytesToHex,
+  createPageUI,
+  fetchJSON,
+  hexToBytes,
+  mustElement,
+} from "../../../src/curvegpu/browser_utils.js";
+import { benchmarkTotalDuration } from "./shared/bench_total.js";
+import { createPreferredByteBaseSource } from "../../../src/curvegpu/msm_bench_sources.js";
+import { makeRandomScalarBatch } from "../../../src/curvegpu/msm_shared.js";
+import type {
+  CurveGPUAffinePoint,
+  CurveGPUElementBytes,
+  CurveGPUJacobianPoint,
+  CurveModule,
+  SupportedCurveID,
+} from "../../../src/index.js";
+import { appendContextDiagnostics, createRequestedCurveModule, getRequestedCurveId } from "./shared/page_library.js";
+
+type AffinePoint = {
+  x_bytes_le: string;
+  y_bytes_le: string;
+};
+
+type G1ScalarMulVectors = {
+  generator_affine: AffinePoint;
+  one_mont_z: string;
+};
+
+type CurveBenchConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  successMessage: string;
+  coordinateBytes: number;
+  pointBytes: number;
+  scalarVectorsPath: string;
+  fixtureJSONPath?: string;
+  fixtureBinPath?: string;
+};
+
+const CURVE_CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 G1 MSM Browser Benchmark",
+    successMessage: "BN254 G1 MSM browser benchmark completed",
+    coordinateBytes: 32,
+    pointBytes: 96,
+    scalarVectorsPath: "/tests/fixtures/api/vectors/g1/bn254_g1_scalar_mul.json",
+    fixtureJSONPath: "/tests/fixtures/api/fixtures/g1/bn254_bases_jacobian.json",
+    fixtureBinPath: "/tests/fixtures/api/fixtures/g1/bn254_bases_jacobian.bin",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 G1 MSM Browser Benchmark",
+    successMessage: "BLS12-377 G1 MSM browser benchmark completed",
+    coordinateBytes: 48,
+    pointBytes: 144,
+    scalarVectorsPath: "/tests/fixtures/api/vectors/g1/bls12_377_g1_scalar_mul.json",
+    fixtureJSONPath: "/tests/fixtures/api/fixtures/g1/bls12_377_bases_jacobian.json",
+    fixtureBinPath: "/tests/fixtures/api/fixtures/g1/bls12_377_bases_jacobian.bin",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 G1 MSM Browser Benchmark",
+    successMessage: "BLS12-381 G1 MSM browser benchmark completed",
+    coordinateBytes: 48,
+    pointBytes: 144,
+    scalarVectorsPath: "/tests/fixtures/api/vectors/g1/bls12_381_g1_scalar_mul.json",
+    fixtureJSONPath: "/tests/fixtures/api/fixtures/g1/bls12_381_bases_jacobian.json",
+    fixtureBinPath: "/tests/fixtures/api/fixtures/g1/bls12_381_bases_jacobian.bin",
+  },
+};
+
+const minLogEl = document.getElementById("min-log") as HTMLInputElement | null;
+const maxLogEl = document.getElementById("max-log") as HTMLInputElement | null;
+const itersEl = document.getElementById("iters") as HTMLInputElement | null;
+const runButton = document.getElementById("run") as HTMLButtonElement | null;
+const statusEl = document.getElementById("status") as HTMLElement | null;
+const logEl = document.getElementById("log") as HTMLElement | null;
+const { setStatus, setPageState, writeLog } = createPageUI(statusEl, logEl);
+
+function getConfig(): CurveBenchConfig {
+  const curve = getRequestedCurveId();
+  const config = CURVE_CONFIGS[curve];
+  if (!config) {
+    throw new Error(`g1 MSM benchmark unavailable for curve ${curve}`);
+  }
+  return config;
+}
+
+function affineFromHex(point: AffinePoint): CurveGPUAffinePoint {
+  return { x: hexToBytes(point.x_bytes_le), y: hexToBytes(point.y_bytes_le) };
+}
+
+function makeScalarHexLEFromUint64(value: bigint): string {
+  const out = new Uint8Array(32);
+  let x = value;
+  for (let i = 0; i < 8; i += 1) {
+    out[i] = Number(x & 0xffn);
+    x >>= 8n;
+  }
+  return bytesToHex(out);
+}
+
+function packJacobianPoints(
+  points: readonly CurveGPUJacobianPoint[],
+  coordinateBytes: number,
+  pointBytes: number,
+): Uint8Array {
+  const out = new Uint8Array(points.length * pointBytes);
+  points.forEach((point, index) => {
+    const base = index * pointBytes;
+    out.set(point.x, base);
+    out.set(point.y, base + coordinateBytes);
+    out.set(point.z, base + 2 * coordinateBytes);
+  });
+  return out;
+}
+
+function unpackAffineBases(
+  bytes: Uint8Array,
+  count: number,
+  coordinateBytes: number,
+  pointBytes: number,
+): CurveGPUAffinePoint[] {
+  const out: CurveGPUAffinePoint[] = [];
+  for (let i = 0; i < count; i += 1) {
+    const base = i * pointBytes;
+    out.push({
+      x: bytes.slice(base, base + coordinateBytes),
+      y: bytes.slice(base + coordinateBytes, base + 2 * coordinateBytes),
+    });
+  }
+  return out;
+}
+
+function makeMSMScalars(count: number): CurveGPUElementBytes[] {
+  return makeRandomScalarBatch(count).hexes.map((hex) => hexToBytes(hex) as CurveGPUElementBytes);
+}
+
+function fixtureGenerationHint(curve: SupportedCurveID, size: number): string {
+  return `make fixture-${curve}-g1 COUNT=${size}`;
+}
+
+async function buildGeneratedBases(
+  curve: CurveModule,
+  coordinateBytes: number,
+  pointBytes: number,
+  generator: CurveGPUAffinePoint,
+  count: number,
+): Promise {
+  const bases = Array.from({ length: count }, () => generator);
+  const scalars = Array.from({ length: count }, (_, index) => hexToBytes(makeScalarHexLEFromUint64(BigInt(index + 1))) as CurveGPUElementBytes);
+  const generated = await curve.g1.scalarMulAffineBatch(bases, scalars);
+  return packJacobianPoints(generated, coordinateBytes, pointBytes);
+}
+
+async function runBenchmark(): Promise {
+  const config = getConfig();
+  const lines = [`=== ${config.title} ===`, ""];
+  writeLog(lines);
+  setStatus("Running");
+  setPageState("running");
+  mustElement(runButton, "run").disabled = true;
+
+  try {
+    const minLog = Number.parseInt(mustElement(minLogEl, "min-log").value, 10);
+    const maxLog = Number.parseInt(mustElement(maxLogEl, "max-log").value, 10);
+    const iters = Number.parseInt(mustElement(itersEl, "iters").value, 10);
+    if (!Number.isInteger(minLog) || !Number.isInteger(maxLog) || !Number.isInteger(iters) || minLog < 1 || maxLog < minLog || iters < 1) {
+      throw new Error("invalid benchmark controls");
+    }
+
+    const initStart = performance.now();
+    const curve = await createRequestedCurveModule(config.curve);
+    const scalarVectors = await fetchJSON(config.scalarVectorsPath);
+    const generator = affineFromHex(scalarVectors.generator_affine);
+    const baseSourceProvider = createPreferredByteBaseSource({
+      locationSearch: window.location.search,
+      pointBytes: config.pointBytes,
+      fixtureJSONPath: config.fixtureJSONPath,
+      fixtureBinPath: config.fixtureBinPath,
+      generatedLoadBases: async (size) => buildGeneratedBases(curve, config.coordinateBytes, config.pointBytes, generator, size),
+      generateHint: (size) => fixtureGenerationHint(config.curve, size <= 0 ? (1 << 19) : size),
+    });
+    const baseSourceInit = await baseSourceProvider.init();
+    const initMs = performance.now() - initStart;
+
+    lines.push("1. Requesting adapter... OK");
+    appendContextDiagnostics(lines, curve.context);
+    lines.push("2. Requesting device... OK");
+    lines.push(`3. Loading base source... OK (${baseSourceInit.context.baseSource})`);
+    lines.push(`init_ms = ${initMs.toFixed(3)}`);
+    if (baseSourceInit.postMetricLines) {
+      lines.push(...baseSourceInit.postMetricLines);
+    }
+    lines.push("");
+    lines.push("size,op,window,init_ms,prep_ms,cold_total_ms,cold_with_init_prep_ms,warm_total_ms");
+    writeLog(lines);
+
+    for (let logSize = minLog; logSize <= maxLog; logSize += 1) {
+      const size = 1 << logSize;
+      const prepStart = performance.now();
+      const { bases: baseBytes } = await baseSourceProvider.loadBases({
+        context: baseSourceInit.context,
+        size,
+      });
+      const bases = unpackAffineBases(baseBytes, size, config.coordinateBytes, config.pointBytes);
+      const scalars = makeMSMScalars(size);
+      const prepMs = performance.now() - prepStart;
+      const window = curve.g1msm.bestWindow(size);
+      const benchmark = await benchmarkTotalDuration(iters, async () => {
+        await curve.g1msm.pippengerAffine(bases, scalars, {
+          termsPerInstance: size,
+          window,
+        });
+      });
+      lines.push(
+        [
+          `${size}`,
+          "msm_jac_pippenger_affine_input",
+          `${window}`,
+          initMs.toFixed(3),
+          prepMs.toFixed(3),
+          benchmark.coldMs.toFixed(3),
+          (initMs + prepMs + benchmark.coldMs).toFixed(3),
+          benchmark.warmMs.toFixed(3),
+        ].join(","),
+      );
+      writeLog(lines);
+    }
+
+    lines.push("");
+    lines.push(`PASS: ${config.successMessage}`);
+    writeLog(lines);
+    setStatus("Pass");
+    setPageState("pass");
+  } catch (error) {
+    lines.push(`FAIL: ${error instanceof Error ? error.message : String(error)}`);
+    writeLog(lines);
+    setStatus("Fail");
+    setPageState("fail");
+  } finally {
+    mustElement(runButton, "run").disabled = false;
+  }
+}
+
+mustElement(runButton, "run").addEventListener("click", () => {
+  void runBenchmark();
+});
diff --git a/backend/accelerated/webgpu/web/tests/api/src/g1_msm_page.ts b/backend/accelerated/webgpu/web/tests/api/src/g1_msm_page.ts
new file mode 100644
index 0000000000..ee72bda1a6
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/g1_msm_page.ts
@@ -0,0 +1,172 @@
+export { };
+
+import { bytesToHex, fetchJSON, hexToBytes } from "../../../src/curvegpu/browser_utils.js";
+import type {
+  CurveGPUAffinePoint,
+  CurveGPUElementBytes,
+  CurveGPUJacobianPoint,
+  CurveModule,
+  SupportedCurveID,
+} from "../../../src/index.js";
+import { curveDisplayName } from "./shared/page_library.js";
+
+type AffinePoint = {
+  x_bytes_le: string;
+  y_bytes_le: string;
+};
+
+type JacobianPoint = {
+  x_bytes_le: string;
+  y_bytes_le: string;
+  z_bytes_le: string;
+};
+
+type MSMCase = {
+  name: string;
+  bases_affine: AffinePoint[];
+  scalars_bytes_le: string[];
+  expected_affine: JacobianPoint;
+};
+
+type G1MSMVectors = {
+  terms_per_instance: number;
+  msm_cases: MSMCase[];
+  one_mont_z: string;
+};
+
+type G1MSMConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  vectorPath: string;
+};
+
+const CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 G1 MSM Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g1/bn254_g1_msm.json",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 G1 MSM Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g1/bls12_377_g1_msm.json",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 G1 MSM Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g1/bls12_381_g1_msm.json",
+  },
+};
+
+function affineFromHex(point: AffinePoint): CurveGPUAffinePoint {
+  return { x: hexToBytes(point.x_bytes_le), y: hexToBytes(point.y_bytes_le) };
+}
+
+function jacobianToHex(point: CurveGPUJacobianPoint): JacobianPoint {
+  return {
+    x_bytes_le: bytesToHex(point.x),
+    y_bytes_le: bytesToHex(point.y),
+    z_bytes_le: bytesToHex(point.z),
+  };
+}
+
+function toAffinePoint(point: CurveGPUJacobianPoint): CurveGPUAffinePoint {
+  return { x: point.x, y: point.y };
+}
+
+function expectPointBatch(name: string, got: readonly CurveGPUJacobianPoint[], want: readonly JacobianPoint[]): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = jacobianToHex(got[i]);
+    if (
+      gotHex.x_bytes_le !== want[i].x_bytes_le ||
+      gotHex.y_bytes_le !== want[i].y_bytes_le ||
+      gotHex.z_bytes_le !== want[i].z_bytes_le
+    ) {
+      throw new Error(
+        `${name}: mismatch at index ${i}` +
+        ` got=(${gotHex.x_bytes_le},${gotHex.y_bytes_le},${gotHex.z_bytes_le})` +
+        ` want=(${want[i].x_bytes_le},${want[i].y_bytes_le},${want[i].z_bytes_le})`,
+      );
+    }
+  }
+}
+
+function expectAffineBatch(name: string, got: readonly CurveGPUAffinePoint[], want: readonly JacobianPoint[]): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = {
+      x_bytes_le: bytesToHex(got[i].x),
+      y_bytes_le: bytesToHex(got[i].y),
+    };
+    if (gotHex.x_bytes_le !== want[i].x_bytes_le || gotHex.y_bytes_le !== want[i].y_bytes_le) {
+      throw new Error(
+        `${name}: mismatch at index ${i}` +
+        ` got=(${gotHex.x_bytes_le},${gotHex.y_bytes_le})` +
+        ` want=(${want[i].x_bytes_le},${want[i].y_bytes_le})`,
+      );
+    }
+  }
+}
+
+async function naiveMSMAffine(
+  curve: CurveModule,
+  bases: readonly CurveGPUAffinePoint[],
+  scalars: readonly CurveGPUElementBytes[],
+): Promise {
+  const scaled = await curve.g1.scalarMulAffineBatch(bases, scalars);
+  if (scaled.length === 0) {
+    return curve.g1.affineInfinity();
+  }
+  let accJacobian = await curve.g1.affineToJacobian(toAffinePoint(scaled[0]));
+  for (let i = 1; i < scaled.length; i += 1) {
+    accJacobian = await curve.g1.addMixed(accJacobian, toAffinePoint(scaled[i]));
+  }
+  return curve.g1.jacobianToAffine(accJacobian);
+}
+
+export async function runSuite(module: CurveModule, log: (msg: string) => void): Promise<{ passed: number; failed: number }> {
+  const config = CONFIGS[module.id];
+  if (!config) {
+    throw new Error(`g1 MSM vectors unavailable for curve ${module.id}`);
+  }
+  log(`=== ${config.title} ===`);
+  log("");
+  const vectors = await fetchJSON(config.vectorPath);
+  log(`terms_per_instance = ${vectors.terms_per_instance}`);
+  log(`cases.msm = ${vectors.msm_cases.length}`);
+
+  const naiveResults: CurveGPUAffinePoint[] = [];
+  for (const msmCase of vectors.msm_cases) {
+    naiveResults.push(
+      await naiveMSMAffine(
+        module,
+        msmCase.bases_affine.map(affineFromHex),
+        msmCase.scalars_bytes_le.map((value) => hexToBytes(value) as CurveGPUElementBytes),
+      ),
+    );
+  }
+  expectAffineBatch("msm_naive_affine", naiveResults, vectors.msm_cases.map((item) => item.expected_affine));
+  log("msm_naive_affine: OK");
+
+  const window = 4;
+  const pippengerResults = await module.g1msm.pippengerAffineBatch(
+    vectors.msm_cases.flatMap((item) => item.bases_affine.map(affineFromHex)),
+    vectors.msm_cases.flatMap((item) => item.scalars_bytes_le.map((value) => hexToBytes(value) as CurveGPUElementBytes)),
+    {
+      count: vectors.msm_cases.length,
+      termsPerInstance: vectors.terms_per_instance,
+      window,
+    },
+  );
+  expectPointBatch(`msm_jac_pippenger_affine_input (window=${window})`, pippengerResults, vectors.msm_cases.map((item) => item.expected_affine));
+  log(`msm_jac_pippenger_affine_input (window=${window}): OK`);
+
+  log("");
+  log(`PASS: ${curveDisplayName(module.id)} G1 MSM browser smoke succeeded`);
+  return { passed: 1, failed: 0 };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/g1_ops_page.ts b/backend/accelerated/webgpu/web/tests/api/src/g1_ops_page.ts
new file mode 100644
index 0000000000..5d9b87cbd1
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/g1_ops_page.ts
@@ -0,0 +1,162 @@
+export { };
+
+import { bytesToHex, fetchJSON, hexToBytes } from "../../../src/curvegpu/browser_utils.js";
+import type {
+  CurveGPUAffinePoint,
+  CurveGPUJacobianPoint,
+  CurveModule,
+  G1Module,
+  SupportedCurveID,
+} from "../../../src/index.js";
+import { curveDisplayName } from "./shared/page_library.js";
+
+type AffinePoint = {
+  x_bytes_le: string;
+  y_bytes_le: string;
+};
+
+type JacobianPoint = {
+  x_bytes_le: string;
+  y_bytes_le: string;
+  z_bytes_le: string;
+};
+
+type G1Case = {
+  name: string;
+  p_affine: AffinePoint;
+  q_affine: AffinePoint;
+  p_jacobian: JacobianPoint;
+  p_affine_output: JacobianPoint;
+  neg_p_jacobian: JacobianPoint;
+  double_p_jacobian: JacobianPoint;
+  add_mixed_p_plus_q_jacobian: JacobianPoint;
+  affine_add_p_plus_q: JacobianPoint;
+};
+
+type G1OpsVectors = {
+  point_cases: G1Case[];
+};
+
+type G1OpsConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  vectorPath: string;
+};
+
+const CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 G1 Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g1/bn254_g1_ops.json",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 G1 Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g1/bls12_377_g1_ops.json",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 G1 Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g1/bls12_381_g1_ops.json",
+  },
+};
+
+function affineFromHex(point: AffinePoint): CurveGPUAffinePoint {
+  return { x: hexToBytes(point.x_bytes_le), y: hexToBytes(point.y_bytes_le) };
+}
+
+function jacobianFromHex(point: JacobianPoint): CurveGPUJacobianPoint {
+  return {
+    x: hexToBytes(point.x_bytes_le),
+    y: hexToBytes(point.y_bytes_le),
+    z: hexToBytes(point.z_bytes_le),
+  };
+}
+
+function jacobianToHex(point: CurveGPUJacobianPoint): JacobianPoint {
+  return {
+    x_bytes_le: bytesToHex(point.x),
+    y_bytes_le: bytesToHex(point.y),
+    z_bytes_le: bytesToHex(point.z),
+  };
+}
+
+function affineToHex(point: CurveGPUAffinePoint): AffinePoint {
+  return {
+    x_bytes_le: bytesToHex(point.x),
+    y_bytes_le: bytesToHex(point.y),
+  };
+}
+
+function expectPointBatch(name: string, got: readonly CurveGPUJacobianPoint[], want: readonly JacobianPoint[], log: (msg: string) => void): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = jacobianToHex(got[i]);
+    if (
+      gotHex.x_bytes_le !== want[i].x_bytes_le ||
+      gotHex.y_bytes_le !== want[i].y_bytes_le ||
+      gotHex.z_bytes_le !== want[i].z_bytes_le
+    ) {
+      throw new Error(`${name}: mismatch at index ${i}`);
+    }
+  }
+  log(`${name}: OK`);
+}
+
+function expectAffineBatch(name: string, got: readonly CurveGPUAffinePoint[], want: readonly AffinePoint[], log: (msg: string) => void): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = affineToHex(got[i]);
+    if (gotHex.x_bytes_le !== want[i].x_bytes_le || gotHex.y_bytes_le !== want[i].y_bytes_le) {
+      throw new Error(`${name}: mismatch at index ${i}`);
+    }
+  }
+  log(`${name}: OK`);
+}
+
+export async function runSuite(module: CurveModule, log: (msg: string) => void): Promise<{ passed: number; failed: number }> {
+  const config = CONFIGS[module.id];
+  if (!config) {
+    throw new Error(`g1 ops vectors unavailable for curve ${module.id}`);
+  }
+  log(`=== ${config.title} ===`);
+  log("");
+  const vectors = await fetchJSON(config.vectorPath);
+  log(`cases.g1 = ${vectors.point_cases.length}`);
+
+  const g1: G1Module = module.g1;
+  const pAffine = vectors.point_cases.map((item) => affineFromHex(item.p_affine));
+  const qAffine = vectors.point_cases.map((item) => affineFromHex(item.q_affine));
+  const pJacobian = vectors.point_cases.map((item) => jacobianFromHex(item.p_jacobian));
+  const negWant = vectors.point_cases.map((item) => item.neg_p_jacobian);
+  const doubleWant = vectors.point_cases.map((item) => item.double_p_jacobian);
+  const addWant = vectors.point_cases.map((item) => item.add_mixed_p_plus_q_jacobian);
+  const affineWant = vectors.point_cases.map((item) => ({
+    x_bytes_le: item.p_affine_output.x_bytes_le,
+    y_bytes_le: item.p_affine_output.y_bytes_le,
+  }));
+  const affineAddWant = vectors.point_cases.map((item) => item.affine_add_p_plus_q);
+  const oneMont = await module.fp.montOne();
+  const jacInfinityWant = vectors.point_cases.map(() => ({
+    x_bytes_le: bytesToHex(oneMont),
+    y_bytes_le: bytesToHex(oneMont),
+    z_bytes_le: bytesToHex(module.fp.zero()),
+  }));
+
+  expectPointBatch("copy", await g1.copyBatch(pJacobian), vectors.point_cases.map((item) => item.p_jacobian), log);
+  expectPointBatch("jac_infinity", await g1.jacobianInfinityBatch(vectors.point_cases.length), jacInfinityWant, log);
+  expectPointBatch("affine_to_jac", await g1.affineToJacobianBatch(pAffine), vectors.point_cases.map((item) => item.p_jacobian), log);
+  expectPointBatch("neg_jac", await g1.negJacobianBatch(pJacobian), negWant, log);
+  expectAffineBatch("jac_to_affine", await g1.jacobianToAffineBatch(pJacobian), affineWant, log);
+  expectPointBatch("double_jac", await g1.doubleJacobianBatch(pJacobian), doubleWant, log);
+  expectPointBatch("add_mixed", await g1.addMixedBatch(pJacobian, qAffine), addWant, log);
+  expectPointBatch("affine_add", await g1.affineAddBatch(pAffine, qAffine), affineAddWant, log);
+
+  log("");
+  log(`PASS: ${curveDisplayName(module.id)} G1 browser smoke succeeded`);
+  return { passed: 1, failed: 0 };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/g1_scalar_mul_page.ts b/backend/accelerated/webgpu/web/tests/api/src/g1_scalar_mul_page.ts
new file mode 100644
index 0000000000..66341bb121
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/g1_scalar_mul_page.ts
@@ -0,0 +1,121 @@
+export { };
+
+import { bytesToHex, fetchJSON, hexToBytes } from "../../../src/curvegpu/browser_utils.js";
+import type {
+  CurveGPUAffinePoint,
+  CurveGPUElementBytes,
+  CurveGPUJacobianPoint,
+  CurveModule,
+  SupportedCurveID,
+} from "../../../src/index.js";
+import { curveDisplayName } from "./shared/page_library.js";
+
+type AffinePoint = {
+  x_bytes_le: string;
+  y_bytes_le: string;
+};
+
+type JacobianPoint = {
+  x_bytes_le: string;
+  y_bytes_le: string;
+  z_bytes_le: string;
+};
+
+type ScalarMulCase = {
+  name: string;
+  base_affine: AffinePoint;
+  scalar_bytes_le: string;
+  scalar_mul_affine: JacobianPoint;
+};
+
+type BaseMulCase = {
+  name: string;
+  scalar_bytes_le: string;
+  scalar_mul_base_affine: JacobianPoint;
+};
+
+type G1ScalarMulVectors = {
+  generator_affine: AffinePoint;
+  scalar_cases: ScalarMulCase[];
+  base_cases: BaseMulCase[];
+};
+
+type G1ScalarConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  vectorPath: string;
+};
+
+const CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 G1 Scalar Mul Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g1/bn254_g1_scalar_mul.json",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 G1 Scalar Mul Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g1/bls12_377_g1_scalar_mul.json",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 G1 Scalar Mul Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g1/bls12_381_g1_scalar_mul.json",
+  },
+};
+
+function affineFromHex(point: AffinePoint): CurveGPUAffinePoint {
+  return { x: hexToBytes(point.x_bytes_le), y: hexToBytes(point.y_bytes_le) };
+}
+
+function jacobianToHex(point: CurveGPUJacobianPoint): JacobianPoint {
+  return {
+    x_bytes_le: bytesToHex(point.x),
+    y_bytes_le: bytesToHex(point.y),
+    z_bytes_le: bytesToHex(point.z),
+  };
+}
+
+function expectPointBatch(name: string, got: readonly CurveGPUJacobianPoint[], want: readonly JacobianPoint[], log: (msg: string) => void): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = jacobianToHex(got[i]);
+    if (
+      gotHex.x_bytes_le !== want[i].x_bytes_le ||
+      gotHex.y_bytes_le !== want[i].y_bytes_le ||
+      gotHex.z_bytes_le !== want[i].z_bytes_le
+    ) {
+      throw new Error(`${name}: mismatch at index ${i}`);
+    }
+  }
+  log(`${name}: OK`);
+}
+
+export async function runSuite(module: CurveModule, log: (msg: string) => void): Promise<{ passed: number; failed: number }> {
+  const config = CONFIGS[module.id];
+  if (!config) {
+    throw new Error(`g1 scalar-mul vectors unavailable for curve ${module.id}`);
+  }
+  log(`=== ${config.title} ===`);
+  log("");
+  const vectors = await fetchJSON(config.vectorPath);
+  log(`cases.scalar = ${vectors.scalar_cases.length}`);
+  log(`cases.base = ${vectors.base_cases.length}`);
+
+  const scalarBases = vectors.scalar_cases.map((item) => affineFromHex(item.base_affine));
+  const scalarScalars = vectors.scalar_cases.map((item) => hexToBytes(item.scalar_bytes_le) as CurveGPUElementBytes);
+  const scalarWant = vectors.scalar_cases.map((item) => item.scalar_mul_affine);
+  expectPointBatch("scalar_mul_affine", await module.g1.scalarMulAffineBatch(scalarBases, scalarScalars), scalarWant, log);
+
+  const generator = affineFromHex(vectors.generator_affine);
+  const baseBases = Array.from({ length: vectors.base_cases.length }, () => generator);
+  const baseScalars = vectors.base_cases.map((item) => hexToBytes(item.scalar_bytes_le) as CurveGPUElementBytes);
+  const baseWant = vectors.base_cases.map((item) => item.scalar_mul_base_affine);
+  expectPointBatch("scalar_mul_base_affine", await module.g1.scalarMulAffineBatch(baseBases, baseScalars), baseWant, log);
+
+  log("");
+  log(`PASS: ${curveDisplayName(module.id)} G1 scalar mul browser smoke succeeded`);
+  return { passed: 1, failed: 0 };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/g2_msm_bench_page.ts b/backend/accelerated/webgpu/web/tests/api/src/g2_msm_bench_page.ts
new file mode 100644
index 0000000000..2e0ac07dd0
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/g2_msm_bench_page.ts
@@ -0,0 +1,312 @@
+export { };
+
+import {
+  bytesToHex,
+  createPageUI,
+  fetchJSON,
+  hexToBytes,
+  mustElement,
+  yieldToBrowser,
+} from "../../../src/curvegpu/browser_utils.js";
+import { benchmarkTotalDuration } from "./shared/bench_total.js";
+import { createPreferredByteBaseSource } from "../../../src/curvegpu/msm_bench_sources.js";
+import { makeRandomScalarBatch } from "../../../src/curvegpu/msm_shared.js";
+import type {
+  CurveGPUElementBytes,
+  CurveGPUFp2Element,
+  CurveGPUG2AffinePoint,
+  CurveGPUG2JacobianPoint,
+  SupportedCurveID,
+} from "../../../src/index.js";
+import { appendContextDiagnostics, createRequestedCurveModule } from "./shared/page_library.js";
+
+type Fp2Point = {
+  c0_bytes_le: string;
+  c1_bytes_le: string;
+};
+
+type AffinePoint = {
+  x: Fp2Point;
+  y: Fp2Point;
+};
+
+type G2Case = {
+  name: string;
+  p_affine: AffinePoint;
+  q_affine: AffinePoint;
+};
+
+type G2OpsVectors = {
+  point_cases: G2Case[];
+};
+
+type CurveBenchConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  successMessage: string;
+  componentBytes: number;
+  pointBytes: number;
+  opsVectorsPath: string;
+  fixtureJSONPath?: string;
+  fixtureBinPath?: string;
+};
+
+const CURVE_CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 G2 MSM Browser Benchmark",
+    successMessage: "BN254 G2 MSM browser benchmark completed",
+    componentBytes: 32,
+    pointBytes: 192,
+    opsVectorsPath: "/tests/fixtures/api/vectors/g2/bn254_g2_ops.json",
+    fixtureJSONPath: "/tests/fixtures/api/fixtures/g2/bn254_bases_jacobian.json",
+    fixtureBinPath: "/tests/fixtures/api/fixtures/g2/bn254_bases_jacobian.bin",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 G2 MSM Browser Benchmark",
+    successMessage: "BLS12-377 G2 MSM browser benchmark completed",
+    componentBytes: 48,
+    pointBytes: 288,
+    opsVectorsPath: "/tests/fixtures/api/vectors/g2/bls12_377_g2_ops.json",
+    fixtureJSONPath: "/tests/fixtures/api/fixtures/g2/bls12_377_bases_jacobian.json",
+    fixtureBinPath: "/tests/fixtures/api/fixtures/g2/bls12_377_bases_jacobian.bin",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 G2 MSM Browser Benchmark",
+    successMessage: "BLS12-381 G2 MSM browser benchmark completed",
+    componentBytes: 48,
+    pointBytes: 288,
+    opsVectorsPath: "/tests/fixtures/api/vectors/g2/bls12_381_g2_ops.json",
+    fixtureJSONPath: "/tests/fixtures/api/fixtures/g2/bls12_381_bases_jacobian.json",
+    fixtureBinPath: "/tests/fixtures/api/fixtures/g2/bls12_381_bases_jacobian.bin",
+  },
+};
+
+const minLogEl = document.getElementById("min-log") as HTMLInputElement | null;
+const maxLogEl = document.getElementById("max-log") as HTMLInputElement | null;
+const itersEl = document.getElementById("iters") as HTMLInputElement | null;
+const runButton = document.getElementById("run") as HTMLButtonElement | null;
+const statusEl = document.getElementById("status") as HTMLElement | null;
+const logEl = document.getElementById("log") as HTMLElement | null;
+const { setStatus, setPageState, writeLog } = createPageUI(statusEl, logEl);
+
+function getConfig(curve: SupportedCurveID): CurveBenchConfig {
+  const config = CURVE_CONFIGS[curve];
+  if (!config) {
+    throw new Error(`g2 MSM benchmark unavailable for curve ${curve}`);
+  }
+  return config;
+}
+
+function fp2FromHex(point: Fp2Point): CurveGPUFp2Element {
+  return { c0: hexToBytes(point.c0_bytes_le), c1: hexToBytes(point.c1_bytes_le) };
+}
+
+function affineFromHex(point: AffinePoint): CurveGPUG2AffinePoint {
+  return { x: fp2FromHex(point.x), y: fp2FromHex(point.y) };
+}
+
+function isAffineInfinity(point: CurveGPUG2AffinePoint): boolean {
+  return (
+    point.x.c0.every((byte) => byte === 0) &&
+    point.x.c1.every((byte) => byte === 0) &&
+    point.y.c0.every((byte) => byte === 0) &&
+    point.y.c1.every((byte) => byte === 0)
+  );
+}
+
+function findGeneratorPoint(vectors: G2OpsVectors): CurveGPUG2AffinePoint {
+  for (const item of vectors.point_cases) {
+    for (const point of [item.p_affine, item.q_affine]) {
+      const parsed = affineFromHex(point);
+      if (!isAffineInfinity(parsed)) {
+        return parsed;
+      }
+    }
+  }
+  throw new Error("no non-infinity G2 point found in vectors");
+}
+
+function makeScalarHexLEFromUint64(value: bigint): string {
+  const out = new Uint8Array(32);
+  let x = value;
+  for (let i = 0; i < 8; i += 1) {
+    out[i] = Number(x & 0xffn);
+    x >>= 8n;
+  }
+  return bytesToHex(out);
+}
+
+function packJacobianPoints(
+  points: readonly CurveGPUG2JacobianPoint[],
+  componentBytes: number,
+  pointBytes: number,
+): Uint8Array {
+  const out = new Uint8Array(points.length * pointBytes);
+  points.forEach((point, index) => {
+    const base = index * pointBytes;
+    out.set(point.x.c0, base);
+    out.set(point.x.c1, base + componentBytes);
+    out.set(point.y.c0, base + 2 * componentBytes);
+    out.set(point.y.c1, base + 3 * componentBytes);
+    out.set(point.z.c0, base + 4 * componentBytes);
+    out.set(point.z.c1, base + 5 * componentBytes);
+  });
+  return out;
+}
+
+function makeMSMScalarsPacked(count: number): Uint8Array {
+  const out = new Uint8Array(count * 32);
+  const hexes = makeRandomScalarBatch(count).hexes;
+  for (let i = 0; i < count; i += 1) {
+    out.set(hexToBytes(hexes[i]), i * 32);
+  }
+  return out;
+}
+
+function fixtureGenerationHint(curve: SupportedCurveID, size: number): string {
+  return `make fixture-${curve}-g2 COUNT=${size}`;
+}
+
+async function buildGeneratedBases(
+  curve: Awaited>,
+  config: CurveBenchConfig,
+  generator: CurveGPUG2AffinePoint,
+  count: number,
+): Promise {
+  const bases = Array.from({ length: count }, () => generator);
+  const scalars = Array.from(
+    { length: count },
+    (_, index) => hexToBytes(makeScalarHexLEFromUint64(BigInt(index + 1))) as CurveGPUElementBytes,
+  );
+  const generated = await curve.g2.scalarMulAffineBatch(bases, scalars);
+  return packJacobianPoints(generated, config.componentBytes, config.pointBytes);
+}
+
+async function runBenchmark(): Promise {
+  const params = new URLSearchParams(window.location.search);
+  const curveId = (params.get("curve") ?? "bn254") as SupportedCurveID;
+  const config = getConfig(curveId);
+  const lines = [`=== ${config.title} ===`, ""];
+  writeLog(lines);
+  setStatus("Running");
+  setPageState("running");
+  mustElement(runButton, "run").disabled = true;
+
+  try {
+    const minLog = Number.parseInt(mustElement(minLogEl, "min-log").value, 10);
+    const maxLog = Number.parseInt(mustElement(maxLogEl, "max-log").value, 10);
+    const iters = Number.parseInt(mustElement(itersEl, "iters").value, 10);
+    if (!Number.isInteger(minLog) || !Number.isInteger(maxLog) || !Number.isInteger(iters) || minLog < 1 || maxLog < minLog || iters < 1) {
+      throw new Error("invalid benchmark controls");
+    }
+
+    const initStart = performance.now();
+    const curve = await createRequestedCurveModule(config.curve);
+    const g2Vectors = await fetchJSON(config.opsVectorsPath);
+    const generator = findGeneratorPoint(g2Vectors);
+    const baseSourceProvider = createPreferredByteBaseSource({
+      locationSearch: window.location.search,
+      pointBytes: config.pointBytes,
+      fixtureJSONPath: config.fixtureJSONPath,
+      fixtureBinPath: config.fixtureBinPath,
+      generatedLoadBases: async (size) => buildGeneratedBases(curve, config, generator, size),
+      generateHint: (size) => fixtureGenerationHint(config.curve, size <= 0 ? (1 << 14) : size),
+      fixtureLabel: "G2 base",
+    });
+    const baseSourceInit = await baseSourceProvider.init();
+    const initMs = performance.now() - initStart;
+
+    lines.push("1. Requesting adapter... OK");
+    appendContextDiagnostics(lines, curve.context);
+    lines.push("2. Requesting device... OK");
+    lines.push(`3. Loading base source... OK (${baseSourceInit.context.baseSource})`);
+    lines.push(`init_ms = ${initMs.toFixed(3)}`);
+    if (baseSourceInit.postMetricLines) {
+      lines.push(...baseSourceInit.postMetricLines);
+    }
+    const prewarmSize = 1 << minLog;
+    lines.push(`4. Prewarming G2 MSM runtime at size ${prewarmSize}...`);
+    writeLog(lines);
+    await yieldToBrowser();
+    {
+      const { bases: prewarmBases } = await baseSourceProvider.loadBases({
+        context: baseSourceInit.context,
+        size: prewarmSize,
+      });
+      const prewarmScalars = makeMSMScalarsPacked(prewarmSize);
+      const prewarmWindow = curve.g2msm.bestWindow(prewarmSize);
+      await curve.g2msm.pippengerPackedJacobianBases(prewarmBases, prewarmScalars, {
+        count: 1,
+        termsPerInstance: prewarmSize,
+        window: prewarmWindow,
+      });
+    }
+    lines[lines.length - 1] = `4. Prewarming G2 MSM runtime at size ${prewarmSize}... OK`;
+    lines.push("");
+    lines.push("size,op,window,init_ms,prep_ms,cold_total_ms,cold_with_init_prep_ms,warm_total_ms");
+    writeLog(lines);
+    await yieldToBrowser();
+
+    for (let logSize = minLog; logSize <= maxLog; logSize += 1) {
+      await yieldToBrowser();
+      const size = 1 << logSize;
+      const prepStart = performance.now();
+      const { bases: baseBytes } = await baseSourceProvider.loadBases({
+        context: baseSourceInit.context,
+        size,
+      });
+      const scalarsPacked = makeMSMScalarsPacked(size);
+      const prepMs = performance.now() - prepStart;
+      const window = curve.g2msm.bestWindow(size);
+      const jacBenchmark = await benchmarkTotalDuration(iters, async () => {
+        await curve.g2msm.pippengerPackedJacobianBases(baseBytes, scalarsPacked, {
+          count: 1,
+          termsPerInstance: size,
+          window,
+        });
+      }, yieldToBrowser);
+      lines.push(
+        [
+          `${size}`,
+          "msm_jac_pippenger_packed",
+          `${window}`,
+          initMs.toFixed(3),
+          prepMs.toFixed(3),
+          jacBenchmark.coldMs.toFixed(3),
+          (initMs + prepMs + jacBenchmark.coldMs).toFixed(3),
+          jacBenchmark.warmMs.toFixed(3),
+        ].join(","),
+      );
+      writeLog(lines);
+    }
+
+    lines.push("");
+    lines.push(`PASS: ${config.successMessage}`);
+    writeLog(lines);
+    setStatus("Pass");
+    setPageState("pass");
+  } catch (error) {
+    lines.push(`FAIL: ${error instanceof Error ? error.message : String(error)}`);
+    writeLog(lines);
+    setStatus("Fail");
+    setPageState("fail");
+  } finally {
+    mustElement(runButton, "run").disabled = false;
+  }
+}
+
+mustElement(runButton, "run").addEventListener("click", () => {
+  void runBenchmark();
+});
+
+const params = new URLSearchParams(window.location.search);
+const curveId = (params.get("curve") ?? "bn254") as SupportedCurveID;
+const config = getConfig(curveId);
+if (params.get("autorun") === "1") {
+  void runBenchmark();
+} else {
+  writeLog([`=== ${config.title} ===`, "", `Press Run to benchmark ${config.curve} G2 MSM in browser WebGPU.`]);
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/g2_msm_page.ts b/backend/accelerated/webgpu/web/tests/api/src/g2_msm_page.ts
new file mode 100644
index 0000000000..b0856ce9a9
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/g2_msm_page.ts
@@ -0,0 +1,255 @@
+export { };
+
+import { bytesToHex, fetchJSON, hexToBytes } from "../../../src/curvegpu/browser_utils.js";
+import type {
+  CurveGPUElementBytes,
+  CurveGPUFp2Element,
+  CurveGPUG2AffinePoint,
+  CurveGPUG2JacobianPoint,
+  CurveModule,
+  SupportedCurveID,
+} from "../../../src/index.js";
+import { curveDisplayName } from "./shared/page_library.js";
+
+type Fp2Point = {
+  c0_bytes_le: string;
+  c1_bytes_le: string;
+};
+
+type AffinePoint = {
+  x: Fp2Point;
+  y: Fp2Point;
+};
+
+type JacobianPoint = {
+  x: Fp2Point;
+  y: Fp2Point;
+  z: Fp2Point;
+};
+
+type MSMCase = {
+  name: string;
+  bases_affine: AffinePoint[];
+  scalars_bytes_le: string[];
+  expected_affine: JacobianPoint;
+};
+
+type G2MSMVectors = {
+  terms_per_instance: number;
+  msm_cases: MSMCase[];
+};
+
+type G2MSMConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  vectorPath: string;
+};
+
+const CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 G2 MSM Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g2/bn254_g2_msm.json",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 G2 MSM Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g2/bls12_377_g2_msm.json",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 G2 MSM Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g2/bls12_381_g2_msm.json",
+  },
+};
+
+function fp2FromHex(point: Fp2Point): CurveGPUFp2Element {
+  return { c0: hexToBytes(point.c0_bytes_le), c1: hexToBytes(point.c1_bytes_le) };
+}
+
+function affineFromHex(point: AffinePoint): CurveGPUG2AffinePoint {
+  return { x: fp2FromHex(point.x), y: fp2FromHex(point.y) };
+}
+
+function affineToHex(point: CurveGPUG2AffinePoint): AffinePoint {
+  return {
+    x: { c0_bytes_le: bytesToHex(point.x.c0), c1_bytes_le: bytesToHex(point.x.c1) },
+    y: { c0_bytes_le: bytesToHex(point.y.c0), c1_bytes_le: bytesToHex(point.y.c1) },
+  };
+}
+
+function toAffinePoint(point: CurveGPUG2JacobianPoint): CurveGPUG2AffinePoint {
+  return { x: point.x, y: point.y };
+}
+
+function equalFp2(a: Fp2Point, b: Fp2Point): boolean {
+  return a.c0_bytes_le === b.c0_bytes_le && a.c1_bytes_le === b.c1_bytes_le;
+}
+
+function expectAffineBatch(name: string, got: readonly CurveGPUG2AffinePoint[], want: readonly JacobianPoint[]): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = affineToHex(got[i]);
+    if (!equalFp2(gotHex.x, want[i].x) || !equalFp2(gotHex.y, want[i].y)) {
+      throw new Error(
+        `${name}: mismatch at index ${i}` +
+        ` got=(${gotHex.x.c0_bytes_le}/${gotHex.x.c1_bytes_le},${gotHex.y.c0_bytes_le}/${gotHex.y.c1_bytes_le})` +
+        ` want=(${want[i].x.c0_bytes_le}/${want[i].x.c1_bytes_le},${want[i].y.c0_bytes_le}/${want[i].y.c1_bytes_le})`,
+      );
+    }
+  }
+}
+
+async function expectJacobianBatchAffineEqual(
+  module: CurveModule,
+  name: string,
+  got: readonly CurveGPUG2JacobianPoint[],
+  want: readonly JacobianPoint[],
+): Promise {
+  const affine = await module.g2.jacobianToAffineBatch(got);
+  expectAffineBatch(name, affine, want);
+}
+
+function packAffinePointsWithOneZ(
+  bases: readonly CurveGPUG2AffinePoint[],
+  componentBytes: number,
+  pointBytes: number,
+  oneMontC0: Uint8Array,
+): Uint8Array {
+  const out = new Uint8Array(bases.length * pointBytes);
+  for (let i = 0; i < bases.length; i += 1) {
+    const base = i * pointBytes;
+    out.set(bases[i].x.c0, base);
+    out.set(bases[i].x.c1, base + componentBytes);
+    out.set(bases[i].y.c0, base + 2 * componentBytes);
+    out.set(bases[i].y.c1, base + 3 * componentBytes);
+    const isInfinity =
+      bases[i].x.c0.every((byte) => byte === 0) &&
+      bases[i].x.c1.every((byte) => byte === 0) &&
+      bases[i].y.c0.every((byte) => byte === 0) &&
+      bases[i].y.c1.every((byte) => byte === 0);
+    if (!isInfinity) {
+      out.set(oneMontC0, base + 4 * componentBytes);
+    }
+  }
+  return out;
+}
+
+function packScalars(scalars: readonly CurveGPUElementBytes[]): Uint8Array {
+  const out = new Uint8Array(scalars.length * 32);
+  for (let i = 0; i < scalars.length; i += 1) {
+    out.set(scalars[i], i * 32);
+  }
+  return out;
+}
+
+function unpackJacobianPoints(
+  bytes: Uint8Array,
+  count: number,
+  componentBytes: number,
+  pointBytes: number,
+): CurveGPUG2JacobianPoint[] {
+  const out: CurveGPUG2JacobianPoint[] = [];
+  for (let i = 0; i < count; i += 1) {
+    const base = i * pointBytes;
+    out.push({
+      x: {
+        c0: bytes.slice(base, base + componentBytes),
+        c1: bytes.slice(base + componentBytes, base + 2 * componentBytes),
+      },
+      y: {
+        c0: bytes.slice(base + 2 * componentBytes, base + 3 * componentBytes),
+        c1: bytes.slice(base + 3 * componentBytes, base + 4 * componentBytes),
+      },
+      z: {
+        c0: bytes.slice(base + 4 * componentBytes, base + 5 * componentBytes),
+        c1: bytes.slice(base + 5 * componentBytes, base + 6 * componentBytes),
+      },
+    });
+  }
+  return out;
+}
+
+async function naiveMSMAffine(
+  module: CurveModule,
+  bases: readonly CurveGPUG2AffinePoint[],
+  scalars: readonly CurveGPUElementBytes[],
+): Promise {
+  const scaled = await module.g2.scalarMulAffineBatch(bases, scalars);
+  if (scaled.length === 0) {
+    return module.g2.affineInfinity();
+  }
+  let accJacobian = await module.g2.affineToJacobian(toAffinePoint(scaled[0]));
+  for (let i = 1; i < scaled.length; i += 1) {
+    accJacobian = await module.g2.addMixed(accJacobian, toAffinePoint(scaled[i]));
+  }
+  return module.g2.jacobianToAffine(accJacobian);
+}
+
+export async function runSuite(module: CurveModule, log: (msg: string) => void): Promise<{ passed: number; failed: number }> {
+  const config = CONFIGS[module.id];
+  if (!config) {
+    throw new Error(`g2 MSM vectors unavailable for curve ${module.id}`);
+  }
+  log(`=== ${config.title} ===`);
+  log("");
+  const vectors = await fetchJSON(config.vectorPath);
+  log(`terms_per_instance = ${vectors.terms_per_instance}`);
+  log(`cases.msm = ${vectors.msm_cases.length}`);
+
+  const naiveResults: CurveGPUG2AffinePoint[] = [];
+  for (const msmCase of vectors.msm_cases) {
+    naiveResults.push(
+      await naiveMSMAffine(
+        module,
+        msmCase.bases_affine.map(affineFromHex),
+        msmCase.scalars_bytes_le.map((value) => hexToBytes(value) as CurveGPUElementBytes),
+      ),
+    );
+  }
+  expectAffineBatch("msm_naive_affine", naiveResults, vectors.msm_cases.map((item) => item.expected_affine));
+  log("msm_naive_affine: OK");
+
+  const window = 4;
+  const pippengerResults = await module.g2msm.pippengerAffineBatch(
+    vectors.msm_cases.flatMap((item) => item.bases_affine.map(affineFromHex)),
+    vectors.msm_cases.flatMap((item) => item.scalars_bytes_le.map((value) => hexToBytes(value) as CurveGPUElementBytes)),
+    {
+      count: vectors.msm_cases.length,
+      termsPerInstance: vectors.terms_per_instance,
+      window,
+    },
+  );
+  await expectJacobianBatchAffineEqual(module, `msm_jac_pippenger_affine_input (window=${window})`, pippengerResults, vectors.msm_cases.map((item) => item.expected_affine));
+  log(`msm_jac_pippenger_affine_input (window=${window}): OK`);
+
+  const oneMontC0 = await module.fp.montOne();
+  const packedBases = packAffinePointsWithOneZ(
+    vectors.msm_cases.flatMap((item) => item.bases_affine.map(affineFromHex)),
+    module.g2.componentBytes,
+    module.g2.pointBytes,
+    oneMontC0,
+  );
+  const packedScalars = packScalars(
+    vectors.msm_cases.flatMap((item) => item.scalars_bytes_le.map((value) => hexToBytes(value) as CurveGPUElementBytes)),
+  );
+
+  const jacPackedResults = unpackJacobianPoints(
+    await module.g2msm.pippengerPackedJacobianBases(packedBases, packedScalars, {
+      count: vectors.msm_cases.length,
+      termsPerInstance: vectors.terms_per_instance,
+      window,
+    }),
+    vectors.msm_cases.length,
+    module.g2.componentBytes,
+    module.g2.pointBytes,
+  );
+  await expectJacobianBatchAffineEqual(module, `msm_jac_pippenger_packed (window=${window})`, jacPackedResults, vectors.msm_cases.map((item) => item.expected_affine));
+  log(`msm_jac_pippenger_packed (window=${window}): OK`);
+
+  log("");
+  log(`PASS: ${curveDisplayName(module.id)} G2 MSM browser smoke succeeded`);
+  return { passed: 1, failed: 0 };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/g2_ops_page.ts b/backend/accelerated/webgpu/web/tests/api/src/g2_ops_page.ts
new file mode 100644
index 0000000000..b47339eb20
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/g2_ops_page.ts
@@ -0,0 +1,172 @@
+export { };
+
+import { bytesToHex, fetchJSON, hexToBytes } from "../../../src/curvegpu/browser_utils.js";
+import type {
+  CurveGPUFp2Element,
+  CurveGPUG2AffinePoint,
+  CurveGPUG2JacobianPoint,
+  CurveModule,
+  G2Module,
+  SupportedCurveID,
+} from "../../../src/index.js";
+import { curveDisplayName } from "./shared/page_library.js";
+
+type Fp2Point = {
+  c0_bytes_le: string;
+  c1_bytes_le: string;
+};
+
+type AffinePoint = {
+  x: Fp2Point;
+  y: Fp2Point;
+};
+
+type JacobianPoint = {
+  x: Fp2Point;
+  y: Fp2Point;
+  z: Fp2Point;
+};
+
+type G2Case = {
+  name: string;
+  p_affine: AffinePoint;
+  q_affine: AffinePoint;
+  p_jacobian: JacobianPoint;
+  p_affine_output: JacobianPoint;
+  neg_p_jacobian: JacobianPoint;
+  double_p_jacobian: JacobianPoint;
+  add_mixed_p_plus_q_jacobian: JacobianPoint;
+  affine_add_p_plus_q: JacobianPoint;
+};
+
+type G2OpsVectors = {
+  point_cases: G2Case[];
+};
+
+type G2OpsConfig = {
+  curve: SupportedCurveID;
+  title: string;
+  vectorPath: string;
+};
+
+const CONFIGS: Partial> = {
+  bn254: {
+    curve: "bn254",
+    title: "BN254 G2 Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g2/bn254_g2_ops.json",
+  },
+  bls12_377: {
+    curve: "bls12_377",
+    title: "BLS12-377 G2 Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g2/bls12_377_g2_ops.json",
+  },
+  bls12_381: {
+    curve: "bls12_381",
+    title: "BLS12-381 G2 Ops Browser Smoke",
+    vectorPath: "/tests/fixtures/api/vectors/g2/bls12_381_g2_ops.json",
+  },
+};
+
+function fp2FromHex(point: Fp2Point): CurveGPUFp2Element {
+  return { c0: hexToBytes(point.c0_bytes_le), c1: hexToBytes(point.c1_bytes_le) };
+}
+
+function affineFromHex(point: AffinePoint): CurveGPUG2AffinePoint {
+  return { x: fp2FromHex(point.x), y: fp2FromHex(point.y) };
+}
+
+function jacobianFromHex(point: JacobianPoint): CurveGPUG2JacobianPoint {
+  return { x: fp2FromHex(point.x), y: fp2FromHex(point.y), z: fp2FromHex(point.z) };
+}
+
+function fp2ToHex(point: CurveGPUFp2Element): Fp2Point {
+  return { c0_bytes_le: bytesToHex(point.c0), c1_bytes_le: bytesToHex(point.c1) };
+}
+
+function affineToHex(point: CurveGPUG2AffinePoint): AffinePoint {
+  return { x: fp2ToHex(point.x), y: fp2ToHex(point.y) };
+}
+
+function jacobianToHex(point: CurveGPUG2JacobianPoint): JacobianPoint {
+  return { x: fp2ToHex(point.x), y: fp2ToHex(point.y), z: fp2ToHex(point.z) };
+}
+
+function equalFp2(a: Fp2Point, b: Fp2Point): boolean {
+  return a.c0_bytes_le === b.c0_bytes_le && a.c1_bytes_le === b.c1_bytes_le;
+}
+
+function expectPointBatch(name: string, got: readonly CurveGPUG2JacobianPoint[], want: readonly JacobianPoint[], log: (msg: string) => void): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = jacobianToHex(got[i]);
+    if (
+      !equalFp2(gotHex.x, want[i].x) ||
+      !equalFp2(gotHex.y, want[i].y) ||
+      !equalFp2(gotHex.z, want[i].z)
+    ) {
+      throw new Error(`${name}: mismatch at index ${i} got=${JSON.stringify(gotHex)} want=${JSON.stringify(want[i])}`);
+    }
+  }
+  log(`${name}: OK`);
+}
+
+function expectAffineBatch(name: string, got: readonly CurveGPUG2AffinePoint[], want: readonly AffinePoint[], log: (msg: string) => void): void {
+  if (got.length !== want.length) {
+    throw new Error(`${name}: length mismatch got=${got.length} want=${want.length}`);
+  }
+  for (let i = 0; i < got.length; i += 1) {
+    const gotHex = affineToHex(got[i]);
+    if (!equalFp2(gotHex.x, want[i].x) || !equalFp2(gotHex.y, want[i].y)) {
+      throw new Error(`${name}: mismatch at index ${i} got=${JSON.stringify(gotHex)} want=${JSON.stringify(want[i])}`);
+    }
+  }
+  log(`${name}: OK`);
+}
+
+function zeroFp2(componentBytes: number): Fp2Point {
+  const zero = bytesToHex(new Uint8Array(componentBytes));
+  return { c0_bytes_le: zero, c1_bytes_le: zero };
+}
+
+export async function runSuite(module: CurveModule, log: (msg: string) => void): Promise<{ passed: number; failed: number }> {
+  const config = CONFIGS[module.id];
+  if (!config) {
+    throw new Error(`g2 ops vectors unavailable for curve ${module.id}`);
+  }
+  log(`=== ${config.title} ===`);
+  log("");
+  const vectors = await fetchJSON(config.vectorPath);
+  log(`cases.g2 = ${vectors.point_cases.length}`);
+
+  const g2: G2Module = module.g2;
+  const pAffine = vectors.point_cases.map((item) => affineFromHex(item.p_affine));
+  const qAffine = vectors.point_cases.map((item) => affineFromHex(item.q_affine));
+  const pJacobian = vectors.point_cases.map((item) => jacobianFromHex(item.p_jacobian));
+  const negWant = vectors.point_cases.map((item) => item.neg_p_jacobian);
+  const doubleWant = vectors.point_cases.map((item) => item.double_p_jacobian);
+  const addWant = vectors.point_cases.map((item) => item.add_mixed_p_plus_q_jacobian);
+  const affineWant = vectors.point_cases.map((item) => ({
+    x: item.p_affine_output.x,
+    y: item.p_affine_output.y,
+  }));
+  const affineAddWant = vectors.point_cases.map((item) => item.affine_add_p_plus_q);
+  const oneMont = await module.fp.montOne();
+  const oneFp2 = { c0_bytes_le: bytesToHex(oneMont), c1_bytes_le: bytesToHex(module.fp.zero()) };
+  const zero = zeroFp2(g2.componentBytes);
+  const jacInfinityWant = vectors.point_cases.map(() => ({ x: oneFp2, y: oneFp2, z: zero }));
+
+  expectPointBatch("copy", await g2.copyBatch(pJacobian), vectors.point_cases.map((item) => item.p_jacobian), log);
+  expectPointBatch("jac_infinity", await g2.jacobianInfinityBatch(vectors.point_cases.length), jacInfinityWant, log);
+  expectPointBatch("affine_to_jac", await g2.affineToJacobianBatch(pAffine), vectors.point_cases.map((item) => item.p_jacobian), log);
+  expectPointBatch("neg_jac", await g2.negJacobianBatch(pJacobian), negWant, log);
+  expectAffineBatch("jac_to_affine", await g2.jacobianToAffineBatch(pJacobian), affineWant, log);
+  expectPointBatch("double_jac", await g2.doubleJacobianBatch(pJacobian), doubleWant, log);
+  expectPointBatch("add_mixed", await g2.addMixedBatch(pJacobian, qAffine), addWant, log);
+  expectPointBatch("affine_add", await g2.affineAddBatch(pAffine, qAffine), affineAddWant, log);
+
+  log("");
+  log(`PASS: ${curveDisplayName(module.id)} G2 browser smoke succeeded`);
+  return { passed: 1, failed: 0 };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/shared/bench_total.ts b/backend/accelerated/webgpu/web/tests/api/src/shared/bench_total.ts
new file mode 100644
index 0000000000..8e1baca2ef
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/shared/bench_total.ts
@@ -0,0 +1,25 @@
+export async function benchmarkTotalDuration(
+  iters: number,
+  run: () => Promise,
+  yieldBetween?: () => Promise,
+): Promise<{ coldMs: number; warmMs: number }> {
+  const measure = async (): Promise => {
+    const start = performance.now();
+    await run();
+    return performance.now() - start;
+  };
+
+  const coldMs = await measure();
+  if (iters === 1) {
+    return { coldMs, warmMs: coldMs };
+  }
+
+  let warmTotal = 0;
+  for (let i = 0; i < iters; i += 1) {
+    if (yieldBetween) {
+      await yieldBetween();
+    }
+    warmTotal += await measure();
+  }
+  return { coldMs, warmMs: warmTotal / iters };
+}
diff --git a/backend/accelerated/webgpu/web/tests/api/src/shared/page_library.ts b/backend/accelerated/webgpu/web/tests/api/src/shared/page_library.ts
new file mode 100644
index 0000000000..e80d06fe29
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/api/src/shared/page_library.ts
@@ -0,0 +1,47 @@
+import {
+  createCurveGPUContext,
+  createCurveModule,
+  type CurveGPUContext,
+  type CurveModule,
+  type SupportedCurveID,
+} from "../../../../src/index.js";
+
+export function getRequestedCurveId(search = window.location.search): SupportedCurveID {
+  const curve = new URLSearchParams(search).get("curve") ?? "bn254";
+  if (curve !== "bn254" && curve !== "bls12_377" && curve !== "bls12_381") {
+    throw new Error(`unsupported curve: ${curve}`);
+  }
+  return curve;
+}
+
+export function curveDisplayName(curve: SupportedCurveID): string {
+  switch (curve) {
+    case "bn254":
+      return "BN254";
+    case "bls12_377":
+      return "BLS12-377";
+    case "bls12_381":
+      return "BLS12-381";
+  }
+}
+
+export function appendContextDiagnostics(lines: string[], context: CurveGPUContext): void {
+  const diagnostics = context.diagnostics;
+  if (diagnostics.isFallbackAdapter !== undefined) {
+    lines.push(`adapter.isFallbackAdapter = ${String(diagnostics.isFallbackAdapter)}`);
+  }
+  if (diagnostics.vendor) {
+    lines.push(`adapter.vendor = ${diagnostics.vendor}`);
+  }
+  if (diagnostics.architecture) {
+    lines.push(`adapter.architecture = ${diagnostics.architecture}`);
+  }
+  if (!diagnostics.vendor && !diagnostics.architecture) {
+    lines.push("adapter.info = unavailable");
+  }
+}
+
+export async function createRequestedCurveModule(curve = getRequestedCurveId()): Promise {
+  const context = await createCurveGPUContext();
+  return await createCurveModule(context, curve);
+}
diff --git a/backend/accelerated/webgpu/web/tests/groth16/index.html b/backend/accelerated/webgpu/web/tests/groth16/index.html
new file mode 100644
index 0000000000..92db71e613
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/groth16/index.html
@@ -0,0 +1,82 @@
+
+
+
+
+  
+  
+  gnark WebGPU Groth16
+  
+
+
+
+  

gnark WebGPU Groth16

+
+ + + + + + +
+
Idle
+

+  
+
+
+
\ No newline at end of file
diff --git a/backend/accelerated/webgpu/web/tests/groth16/main.js b/backend/accelerated/webgpu/web/tests/groth16/main.js
new file mode 100644
index 0000000000..3802279209
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/groth16/main.js
@@ -0,0 +1,315 @@
+/* eslint-disable */
+
+import "../../src/curvegpu/shader_bundle.generated.js";
+import { createBLS12377, createBLS12381, createBN254, createCurveGPUContext, curveDefinition } from "../../index.js";
+
+const implSelect = document.getElementById("impl");
+const curveSelect = document.getElementById("curve");
+const sizeLogSelect = document.getElementById("size-log");
+const commitmentsSelect = document.getElementById("commitments");
+const proveRunsInput = document.getElementById("prove-runs");
+const runButton = document.getElementById("run");
+const statusEl = document.getElementById("status");
+const logEl = document.getElementById("log");
+const SUPPORTED_CURVES = ["bn254", "bls12_377", "bls12_381"];
+
+function appendLog(line = "") {
+  logEl.textContent += `${line}\n`;
+}
+
+function clearLog() {
+  logEl.textContent = "";
+}
+
+function setStatus(text) {
+  statusEl.textContent = text;
+}
+
+function formatMs(value) {
+  return Number(value).toFixed(3);
+}
+
+function readMs(result, key, fallback = 0) {
+  if (result && typeof result[key] === "number") {
+    return result[key];
+  }
+  return fallback;
+}
+
+function readConfig() {
+  return {
+    curve: curveSelect.value,
+    sizeLog: Number.parseInt(sizeLogSelect.value, 10),
+    commitments: Number.parseInt(commitmentsSelect.value, 10),
+    proveRuns: Number.parseInt(proveRunsInput.value, 10),
+  };
+}
+
+function applyQueryDefaults() {
+  const params = new URLSearchParams(window.location.search);
+  const impl = params.get("impl");
+  const curve = params.get("curve");
+  const sizeLog = params.get("size-log") ?? params.get("sizeLog");
+  const commitments = params.get("commitments") ?? params.get("commitment-count") ?? params.get("commitmentCount");
+  const proveRuns = params.get("prove-runs") ?? params.get("proveRuns");
+
+  if (impl && ["both", "webgpu-go", "native-go"].includes(impl)) {
+    implSelect.value = impl;
+  }
+  if (curve && SUPPORTED_CURVES.includes(curve)) {
+    curveSelect.value = curve;
+  }
+  if (sizeLog && ["12", "15", "18"].includes(sizeLog)) {
+    sizeLogSelect.value = sizeLog;
+  }
+  if (commitments && ["0", "1", "2"].includes(commitments)) {
+    commitmentsSelect.value = commitments;
+  }
+  if (proveRuns) {
+    proveRunsInput.value = proveRuns;
+  }
+}
+
+function fixtureBasePath(config) {
+  return `/tests/fixtures/groth16/${config.curve}/2pow${config.sizeLog}/commit${config.commitments}`;
+}
+
+async function fetchBytes(path) {
+  const response = await fetch(path);
+  if (!response.ok) {
+    throw new Error(`failed to fetch ${path}: ${response.status}`);
+  }
+  return new Uint8Array(await response.arrayBuffer());
+}
+
+function computeOutput(modulus, x, y, depth) {
+  let acc = x % modulus;
+  const mul = y % modulus;
+  for (let i = 0; i < depth; i++) {
+    acc = (acc * mul + 1n) % modulus;
+  }
+  return acc;
+}
+
+function buildWitnesses(curve, config) {
+  const definition = curveDefinition(config.curve);
+  if (!definition.frModulusHex) {
+    throw new Error(`missing scalar modulus for ${config.curve}`);
+  }
+  const depth = 1 << config.sizeLog;
+  const modulus = BigInt(definition.frModulusHex);
+  const x = 3n;
+  const y = 5n;
+  const out = computeOutput(modulus, x, y, depth);
+  return {
+    depth,
+    fullWitness: curve.groth16.encodeWitness([out, x, y], { publicCount: 1 }),
+    publicWitness: curve.groth16.encodeWitness([out], { publicCount: 1 }),
+  };
+}
+
+async function createCurve(config) {
+  const context = await createCurveGPUContext();
+  switch (config.curve) {
+    case "bn254":
+      return createBN254(context);
+    case "bls12_377":
+      return createBLS12377(context);
+    case "bls12_381":
+      return createBLS12381(context);
+    default:
+      throw new Error(`unsupported curve ${config.curve}`);
+  }
+}
+
+async function loadFixture(curve, config) {
+  const base = fixtureBasePath(config);
+  const [ccsBytes, pkBytes, vkBytes] = await Promise.all([
+    fetchBytes(`${base}/ccs.bin`),
+    fetchBytes(`${base}/pk.dump`),
+    fetchBytes(`${base}/vk.bin`),
+  ]);
+  const [ccs, pk, vk] = await Promise.all([
+    curve.groth16.readConstraintSystem(ccsBytes),
+    curve.groth16.readProvingKey(pkBytes, { format: "dump" }),
+    curve.groth16.readVerificationKey(vkBytes),
+  ]);
+  return { ccs, pk, vk };
+}
+
+async function disposeAll(handles) {
+  await Promise.allSettled(handles.map((handle) => handle.dispose()));
+}
+
+async function runGroth16Impl(label, runtimeKind, curve, config) {
+  appendLog(`--- ${label} ---`);
+  appendLog(`=== ${runtimeKind === "webgpu" ? "TS -> WebGPU Groth16" : "TS -> Native Groth16"} (${config.curve}) ===`);
+  appendLog(`fixture = 2^${config.sizeLog}`);
+  appendLog(`commitments = ${config.commitments}`);
+  appendLog(`prove_runs = ${config.proveRuns}`);
+
+  const handles = [];
+  const overallStart = performance.now();
+
+  try {
+    setStatus(`Loading ${label} runtime`);
+    await curve.groth16.loadRuntime({ kind: runtimeKind });
+
+    setStatus(`Loading ${label} fixture`);
+    const fixtureStart = performance.now();
+    const fixture = await loadFixture(curve, config);
+    handles.push(fixture.ccs, fixture.pk, fixture.vk);
+    const fixtureDuration = performance.now() - fixtureStart;
+    appendLog(`fixture_load_ms = ${formatMs(fixtureDuration)}`);
+    appendLog(`constraints = ${fixture.ccs.constraints}`);
+
+    setStatus(`Building ${label} witness`);
+    const witnessStart = performance.now();
+    const { depth, fullWitness, publicWitness } = buildWitnesses(curve, config);
+    const witnessDuration = performance.now() - witnessStart;
+    appendLog(`depth = ${depth}`);
+    appendLog(`witness_build_ms = ${formatMs(witnessDuration)}`);
+
+    setStatus(`Preparing ${label} proving key`);
+    const prepareStart = performance.now();
+    await curve.groth16.prepareProvingKey(fixture.pk);
+    const prepareDuration = performance.now() - prepareStart;
+    if (runtimeKind === "webgpu") {
+      appendLog(`prepare_ms = ${formatMs(prepareDuration)}`);
+    }
+
+    const startupDuration = fixtureDuration + witnessDuration + (runtimeKind === "webgpu" ? prepareDuration : 0);
+    appendLog(`startup_ms = ${formatMs(startupDuration)}`);
+
+    let proveDuration = 0;
+    let verifyDuration = 0;
+    let proofSizeBytes = 0;
+    let firstProofHash = "";
+
+    const steadyStateStart = performance.now();
+    for (let i = 0; i < config.proveRuns; i++) {
+      setStatus(`Proving ${label} round ${i + 1}/${config.proveRuns}`);
+      const proveStart = performance.now();
+      const proofBytes = await curve.groth16.prove(fixture.ccs, fixture.pk, fullWitness);
+      const roundProveDuration = performance.now() - proveStart;
+      proveDuration += roundProveDuration;
+      appendLog(`prove_round_${i}_ms = ${formatMs(roundProveDuration)}`);
+
+      const verifyStart = performance.now();
+      const verified = await curve.groth16.verify(proofBytes, fixture.vk, publicWitness);
+      const roundVerifyDuration = performance.now() - verifyStart;
+      verifyDuration += roundVerifyDuration;
+      if (!verified) {
+        throw new Error(`verify round ${i}: proof rejected`);
+      }
+
+      if (proofSizeBytes === 0) {
+        proofSizeBytes = proofBytes.byteLength;
+        appendLog(`proof_size_bytes = ${proofSizeBytes}`);
+      }
+      appendLog(`roundtrip_verify_round_${i} = OK`);
+    }
+
+    const steadyStateDuration = performance.now() - steadyStateStart;
+    const overallDuration = performance.now() - overallStart;
+
+    appendLog(`prove_total_ms = ${formatMs(proveDuration)}`);
+    appendLog(`prove_avg_ms = ${formatMs(proveDuration / config.proveRuns)}`);
+    appendLog(`verify_total_ms = ${formatMs(verifyDuration)}`);
+    appendLog(`verify_avg_ms = ${formatMs(verifyDuration / config.proveRuns)}`);
+    appendLog(`steady_state_total_ms = ${formatMs(steadyStateDuration)}`);
+    appendLog(`overall_total_ms = ${formatMs(overallDuration)}`);
+
+    return {
+      impl: label,
+      curve: config.curve,
+      prove_runs: config.proveRuns,
+      constraints: fixture.ccs.constraints,
+      size_log: config.sizeLog,
+      commitments: config.commitments,
+      depth_size: depth,
+      fixture_duration_ms: fixtureDuration,
+      witness_duration_ms: witnessDuration,
+      prepare_duration_ms: runtimeKind === "webgpu" ? prepareDuration : 0,
+      startup_duration_ms: startupDuration,
+      prove_duration_ms: proveDuration,
+      verify_duration_ms: verifyDuration,
+      steady_state_duration_ms: steadyStateDuration,
+      overall_duration_ms: overallDuration,
+      proof_size_bytes: proofSizeBytes,
+      roundtrip_verify_succeeded: true,
+    };
+  } finally {
+    await disposeAll(handles);
+  }
+}
+
+function compareResults(webgpu, nativeImpl) {
+  appendLog("");
+  appendLog("--- comparison ---");
+  appendLog(`curve: ${webgpu.curve}`);
+  appendLog(`fixture: 2^${webgpu.size_log}`);
+  appendLog(`commitments: ${webgpu.commitments}`);
+  appendLog(`depth: ${webgpu.depth_size}`);
+  appendLog(`prove runs: ${webgpu.prove_runs}`);
+  appendLog(`constraints: ${webgpu.constraints}`);
+  appendLog(`roundtrip verify: webgpu=${webgpu.roundtrip_verify_succeeded} native=${nativeImpl.roundtrip_verify_succeeded}`);
+  appendLog(`proof size bytes: webgpu=${webgpu.proof_size_bytes} native=${nativeImpl.proof_size_bytes}`);
+  appendLog(`startup ms: webgpu=${formatMs(webgpu.startup_duration_ms)} native=${formatMs(nativeImpl.startup_duration_ms)}`);
+  appendLog(`startup breakdown: webgpu fixture=${formatMs(readMs(webgpu, "fixture_duration_ms"))} witness=${formatMs(readMs(webgpu, "witness_duration_ms"))} prepare=${formatMs(readMs(webgpu, "prepare_duration_ms"))} | native fixture=${formatMs(readMs(nativeImpl, "fixture_duration_ms"))} witness=${formatMs(readMs(nativeImpl, "witness_duration_ms"))}`);
+  appendLog(`steady-state total ms: webgpu=${formatMs(webgpu.steady_state_duration_ms)} native=${formatMs(nativeImpl.steady_state_duration_ms)}`);
+  appendLog(`overall total ms: webgpu=${formatMs(webgpu.overall_duration_ms)} native=${formatMs(nativeImpl.overall_duration_ms)}`);
+  appendLog(`prove avg ms: webgpu=${formatMs(webgpu.prove_duration_ms / webgpu.prove_runs)} native=${formatMs(nativeImpl.prove_duration_ms / nativeImpl.prove_runs)}`);
+  appendLog(`serialize avg ms: webgpu=${formatMs(webgpu.serialize_duration_ms / webgpu.prove_runs)} native=${formatMs(nativeImpl.serialize_duration_ms / nativeImpl.prove_runs)}`);
+  appendLog(`verify avg ms: webgpu=${formatMs(webgpu.verify_duration_ms / webgpu.prove_runs)} native=${formatMs(nativeImpl.verify_duration_ms / nativeImpl.prove_runs)}`);
+}
+
+async function runSelected() {
+  clearLog();
+  runButton.disabled = true;
+  const impl = implSelect.value;
+  const config = readConfig();
+
+  appendLog("=== Groth16 ===");
+  appendLog(`impl = ${impl}`);
+  appendLog(`curve = ${config.curve}`);
+  appendLog(`fixture = 2^${config.sizeLog}`);
+  appendLog(`commitments = ${config.commitments}`);
+  appendLog(`prove_runs = ${config.proveRuns}`);
+  appendLog("");
+
+  setStatus("Initializing curve module");
+  try {
+    const curve = await createCurve(config);
+    let webgpuResult = null;
+    let nativeResult = null;
+
+    if (impl === "webgpu-go" || impl === "both") {
+      webgpuResult = await runGroth16Impl("webgpu-go", "webgpu", curve, config);
+    }
+    if (impl === "native-go" || impl === "both") {
+      nativeResult = await runGroth16Impl("native-go", "native", curve, config);
+    }
+    if (webgpuResult && nativeResult) {
+      compareResults(webgpuResult, nativeResult);
+    }
+    setStatus("PASS");
+  } catch (error) {
+    setStatus("FAIL");
+    appendLog("");
+    appendLog(`FAIL: ${error instanceof Error ? error.message : String(error)}`);
+    throw error;
+  } finally {
+    runButton.disabled = false;
+  }
+}
+
+runButton.addEventListener("click", () => {
+  void runSelected();
+});
+
+applyQueryDefaults();
+
+if (new URLSearchParams(window.location.search).get("autorun") === "1") {
+  void runSelected();
+}
diff --git a/backend/accelerated/webgpu/web/tests/index.html b/backend/accelerated/webgpu/web/tests/index.html
new file mode 100644
index 0000000000..9f8c3f8204
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+  
+  
+  gnark WebGPU Browser Tests
+  
+
+
+
+  
+

gnark WebGPU Browser Tests

+ +
+ + + \ No newline at end of file diff --git a/backend/accelerated/webgpu/web/tests/plonk/index.html b/backend/accelerated/webgpu/web/tests/plonk/index.html new file mode 100644 index 0000000000..a2ca7f793c --- /dev/null +++ b/backend/accelerated/webgpu/web/tests/plonk/index.html @@ -0,0 +1,82 @@ + + + + + + + gnark WebGPU PLONK + + + + +

gnark WebGPU PLONK

+
+ + + + + + +
+
Idle
+

+  
+
+
+
\ No newline at end of file
diff --git a/backend/accelerated/webgpu/web/tests/plonk/main.js b/backend/accelerated/webgpu/web/tests/plonk/main.js
new file mode 100644
index 0000000000..2fcfe268bb
--- /dev/null
+++ b/backend/accelerated/webgpu/web/tests/plonk/main.js
@@ -0,0 +1,340 @@
+/* eslint-disable */
+
+import "../../src/curvegpu/shader_bundle.generated.js";
+import { createBLS12377, createBLS12381, createBN254, createCurveGPUContext, curveDefinition } from "../../index.js";
+
+const implSelect = document.getElementById("impl");
+const curveSelect = document.getElementById("curve");
+const sizeLogSelect = document.getElementById("size-log");
+const commitmentsSelect = document.getElementById("commitments");
+const proveRunsInput = document.getElementById("prove-runs");
+const runButton = document.getElementById("run");
+const statusEl = document.getElementById("status");
+const logEl = document.getElementById("log");
+const SUPPORTED_CURVES = ["bn254", "bls12_377", "bls12_381"];
+
+function appendLog(line = "") {
+  logEl.textContent += `${line}\n`;
+}
+
+function clearLog() {
+  logEl.textContent = "";
+}
+
+function setStatus(text) {
+  statusEl.textContent = text;
+}
+
+function formatMs(value) {
+  return Number(value).toFixed(3);
+}
+
+function readMs(result, key, fallback = 0) {
+  if (result && typeof result[key] === "number") {
+    return result[key];
+  }
+  return fallback;
+}
+
+function readConfig() {
+  return {
+    curve: curveSelect.value,
+    sizeLog: Number.parseInt(sizeLogSelect.value, 10),
+    commitments: Number.parseInt(commitmentsSelect.value, 10),
+    proveRuns: Number.parseInt(proveRunsInput.value, 10),
+  };
+}
+
+function applyQueryDefaults() {
+  const params = new URLSearchParams(window.location.search);
+  const impl = params.get("impl");
+  const curve = params.get("curve");
+  const sizeLog = params.get("size-log") ?? params.get("sizeLog");
+  const commitments = params.get("commitments") ?? params.get("commitment-count") ?? params.get("commitmentCount");
+  const proveRuns = params.get("prove-runs") ?? params.get("proveRuns");
+
+  if (impl && ["both", "webgpu-go", "native-go"].includes(impl)) {
+    implSelect.value = impl;
+  }
+  if (curve && SUPPORTED_CURVES.includes(curve)) {
+    curveSelect.value = curve;
+  }
+  if (sizeLog && ["12", "15", "18"].includes(sizeLog)) {
+    sizeLogSelect.value = sizeLog;
+  }
+  if (commitments && ["0", "1", "2"].includes(commitments)) {
+    commitmentsSelect.value = commitments;
+  }
+  if (proveRuns) {
+    proveRunsInput.value = proveRuns;
+  }
+}
+
+function fixtureBasePath(config) {
+  return `/tests/fixtures/plonk/${config.curve}/2pow${config.sizeLog}/commit${config.commitments}`;
+}
+
+async function fetchBytes(path) {
+  const response = await fetch(path);
+  if (!response.ok) {
+    throw new Error(`failed to fetch ${path}: ${response.status}`);
+  }
+  return new Uint8Array(await response.arrayBuffer());
+}
+
+function computeOutput(modulus, x, y, depth) {
+  let acc = x % modulus;
+  const mul = y % modulus;
+  for (let i = 0; i < depth; i++) {
+    acc = (acc * mul + acc + x + 1n) % modulus;
+  }
+  return acc;
+}
+
+function targetConstraints(sizeLog) {
+  return 1 << sizeLog;
+}
+
+function estimatedConstraints(steps, commitments) {
+  return 4 * steps + commitments * (Math.floor(steps / 4) + 2);
+}
+
+function chainStepsForTarget(sizeLog, commitments) {
+  const target = targetConstraints(sizeLog) - 4;
+  const commitmentCount = Math.max(0, Math.min(2, commitments));
+  let steps = 4;
+  for (; ;) {
+    const next = steps + 4;
+    if (estimatedConstraints(next, commitmentCount) > target) {
+      return steps;
+    }
+    steps = next;
+  }
+}
+
+function buildWitnesses(curve, config) {
+  const definition = curveDefinition(config.curve);
+  if (!definition.frModulusHex) {
+    throw new Error(`missing scalar modulus for ${config.curve}`);
+  }
+  const depth = chainStepsForTarget(config.sizeLog, config.commitments);
+  const modulus = BigInt(definition.frModulusHex);
+  const x = 3n;
+  const y = 5n;
+  const out = computeOutput(modulus, x, y, depth);
+  return {
+    depth,
+    targetConstraints: targetConstraints(config.sizeLog),
+    fullWitness: curve.plonk.encodeWitness([out, x, y], { publicCount: 1 }),
+    publicWitness: curve.plonk.encodeWitness([out], { publicCount: 1 }),
+  };
+}
+
+async function createCurve(config) {
+  const context = await createCurveGPUContext();
+  switch (config.curve) {
+    case "bn254":
+      return createBN254(context);
+    case "bls12_377":
+      return createBLS12377(context);
+    case "bls12_381":
+      return createBLS12381(context);
+    default:
+      throw new Error(`unsupported PLONK scaffold curve ${config.curve}`);
+  }
+}
+
+async function loadFixture(curve, config) {
+  const base = fixtureBasePath(config);
+  const [ccsBytes, pkBytes, vkBytes] = await Promise.all([
+    fetchBytes(`${base}/ccs.bin`),
+    fetchBytes(`${base}/pk.bin`),
+    fetchBytes(`${base}/vk.bin`),
+  ]);
+  const [ccs, pk, vk] = await Promise.all([
+    curve.plonk.readConstraintSystem(ccsBytes),
+    // Note: using "unsafe" as the proving key is trusted and this avoid subgroup membership checks
+    curve.plonk.readProvingKey(pkBytes, { format: "unsafe" }),
+    curve.plonk.readVerificationKey(vkBytes),
+  ]);
+  return { ccs, pk, vk };
+}
+
+async function disposeAll(handles) {
+  await Promise.allSettled(handles.map((handle) => handle.dispose()));
+}
+
+async function runPlonkImpl(label, runtimeKind, curve, config) {
+  appendLog(`--- ${label} ---`);
+  appendLog(`=== ${runtimeKind === "webgpu" ? "TS -> WebGPU PLONK" : "TS -> Native PLONK"} (${config.curve}) ===`);
+  appendLog(`fixture = 2^${config.sizeLog}`);
+  appendLog(`commitments = ${config.commitments}`);
+  appendLog(`prove_runs = ${config.proveRuns}`);
+
+  const handles = [];
+  const overallStart = performance.now();
+
+  try {
+    setStatus(`Loading ${label} runtime`);
+    await curve.plonk.loadRuntime({ kind: runtimeKind });
+
+    setStatus(`Loading ${label} fixture`);
+    const fixtureStart = performance.now();
+    const fixture = await loadFixture(curve, config);
+    handles.push(fixture.ccs, fixture.pk, fixture.vk);
+    const fixtureDuration = performance.now() - fixtureStart;
+    appendLog(`fixture_load_ms = ${formatMs(fixtureDuration)}`);
+    appendLog(`constraints = ${fixture.ccs.constraints}`);
+
+    setStatus(`Building ${label} witness`);
+    const witnessStart = performance.now();
+    const { depth, targetConstraints, fullWitness, publicWitness } = buildWitnesses(curve, config);
+    const witnessDuration = performance.now() - witnessStart;
+    appendLog(`target_constraints = ${targetConstraints}`);
+    appendLog(`chain_steps = ${depth}`);
+    appendLog(`witness_build_ms = ${formatMs(witnessDuration)}`);
+
+    setStatus(`Preparing ${label} proving key`);
+    const prepareStart = performance.now();
+    await curve.plonk.prepareProvingKey(fixture.pk, fixture.ccs);
+    const prepareDuration = performance.now() - prepareStart;
+    if (runtimeKind === "webgpu") {
+      appendLog(`prepare_ms = ${formatMs(prepareDuration)}`);
+    }
+
+    const startupDuration = fixtureDuration + witnessDuration + (runtimeKind === "webgpu" ? prepareDuration : 0);
+    appendLog(`startup_ms = ${formatMs(startupDuration)}`);
+
+    let proveDuration = 0;
+    let verifyDuration = 0;
+    let proofSizeBytes = 0;
+    let firstProofHash = "";
+
+    const steadyStateStart = performance.now();
+    for (let i = 0; i < config.proveRuns; i++) {
+      setStatus(`Proving ${label} round ${i + 1}/${config.proveRuns}`);
+      const proveStart = performance.now();
+      const proofBytes = await curve.plonk.prove(fixture.ccs, fixture.pk, fullWitness);
+      const roundProveDuration = performance.now() - proveStart;
+      proveDuration += roundProveDuration;
+      appendLog(`prove_round_${i}_ms = ${formatMs(roundProveDuration)}`);
+
+      const verifyStart = performance.now();
+      const verified = await curve.plonk.verify(proofBytes, fixture.vk, publicWitness);
+      const roundVerifyDuration = performance.now() - verifyStart;
+      verifyDuration += roundVerifyDuration;
+      if (!verified) {
+        throw new Error(`verify round ${i}: proof rejected`);
+      }
+
+      if (proofSizeBytes === 0) {
+        proofSizeBytes = proofBytes.byteLength;
+        appendLog(`proof_size_bytes = ${proofSizeBytes}`);
+      }
+      appendLog(`roundtrip_verify_round_${i} = OK`);
+    }
+
+    const steadyStateDuration = performance.now() - steadyStateStart;
+    const overallDuration = performance.now() - overallStart;
+
+    appendLog(`prove_total_ms = ${formatMs(proveDuration)}`);
+    appendLog(`prove_avg_ms = ${formatMs(proveDuration / config.proveRuns)}`);
+    appendLog(`verify_total_ms = ${formatMs(verifyDuration)}`);
+    appendLog(`verify_avg_ms = ${formatMs(verifyDuration / config.proveRuns)}`);
+    appendLog(`steady_state_total_ms = ${formatMs(steadyStateDuration)}`);
+    appendLog(`overall_total_ms = ${formatMs(overallDuration)}`);
+
+    return {
+      impl: label,
+      curve: config.curve,
+      prove_runs: config.proveRuns,
+      constraints: fixture.ccs.constraints,
+      size_log: config.sizeLog,
+      commitments: config.commitments,
+      target_constraints: targetConstraints,
+      depth_size: depth,
+      fixture_duration_ms: fixtureDuration,
+      witness_duration_ms: witnessDuration,
+      prepare_duration_ms: runtimeKind === "webgpu" ? prepareDuration : 0,
+      startup_duration_ms: startupDuration,
+      prove_duration_ms: proveDuration,
+      verify_duration_ms: verifyDuration,
+      steady_state_duration_ms: steadyStateDuration,
+      overall_duration_ms: overallDuration,
+      proof_size_bytes: proofSizeBytes,
+      roundtrip_verify_succeeded: true,
+    };
+  } finally {
+    await disposeAll(handles);
+  }
+}
+
+function compareResults(webgpu, nativeImpl) {
+  appendLog("");
+  appendLog("--- comparison ---");
+  appendLog(`curve: ${webgpu.curve}`);
+  appendLog(`fixture: 2^${webgpu.size_log}`);
+  appendLog(`commitments: ${webgpu.commitments}`);
+  appendLog(`target constraints: ${webgpu.target_constraints}`);
+  appendLog(`depth: ${webgpu.depth_size}`);
+  appendLog(`prove runs: ${webgpu.prove_runs}`);
+  appendLog(`constraints: ${webgpu.constraints}`);
+  appendLog(`roundtrip verify: webgpu=${webgpu.roundtrip_verify_succeeded} native=${nativeImpl.roundtrip_verify_succeeded}`);
+  appendLog(`proof size bytes: webgpu=${webgpu.proof_size_bytes} native=${nativeImpl.proof_size_bytes}`);
+  appendLog(`startup ms: webgpu=${formatMs(webgpu.startup_duration_ms)} native=${formatMs(nativeImpl.startup_duration_ms)}`);
+  appendLog(`startup breakdown: webgpu fixture=${formatMs(readMs(webgpu, "fixture_duration_ms"))} witness=${formatMs(readMs(webgpu, "witness_duration_ms"))} prepare=${formatMs(readMs(webgpu, "prepare_duration_ms"))} | native fixture=${formatMs(readMs(nativeImpl, "fixture_duration_ms"))} witness=${formatMs(readMs(nativeImpl, "witness_duration_ms"))}`);
+  appendLog(`steady-state total ms: webgpu=${formatMs(webgpu.steady_state_duration_ms)} native=${formatMs(nativeImpl.steady_state_duration_ms)}`);
+  appendLog(`overall total ms: webgpu=${formatMs(webgpu.overall_duration_ms)} native=${formatMs(nativeImpl.overall_duration_ms)}`);
+  appendLog(`prove avg ms: webgpu=${formatMs(webgpu.prove_duration_ms / webgpu.prove_runs)} native=${formatMs(nativeImpl.prove_duration_ms / nativeImpl.prove_runs)}`);
+  appendLog(`verify avg ms: webgpu=${formatMs(webgpu.verify_duration_ms / webgpu.prove_runs)} native=${formatMs(nativeImpl.verify_duration_ms / nativeImpl.prove_runs)}`);
+}
+
+async function runSelected() {
+  clearLog();
+  runButton.disabled = true;
+  const impl = implSelect.value;
+  const config = readConfig();
+
+  appendLog("=== PLONK TS Browser POC ===");
+  appendLog(`impl = ${impl}`);
+  appendLog(`curve = ${config.curve}`);
+  appendLog(`fixture = 2^${config.sizeLog}`);
+  appendLog(`commitments = ${config.commitments}`);
+  appendLog(`prove_runs = ${config.proveRuns}`);
+  appendLog("");
+
+  setStatus("Initializing curve module");
+  try {
+    const curve = await createCurve(config);
+    let webgpuResult = null;
+    let nativeResult = null;
+
+    if (impl === "webgpu-go" || impl === "both") {
+      webgpuResult = await runPlonkImpl("webgpu-go", "webgpu", curve, config);
+    }
+    if (impl === "native-go" || impl === "both") {
+      nativeResult = await runPlonkImpl("native-go", "native", curve, config);
+    }
+    if (webgpuResult && nativeResult) {
+      compareResults(webgpuResult, nativeResult);
+    }
+    setStatus("PASS");
+  } catch (error) {
+    setStatus("FAIL");
+    appendLog("");
+    appendLog(`FAIL: ${error instanceof Error ? error.message : String(error)}`);
+    throw error;
+  } finally {
+    runButton.disabled = false;
+  }
+}
+
+runButton.addEventListener("click", () => {
+  void runSelected();
+});
+
+applyQueryDefaults();
+
+if (new URLSearchParams(window.location.search).get("autorun") === "1") {
+  void runSelected();
+}
diff --git a/backend/accelerated/webgpu/web/tsconfig.json b/backend/accelerated/webgpu/web/tsconfig.json
index c297fc9149..9b96fbde75 100644
--- a/backend/accelerated/webgpu/web/tsconfig.json
+++ b/backend/accelerated/webgpu/web/tsconfig.json
@@ -16,5 +16,5 @@
     "lib": ["ES2022", "DOM"],
     "types": ["@webgpu/types"]
   },
-  "include": ["index.ts", "src/**/*.ts"]
+  "include": ["index.ts", "src/**/*.ts", "tests/**/*.ts", "tests/**/*.js"]
 }

From f698273e7b3d98ccafd354e4bef7b2dd62533882 Mon Sep 17 00:00:00 2001
From: Ivo Kubjas 
Date: Wed, 22 Jul 2026 01:24:34 +0200
Subject: [PATCH 5/8] docs: small update

---
 backend/accelerated/webgpu/web/src/curvegpu/curves.ts | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/backend/accelerated/webgpu/web/src/curvegpu/curves.ts b/backend/accelerated/webgpu/web/src/curvegpu/curves.ts
index 7ef8148fcd..afafc79ee9 100644
--- a/backend/accelerated/webgpu/web/src/curvegpu/curves.ts
+++ b/backend/accelerated/webgpu/web/src/curvegpu/curves.ts
@@ -14,9 +14,6 @@ import { shapeFor } from "./types.js";
 
 /**
  * Runtime metadata for a supported curve.
- *
- * This is kept separate from the page harnesses so the library can evolve
- * around one shared source of curve-specific facts.
  */
 export interface CurveDefinition {
   readonly id: SupportedCurveID;

From 9852edc63ad3c8490a7204e1517f057f2d873153 Mon Sep 17 00:00:00 2001
From: Ivo Kubjas 
Date: Wed, 22 Jul 2026 01:24:52 +0200
Subject: [PATCH 6/8] docs: add webgpu README

---
 backend/accelerated/webgpu/README.md | 41 ++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)
 create mode 100644 backend/accelerated/webgpu/README.md

diff --git a/backend/accelerated/webgpu/README.md b/backend/accelerated/webgpu/README.md
new file mode 100644
index 0000000000..c7e72cf8ed
--- /dev/null
+++ b/backend/accelerated/webgpu/README.md
@@ -0,0 +1,41 @@
+# gnark WebGPU Backend
+
+This directory contains gnark's browser WebGPU prover backend:
+
+- `groth16/` contains the Go Groth16 accelerated backend and wasm entrypoints.
+- `plonk/` contains the Go PLONK accelerated backend and wasm entrypoints.
+- `internal/` contains shared Go bridge and wasm runtime helpers.
+- `shaders/` contains the WGSL kernels used by the TypeScript runtime.
+- `web/` contains the browser-facing TypeScript API and build configuration.
+
+The Go packages are built only for `GOOS=js GOARCH=wasm`. They call the
+TypeScript WebGPU runtime through `syscall/js`, and the TypeScript runtime
+loads the Go wasm entrypoints from `web/dist/assets`.
+
+## Build
+
+Install TypeScript dependencies from `web/package-lock.json`:
+
+```sh
+cd backend/accelerated/webgpu/web
+npm ci
+```
+
+Build the TypeScript package, bundled shaders, and Go wasm assets:
+
+```sh
+npm run build:all
+```
+
+Useful narrower targets:
+
+```sh
+npm run build
+npm run build:shaders
+npm run build:wasm
+npm run build:wasm:groth16
+npm run build:wasm:plonk
+npm run lint
+```
+
+`npm run build:shaders` generates `web/src/curvegpu/shader_bundle.generated.ts` from `shaders/`.

From 394a68bd5139e52a1688fcba11394ef4131dafd4 Mon Sep 17 00:00:00 2001
From: Ivo Kubjas 
Date: Wed, 22 Jul 2026 01:34:41 +0200
Subject: [PATCH 7/8] docs: update README

---
 backend/accelerated/webgpu/README.md | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/backend/accelerated/webgpu/README.md b/backend/accelerated/webgpu/README.md
index c7e72cf8ed..b585405359 100644
--- a/backend/accelerated/webgpu/README.md
+++ b/backend/accelerated/webgpu/README.md
@@ -1,5 +1,25 @@
 # gnark WebGPU Backend
 
+This package implements gnark prover using WSGL shaders for speeding up most heavy
+cryptographic operations. For the prover coordination, we use Go implementation which
+is compiled to WASM using Go toolchain. The Go implementation then calls the WSGL
+shaders through a Typescrip bridge which in turn executes the WSGL shaders.
+
+It supports Groth16 and PLONK proof systems over BN254, BLS12-377 and BLS12-381.
+
+## Disclaimer
+
+This is very experimental package. The APIs may change. The backend is not audited.
+
+Currently G2 API tests are failing for BLS12-377 and BLS12-381, but the Groth16/PLONK
+prover tests pass.
+
+Due to using Go toolchain for compiling the proving coordinator to WASM, then the
+assets are quite big. We have tried TinyGo, but it is incompatible with gnark-crypto
+dependency as is.
+
+## Overview
+
 This directory contains gnark's browser WebGPU prover backend:
 
 - `groth16/` contains the Go Groth16 accelerated backend and wasm entrypoints.

From 1bea45a0630ce78e795f9002214acc14a611aebb Mon Sep 17 00:00:00 2001
From: Ivo Kubjas 
Date: Wed, 22 Jul 2026 01:39:26 +0200
Subject: [PATCH 8/8] fix: use goimports as tool

---
 backend/accelerated/webgpu/internal/generator/main.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/backend/accelerated/webgpu/internal/generator/main.go b/backend/accelerated/webgpu/internal/generator/main.go
index 221498cf8b..71b7d2ed14 100644
--- a/backend/accelerated/webgpu/internal/generator/main.go
+++ b/backend/accelerated/webgpu/internal/generator/main.go
@@ -82,7 +82,7 @@ func main() {
 		panic(err)
 	}
 	runCmd("gofmt", "-w", testdataDir)
-	runCmd("goimports", "-w", testdataDir)
+	runCmd("go", "tool", "goimports", "-w", testdataDir)
 
 	for _, d := range data {
 		entries := []bavard.Entry{
@@ -101,7 +101,7 @@ func main() {
 			panic(err)
 		}
 		runCmd("gofmt", "-w", filepath.Join(testdataDir, d.CurveDir))
-		runCmd("goimports", "-w", filepath.Join(testdataDir, d.CurveDir))
+		runCmd("go", "tool", "goimports", "-w", filepath.Join(testdataDir, d.CurveDir))
 	}
 
 }