{"id":13630173,"url":"https://github.com/erhant/zkbrainfuck","last_synced_at":"2025-03-21T08:31:54.365Z","repository":{"id":182871589,"uuid":"667171675","full_name":"erhant/zkbrainfuck","owner":"erhant","description":"A Brainfuck zkVM with Circom.","archived":false,"fork":false,"pushed_at":"2023-07-21T15:43:44.000Z","size":118,"stargazers_count":58,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-17T23:14:26.870Z","etag":null,"topics":["brainfuck","circom","golang","zero-knowledge","zkvm"],"latest_commit_sha":null,"homepage":"","language":"Circom","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/erhant.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-07-16T21:52:30.000Z","updated_at":"2024-12-28T14:08:39.000Z","dependencies_parsed_at":"2023-07-21T23:56:02.492Z","dependency_job_id":null,"html_url":"https://github.com/erhant/zkbrainfuck","commit_stats":null,"previous_names":["erhant/zkbrainfuck"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erhant%2Fzkbrainfuck","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erhant%2Fzkbrainfuck/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erhant%2Fzkbrainfuck/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erhant%2Fzkbrainfuck/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/erhant","download_url":"https://codeload.github.com/erhant/zkbrainfuck/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244765496,"owners_count":20506823,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["brainfuck","circom","golang","zero-knowledge","zkvm"],"created_at":"2024-08-01T22:01:32.594Z","updated_at":"2025-03-21T08:31:53.307Z","avatar_url":"https://github.com/erhant.png","language":"Circom","funding_links":[],"categories":["Circom"],"sub_categories":[],"readme":"# zkBrainfuck\n\n\u003e A [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) zkVM to prove correct execution of a Brainfuck program with secret inputs.\n\nBrainfuck is a Turing-complete language that has only 8 operations, shown in the table below. Any other symbol is ignored, and may effectively be used as comments.\n\n| Code | Operation                                        |\n| ---- | ------------------------------------------------ |\n| `\u003e`  | increment data pointer                           |\n| `\u003c`  | decrement data pointer                           |\n| `+`  | increment pointed data                           |\n| `-`  | decrement pointed data                           |\n| `,`  | write to pointed data                            |\n| `.`  | read from pointed data                           |\n| `[`  | if pointed data is zero, `goto` matching `]`     |\n| `]`  | if pointed data is non-zero, `goto` matching `[` |\n\n## Usage\n\nWe have written a small Brainfuck compiler \u0026 executer in Go, which you can find under the [vm](./vm/) folder. Assuming you have Go installed, you can build the binary simply via:\n\n```sh\nyarn vm:build\n```\n\nAfterwards, you can run the binary with:\n\n```sh\nyarn vm:run\n  -code   string   # brainfuck code (default \",[.-]\")\n  -export string   # path to export program information\n  -path   string   # path to import brainfuck code\n  -memory uint     # memory size (default 128)\n  -opsize uint     # operations size\n  -ticks  uint     # maximum number of ticks (default 2048)\n  -num             # use numbers for input \u0026 output instead of runes\n```\n\nYou may find a few example Brainfuck codes in [here](./vm/sample). To run your own Brainfuck code, provide its path to the `--path` option. If your inputs \u0026 outputs are numbers (not characters) make sure you also pass in the `--num` option.\n\nThe VM has a maximum number of ticks to prevent infinite loops. If the default tick amount is not enough, increase it with `--tick \u003camount\u003e`.\n\nTo export the execution of the code, you will need to pass in `--export \u003cpath\u003e` option. This will include all the operations, inputs and outputs that were encountered within the program. You can use the `ops`, `inputs` and `outputs` there as circuit signals. Note that you may have to append zeros to match signal sizes depending on the circuit template parameters. We do this [automatically](./tests/utils/index.ts) in our tests.\n\nWe have prepared tests with [Circomkit](https://github.com/erhant/circomkit) for 3 different brainfuck programs. You can run them with:\n\n```sh\nyarn test\n```\n\nOf course, you will need to have [Circom](https://docs.circom.io/) installed on your machine.\n\n## Brainfuck Circuit\n\nWe will write the Brainfuck VM as an algebraic circuit, meaning that instead of tokens (like `+` or `-`) we shall operate on numbers. This is the reason we compile Brainfuck code in the first place, the result of compilation is simply an array of non-negative integers. The circuit asserts each \"tick\" to be valid, eventually running the program until no more ticks are left. Here is a rough demonstration of the instructions:\n\n| `op`           | Code  | Relevant Constraint                                     |\n| -------------- | ----- | ------------------------------------------------------- |\n| 0              | no-op | `next_pgm_ctr \u003c== pgm_ctr`                              |\n| 1              | `\u003e`   | `next_mem_ptr \u003c== mem_ptr + 1`                          |\n| 2              | `\u003c`   | `next_mem_ptr \u003c== mem_ptr - 1`                          |\n| 3              | `+`   | `next_mem[mem_ptr] \u003c== mem[mem_ptr] + 1`                |\n| 4              | `-`   | `next_mem[mem_ptr] \u003c== mem[mem_ptr] - 1`                |\n| 5              | `,`   | `next_mem[mem_ptr] \u003c== in`                              |\n| 6              | `.`   | `out === mem[mem_ptr]`                                  |\n| `pgm_ctr \u003c op` | `[`   | `next_pgm_ctr \u003c== mem[mem_ptr] == 0 ? op : pgm_ctr + 1` |\n| `pgm_ctr \u003e op` | `]`   | `next_pgm_ctr \u003c== mem[mem_ptr] != 0 ? op : pgm_ctr + 1` |\n\nTo disambugate `op` values from jump targets, compiled code will be prepended with 7 zeros, one for each `op`. This way, `op` checks can be made with simple equality checks, and jump targets can be assumed safe. Brainfuck programs usually terminate when there is no more instructions left; however, we can't do that in our circuit.\n\nIn particular, the circuit operates until each \"tick\" is processed, whether there are any ops left or not is not of concern. For this reason, the compiled code will have a zero at the end, corresponding to \"terminating the program\". In a no-op, the program counter is NOT incremented, thereby consuming ticks at that position until the circuit is finished.\n\nBy default, all signals stay the same from a tick to next, except the program counter which is incremented.\n\n### Parameters\n\nThe Brainfuck circuit is instantiated with the following parameters:\n\n- `TICKS`: number of ticks to run\n- `MEMSIZE`: maximum memory size\n- `OPSIZE`: maximum number of operations\n- `INSIZE`: maximum number of inputs\n- `OUTSIZE`: maximum number of outputs\n\nNote that we particularly use the word \"maximum\" because you do not necessarily have to provide exactly that many inputs, for any input with less elements for that parameter is assumed to be appended zeros. Our circuit has three inputs:\n\n- `ops`: compiled code\n- `inputs`: inputs in the order they appear\n- `outputs`: outputs in the order they appear\n\nFor example, the object below belongs to the execution of `,[.-]`. Notice the prepended 7 zeros and 1 extra zero at the end for `op`. In this particular execution, the user has given the input `5` and got the output `5 4 3 2 1`. Extra information such as memory usage and ticks is also included here.\n\n```json\n{\n  \"ticks\": 22,\n  \"memsize\": 0,\n  \"opsize\": 13,\n  \"insize\": 1,\n  \"outsize\": 5,\n  \"ops\": [0, 0, 0, 0, 0, 0, 0, 5, 11, 6, 4, 8, 0],\n  \"inputs\": [5],\n  \"outputs\": [5, 4, 3, 2, 1]\n}\n```\n\nTo prepare this object as a circuit input, we append necessary zeros to inputs, outputs, and ops to match `INSIZE`, `OUTSIZE` and `OPSIZE` respectively. We also check the tick count and memory size to see if we can safely use the circuit.\n\n### Constraints\n\nWe have some example constraint counts [here](./CONSTRAINTS.md). We can infer the following results:\n\n- x2 `TICKS` results in ~x2 constraints\n- x2 `OPSIZE` results in ~x1.5 constraints\n- x2 `MEMSIZE` results in ~x1.3 constraints\n\nWe have an example circuit parameter ready in [circuits.json](./circuits.json): 1000 ticks, 8 memory size, 200 ops, 5 inputs, 15 outputs. This circuit results in close to **1 million** constraints. You can compile it via:\n\n```sh\nnpx circomkit compile brainfuck\n```\n\nThere could be further optimizations regarding `ArrayRead`. For example, we know that the maximum value an `input_ptr` or `output_ptr` can take a tick `t` is `t-1`. Therefore, instead of reading the entire array each tick, they can read from `0..(t-1)` thereby halving the number of constraints until `t == INSIZE` or `t == OUTSIZE` respectively.\n\nSince this work is just for fun, constraint golfing is left for later at more times to kill.\n\n### Drawbacks\n\nFirst and foremost, the constraint count is HUGE. This is mostly because of the `ArrayRead` circuit which reads from an array with unknown index. Doing so requires an entire pass over the array with an equality check for each index. For small input arrays this should not be too much of a problem, but inputs may change from time to time.\n\nThe second problem is that due to the constraint count, a single huge circuit with many ticks, sufficient memory size, input size, and output size would be rather expensive (although possible). We believe folding may be used to fold each tick in a single recursive proof, perhaps using a tool such as Nova Scotia.\n\nThird drawback is that, who even writes Brainfuck?\n\n## See Also\n\n- [Typefuck](https://github.com/susisu/typefuck) is a Brainfuck interpreter using the type-system of Typescript alone.\n- [How Brainfuck Works](https://gist.github.com/roachhd/dce54bec8ba55fb17d3a) is a great Gist about Brainfuck.\n- [Brainfuck in STARK](https://neptune.cash/learn/brainfuck-tutorial/) is a quite nice example of another zkBrainfuck based on STARK.\n- [Brainfuck in Golang](https://github.com/kgabis/brainfuck-go/blob/master/bf.go) is another implementation of Brainfuck in go.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferhant%2Fzkbrainfuck","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ferhant%2Fzkbrainfuck","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferhant%2Fzkbrainfuck/lists"}