Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new chapter for common types and expand the basic concepts #846

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

yunus433
Copy link
Contributor

The first commit to update o1js docs based on the O(1) Labs feedback. Changed common types chapter and expanded basic concepts. Added the following information:

  • Detailed Field description
  • Explanation on assertions
  • Information on printing
  • Detailed Bool description
  • Added UInt32 / UInt64
  • Added Public / Private Keys
  • Expanded conditions
  • Added loops
  • Expanded functions

@yunus433 yunus433 requested a review from a team as a code owner February 13, 2024 22:07
Copy link

vercel bot commented Feb 13, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
docs2 ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 20, 2024 4:10pm

Copy link

vercel bot commented Feb 13, 2024

@yunus433 is attempting to deploy a commit to the o1labs Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Contributor

@mitschabaude mitschabaude left a comment

Choose a reason for hiding this comment

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

First pass from me

docs/zkapps/o1js/basic-concepts.md Outdated Show resolved Hide resolved
docs/zkapps/o1js/basic-concepts.md Outdated Show resolved Hide resolved

:::note

For the cryptography inclined, the exact max value that a field can store is: 28,948,022,309,329,048,855,892,746,252,171,976,963,363,056,481,941,560,715,954,676,764,349,967,630,336
For the cryptography inclined, the prime order of the o1js `Field` is: 28,948,022,309,329,048,855,892,746,252,171,976,963,363,056,481,941,560,715,954,676,764,349,967,630,336
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an improvement to before. But what would also be interesting for cryptographically inclined readers is to know what kind of field this is (Pallas base field) with a link to more details.

Also, I suggest to write the modulus in hex format, because that visually shows the twoadicity

docs/zkapps/o1js/basic-concepts.md Outdated Show resolved Hide resolved
docs/zkapps/o1js/basic-concepts.md Outdated Show resolved Hide resolved
const MAX_ITERATION_COUNT = 64;

pow.assertGreaterThanOrEqual(0);
pow.assertSmallerThanOrEqual(MAX_ITERATION_COUNT); // This code is to calculate up to 2^64, you can optimize this based on your needs
Copy link
Contributor

Choose a reason for hiding this comment

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

Use a 64 bit range check, or pass in a UInt64

let loopEnded = Bool(false); // Checking if the loop has ended yet

for (let i = 0; i <= MAX_ITERATION_COUNT; i++) { // Static sized loop
loopEnded = Provable.if( // End Condition
Copy link
Contributor

Choose a reason for hiding this comment

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

Redundant if

Copy link
Contributor

Choose a reason for hiding this comment

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

I would do this:

loopEnded = power.equals(i).or(loopEnded);

but yours is also ok


for (let i = 0; i <= MAX_ITERATION_COUNT; i++) { // Static sized loop
loopEnded = Provable.if( // End Condition
Field(i).greaterThanOrEqual(power),
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Field(i).greaterThanOrEqual(power),
power.equals(i),


# Functions

Functions work as you would expect in TS. However, your functions inside ZKPs must take provable arguments and return provable types, just like anything in o1js. For example:
Copy link
Contributor

Choose a reason for hiding this comment

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

That's not correct. It's common for functions to take parameters that define the circuit, not only variables.

}
```

**Warning:** You should _not_ use recursion with functions in o1js. If you want to recurse a ZKP, you can use [o1js recursion](./recursion.mdx).
Copy link
Contributor

@mitschabaude mitschabaude Feb 14, 2024

Choose a reason for hiding this comment

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

Incorrect, if the recursion terminates on a condition that's static it's fine to use (much like if statements with a static condition)

const x = Provable.if(
Bool(foo),
(() => { // Here, we use arrow functions to return a Field element, but you can use any function that returns a provable type
return Field(5).div(3);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a big fan of how all our examples only ever use constant field elements as examples, where none of these circuit-writing rules and caveats actually apply

Explaining constants vs variables and then using variables in examples seems an important step in moving to more in-depth docs about circuit writing

x.not().and(y).or(true); // True
```

**Important:** All logical expressions are executed in the method call order, as with any other regular language or framework. However, as these are logical statements inside ZKPs, all expressions are always evaluated regardless of the logical ordering. Please see conditionals chapter [below](./common-types-and-functions.md#conditionals) for more details.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
**Important:** All logical expressions are executed in the method call order, as with any other regular language or framework. However, as these are logical statements inside ZKPs, all expressions are always evaluated regardless of the logical ordering. Please see conditionals chapter [below](./common-types-and-functions.md#conditionals) for more details.
**Important:** All logical expressions are executed in the method call order, as with any other regular language or framework. However, as these are logical statements inside ZKPs, all expressions are always evaluated regardless of the logical ordering. See [Conditionals](./common-types-and-functions.md#conditionals) for more details.

Copy link
Contributor

@barriebyron barriebyron Feb 14, 2024

Choose a reason for hiding this comment

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

I agree with Gregor, nice work @yunus433 !
Guidance is to omit "please", see Politeness and use of please

Instead of "below" or "here" be sure to use short descriptive link text that helps provide context for the material that you're linking to.

See the new links section in the docs style guide.


`Provable.if()` allows you to include any logical computation you need inside your ZKP. There is nothing wrong with evaluating more complex expressions inside `Provable.if()` and returning the final result.

However, it works differently from ternary statements in the other languages and frameworks. Both arguments of `Provable.if()` _always_ executes, regardless of the truth value of the condition. `Provable.if()` choses which one to return based on the truth value and sets the new variable. This is why you should never assign anything outside of the if scope or assert anything.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
However, it works differently from ternary statements in the other languages and frameworks. Both arguments of `Provable.if()` _always_ executes, regardless of the truth value of the condition. `Provable.if()` choses which one to return based on the truth value and sets the new variable. This is why you should never assign anything outside of the if scope or assert anything.
However, it works differently from ternary statements in the other languages and frameworks. Both arguments of `Provable.if()` _always_ execute, regardless of the truth value of the condition. `Provable.if()` chooses which one to return based on the truth value and sets the new variable. This is why you should never assign anything outside of the scope or assert anything.

@@ -27,109 +27,205 @@ zkApp programmability is not yet available on the Mina Mainnet. You can get star

# o1js Basic Concepts

o1js, fka. SnarkyJS, is a TypeScript (TS) library for writing general-purpose zk programs and writing zk smart contracts for Mina.
o1js, fka. SnarkyJS, is a TypeScript (TS) library for writing general-purpose ZK programs and writing ZK smart contracts for the Mina Blockchain. In order to create a ZKP (zero knowledge proof), you should use types and operations that can be converted into a ZKP. o1js gives you a lot of different built in types and customizable structures that you can use to create ZKPs.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
o1js, fka. SnarkyJS, is a TypeScript (TS) library for writing general-purpose ZK programs and writing ZK smart contracts for the Mina Blockchain. In order to create a ZKP (zero knowledge proof), you should use types and operations that can be converted into a ZKP. o1js gives you a lot of different built in types and customizable structures that you can use to create ZKPs.
o1js, fka. SnarkyJS, is a TypeScript (TS) library for writing general-purpose ZK programs and writing ZK smart contracts for the Mina Blockchain. To create a ZKP (zero knowledge proof), you use types and operations that can be converted into a ZKP. o1js provides different built-in types and customizable structures that you can use to create ZKPs.


<!-- prettier-ignore -->
```ts
const x = new Field(42);
const y = Field(923); // Best practice
let x = Field(42);
```

You can create a `Field` from anything that is "field-like": `number`, `string`, `bigint`, or another `Field`. Note that the argument you give must be an integer in the `Field` range.
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that the argument you give must be an integer in the Field range.

Can you remove the second sentence here? It's not true, an integer outside the field range is also accepted and reduced modulo the field size (as you show below, with negative inputs)

Copy link
Contributor

@mitschabaude mitschabaude Feb 20, 2024

Choose a reason for hiding this comment

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

Or replace it with just "Note that the argument you give must be an integer."


## Deeper into the Field Type

`Field` element is a positive integer in a prime order [finite field](https://en.wikipedia.org/wiki/Finite_field), which is a set of integers in between 1 and the prime order of the field (a.k.a. an arbitrary prime number describing the field).
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually it's the set of integers between 0 and p-1, where p is the prime order!

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
`Field` element is a positive integer in a prime order [finite field](https://en.wikipedia.org/wiki/Finite_field), which is a set of integers in between 1 and the prime order of the field (a.k.a. an arbitrary prime number describing the field).
A `Field` represents a positive integer in a prime order [finite field](https://en.wikipedia.org/wiki/Finite_field), which is the set of integers $0,...,p-1$, where $p$ is the field order.
Concretely, `Field` refers to the [Pallas base field](TODO), which is the native circuit field of the proof system underlying o1js.

@yunus433 this is how I would describe the field

However, note that these methods are _not_ provable, meaning you cannot use them in your final zkApp as a part of the ZKP. Use them only to debug your code.
When you perform an operation inside a finite field, the result always stays in the range. This is achieved through [modular arithmetic](https://en.wikipedia.org/wiki/Modular_arithmetic). This behaviour may be unfamiliar to you if you have not worked with ZKPs before. Let us explain more with some examples.

In the addition or multiplication operations where the result is bigger than the order of the finite field, you receive the modulus of the result with the prime order of the field.
Copy link
Contributor

Choose a reason for hiding this comment

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

- you receive the modulus of the result with the prime order of the field.
+ you receive the remainder of the result modulo the prime order of the field.

```

However, note that these methods are _not_ provable, meaning you cannot use them in your final zkApp as a part of the ZKP. Use them only to debug your code.
When you perform an operation inside a finite field, the result always stays in the range. This is achieved through [modular arithmetic](https://en.wikipedia.org/wiki/Modular_arithmetic). This behaviour may be unfamiliar to you if you have not worked with ZKPs before. Let us explain more with some examples.
Copy link
Contributor

Choose a reason for hiding this comment

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

this is great!

Comment on lines 167 to 174
If you try creating a `Field` from a negative number you will receive the modular additive inverse of the number. For instance `(1 + (Field.ORDER - 1)) % Field.ORDER` is 0, so the additive inverse of 1 is `Field.ORDER - 1` in this field.

In addition to the `Field` type, o1js gives you a variety structures to help you build a powerful zkApp. You can learn more about them in the next chapter, [Common Types and Functions](./common-types-and-functions.md).
<!-- prettier-ignore -->
```ts
Provable.log(Field(-1)); // This will print `Field.ORDER - 1`
```

Similarly, if your substraction results in a negative number you will get the modular additive inverse.
Copy link
Contributor

Choose a reason for hiding this comment

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

it's confusing to me that you describe negative numbers / subtraction results in different language than positive overflow. it's the same so should be described in the same language:

"Similarly, if your subtraction results in a negative number, you receive the positive remainder of the result modulo the prime order of the field."


Similarly, if your substraction results in a negative number you will get the modular additive inverse.

For division, if the result is not an integer, you receive the [modular multiplicative inverse](https://en.wikipedia.org/wiki/Modular_multiplicative_inverse) of the division: If `(x * y) % p` is `z` (where `p` is the order of the finite field), then the modular multiplicative inverse of the division `z / x` equals `y`. Note that `x`, `y`, and `z` are all integers.
Copy link
Contributor

Choose a reason for hiding this comment

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

- For division, if the result is not an integer, you receive the 
+ For division, you receive the 

of course, if the result is an integer, it's still the modular inverse

Comment on lines 183 to 189
Because of this behaviour, it is usually not very useful to use the final result of an uneven division. Instead, you can prove the result with multiplication.

<!-- prettier-ignore -->
```ts
// We want x, an argument of the smart contract, to be equal to 7 / 3
x.mul(Field(3)).assertEquals(Field(7));
```
Copy link
Contributor

@mitschabaude mitschabaude Feb 20, 2024

Choose a reason for hiding this comment

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

I don't see how this is "more useful" than doing let x = Field(7).div(3)


<!-- prettier-ignore -->
```ts
Provable.log(Field(3).div(Field(5))); // This will print a very big integer
Copy link
Contributor

Choose a reason for hiding this comment

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

we should always use number inputs to methods instead of Field(number), looks cleaner:

Provable.log(Field(3).div(5)); // This will print a very big integer

this applies to the entire PR!


We have stated many times above that all the code you write in o1js must be provable, but what is provability?

A provable code can be thought as a code that o1js compiler can convert into a ZKP. Just because you write something with o1js, it does not become automatically provable. You must follow all the rules above and always use provable types in your code.
Copy link
Contributor

Choose a reason for hiding this comment

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

o1js compiler

let's not use the term "compiler", it's a confusion already that people think we compile TS to circuits

let x_provable = Field(x); // This code is perfectly valid
```

In reality, the `x_provable` variable in the example above is a _constant_ `Field` variable. By constant, we do not mean that it is a TS constant (which you define by using `const`), but we say that the value of `x_provable` is known at the compilation time. When you receive an argument to your smart contract method, then the argument is not a constant, thus you should only use provable methods on it.
Copy link
Contributor

Choose a reason for hiding this comment

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

nice!

Comment on lines +224 to +231
Always remember that o1js is used for writing ZKPs, and what you write with o1js must have a sense in the final proof. The public or private inputs of a ZKP may change depending on the user input, but once the inputs are provided, the final ZKP must make sure they are not changed. By using provable o1js variables, you make sure that your variables are always consistent with your proof logic.

<!-- prettier-ignore -->
```ts
@method foo(x: Field, y: Field) {
x.add(y).assertEquals(5); // we know neither the value of x, nor of the y, but we are sure that (x + y) % Field.ORDER equals 5
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

all this is really good and important to add, thanks @yunus433

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants