Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Bring (py)Chiquito in #86

Merged
merged 8 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/target
/Cargo.lock
.vscode
.env
__pycache__
*.so
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entirely cosmetic but we typically add one empty line to end of file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cosmetic haha

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, in some edge cases it can lead to unnecessary git conflicts when several people changes the last lines of the file. So we have this rule of always ending all files with an empty line

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ authors = ["Leo Lara <[email protected]>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
pyo3 = { version = "0.19.1", features = ["extension-module"] }
halo2_proofs = { git = "https://github.com/privacy-scaling-explorations/halo2.git", features = [
"circuit-params",
"circuit-params",
], tag = "v2023_04_20" }
halo2curves = { git = 'https://github.com/privacy-scaling-explorations/halo2curves', tag = "0.3.2", features = [ "derive_serde" ] }
halo2curves = { git = 'https://github.com/privacy-scaling-explorations/halo2curves', tag = "0.3.2", features = [
"derive_serde",
] }
polyexen = { git = "https://github.com/Dhole/polyexen.git", rev = "67148a8aabb000a61a169b2533b2526c5b76c42f" }
num-bigint = { version = "0.4", features = ["rand"] }
uuid = { version = "1.4.0", features = ["v1", "rng"] }
Expand Down
34 changes: 30 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@

Chiquito is a step-based high-level rust DSL (pychiquito is a python DSL for chiquito) that provides better syntax and abstraction for constraint building and column placement when writing plonkish circuits and has a Halo2 backend, and other backends are in the works. It allows the user to manipulate an AST that’s compiled to a Chiquito Halo2 backend, which can be integrated into any Halo2 circuit.

It's **HIGHLY RECOMMENDED** that you read the [design principles](https://github.com/privacy-scaling-explorations/chiquito/blob/main/Appendix.md/#design-principles), [architecture, and specific terms](https://github.com/privacy-scaling-explorations/chiquito/blob/main/Appendix.md/#architecture) of a Chiquito circuit before getting started.
It's **HIGHLY RECOMMENDED** that you read the [design principles](https://github.com/privacy-scaling-explorations/chiquito/blob/main/Appendix.md/#design-principles), [architecture, and specific terms](https://github.com/privacy-scaling-explorations/chiquito/blob/main/Appendix.md/#architecture) of a Chiquito circuit before getting started.

You can also learn about the project's [current status](https://github.com/privacy-scaling-explorations/chiquito/blob/main/Appendix.md/#project-status-as-of-april-2023)) and its [next steps](https://github.com/privacy-scaling-explorations/chiquito/blob/main/Appendix.md/#vision-and-next-steps).
You can also learn about the project's [current status](https://github.com/privacy-scaling-explorations/chiquito/blob/main/Appendix.md/#project-status-as-of-april-2023) and its [next steps](https://github.com/privacy-scaling-explorations/chiquito/blob/main/Appendix.md/#vision-and-next-steps).

# Getting Started

## Setup

Run the following in command line to add Chiquito as a dependency for your project:

```
cargo add --git https://github.com/privacy-scaling-explorations/chiquito
cargo add --git https://github.com/privacy-scaling-explorations/chiquito
```

Use the following examples to understand how Chiquito works or use them as starting templates for building your own Chiquito circuit.
Expand All @@ -22,29 +24,53 @@ Refer to the Appendix on the [exposed user functions](https://github.com/privacy

Refer to [Testing and Links](#testing-and-links) on detailed API documentations.


## Example: Fibonacci Circuit

Run the following in command line:

```
cargo run --example fibonacci
```

This example demonstrates how to construct signals, step types, constraints, and witness generation in Chiquito. Best for first time Chiquito users.

## Example: MiMC7 Circuit

TODO: annotate this code example

This example demonstrates how to construct a lookup table and use external inputs as trace arguments in Chiquito, besides covering concepts in the Fibonacci example. MiMC7 is a zk-friendly hashing function.

## Example: zkEVM Bytecode Circuit

https://github.com/privacy-scaling-explorations/zkevm-circuits/pull/1348

This example rewrites the zkEVM bytecode circuit using Chiquito and passes all original tests. It demonstrates how Chiquito can standardize and simplify larger scale circuits on the production level.

## Python bindings

### Install chiquito with pip

```bash
pip install chiquito
```

### Build from source

In the root of this repo run

```bash
python -m venv .env
source .env/bin/activate
pip install maturin
maturin develop
```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this!

Are "install chiquito with pip" and "build from source" considered two different methods for installation or two steps of installation?

To confirm, after pip install chiquito, is rust_chiquito directly accessible from chiquito.rust_chiquito or requires maturin develop? My guess is the latter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's either to install, pip package will be available for python user, maturin develop is only for who want to build from source/develop.
Will make it clear


# Testing and Links

**API documentation**: `cargo doc --no-deps --package chiquito --open`

Currently API documentation is only written for exposed user functions, which are scattered across the DSL, constraint builder, compiler, and AST. **Refer to the following subdirectories for specific functions:**

- Circuit building (DSL): https://qwang98.github.io/chiquito/chiquito/dsl/index.html
- Constraint building (constraint builder): https://qwang98.github.io/chiquito/chiquito/dsl/cb/index.html
- Witness generation (compiler): https://qwang98.github.io/chiquito/chiquito/compiler/trait.WitnessGenContext.html
Expand Down
62 changes: 62 additions & 0 deletions examples/fibonacci.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I've updated the example in the PyChiquito repo, but we can focus on getting the structure working before porting over the latest PyChiquito updates.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: there are also some python comments that I should delete once we have this merged.

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from __future__ import annotations
from typing import Tuple

from chiquito.dsl import Circuit, StepType
from chiquito.cb import eq
from chiquito.query import Queriable
from chiquito.util import F


class Fibonacci(Circuit):
def setup(self: Fibonacci):
self.a: Queriable = self.forward("a")
self.b: Queriable = self.forward("b")

self.fibo_step = self.step_type(FiboStep(self, "fibo_step"))
self.fibo_last_step = self.step_type(FiboLastStep(self, "fibo_last_step"))

self.pragma_first_step(self.fibo_step)
self.pragma_last_step(self.fibo_last_step)
self.pragma_num_steps(11)

def trace(self: Fibonacci, args: Any):
self.add(self.fibo_step, (1, 1))
a = 1
b = 2
for i in range(1, 10):
self.add(self.fibo_step, (a, b))
prev_a = a
a = b
b += prev_a
self.add(self.fibo_last_step, (a, b))


class FiboStep(StepType):
def setup(self: FiboStep):
self.c = self.internal("c")
self.constr(eq(self.circuit.a + self.circuit.b, self.c))
self.transition(eq(self.circuit.b, self.circuit.a.next()))
self.transition(eq(self.c, self.circuit.b.next()))

def wg(self: FiboStep, args: Tuple[int, int]):
a_value, b_value = args
self.assign(self.circuit.a, F(a_value))
self.assign(self.circuit.b, F(b_value))
self.assign(self.c, F(a_value + b_value))


class FiboLastStep(StepType):
def setup(self: FiboLastStep):
self.c = self.internal("c")
self.constr(eq(self.circuit.a + self.circuit.b, self.c))

def wg(self: FiboLastStep, values=Tuple[int, int]):
a_value, b_value = values
self.assign(self.circuit.a, F(a_value))
self.assign(self.circuit.b, F(b_value))
self.assign(self.c, F(a_value + b_value))


fibo = Fibonacci()
fibo_witness = fibo.gen_witness(None)
fibo.halo2_mock_prover(fibo_witness)
Empty file.
224 changes: 224 additions & 0 deletions frontend/python/chiquito/cb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum, auto
from typing import List

from chiquito.util import F
from chiquito.expr import Expr, Const, Neg, to_expr, ToExpr
from chiquito.query import StepTypeNext
from chiquito.chiquito_ast import ASTStepType


class Typing(Enum):
Unknown = auto()
Boolean = auto()
AntiBooly = auto()


@dataclass
class Constraint:
annotation: str
expr: Expr
typing: Typing

def from_expr(
expr: Expr,
) -> Constraint: # Cannot call function `from`, a reserved keyword in Python.
annotation: str = str(expr)
if isinstance(expr, StepTypeNext):
return Constraint(annotation, expr, Typing.Boolean)
else:
return Constraint(annotation, expr, Typing.Unknown)

def __str__(self: Constraint) -> str:
return self.annotation


def cb_and(
inputs: List[ToConstraint],
) -> Constraint: # Cannot call function `and`, a reserved keyword in Python
inputs = [to_constraint(input) for input in inputs]
annotations: List[str] = []
expr = Const(F(1))
for constraint in inputs:
if constraint.typing == Typing.Boolean or constraint.typing == Typing.Unknown:
annotations.append(constraint.annotation)
expr = expr * constraint.expr
else:
raise ValueError(
f"Expected Boolean or Unknown constraint, got AntiBooly (constraint: {constraint.annotation})"
)
return Constraint(f"({' AND '.join(annotations)})", expr, Typing.Boolean)


def cb_or(
inputs: List[ToConstraint],
) -> Constraint: # Cannot call function `or`, a reserved keyword in Python
inputs = [to_constraint(input) for input in inputs]
annotations: List[str] = []
exprs: List[Expr] = []
for constraint in inputs:
if constraint.typing == Typing.Boolean or constraint.typing == Typing.Unknown:
annotations.append(constraint.annotation)
exprs.append(constraint.expr)
else:
raise ValueError(
f"Expected Boolean or Unknown constraint, got AntiBooly (constraint: {constraint.annotation})"
)
result: Constraint = Constraint.cb_not(
Constraint.cb_and([Constraint.cb_not(expr) for expr in exprs])
)
return Constraint(f"({' OR '.join(annotations)})", result.expr, Typing.Boolean)


def xor(lhs: ToConstraint, rhs: ToConstraint) -> Constraint:
(lhs, rhs) = (to_constraint(lhs), to_constraint(rhs))
if (lhs.typing == Typing.Boolean or lhs.typing == Typing.Unknown) and (
rhs.typing == Typing.Boolean or rhs.typing == Typing.Unknown
):
return Constraint(
f"({lhs.annotation} XOR {rhs.annotation})",
lhs.expr + rhs.expr - F(2) * lhs.expr * rhs.expr,
Typing.Boolean,
)
else:
raise ValueError(
f"Expected Boolean or Unknown constraints, got AntiBooly in one of lhs or rhs constraints (lhs constraint: {lhs.annotation}) (rhs constraint: {rhs.annotation})"
)


def eq(lhs: ToConstraint, rhs: ToConstraint) -> Constraint:
(lhs, rhs) = (to_constraint(lhs), to_constraint(rhs))
return Constraint(
f"({lhs.annotation} == {rhs.annotation})",
lhs.expr - rhs.expr,
Typing.AntiBooly,
)


def select(
selector: ToConstraint, when_true: ToConstraint, when_false: ToConstraint
) -> Constraint:
(selector, when_true, when_false) = (
to_constraint(selector),
to_constraint(when_true),
to_constraint(when_false),
)
if selector.typing == Typing.AntiBooly:
raise ValueError(
f"Expected Boolean or Unknown selector, got AntiBooly (selector: {selector.annotation})"
)
return Constraint(
f"if({selector.annotation})then({when_true.annotation})else({when_false.annotation})",
selector.expr * when_true.expr + (F(1) - selector.expr) * when_false.expr,
when_true.typing if when_true.typing == when_false.typing else Typing.Unknown,
)


def when(selector: ToConstraint, when_true: ToConstraint) -> Constraint:
(selector, when_true) = (to_constraint(selector), to_constraint(when_true))
if selector.typing == Typing.AntiBooly:
raise ValueError(
f"Expected Boolean or Unknown selector, got AntiBooly (selector: {selector.annotation})"
)
return Constraint(
f"if({selector.annotation})then({when_true.annotation})",
selector.expr * when_true.expr,
when_true.typing,
)


def unless(selector: ToConstraint, when_false: ToConstraint) -> Constraint:
(selector, when_false) = (to_constraint(selector), to_constraint(when_false))
if selector.typing == Typing.AntiBooly:
raise ValueError(
f"Expected Boolean or Unknown selector, got AntiBooly (selector: {selector.annotation})"
)
return Constraint(
f"unless({selector.annotation})then({when_false.annotation})",
(F(1) - selector.expr) * when_false.expr,
when_false.typing,
)


def cb_not(
constraint: ToConstraint,
) -> Constraint: # Cannot call function `not`, a reserved keyword in Python
constraint = to_constraint(constraint)
if constraint.typing == Typing.AntiBooly:
raise ValueError(
f"Expected Boolean or Unknown constraint, got AntiBooly (constraint: {constraint.annotation})"
)
return Constraint(
f"NOT({constraint.annotation})", F(1) - constraint.expr, Typing.Boolean
)


def isz(constraint: ToConstraint) -> Constraint:
constraint = to_constraint(constraint)
return Constraint(
f"0 == {constraint.annotation}", constraint.expr, Typing.AntiBooly
)


def if_next_step(step_type: ASTStepType, constraint: ToConstraint) -> Constraint:
constraint = to_constraint(constraint)
return Constraint(
f"if(next step is {step_type.annotation})then({constraint.annotation})",
StepTypeNext(step_type) * constraint.expr,
constraint.typing,
)


def next_step_must_be(step_type: ASTStepType) -> Constraint:
return Constraint(
f"next step must be {step_type.annotation}",
Constraint.cb_not(StepTypeNext(step_type)),
Typing.AntiBooly,
)


def next_step_must_not_be(step_type: ASTStepType) -> Constraint:
return Constraint(
f"next step must not be {step_type.annotation}",
StepTypeNext(step_type),
Typing.AntiBooly,
)


def rlc(exprs: List[ToExpr], randomness: Expr) -> Expr:
if len(exprs) > 0:
exprs: List[Expr] = [to_expr(expr) for expr in exprs].reverse()
init: Expr = exprs[0]
for expr in exprs[1:]:
init = init * randomness + expr
return init
else:
return Expr(Const(F(0)))


# TODO: Implement lookup table after the lookup abstraction PR is merged.


ToConstraint = Constraint | Expr | int | F


def to_constraint(v: ToConstraint) -> Constraint:
if isinstance(v, Constraint):
return v
elif isinstance(v, Expr):
if isinstance(v, StepTypeNext):
return Constraint(str(v), v, Typing.Boolean)
else:
return Constraint(str(v), v, Typing.Unknown)
elif isinstance(v, int):
if v >= 0:
return to_constraint(Const(F(v)))
else:
return to_constraint(Neg(Const(F(-v))))
elif isinstance(v, F):
return to_constraint(Const(v))
else:
raise TypeError(
f"Type `{type(v)}` is not ToConstraint (one of Constraint, Expr, int, or F)."
)
Loading
Loading