From fab310c473f52e3d6c645f01b7517dc4eb36e471 Mon Sep 17 00:00:00 2001 From: Oleg Komendant <44612825+Hrom131@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:57:53 +0300 Subject: [PATCH] Feature/constraints testing (#5) * Add constraints testing * Change project structure * Update version * update readme --------- Co-authored-by: Artem Chystiakov --- README.md | 14 +++ package-lock.json | 4 +- package.json | 2 +- src/chai-zkit.ts | 4 +- src/core/constraints.ts | 169 +++++++++++++++++++++++++++++++++++++ src/core/index.ts | 3 + src/{ => core}/proof.ts | 4 +- src/{ => core}/witness.ts | 4 +- src/types.ts | 2 + src/utils/compare-utils.ts | 4 + src/utils/utils.ts | 32 +++++++ test/constraints.test.ts | 153 +++++++++++++++++++++++++++++++++ 12 files changed, 386 insertions(+), 9 deletions(-) create mode 100644 src/core/constraints.ts create mode 100644 src/core/index.ts rename src/{ => core}/proof.ts (96%) rename src/{ => core}/witness.ts (97%) create mode 100644 test/constraints.test.ts diff --git a/README.md b/README.md index 0614a47..7c35a47 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,20 @@ await expect(matrix).to.not.verifyProof(otherProof); await expect(matrix).to.useSolidityVerifier(matrixVerifier).and.verifyProof(proof); ``` +### Constraints testing + +```ts +const matrix = await zkit.getCircuit("Matrix"); + +// constraints > 6 +expect(matrix).to.have.constraints.gt(6); +expect(matrix).to.have.constraints.greaterThan(6); + +// constraints < 10 +expect(matrix).to.have.constraints.lt(10); +expect(matrix).to.have.constraints.lessThan(10); +``` + ## Known limitations - Do not use `not` chai negation prior `witnessInputs` call, this will break the typization. diff --git a/package-lock.json b/package-lock.json index 61a0c17..3d26bac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solarity/chai-zkit", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solarity/chai-zkit", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "@solarity/zkit": "0.2.4", diff --git a/package.json b/package.json index cb20822..b55c1c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solarity/chai-zkit", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "author": "Distributed Lab", "readme": "README.md", diff --git a/src/chai-zkit.ts b/src/chai-zkit.ts index 22b14cf..cb269ab 100644 --- a/src/chai-zkit.ts +++ b/src/chai-zkit.ts @@ -1,7 +1,7 @@ -import { witness } from "./witness"; -import { proof } from "./proof"; +import { witness, proof, constraints } from "./core"; export function chaiZkit(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils): void { witness(chai, utils); proof(chai, utils); + constraints(chai, utils); } diff --git a/src/core/constraints.ts b/src/core/constraints.ts new file mode 100644 index 0000000..4fe3b20 --- /dev/null +++ b/src/core/constraints.ts @@ -0,0 +1,169 @@ +import { getConstraintsNumber, isCircuitZKit } from "../utils"; + +export function constraints(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils): void { + chai.Assertion.addProperty("constraints", function (this: any) { + utils.flag(this, "constraints", true); + + return this; + }); + + chai.Assertion.overwriteMethod("above", function (_super) { + return function (this: any, num: number | Date) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.above(num); + } else { + _super.apply(this, arguments); + } + }; + }); + + chai.Assertion.overwriteMethod("gt", function (_super) { + return function (this: any, num: number | Date) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.gt(num); + } else { + _super.apply(this, arguments); + } + }; + }); + + chai.Assertion.overwriteMethod("greaterThan", function (_super) { + return function (this: any, num: number | Date) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.greaterThan(num); + } else { + _super.apply(this, arguments); + } + }; + }); + + chai.Assertion.overwriteMethod("below", function (_super) { + return function (this: any, num: number | Date) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.below(num); + } else { + _super.apply(this, arguments); + } + }; + }); + + chai.Assertion.overwriteMethod("lt", function (_super) { + return function (this: any, num: number | Date) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.lt(num); + } else { + _super.apply(this, arguments); + } + }; + }); + + chai.Assertion.overwriteMethod("lessThan", function (_super) { + return function (this: any, num: number | Date) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.lessThan(num); + } else { + _super.apply(this, arguments); + } + }; + }); + + chai.Assertion.overwriteMethod("most", function (_super) { + return function (this: any, num: number | Date) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.most(num); + } else { + _super.apply(this, arguments); + } + }; + }); + + chai.Assertion.overwriteMethod("lte", function (_super) { + return function (this: any, num: number | Date) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.lte(num); + } else { + _super.apply(this, arguments); + } + }; + }); + + chai.Assertion.overwriteMethod("lessThanOrEqual", function (_super) { + return function (this: any, num: number | Date) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.lessThanOrEqual(num); + } else { + _super.apply(this, arguments); + } + }; + }); + + chai.Assertion.overwriteMethod("within", function (_super) { + return function (this: any, start: number, finish: number) { + const obj = utils.flag(this, "object"); + + if (isCircuitZKit(obj) && utils.flag(this, "constraints")) { + const newAssertation = new chai.Assertion(getConstraintsNumber(obj.mustGetArtifactsFilePath("r1cs"))); + + utils.flag(newAssertation, "negate", utils.flag(this, "negate")); + + newAssertation.to.within(start, finish); + } else { + _super.apply(this, arguments); + } + }; + }); +} diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..e81aebe --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,3 @@ +export * from "./proof"; +export * from "./witness"; +export * from "./constraints"; diff --git a/src/proof.ts b/src/core/proof.ts similarity index 96% rename from src/proof.ts rename to src/core/proof.ts index 637747b..0aaef5b 100644 --- a/src/proof.ts +++ b/src/core/proof.ts @@ -1,7 +1,7 @@ import { Signals } from "@solarity/zkit"; -import { GENERATE_PROOF_METHOD, USE_SOLIDITY_VERIFIER_METHOD, VERIFY_PROOF_METHOD } from "./constants"; -import { checkCircuitZKit } from "./utils"; +import { GENERATE_PROOF_METHOD, USE_SOLIDITY_VERIFIER_METHOD, VERIFY_PROOF_METHOD } from "../constants"; +import { checkCircuitZKit } from "../utils"; export function proof(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils): void { chai.Assertion.addMethod(GENERATE_PROOF_METHOD, function (this: any, inputs: Signals) { diff --git a/src/witness.ts b/src/core/witness.ts similarity index 97% rename from src/witness.ts rename to src/core/witness.ts index 6ebd676..36a1ebc 100644 --- a/src/witness.ts +++ b/src/core/witness.ts @@ -1,7 +1,7 @@ import { CircuitZKit, NumberLike, Signals } from "@solarity/zkit"; -import { checkCircuitZKit, loadOutputs, outputSignalsCompare } from "./utils"; -import { STRICT_PROPERTY, WITNESS_INPUTS_METHOD, WITNESS_OUTPUTS_METHOD } from "./constants"; +import { STRICT_PROPERTY, WITNESS_INPUTS_METHOD, WITNESS_OUTPUTS_METHOD } from "../constants"; +import { checkCircuitZKit, loadOutputs, outputSignalsCompare } from "../utils"; export function witness(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils): void { chai.Assertion.addProperty(STRICT_PROPERTY, function (this: any) { diff --git a/src/types.ts b/src/types.ts index c23b3b4..34f888c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,6 +35,7 @@ declare global { interface AsyncAssertion extends Promise, Witness, Proof { not: AsyncAssertion; strict: AsyncAssertion; + constraints: AsyncAssertion; to: AsyncAssertion; be: AsyncAssertion; been: AsyncAssertion; @@ -53,6 +54,7 @@ declare global { } interface Assertion extends Witness, Proof { + constraints: Assertion; to: Assertion; be: Assertion; been: Assertion; diff --git a/src/utils/compare-utils.ts b/src/utils/compare-utils.ts index 532d60d..0538152 100644 --- a/src/utils/compare-utils.ts +++ b/src/utils/compare-utils.ts @@ -48,6 +48,10 @@ export function checkCircuitZKit(circuitZKit: any, methodName: string) { } } +export function isCircuitZKit(circuitZKit: any): circuitZKit is CircuitZKit { + return circuitZKit instanceof CircuitZKit; +} + function compareSignals(actualSignal: Signal, expectedSignal: Signal): boolean { const actualSignalValues: NumberLike[] = flattenSignal(actualSignal); const expectedSignalValues: NumberLike[] = flattenSignal(expectedSignal); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 1aa6fa6..246cab6 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -44,6 +44,38 @@ export function stringifySignal(signal: Signal): string { return JSON.stringify(signal, (_, v) => (typeof v === "bigint" ? v.toString() : v)).replaceAll(`"`, ""); } +export function getConstraintsNumber(r1csFilePath: string): number { + const r1csDescriptor = fs.openSync(r1csFilePath, "r"); + + const readBytes = (position: number, length: number): bigint => { + const buffer = Buffer.alloc(length); + + fs.readSync(r1csDescriptor, buffer, { length, position }); + + return BigInt(`0x${buffer.reverse().toString("hex")}`); + }; + + /// @dev https://github.com/iden3/r1csfile/blob/d82959da1f88fbd06db0407051fde94afbf8824a/doc/r1cs_bin_format.md#format-of-the-file + const numberOfSections = readBytes(8, 4); + let sectionStart = 12; + + for (let i = 0; i < numberOfSections; ++i) { + const sectionType = Number(readBytes(sectionStart, 4)); + const sectionSize = Number(readBytes(sectionStart + 4, 8)); + + /// @dev Reading header section + if (sectionType == 1) { + const totalConstraintsOffset = 4 + 8 + 4 + 32 + 4 + 4 + 4 + 4 + 8; + + return Number(readBytes(sectionStart + totalConstraintsOffset, 4)); + } + + sectionStart += 4 + 8 + sectionSize; + } + + throw new Error(`Header section in ${r1csFilePath} file is not found.`); +} + function loadSym(zkit: CircuitZKit): Map { const symFile = zkit.mustGetArtifactsFilePath("sym"); const signals = new Map(); diff --git a/test/constraints.test.ts b/test/constraints.test.ts new file mode 100644 index 0000000..9aacfaa --- /dev/null +++ b/test/constraints.test.ts @@ -0,0 +1,153 @@ +import fs from "fs"; +import path from "path"; +import { expect } from "chai"; + +import { useFixtureProject } from "./helpers"; + +import "../src"; + +import { Matrix } from "./fixture-projects/complex-circuits/generated-types/zkit"; +import { getConstraintsNumber } from "../src/utils"; + +describe("constraints", () => { + let matrix: Matrix; + + function getArtifactsFullPath(circuitDirSourceName: string): string { + return path.join(process.cwd(), "zkit", "artifacts", "circuits", circuitDirSourceName); + } + + function getVerifiersDirFullPath(): string { + return path.join(process.cwd(), "contracts", "verifiers"); + } + + useFixtureProject("complex-circuits"); + + beforeEach(() => { + const circuitName = "Matrix"; + const circuitArtifactsPath = getArtifactsFullPath(`${circuitName}.circom`); + const verifierDirPath = getVerifiersDirFullPath(); + + matrix = new Matrix({ circuitName, circuitArtifactsPath, verifierDirPath }); + }); + + describe("above", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.above(6); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.above(10); + }); + }); + + describe("gt", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.gt(6); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.gt(10); + }); + }); + + describe("greaterThan", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.greaterThan(6); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.greaterThan(10); + }); + }); + + describe("below", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.below(10); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.below(6); + }); + }); + + describe("lt", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.lt(10); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.lt(6); + }); + }); + + describe("lessThan", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.lessThan(10); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.lessThan(6); + }); + }); + + describe("most", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.most(8); + expect(matrix).constraints.lessThanOrEqual(10); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.most(7); + }); + }); + + describe("lte", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.lte(8); + expect(matrix).constraints.lessThanOrEqual(10); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.lte(7); + }); + }); + + describe("lessThanOrEqual", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.lessThanOrEqual(8); + expect(matrix).constraints.lessThanOrEqual(10); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.lessThanOrEqual(7); + }); + }); + + describe("within", () => { + it("should pass with circuitZKit obj", async () => { + expect(matrix).constraints.within(7, 10); + }); + + it("should pass with circuitZKit obj and negation", async () => { + expect(matrix).constraints.not.within(10, 12); + expect(matrix).constraints.not.within(5, 7); + }); + }); + + describe("getConstraintsNumber", () => { + it("should get exception if pass invalid r1cs file path", async () => { + const r1csFilePath: string = matrix.getArtifactsFilePath("r1cs"); + + const r1csFileContent = fs.readFileSync(r1csFilePath); + + fs.rmSync(r1csFilePath); + fs.writeFileSync(r1csFilePath, ""); + + expect(function () { + getConstraintsNumber(r1csFilePath); + }).to.throw(`Header section in ${r1csFilePath} file is not found.`); + + fs.writeFileSync(r1csFilePath, r1csFileContent); + }); + }); +});