-
Notifications
You must be signed in to change notification settings - Fork 133
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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. | ||||||||||
|
||||||||||
## Field | ||||||||||
|
||||||||||
Field elements are the basic unit of data in zero-knowledge proof programming. Each field element can store a number up to almost 256 bits in size. You can think of it as a uint256 in Solidity. | ||||||||||
Field elements are the basic unit of data in ZK programming. Every other data type (either built-in or composite) in o1js is made up of field elements. | ||||||||||
|
||||||||||
Each `Field` element in o1js can store a number up to almost 256 bits in size. You can think of it as a `uint256` in Solidity, but there are some fundamental differences explained below. | ||||||||||
|
||||||||||
:::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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||
|
||||||||||
::: | ||||||||||
|
||||||||||
For example, in typical programming, you might use: | ||||||||||
## Creating a `Field` Element | ||||||||||
|
||||||||||
`const sum = 1 + 3`. | ||||||||||
You can create a `Field` either with the `new` constructor or by directly calling the `Field` as a constructor. The convention is to use `Field` without the `new` constructor. | ||||||||||
|
||||||||||
In o1js, you write this as: | ||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
let x = Field(42); | ||||||||||
``` | ||||||||||
|
||||||||||
`const sum = new Field(1).add(new Field(3))` | ||||||||||
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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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." |
||||||||||
|
||||||||||
This can be simplified as: | ||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
let x = Field("12347"); // From string | ||||||||||
let y = Field(172384782343434n); // From bigint | ||||||||||
let z = Field(x); // From another Field | ||||||||||
|
||||||||||
`const sum = new Field(1).add(3)` | ||||||||||
let k = Field("asdf"); // Throws, as this is not a number | ||||||||||
let l = Field("234.34"); // Throws, as this is not an integer | ||||||||||
``` | ||||||||||
|
||||||||||
Note that the 3 is auto-promoted to a field type to make this cleaner. | ||||||||||
Nevertheless, you can create a `Field` from a negative number. This method implicity calculates the additive modular inverse of the given argument. This is an advanced level information, and you do not need it for simple zkApps. | ||||||||||
|
||||||||||
## Built-in data types | ||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
let x = Field(-1); // If you try printing this to the screen, you will see a very big integer | ||||||||||
``` | ||||||||||
|
||||||||||
## Methods on the `Field` Type | ||||||||||
|
||||||||||
`Field` type includes a lot of built in methods. You can call all methods in the `Field` type either with a `Field` or with a "field-like" value. In the latter case, the argument is implicity converted to a `Field`. | ||||||||||
|
||||||||||
All common arithmetic operations are available on the `Field` type. They create and return a new `Field` element. | ||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
let x = Field(45); | ||||||||||
let y = Field(78); | ||||||||||
|
||||||||||
let sum = x.add(y); | ||||||||||
let sub = x.sub(y); | ||||||||||
let mul = x.mul(47); // You can also use "field-like" values | ||||||||||
let div = x.div(89); // This is a modular division | ||||||||||
let neg = x.neg(); // This is equivalent to x.mul(-1) | ||||||||||
|
||||||||||
let square = x.square(); // x^2, equivalent to x.mul(x) | ||||||||||
let sqrt = x.sqrt(); // Modular square root of x | ||||||||||
let inv = x.inv(); // Modular inverse of x | ||||||||||
``` | ||||||||||
|
||||||||||
Some common data types you may use are: | ||||||||||
You can also perform logical operations and comparisons with `Field` elements. These methods return a [Bool](./common-types-and-functions.md#bool) element, which is another common o1js built in type, equivalent to `boolean` in other languages and frameworks. | ||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
new Bool(x); // accepts true or false | ||||||||||
new Field(x); // accepts an integer, or a numeric string if you want to represent a number greater than JavaScript can represent but within the max value that a field can store. | ||||||||||
new UInt64(x); // accepts a Field - useful for constraining numbers to 64 bits | ||||||||||
new UInt32(x); // accepts a Field - useful for constraining numbers to 32 bits | ||||||||||
let x = Field(45); | ||||||||||
|
||||||||||
PrivateKey, PublicKey, Signature; // useful for accounts and signing | ||||||||||
new Group(x, y); // a point on our elliptic curve, accepts two Fields/numbers/strings | ||||||||||
Scalar; // the corresponding scalar field (different than Field) | ||||||||||
x.equals(45); // True | ||||||||||
x.greaterThan(45); // False | ||||||||||
x.greaterThanOrEquals(45); // True | ||||||||||
x.lessThan(45); // False | ||||||||||
x.lessThanOrEquals(45); // True | ||||||||||
|
||||||||||
CircuitString.from('some string'); // string of max length 128 | ||||||||||
// It is also possible to check if a Field is even or not | ||||||||||
x.isEven(); // False | ||||||||||
``` | ||||||||||
|
||||||||||
In the case of `Field` and `Bool`, you can also call the constructor without `new`: | ||||||||||
## Assertions in o1js | ||||||||||
|
||||||||||
If you want to reject a TX (transaction) in your zkApp smart contract, you can use assertions in o1js. If you create an assertion inside your ZKP, the proof fails if the assertion does not evaluate to `true`. All conditional methods on the `Field` type can be used as assertions. | ||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
let x = Field(10); | ||||||||||
let b = Bool(true); | ||||||||||
let x = Field(45); | ||||||||||
|
||||||||||
x.assertEquals(45); // True - passes | ||||||||||
x.assertGreaterThan(45); // False - throws | ||||||||||
x.assertGreaterThanOrEquals(45); // True - passes | ||||||||||
x.assertLessThan(45); // False - throws | ||||||||||
x.assertLessThanOrEquals(45); // True - passes | ||||||||||
``` | ||||||||||
|
||||||||||
## Conditionals | ||||||||||
You can think assertions in o1js similar to `require()` function in Solidity. | ||||||||||
|
||||||||||
Traditional conditional statements are not supported by o1js: | ||||||||||
## Printing a Field to the Screen | ||||||||||
yunus433 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
`Field` elements are stored as classes in o1js. This is why if you print a `Field` using `console.log`, you will get an unreadable output. Hopefully, o1js provides an equivalent function to `console.log` for provable types: `Provable.log` | ||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
// this will NOT work | ||||||||||
if (foo) { | ||||||||||
x.assertEquals(y); | ||||||||||
} | ||||||||||
let x = Field(45); | ||||||||||
Provable.log(x); // Prints 45 | ||||||||||
``` | ||||||||||
|
||||||||||
## Other Types and Methods | ||||||||||
|
||||||||||
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). | ||||||||||
|
||||||||||
:::note | ||||||||||
|
||||||||||
The information given until here is more than enough for you to start creating powerful zkApps with o1js. For more advanced level content, you can read the following sections. If you are new to o1js, just continue with the next chapter, [Common Types and Functions](./common-types-and-functions.md). | ||||||||||
|
||||||||||
::: | ||||||||||
|
||||||||||
## 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). | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
@yunus433 this is how I would describe the field |
||||||||||
|
||||||||||
The order of the o1js `Field` can be accessed as a static property of the class. | ||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
console.log(Field.ORDER); | ||||||||||
``` | ||||||||||
|
||||||||||
Instead, use the o1js built-in `Circuit.if()` method, which is a ternary operator: | ||||||||||
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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is great! |
||||||||||
|
||||||||||
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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
const x = Circuit.if(new Bool(foo), a, b); // behaves like `foo ? a : b` | ||||||||||
Provable.log(Field(Field.ORDER - 1) + Field(2)); // This will print 1 | ||||||||||
``` | ||||||||||
|
||||||||||
## Functions | ||||||||||
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. | ||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
Provable.log(Field(-1)); // This will print `Field.ORDER - 1` | ||||||||||
``` | ||||||||||
|
||||||||||
Functions work as you would expect in TypeScript. For example: | ||||||||||
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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
function addOneAndDouble(x: Field): Field { | ||||||||||
return x.add(1).mul(2); | ||||||||||
} | ||||||||||
Provable.log(Field(3).div(Field(5))); // This will print a very big integer | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should always use number inputs to methods instead of Provable.log(Field(3).div(5)); // This will print a very big integer this applies to the entire PR! |
||||||||||
``` | ||||||||||
|
||||||||||
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)); | ||||||||||
``` | ||||||||||
|
||||||||||
## Common methods | ||||||||||
If you want to have integer division instead, you can use `UInt32` or `UInt64` types (see [Common Types and Functions](./common-types-and-functions.md)). | ||||||||||
|
||||||||||
Some common methods you will use often are: | ||||||||||
## Provability and Provable Code in o1js | ||||||||||
|
||||||||||
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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
let's not use the term "compiler", it's a confusion already that people think we compile TS to circuits |
||||||||||
|
||||||||||
However, provable types are created from regular TS variables. | ||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
let x = new Field(4); // x = 4 | ||||||||||
x = x.add(3); // x = 7 | ||||||||||
x = x.sub(1); // x = 6 | ||||||||||
x = x.mul(3); // x = 18 | ||||||||||
x = x.div(2); // x = 9 | ||||||||||
x = x.square(); // x = 81 | ||||||||||
x = x.sqrt(); // x = 9 | ||||||||||
const x = 42; // This is TS number | ||||||||||
let x_provable = Field(x); // This code is perfectly valid | ||||||||||
``` | ||||||||||
|
||||||||||
let b = x.equals(8); // b = Bool(false) | ||||||||||
b = x.greaterThan(8); // b = Bool(true) | ||||||||||
b = b.not().or(b).and(b); // b = Bool(true) | ||||||||||
b.toBoolean(); // true | ||||||||||
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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! |
||||||||||
|
||||||||||
let hash = Poseidon.hash([x]); // takes array of Fields, returns Field | ||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
@method foo(x: Field) { | ||||||||||
// Here, the value of x depends on the user input, thus type of x must be provable | ||||||||||
} | ||||||||||
``` | ||||||||||
|
||||||||||
let privKey = PrivateKey.random(); // create a private key | ||||||||||
let pubKey = PublicKey.fromPrivateKey(privKey); // derive public key | ||||||||||
let msg = [hash]; | ||||||||||
let sig = Signature.create(privKey, msg); // sign a message | ||||||||||
sig.verify(pubKey, msg); // Bool(true) | ||||||||||
You can learn if a variable is a constant or not in o1js by using `isConstant()` method. Note that this is a function mostly for internal use, but it might give you a good insight on how to work with types in o1js. | ||||||||||
|
||||||||||
<!-- prettier-ignore --> | ||||||||||
```ts | ||||||||||
let x = Field(42); | ||||||||||
console.log(x.isConstant()); // This will print true | ||||||||||
``` | ||||||||||
|
||||||||||
For a full list, see the [o1js reference](../o1js-reference). | ||||||||||
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 | ||||||||||
} | ||||||||||
``` | ||||||||||
Comment on lines
+224
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. all this is really good and important to add, thanks @yunus433 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.