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
1 change: 1 addition & 0 deletions string_search_example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
8 changes: 8 additions & 0 deletions string_search_example/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/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
94 changes: 94 additions & 0 deletions string_search_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 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

## License

MIT or Apache-2.0
48 changes: 48 additions & 0 deletions string_search_example/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use noir_string_search::{StringBody256, SubString32};
mod test_inputs;
/// Convert a bool to Field (1 or 0 for selector logic)
fn bool_to_field(b: bool) -> Field {
if b { 1 } else { 0 }
}

/// 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, use library match.
pub fn substring_search(
haystack: [u8; 256],
haystack_len: u32,
needle: [u8; 32],
needle_len: u32,
) -> (bool, u32) {
assert(haystack_len <= 256);
assert(needle_len <= 32);

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);

let is_empty = needle_len == 0;
let too_long = needle_len > haystack_len;

// Field selectors for circuit-friendly output selection
let is_empty_f = bool_to_field(is_empty);
let too_long_f = bool_to_field(too_long);
let normal_case_f = 1 - is_empty_f - too_long_f;

// Use selectors to choose output
let result_found_f: Field = is_empty_f + normal_case_f * bool_to_field(found);
let result_index: u32 = (is_empty_f * 0 + too_long_f * 0 + normal_case_f * index as Field) as u32;

(result_found_f == 1, 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)
}
60 changes: 60 additions & 0 deletions string_search_example/src/test_inputs.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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);
}

// Add more positive tests for chunk boundaries, maximal lengths, etc. as needed.