/*! micro-zk-proofs - MIT License (c) 2025 Paul Miller (paulmillr.com) */
import type { BlsCurvePair as BLSCurvePair } from '@noble/curves/abstract/bls.js';
import { pippenger } from '@noble/curves/abstract/curve.js';
import { FFT, poly as polyCurves, rootsOfUnity } from '@noble/curves/abstract/fft.js';
import type { Fp2 } from '@noble/curves/abstract/tower.js';
import type { WeierstrassPoint, WeierstrassPointCons } from '@noble/curves/abstract/weierstrass.js';
import { bn254 as nobleBn254 } from '@noble/curves/bn254.js';
import { bytesToNumberBE } from '@noble/curves/utils.js';
import { randomBytes } from '@noble/hashes/utils.js';
import type { TArg, TRet } from '@noble/hashes/utils.js';
import type { MSMInput } from './msm-worker.ts';
import { modifyArgs } from './msm.ts';

export type { TArg, TRet } from '@noble/hashes/utils.js';

// It is hard to make groth16 async / fast, because MSM perf is
// non-linear (2048 => 1024 points is not 2x faster).
// It also depends on hamming weight (amount of zeros) on scalars.
// Workers may not significantly increase performance on small circuits.
// Check out 'msm.ts' for web workers.

// Utils
/** Bidirectional value coder. */
export interface Coder<F, T> {
  /**
   * Encodes a value into its serialized form.
   * @param from - Value to encode.
   * @returns Encoded representation.
   */
  encode(from: F): T;
  /**
   * Decodes a serialized value back into its native form.
   * @param to - Encoded representation to decode.
   * @returns Decoded value.
   */
  decode(to: T): F;
}
type RandFn = (len: number) => Uint8Array;
const U32_MAX = 0xffffffff;
const MAX_DOMAIN_BITS = 30;
const _0n = /* @__PURE__ */ BigInt(0);
const _1n = /* @__PURE__ */ BigInt(1);

function log2(n: number) {
  // Domain sizes are serialized as U32LE in witness artifacts, and clz32 truncates above uint32.
  if (!Number.isSafeInteger(n) || n <= 0 || n > U32_MAX)
    throw new Error(`expected uint32 positive integer, got ${n}`);
  return 31 - Math.clz32(n);
}

// Basic utility to deep convert bigints to strings and back
function deepConvert(o: any, mapper: (o: any) => any): any {
  const t = mapper(o);
  if (t !== undefined) return t;
  if (o === null) return o as any;
  if (Array.isArray(o)) return o.map((i) => deepConvert(i, mapper)) as any;
  if (typeof o == 'object') {
    return Object.fromEntries(
      Object.entries(o).map(([k, v]) => [k, deepConvert(v, mapper)])
    ) as any;
  }
  return o as any;
}
// TODO: should be something like 'Deep' type here?
// prettier-ignore
/** Recursively converts bigint leaves into decimal strings. */
export type BigintToString<T> =
  T extends bigint ? `${T}` :
  T extends Array<infer U> ? Array<BigintToString<U>> :
  T extends null ? null :
  T extends object ? { [K in keyof T]: BigintToString<T[K]> } :
  T;

// prettier-ignore
/** Recursively converts decimal-string leaves back into bigint values. */
export type StringToBigint<T> =
  T extends `-${string}` ? T :
  T extends `${bigint}` ? bigint :
  T extends Array<infer U> ? Array<StringToBigint<U>> :
  T extends null ? null :
  T extends object ? { [K in keyof T]: StringToBigint<T[K]> } :
  T;
/**
 * Helper to serialize bigint-heavy objects through JSON-compatible strings.
 * @example
 * Encode bigint-heavy data for JSON transport, then decode it back.
 * ```ts
 * const encoded = stringBigints.encode({ value: 1n });
 * const decoded = stringBigints.decode(encoded);
 * ```
 */
export const stringBigints: {
  encode: <F>(o: F) => BigintToString<F>;
  decode: <T>(o: T) => StringToBigint<T>;
} = /* @__PURE__ */ Object.freeze({
  encode: <F>(o: F): BigintToString<F> => {
    return deepConvert(o, (o) => {
      if (typeof o !== 'bigint') return undefined;
      // Old proof/key JSON is unsigned field-element data; signed values would not decode back.
      if (o < _0n) throw new Error(`expected non-negative bigint, got ${o}`);
      return o.toString(10);
    }) as BigintToString<F>;
  },
  decode: <T>(o: T): StringToBigint<T> => {
    // Only unsigned base-10 strings are decoded for old proof/key JSON compatibility.
    return deepConvert(o, (o) =>
      typeof o == 'string' && /^[0-9]+$/.test(o) ? BigInt(o) : undefined
    ) as StringToBigint<T>;
  },
});

function pointCoder<T, F>(
  cons: WeierstrassPointCons<T>,
  coder: Coder<T, F>
): Coder<WeierstrassPoint<T>, [F, F, F]> {
  return Object.freeze({
    encode: (p: WeierstrassPoint<T>): [F, F, F] => {
      const { X: px, Y: py, Z: pz } = cons.fromAffine(p.toAffine());
      return [px, py, pz].map(coder.encode) as [F, F, F];
    },
    decode: (p: [F, F, F] | undefined) => {
      // snarkjs-style proving keys use null placeholders for zero C-query entries.
      if (!p) return cons.ZERO; // sometimes can be null?
      const [x, y, z] = p.map(coder.decode);
      // TODO: validation increases time 3x
      // res.assertValidity();
      return new cons(x, y, z);
    },
  });
}

/** One linear constraint side keyed by signal index. */
export type Constraint = Record<number, bigint>;
/** Jacobian point over G1. */
export type G1Point = [bigint, bigint, bigint];
/** Jacobian point over G2. */
export type G2Point = [[bigint, bigint], [bigint, bigint], [bigint, bigint]];
/** Sparse coefficient reference inside a proving key matrix. */
export type Coefficient = {
  /** Field coefficient value. */
  value: bigint;
  /** Matrix index that owns the coefficient. */
  matrix: number;
  /** Constraint row index. */
  constraint: number;
  /** Signal column index. */
  signal: number;
};

/** Groth16 proving key. */
export interface ProvingKey {
  /** Protocol name. */
  protocol?: 'groth';
  /** Total number of circuit variables. */
  nVars: number;
  /** Number of public signals, including outputs and inputs. */
  nPublic: number;
  /** Log2 of the evaluation domain size. */
  domainBits: number;
  /** Evaluation domain size. */
  domainSize: number;
  // Polynominals
  /** Sparse A-matrix polynomials. */
  polsA?: Constraint[];
  /** Sparse B-matrix polynomials. */
  polsB?: Constraint[];
  /** Sparse C-matrix polynomials. */
  polsC?: Constraint[];
  /** Compact coefficient table used to reconstruct the sparse matrices. */
  ccoefs?: Coefficient[];
  //
  /** A-query points in G1. */
  A: G1Point[];
  /** B-query points in G1. */
  B1: G1Point[];
  /** B-query points in G2. */
  B2: G2Point[];
  /** C-query points in G1. */
  C: G1Point[];
  //
  /** Alpha verifier key element in G1. */
  vk_alfa_1: G1Point;
  /** Beta verifier key element in G1. */
  vk_beta_1: G1Point;
  /** Delta verifier key element in G1. */
  vk_delta_1: G1Point;
  /** Beta verifier key element in G2. */
  vk_beta_2: G2Point;
  /** Delta verifier key element in G2. */
  vk_delta_2: G2Point;
  //
  /** H-exponent points used for quotient commitments. */
  hExps: G1Point[];
}

/** Groth16 verification key. */
export interface VerificationKey {
  /** Protocol name. */
  protocol?: 'groth';
  /** Number of public signals expected by the verifier. */
  nPublic: number;
  /** Public input commitment bases. */
  IC: G1Point[];
  //
  /** Alpha verifier key element in G1. */
  vk_alfa_1: G1Point;
  /** Beta verifier key element in G2. */
  vk_beta_2: G2Point;
  /** Gamma verifier key element in G2. */
  vk_gamma_2: G2Point;
  /** Delta verifier key element in G2. */
  vk_delta_2: G2Point;
}

/** Witness vector produced by a circuit execution. */
export type Witness = bigint[];

/** Groth16 proof object. */
export interface GrothProof {
  /** Protocol name. */
  protocol: 'groth';
  /** A proof element in G1. */
  pi_a: G1Point;
  /** B proof element in G2. */
  pi_b: G2Point;
  /** C proof element in G1. */
  pi_c: G1Point;
}
/** Proof together with public signals. */
export interface ProofWithSignals {
  /** Groth16 proof object. */
  proof: GrothProof;
  /** Public signals used during verification. */
  publicSignals: Witness;
  /** Optional commitment points for proof systems that emit them. */
  commitments?: G1Point[];
}

/** Minimal circuit metadata required for setup. */
export type CircuitInfo = {
  /** Total number of circuit variables. */
  nVars: number;
  /** Number of public inputs. */
  nPubInputs: number;
  /** Number of public outputs. */
  nOutputs: number;
  /** Constraint rows in `[A, B, C]` order. */
  constraints: [Constraint, Constraint, Constraint][];
};

/** Toxic waste values produced during setup. */
export interface ToxicWaste {
  /** Secret evaluation point. */
  t: bigint;
  /** Alpha secret multiplier. */
  kalfa: bigint;
  /** Beta secret multiplier. */
  kbeta: bigint;
  /** Gamma secret multiplier. */
  kgamma: bigint;
  /** Delta secret multiplier. */
  kdelta: bigint;
}

/** Groth16 constructor options. */
export type GrothOpts = {
  /** Override the non-quadratic residue used by the FFT helper tables. */
  nqr?: number | bigint;
  /** Return toxic waste values for tests and fixture generation. */
  unsafePreserveToxic?: boolean;
  /**
   * Custom G1 MSM implementation.
   * @param input - Point-scalar pairs to multiply.
   * @returns MSM result in G1.
   */
  G1msm?: (input: MSMInput<bigint>[]) => Promise<WeierstrassPoint<bigint>>;
  /**
   * Custom G2 MSM implementation.
   * @param input - Point-scalar pairs to multiply.
   * @returns MSM result in G2.
   */
  G2msm?: (input: MSMInput<Fp2>[]) => Promise<WeierstrassPoint<Fp2>>;
};

/** Curve points bundled together with their coders. */
export interface PointsWithCoders {
  /** G1 point constructor. */
  G1: WeierstrassPointCons<bigint>;
  /** G2 point constructor. */
  G2: WeierstrassPointCons<Fp2>;
  /** Coder for G1 points. */
  G1c: Coder<WeierstrassPoint<bigint>, G1Point>;
  /** Coder for G2 points. */
  G2c: Coder<WeierstrassPoint<Fp2>, G2Point>;
}

/** Snark helpers returned by `buildSnark()`. */
export interface SnarkConstructorOutput {
  /** Curve constructors and point coders used by the proof helpers. */
  utils: PointsWithCoders;
  /** Groth16 setup, proving, and verification helpers. */
  groth: {
    /**
     * Builds proving and verification keys for a circuit.
     * @param circuit - Circuit metadata and constraints.
     * @param rnd - Optional randomness source used to sample toxic waste.
     * @returns Proving key, verification key, and optional toxic waste.
     */
    setup(
      circuit: CircuitInfo,
      rnd?: TArg<RandFn>
    ): {
      pkey: ProvingKey;
      vkey: VerificationKey;
      toxic: ToxicWaste | undefined;
    };
    /**
     * Creates a Groth16 proof for a witness.
     * @param pkey - Proving key.
     * @param witness - Witness vector.
     * @param rnd - Optional randomness source.
     * @returns Proof and public signals.
     */
    createProof(pkey: ProvingKey, witness: Witness, rnd?: TArg<RandFn>): Promise<ProofWithSignals>;
    /**
     * Verifies a Groth16 proof.
     * @param vkey - Verification key.
     * @param proofWithSignals - Proof plus public signals.
     * @returns `true` when the proof verifies.
     */
    verifyProof(vkey: VerificationKey, proofWithSignals: ProofWithSignals): boolean;
  };
}

/**
 * Builds Groth16 helpers for a pairing-friendly curve.
 * @param curve - Pairing-friendly curve implementation.
 * @param opts - Options for FFT setup and optional MSM backends. See {@link GrothOpts}.
 * @returns Snark setup, proof, and verification helpers.
 * @throws If the curve root table exceeds the supported FFT domain size. {@link Error}
 * @example
 * Build curve-specific Groth16 helpers, then run a tiny self-contained proof round-trip.
 * ```ts
 * import { buildSnark, type CircuitInfo } from 'micro-zk-proofs';
 * const { bn254: nobleBn254 } = await import('@noble/curves/bn254.js');
 * const snark = buildSnark(nobleBn254);
 * const circuit: CircuitInfo = {
 *   nVars: 2,
 *   nPubInputs: 0,
 *   nOutputs: 0,
 *   constraints: [[{}, {}, {}]],
 * };
 * const setup = snark.groth.setup(circuit);
 * const proof = await snark.groth.createProof(setup.pkey, [1n, 0n]);
 * snark.groth.verifyProof(setup.vkey, proof);
 * ```
 */
export function buildSnark(
  curve: BLSCurvePair,
  opts: GrothOpts = {}
): TRet<SnarkConstructorOutput> {
  // Utils
  const G1 = curve.G1.Point;
  const G2 = curve.G2.Point;
  type G1Point = typeof G1.BASE;
  type G2Point = typeof G2.BASE;

  const { Fr, Fp, Fp2, Fp12 } = curve.fields;
  const Fpc: Coder<bigint, bigint> = {
    encode: (from) => from,
    decode: (to) => Fp.create(to),
  };
  const Fp2c: Coder<Fp2, [bigint, bigint]> = {
    encode: (from) => [from.c0, from.c1],
    decode: (to) => Fp2.create({ c0: Fp.create(to[0]), c1: Fp.create(to[1]) }),
  };
  const G1c = pointCoder(G1, Fpc);
  const G2c = pointCoder(G2, Fp2c);

  const G1msm = !opts.G1msm
    ? (p: G1Point[], s: bigint[]) => pippenger(curve.G1.Point, p, s)
    : modifyArgs(Fr, G1, opts.G1msm);
  const G2msm = !opts.G2msm
    ? (p: G2Point[], s: bigint[]) => pippenger(curve.G2.Point, p, s)
    : modifyArgs(Fr, G2, opts.G2msm);

  const Frandom = (rnd: TArg<RandFn> = randomBytes) => {
    // Reduce random bytes once so unsafePreserveToxic exposes the same field elements setup uses.
    return Fr.create(bytesToNumberBE(rnd(Fr.BYTES)));
  };
  const roots = rootsOfUnity(Fr, opts.nqr ? BigInt(opts.nqr) : undefined);
  // Some internal FFT paths still use signed JS shifts; reject roots that could reach bit 31+.
  if (roots.info.powerOfTwo > MAX_DOMAIN_BITS)
    throw new Error(
      `expected roots powerOfTwo <= ${MAX_DOMAIN_BITS}, got ${roots.info.powerOfTwo}`
    );
  const fftFr = FFT(roots, Fr);
  const polyFr = polyCurves(Fr, roots, undefined, fftFr);
  const checkDomainBits = (bits: number) => {
    if (!Number.isSafeInteger(bits) || bits < 0 || bits > roots.info.powerOfTwo)
      throw new Error(`expected domainBits <= ${roots.info.powerOfTwo}, got ${bits}`);
  };
  // TODO: cleanup more later
  const poly = {
    reduce(p: bigint[]) {
      while (p.length > 0 && Fr.is0(p[p.length - 1])) p.pop();
      return p;
    },
    sub(a: bigint[], b: bigint[]) {
      const len = Math.max(a.length, b.length);
      return poly.reduce(polyFr.sub(polyFr.extend(a, len), polyFr.extend(b, len)));
    },
    fft(p: bigint[], bits: number): bigint[] {
      const n = 1 << bits;
      while (p.length < n) p.push(Fr.ZERO);
      return fftFr.direct(p);
    },
    ifft(p: bigint[]) {
      if (p.length <= 1) return p;
      return fftFr.inverse(p);
    },
    // Polynomial multiplication via FFT.
    mul(a: bigint[], b: bigint[]) {
      if (a.length !== b.length || a.length < 2) throw new Error('wrong polynominal length');
      return poly.reduce(polyFr.convolve(a, b));
    },
    evaluateLagrangePolynomials(bits: number, t: bigint): bigint[] {
      return polyFr.lagrange.basis(t, 2 ** bits);
    },
  };
  function sumABC(
    size: number,
    weights: bigint[],
    A: Constraint[],
    B: Constraint[],
    C: Constraint[],
    transpose = false
  ) {
    function build(constraints: Constraint[]) {
      const res = new Array(size).fill(Fr.ZERO);
      for (let s = 0; s < weights.length; s++) {
        for (let c in constraints[s]) {
          // Setup stores sparse matrices by signal, while proving uses row-major proving-key matrices.
          const idx = transpose ? s : +c;
          res[idx] = Fr.add(
            res[idx],
            Fr.mul(transpose ? weights[+c] : weights[s], constraints[s][c])
          );
        }
      }
      return res;
    }
    return { pA: build(A), pB: build(B), pC: build(C) };
  }

  function calculateH(proof: ProvingKey, witness: Witness) {
    const m = proof.domainSize;
    const bits = log2(m);
    checkDomainBits(bits);
    // new snarkjs omit polsC and re-construct them via coset stuff & shifts.
    if (proof.ccoefs) {
      const pols = [];
      for (let i = 0; i < 3; i++) pols.push(new Array(m).fill(Fr.ZERO));
      for (const { matrix, constraint, signal, value } of proof.ccoefs) {
        pols[matrix][constraint] = Fr.add(pols[matrix][constraint], Fr.mul(value, witness[signal]));
      }
      const [pA, pB] = pols; // ignore polC
      const pC = polyFr.dot(pA, pB);
      // FFT to the shifted (coset) domain
      // A(x)·B(x) − C(x) = H(x)·Z_H(x) -> H(g·ω^i) = (Acos[i]·Bcos[i] − Ccos[i]) / Z_H(g·ω^i)
      const shift =
        bits === roots.info.powerOfTwo ? Fr.mul(roots.info.G, roots.info.G) : roots.omega(bits + 1);
      const Acos = poly.fft(polyFr.shift(poly.ifft(pA), shift), bits);
      const Bcos = poly.fft(polyFr.shift(poly.ifft(pB), shift), bits);
      const Ccos = poly.fft(polyFr.shift(poly.ifft(pC), shift), bits);
      return polyFr.sub(polyFr.dot(Acos, Bcos), Ccos);
    } else if (proof.polsA && proof.polsB && proof.polsC) {
      const { pA, pB, pC } = sumABC(m, witness, proof.polsA, proof.polsB, proof.polsC);
      // FFT only needed to optimize multiplication O(n²) to O(n log n)
      // pA * pB - pC
      return poly.sub(poly.mul(poly.ifft(pA), poly.ifft(pB)), poly.ifft(pC)).slice(m);
    }
    throw new Error('wrong proving key: no polynomials');
  }
  const utils = Object.freeze({ G1, G2, G1c, G2c } satisfies PointsWithCoders);
  // TODO: add other proofs, which re-use many polynomial operations
  // * We don't export alfabeta_12! It is only used for optimization, and is specific to
  //   pairing implementation (different values after final exponentiation).
  // * We accept raw circuit json here, no need for Circuit object!
  return Object.freeze({
    utils: utils,
    groth: Object.freeze({
      setup(circuit: CircuitInfo, rnd: TArg<RandFn> = randomBytes) {
        // Sizes
        const nConstraints = circuit.constraints.length;
        const domainBits = log2(nConstraints + circuit.nPubInputs + circuit.nOutputs + 1 - 1) + 1;
        checkDomainBits(domainBits);
        const domainSize = 2 ** domainBits;
        const nPublic = circuit.nPubInputs + circuit.nOutputs;
        const maxH = domainSize + 1;
        // Toxic
        const toxic = {
          t: Frandom(rnd),
          kalfa: Frandom(rnd),
          kbeta: Frandom(rnd),
          kgamma: Frandom(rnd),
          kdelta: Frandom(rnd),
        };
        // Zero toxic scalars make degenerate setup data and currently hit inverse paths for gamma/delta.
        for (const [k, v] of Object.entries(toxic))
          if (Fr.is0(v)) throw new Error(`expected non-zero toxic ${k}`);
        // G1
        const alfaP1 = G1c.encode(G1.BASE.multiplyUnsafe(Fr.create(toxic.kalfa)));
        const betaP1 = G1c.encode(G1.BASE.multiplyUnsafe(Fr.create(toxic.kbeta)));
        const deltaP1 = G1c.encode(G1.BASE.multiplyUnsafe(Fr.create(toxic.kdelta)));
        // G2
        const betaP2 = G2c.encode(G2.BASE.multiplyUnsafe(Fr.create(toxic.kbeta)));
        const deltaP2 = G2c.encode(G2.BASE.multiplyUnsafe(Fr.create(toxic.kdelta)));
        const gammaP2 = G2c.encode(G2.BASE.multiplyUnsafe(Fr.create(toxic.kgamma)));
        // Pols
        const pols: Constraint[][] = [0, 1, 2].map((side) =>
          Array.from({ length: circuit.nVars }, (_, s) =>
            Object.fromEntries(
              circuit.constraints
                .map((constraint, c) => [c, constraint[side]?.[s]])
                .filter(([, v]) => v !== undefined)
                .map(([c, v]) => [c, BigInt(v)])
            )
          )
        );
        const [polsA, polsB, polsC] = pols;
        for (let i = 0; i < circuit.nPubInputs + circuit.nOutputs + 1; i++)
          polsA[i][nConstraints + i] = Fr.ONE;
        // Evaluate
        const zt = Fr.sub(Fr.pow(toxic.t, BigInt(domainSize)), Fr.ONE);
        const u = poly.evaluateLagrangePolynomials(domainBits, toxic.t);
        const { pA, pB, pC } = sumABC(circuit.nVars, u, polsA, polsB, polsC, true);
        // C
        const C = new Array(circuit.nVars);
        const invDelta = Fr.inv(toxic.kdelta);
        for (let s = nPublic + 1; s < circuit.nVars; s++) {
          C[s] = G1c.encode(
            G1.BASE.multiplyUnsafe(
              Fr.mul(
                invDelta,
                Fr.add(Fr.add(Fr.mul(pA[s], toxic.kbeta), Fr.mul(pB[s], toxic.kalfa)), pC[s])
              )
            )
          );
        }
        // IC
        const IC = [];
        const invGamma = Fr.inv(toxic.kgamma);
        for (let s = 0; s <= nPublic; s++) {
          IC.push(
            G1c.encode(
              G1.BASE.multiplyUnsafe(
                Fr.mul(
                  invGamma,
                  Fr.add(Fr.add(Fr.mul(pA[s], toxic.kbeta), Fr.mul(pB[s], toxic.kalfa)), pC[s])
                )
              )
            )
          );
        }
        // hExps
        const zod = Fr.mul(invDelta, zt);
        const hExps = [G1c.encode(G1.BASE.multiplyUnsafe(zod))];
        for (let i = 1, eT = toxic.t; i < maxH; i++, eT = Fr.mul(eT, toxic.t))
          hExps.push(G1c.encode(G1.BASE.multiplyUnsafe(Fr.mul(eT, zod))));

        const pkey: ProvingKey = {
          protocol: 'groth',
          nVars: circuit.nVars,
          nPublic,
          domainBits,
          domainSize,
          // Polynominals
          polsA,
          polsB,
          polsC,
          //
          A: Array.from({ length: circuit.nVars }, (_, j) => G1.BASE.multiplyUnsafe(pA[j])).map(
            G1c.encode
          ),
          B1: Array.from({ length: circuit.nVars }, (_, j) => G1.BASE.multiplyUnsafe(pB[j])).map(
            G1c.encode
          ),
          B2: Array.from({ length: circuit.nVars }, (_, j) => G2.BASE.multiplyUnsafe(pB[j])).map(
            G2c.encode
          ),
          C,
          //
          vk_alfa_1: alfaP1,
          vk_beta_1: betaP1,
          vk_delta_1: deltaP1,
          vk_beta_2: betaP2,
          vk_delta_2: deltaP2,
          //
          hExps,
        };
        const vkey: VerificationKey = {
          protocol: 'groth',
          nPublic: circuit.nPubInputs + circuit.nOutputs,
          IC,
          //
          vk_alfa_1: alfaP1,
          vk_beta_2: betaP2,
          vk_gamma_2: gammaP2,
          vk_delta_2: deltaP2,
        };
        return {
          pkey,
          vkey,
          toxic: opts.unsafePreserveToxic ? toxic : undefined,
        };
      },
      async createProof(
        pkey: ProvingKey,
        witness: Witness,
        rnd: TArg<RandFn> = randomBytes
      ): Promise<ProofWithSignals> {
        witness = witness.map((i) => Fr.create(i));
        // Blinding salt for zero-knowledge
        const r = Frandom(rnd);
        const s = Frandom(rnd);
        const A = pkey.A.map(G1c.decode);
        const B1 = pkey.B1.map(G1c.decode);
        const B2 = pkey.B2.map(G2c.decode);
        const C = pkey.C.map(G1c.decode);
        const hExps = pkey.hExps.map(G1c.decode);
        const vk_alfa_1 = G1c.decode(pkey.vk_alfa_1);
        const vk_beta_1 = G1c.decode(pkey.vk_beta_1);
        const vk_beta_2 = G2c.decode(pkey.vk_beta_2);
        const vk_delta_1 = G1c.decode(pkey.vk_delta_1);
        const vk_delta_2 = G2c.decode(pkey.vk_delta_2);
        // Actual algorithm
        // pi_a = WITNESS_A + delta1*r
        const pi_a_msm = await G1msm(A, witness);
        const pi_a = pi_a_msm.add(vk_alfa_1).add(vk_delta_1.multiplyUnsafe(r));
        // pi_b = WITNESS_B + delta2*s
        const pi_b_msm = await G2msm(B2, witness);
        const pi_b = pi_b_msm.add(vk_beta_2).add(vk_delta_2.multiplyUnsafe(s));
        const pib1n_msm = await G1msm(B1, witness);
        const pib1n = pib1n_msm.add(vk_beta_1).add(vk_delta_1.multiplyUnsafe(s));
        const cOffset = pkey.nPublic + 1;
        const h = calculateH(pkey, witness).map((i) => Fr.create(i));
        //WITNESS3 + pi_a * s + WITNESS4 * r
        const pi_c_msm = await G1msm(
          C.slice(cOffset).concat(hExps.slice(0, h.length)),
          witness.slice(cOffset).concat(h)
        );
        const pi_c = pi_c_msm
          .add(pi_a.multiplyUnsafe(s))
          .add(pib1n.multiplyUnsafe(r))
          .add(vk_delta_1.multiplyUnsafe(Fr.create(Fr.neg(Fr.mul(r, s)))));
        return {
          proof: {
            protocol: 'groth',
            pi_a: G1c.encode(pi_a),
            pi_b: G2c.encode(pi_b),
            pi_c: G1c.encode(pi_c),
          },
          publicSignals: witness.slice(1, pkey.nPublic + 1),
        };
      },
      verifyProof(vkey: VerificationKey, proofWithSignals: ProofWithSignals): boolean {
        const { proof, publicSignals, commitments } = proofWithSignals;
        let cpub = pippenger(G1, vkey.IC.map(G1c.decode), [_1n, ...publicSignals]);
        if (commitments) {
          commitments.forEach((cm) => {
            cpub = cpub.add(G1c.decode(cm));
          });
        }
        // old e(pi_a, pi_b) = alfa_beta * e(cpub, gamma_2) * e(pi_c, delta_2)
        // new: e(-pi_a, pi_b) * e(cpub, gamma_2) * e(pi_c, delta_2) * e(alfa_1, beta_2) = 1
        // Major difference: old version uses pre-computed alfa_beta,
        // but this makes it incompatible with noble, because we use cyclomatic exp
        // (Fp12 values different even if math is same).
        const newRes = curve.pairingBatch([
          { g1: G1c.decode(proof.pi_a).negate(), g2: G2c.decode(proof.pi_b) },
          { g1: cpub, g2: G2c.decode(vkey.vk_gamma_2) },
          { g1: G1c.decode(proof.pi_c), g2: G2c.decode(vkey.vk_delta_2) },
          { g1: G1c.decode(vkey.vk_alfa_1), g2: G2c.decode(vkey.vk_beta_2) },
        ]);
        return Fp12.eql(newRes, Fp12.ONE);
      },
    }),
  }) as TRet<SnarkConstructorOutput>;
}

/**
 * ZK Snarks over bn254 (aka bn128) curve.
 * @example
 * Build the bundled bn254 helpers and run a tiny self-contained Groth16 round-trip.
 * ```ts
 * import { bn254, type CircuitInfo } from 'micro-zk-proofs';
 * const circuit: CircuitInfo = {
 *   nVars: 2,
 *   nPubInputs: 0,
 *   nOutputs: 0,
 *   constraints: [[{}, {}, {}]],
 * };
 * const setup = bn254.groth.setup(circuit);
 * const proof = await bn254.groth.createProof(setup.pkey, [1n, 0n]);
 * bn254.groth.verifyProof(setup.vkey, proof);
 * ```
 */
export const bn254: TRet<SnarkConstructorOutput> = /* @__PURE__ */ buildSnark(nobleBn254, {});
// NOTE: this is unsafe and may not work (untested for now)
//export const bls12_381 = buildSnark(nobleBls12, {});
