diff --git a/packages/circuits/tests/check-substring-match.test.ts b/packages/circuits/tests/check-substring-match.test.ts index c1d94410..1998d783 100644 --- a/packages/circuits/tests/check-substring-match.test.ts +++ b/packages/circuits/tests/check-substring-match.test.ts @@ -25,7 +25,7 @@ describe("CheckSubstringMatch Circuit", () => { }; const witness = await circuit.calculateWitness(input); await circuit.checkConstraints(witness); - expect(witness[1]).toBe(1n); // isMatch should be 1 + expect(witness[1]).toBe(1n); }); it("should not match when substring is different", async () => { @@ -35,7 +35,7 @@ describe("CheckSubstringMatch Circuit", () => { }; const witness = await circuit.calculateWitness(input); await circuit.checkConstraints(witness); - expect(witness[1]).toBe(0n); // isMatch should be 0 + expect(witness[1]).toBe(0n); }); it("should match with full length substring", async () => { @@ -49,7 +49,7 @@ describe("CheckSubstringMatch Circuit", () => { }; const witness = await circuit.calculateWitness(input); await circuit.checkConstraints(witness); - expect(witness[1]).toBe(1n); // isMatch should be 1 + expect(witness[1]).toBe(1n); }); it("should fail when first element of substring is zero", async () => { @@ -69,7 +69,7 @@ describe("CheckSubstringMatch Circuit", () => { }; const witness = await circuit.calculateWitness(input); await circuit.checkConstraints(witness); - expect(witness[1]).toBe(0n); // isMatch should be 0 + expect(witness[1]).toBe(0n); }); it("should match with single-element substring", async () => { @@ -79,7 +79,7 @@ describe("CheckSubstringMatch Circuit", () => { }; const witness = await circuit.calculateWitness(input); await circuit.checkConstraints(witness); - expect(witness[1]).toBe(1n); // isMatch should be 1 + expect(witness[1]).toBe(1n); }); it("should not match when input is all zeros", async () => { @@ -89,7 +89,7 @@ describe("CheckSubstringMatch Circuit", () => { }; const witness = await circuit.calculateWitness(input); await circuit.checkConstraints(witness); - expect(witness[1]).toBe(0n); // isMatch should be 0 + expect(witness[1]).toBe(0n); }); it("should not match when substring is longer than non-zero part of input", async () => { @@ -99,6 +99,6 @@ describe("CheckSubstringMatch Circuit", () => { }; const witness = await circuit.calculateWitness(input); await circuit.checkConstraints(witness); - expect(witness[1]).toBe(0n); // isMatch should be 0 + expect(witness[1]).toBe(0n); }); }); diff --git a/packages/circuits/tests/count-substring-occurrences.test.ts b/packages/circuits/tests/count-substring-occurrences.test.ts new file mode 100644 index 00000000..f0d7cd33 --- /dev/null +++ b/packages/circuits/tests/count-substring-occurrences.test.ts @@ -0,0 +1,106 @@ +import { wasm as wasm_tester } from "circom_tester"; +import path from "path"; + +describe("CountSubstringOccurrences Circuit", () => { + jest.setTimeout(10 * 60 * 1000); // 10 minutes + + let circuit: any; + + beforeAll(async () => { + circuit = await wasm_tester( + path.join( + __dirname, + "./test-circuits/count-substring-occurrences-test.circom" + ), + { + recompile: true, + include: path.join(__dirname, "../../../node_modules"), + output: path.join(__dirname, "./compiled-test-circuits"), + } + ); + }); + + it("should count single occurrence at the beginning", async () => { + const input = { + in: [1, 2, 3, 4, 5, ...Array(1019).fill(0)], + substring: [1, 2, 3, ...Array(125).fill(0)], + }; + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + expect(witness[1]).toBe(1n); + }); + + it("should count multiple occurrences", async () => { + const input = { + in: [1, 2, 3, 4, 1, 2, 3, 5, 1, 2, 3, ...Array(1013).fill(0)], + substring: [1, 2, 3, ...Array(125).fill(0)], + }; + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + expect(witness[1]).toBe(3n); + }); + + it("should return 0 for no occurrences", async () => { + const input = { + in: [1, 2, 4, 5, 6, ...Array(1019).fill(0)], + substring: [1, 2, 3, ...Array(125).fill(0)], + }; + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + expect(witness[1]).toBe(0n); + }); + + it("should count occurrences with overlap", async () => { + const input = { + in: [1, 1, 1, 2, 1, 1, ...Array(1018).fill(0)], + substring: [1, 1, ...Array(126).fill(0)], + }; + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + expect(witness[1]).toBe(3n); + }); + + it("should handle full match of input", async () => { + const repeatedPattern = [1, 2, 3, 4]; + const input = { + in: Array(256) + .fill(repeatedPattern) + .flat() + .concat(Array(1024 - 256 * 4).fill(0)), + substring: [1, 2, 3, 4, ...Array(124).fill(0)], + }; + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + expect(witness[1]).toBe(256n); + }); + + it("should handle single character substring", async () => { + const input = { + in: [1, 2, 1, 3, 1, 4, 1, ...Array(1017).fill(0)], + substring: [1, ...Array(127).fill(0)], + }; + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + expect(witness[1]).toBe(4n); + }); + + it("should handle substring at the end of input", async () => { + const input = { + in: [...Array(1021).fill(0), 1, 2, 3], + substring: [1, 2, 3, ...Array(125).fill(0)], + }; + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + expect(witness[1]).toBe(1n); + }); + + it("should return 0 for empty substring", async () => { + const input = { + in: [1, 2, 3, 4, 5, ...Array(1019).fill(0)], + substring: Array(128).fill(0), + }; + await expect(async () => { + await circuit.calculateWitness(input); + }).rejects.toThrow("Assert Failed"); + }); +}); diff --git a/packages/circuits/tests/test-circuits/count-substring-occurrences-test.circom b/packages/circuits/tests/test-circuits/count-substring-occurrences-test.circom new file mode 100644 index 00000000..e7ed7d41 --- /dev/null +++ b/packages/circuits/tests/test-circuits/count-substring-occurrences-test.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.6; + +include "../../utils/array.circom"; + +component main = CountSubstringOccurrences(1024, 128); \ No newline at end of file diff --git a/packages/circuits/utils/array.circom b/packages/circuits/utils/array.circom index d495d4f6..8538970d 100644 --- a/packages/circuits/utils/array.circom +++ b/packages/circuits/utils/array.circom @@ -231,18 +231,22 @@ template CountSubstringOccurrences(maxLen, maxSubstringLen) { signal output count; // Check for matches at each possible starting position - component matches[maxLen - maxSubstringLen]; - for (var i = 0; i < maxLen - maxSubstringLen; i++) { + component matches[maxLen]; + for (var i = 0; i < maxLen; i++) { matches[i] = CheckSubstringMatch(maxSubstringLen); for (var j = 0; j < maxSubstringLen; j++) { - matches[i].in[j] <== in[i + j]; + if (i + j < maxLen) { + matches[i].in[j] <== in[i + j]; + } else { + matches[i].in[j] <== 0; + } } matches[i].substring <== substring; } // Sum up all matches to get the total count - component summer = CalculateTotal(maxLen - maxSubstringLen); - for (var i = 0; i <= maxLen - maxSubstringLen; i++) { + component summer = CalculateTotal(maxLen); + for (var i = 0; i < maxLen; i++) { summer.nums[i] <== matches[i].isMatch; }