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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/string_search_example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Run string_search_example Tests on PR
on:
pull_request:
paths:
- "string_search_example/**"
schedule:
- cron: "0 2 * * *"
workflow_dispatch:

jobs:
test:
defaults:
run:
working-directory: string_search_example
permissions:
issues: write

runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Nargo
uses: noir-lang/[email protected]
with:
toolchain: stable

- name: Run Noir unit tests
working-directory: string_search_example/string_search
run: |
nargo test

- name: Create issue on failure (nightly)
if: failure() && github.event_name == 'schedule'
uses: actions/github-script@v6
with:
script: |
github.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '[Nightly] string_search_example workflow failed',
body: `The nightly string_search_example workflow failed. Please investigate.\n\n/cc @noir-lang/developerrelations`,
labels: ['nightly', 'bug']
})
1 change: 1 addition & 0 deletions string_search_example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
90 changes: 90 additions & 0 deletions string_search_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Noir Substring Search Circuit

This project demonstrates a robust substring search circuit using the Noir language and the [`noir_string_search`](https://github.com/noir-lang/noir_string_search) Noir library.
It provides a wrapper around the library's substring search to handle edge cases safely, making it suitable for use in zero-knowledge circuits.

## Features

- **Safe Substring Search:** Returns both a `found` boolean and the index of the first match.
- **Edge Case Handling:** Returns `(false, 0)` if the needle is empty, longer than the haystack, or not present.
- **Noir Library Compatibility:** Only calls the underlying library when it is safe to do so, avoiding panics/assertion failures.
- **Comprehensive Tests:** Includes unit tests for common and edge cases.

## Usage

### Adding noir_string_search to your project

To use `noir_string_search`, add it to your `Nargo.toml` dependencies section like this:

```toml
[dependencies]
noir_string_search = { git = "https://github.com/noir-lang/noir_string_search" }
```

Then, import it in your Noir code:

```rust
use noir_string_search::{StringBody256, SubString32};
```

### Circuit Interface

```rust
pub fn substring_search(
haystack: [u8; 256],
haystack_len: u32,
needle: [u8; 32],
needle_len: u32,
) -> (bool, u32)
```

- `haystack`: The byte array to search in (max length 256).
- `haystack_len`: The actual length of the haystack.
- `needle`: The substring to search for (max length 32).
- `needle_len`: The actual length of the needle.
- **Returns**: `(found, index)`
- `found`: `true` if the substring was found, `false` otherwise.
- `index`: the starting index of the first match (0 if not found).

### Example

```rust
let (found, index) = main(haystack, haystack_len, needle, needle_len);
assert(found == true);
assert(index == 3);
```

## Implementation

The circuit only attempts the underlying library search if:
- The needle is not empty.
- The needle length does not exceed the haystack length.

Otherwise, it returns `(false, 0)`.

## Test Coverage

The provided tests cover:
- Substring at start, middle, and end of haystack.
- Needle absent from haystack.
- Needle longer than haystack.
- Needle at second position.
- Full haystack and needle match.

Example test:

```rust
#[test]
fn finds_substring_at_start() -> Field {
// haystack = "hello", needle = "hello"
let (found, index) = main(haystack, 5, needle, 5);
assert(found == true);
assert(index == 0);
1
}
```

## Requirements

- [Noir](https://noir-lang.org/)
- [`noir_string_search`](https://github.com/noir-lang/noir_string_search) Noir library
8 changes: 8 additions & 0 deletions string_search_example/string_search/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "string_search_example"
type = "bin"
authors = ["cypriansakwa"]
compiler_version = ">=1.0.0"

[dependencies]
noir_string_search = {tag = "v0.3.3", git = "https://github.com/noir-lang/noir_string_search.git"}
6 changes: 6 additions & 0 deletions string_search_example/string_search/Prover.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
haystack = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
haystack_len = 11
needle = [119, 111, 114, 108, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
needle_len = 5
52 changes: 52 additions & 0 deletions string_search_example/string_search/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use noir_string_search::{StringBody256, SubString32};
mod test_inputs;

/// Substring search circuit-friendly: returns (found, index)
/// - If needle is empty -> found=true at index 0.
/// - If needle too long -> found=false at index 0.
/// - Otherwise -> uses library substring_match.
pub fn substring_search(
haystack: [u8; 256],
haystack_len: u32,
needle: [u8; 32],
needle_len: u32,
) -> (bool, u32) {
// Input validation
assert(haystack_len <= 256);
assert(needle_len <= 32);

// Initialize default output
let mut result_found = false;
let mut result_index = 0;

// Handle cases
if needle_len == 0 {
// Empty needle -> deterministic match at index 0
result_found = true;
result_index = 0;
} else if needle_len > haystack_len {
// Invalid case -> needle longer than haystack
result_found = false;
result_index = 0;
} else {
// Normal case: call substring library
let haystack_body: StringBody256 = StringBody256::new(haystack, haystack_len);
let needle_body: SubString32 = SubString32::new(needle, needle_len);
let (found, index) = haystack_body.substring_match(needle_body);

result_found = found;
result_index = index;
}

(result_found, result_index)
}

/// Noir entry point
fn main(
haystack: [u8; 256],
haystack_len: u32,
needle: [u8; 32],
needle_len: u32,
) -> pub (bool, u32) {
substring_search(haystack, haystack_len, needle, needle_len)
}
121 changes: 121 additions & 0 deletions string_search_example/string_search/src/test_inputs.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use crate::substring_search;

/// Helper to pad array to fixed length
fn pad_to_256(input: [u8], pad: u8) -> [u8; 256] {
let mut out: [u8; 256] = [0; 256];
let len = if input.len() > 256 { 256 } else { input.len() };
for i in 0..len { out[i] = input[i]; }
for i in len..256 { out[i] = pad; }
out
}

fn pad_to_32(input: [u8], pad: u8) -> [u8; 32] {
let mut out: [u8; 32] = [0; 32];
let len = if input.len() > 32 { 32 } else { input.len() };
for i in 0..len { out[i] = input[i]; }
for i in len..32 { out[i] = pad; }
out
}

#[test]
fn test_exact_match() {
// "hello world", "world"
let haystack = pad_to_256([104,101,108,108,111,32,119,111,114,108,100], 0);
let needle = pad_to_32([119,111,114,108,100], 0);
let (found, index) = substring_search(haystack, 11, needle, 5);
assert(found == true);
assert(index == 6);
}

#[test]
fn test_needle_at_start() {
// "abc123", "abc"
let haystack = pad_to_256([97, 98, 99, 49, 50, 51], 0);
let needle = pad_to_32([97, 98, 99], 0);
let (found, index) = substring_search(haystack, 6, needle, 3);
assert(found == true);
assert(index == 0);
}

#[test]
fn test_needle_at_end() {
// "good morning", "ning"
let haystack = pad_to_256([103, 111, 111, 100, 32, 109, 111, 114, 110, 105, 110, 103], 0);
let needle = pad_to_32([110, 105, 110, 103], 0);
let (found, index) = substring_search(haystack, 12, needle, 4);
assert(found == true);
assert(index == 8);
}

#[test]
fn test_haystack_equals_needle() {
let text = [104,101,108,108,111,32,119,111,114,108,100];
let haystack = pad_to_256(text, 0);
let needle = pad_to_32(text, 0);
let (found, index) = substring_search(haystack, 11, needle, 11);
assert(found == true);
assert(index == 0);
}
#[test]
fn test_empty_needle() {
// Empty needle should return (true, 0)
let haystack = pad_to_256([104,101,108,108,111], 0);
let needle = pad_to_32([], 0);
let (found, index) = substring_search(haystack, 5, needle, 0);
assert(found == true);
assert(index == 0);
}

#[test]
fn test_empty_haystack() {
// Empty haystack and non-empty needle -> no match
let haystack = pad_to_256([], 0);
let needle = pad_to_32([97, 98, 99], 0);
let (found, index) = substring_search(haystack, 0, needle, 3);
assert(found == false);
assert(index == 0);
}

#[test]
fn test_needle_longer_than_haystack() {
// Needle length > haystack length -> (false, 0)
let haystack = pad_to_256([97, 98, 99], 0);
let needle = pad_to_32([97, 98, 99, 100, 101], 0);
let (found, index) = substring_search(haystack, 3, needle, 5);
assert(found == false);
assert(index == 0);
}

#[test]
fn test_match_near_boundary() {
// Haystack length near 256, needle length 4 starting at 252
let mut data = [0; 256];
for i in 0..256 { data[i] = 97; } // fill with 'a'
data[252] = 120; data[253] = 121; data[254] = 122; data[255] = 119; // "xyzw" at the end
let haystack = pad_to_256(data, 0);
let needle = pad_to_32([120, 121, 122, 119], 0);
let (found, index) = substring_search(haystack, 256, needle, 4);
assert(found == true);
assert(index == 252);
}

#[test(should_fail)]
fn test_needle_not_found() {
// "abcdef", "xyz"
let haystack = pad_to_256([97, 98, 99, 100, 101, 102], 0); // "abcdef"
let needle = pad_to_32([120, 121, 122], 0); // "xyz"
let (found, index) = substring_search(haystack, 6, needle, 3);
assert(found == false);
assert(index == 0);
}

#[test(should_fail)]
fn test_max_length_inputs_no_match() {
// Full 256 haystack of 'a', full 32 needle of 'b' -> no match
let haystack = pad_to_256([97; 256], 0);
let needle = pad_to_32([98; 32], 0);
let (found, index) = substring_search(haystack, 256, needle, 32);
assert(found == false);
assert(index == 0);
}