mina-mastermind
v0.1.0
Published
![alt text](./mastermind-board.png)
Downloads
4
Maintainers
Readme
Mina zkApp: Mina Mastermind
Table of Contents
This README is divided into two main sections: Mastermind Game Documentation and General zkApp Documentation.
The Mastermind Game Documentation is focused on the specific implementation of the Mastermind game as a zkApp example. This section details the game rules, the structure of the zkApp tailored for Mastermind, and the methods specific to this game.
The General zkApp Documentation provides broader information about zkApps, including their structure, security considerations, best practices, and relevant APIs. This section is applicable to any zkApp you might develop, not just the Mastermind game.
Mastermind Game Documentation
General zkApp Documentation
Understanding the Mastermind Game
Overview
- The game involves two players: a
Code Master
and aCode Breaker
. - Inspired by mastermind-noir, this version replaces colored pegs with a combination of 4 unique, non-zero digits.
Game Rules
The Code Master hosts a game and sets a secret combination for the Code Breaker to guess.
The Code Breaker makes a guess and waits for the Code Master to provide a clue.
The clue indicates the following:
- Hits: Digits that are correctly guessed and in the correct position.
- Blows: Digits that are correct but in the wrong position.
Example:
| | P1 | P2 | P3 | P4 | | ------ | --- | --- | --- | --- | | Secret | 5 | 9 | 3 | 4 | | Guess | 5 | 7 | 8 | 9 | | Clue | 2 | 0 | 0 | 1 |
- Code Master's secret combination: 5 9 3 4
- Code Breaker's guess: 5 7 8 9
- Clue: 2 0 0 1
- Result:
1
hit and1
blow.- The hit is
5
in the first position. - The blow is
9
in the fourth position.
- The hit is
- Result:
The game continues with alternating guesses and clues until the Code Breaker achieves 4 hits and uncovers the secret combination or fails to do so within the maximum allowed attempts.
Mastermind zkApp Structure
Following the game rules, the MastermindZkApp should be deployed:
The zkApp is initialized by calling the
initGame
method, withmaxAttempts
as the method parameter to set an upper limit.After initialization, the Code Master calls the
createGame
method to start the game and set a secret combination for the Code Breaker to solve.The Code Breaker then makes a guess by calling the
makeGuess
method with a valid combination as an argument.The Code Master submits the solution again to be checked against the previous guess and provides a clue.
The Code Breaker should analyze the given clue and make another meaningful guess.
The game continues by alternating between
makeGuess
andgiveClue
methods until the Code Breaker either uncovers the secret combination or fails by exceeding the allowedmaxAttempts
, concluding the game.
Now, let's dive deeper into the states and methods of our Mastermind zkApp.
Mastermind States
The Mastermind zkApp uses all 8 available states. Exceeding this limit would render the zkApp unusable, as it would surpass the maximum storage capacity.
Let's break down the purpose of each state and discuss the small workarounds used to minimize the number of states stored on-chain.
maxAttempts
This state is set during game initialization and is crucial for limiting the number of attempts in the game.
Without this state, the game would be biased in favor of the Code Breaker, allowing the game to continue indefinitely until the secret combination is solved.
turnCount
- This state is essential for tracking game progress. It helps determine when the maximum number of attempts (
maxAttempts
) has been reached and also identifies whose turn it is to make a move. If theturnCount
is even, it's the Code Master's turn to give a clue; if it's odd, it's the Code Breaker's turn to make a guess.
codemasterId & codebreakerId
These states represent the unique identifiers of the players, which are stored as the hash of their
PublicKey
.We avoid storing the
PublicKey
directly because it occupies two fields. By hashing thePublicKey
, we save two storage states, reducing the total required states from four to two.Player identifiers are crucial for correctly associating each method call with the appropriate player, such as linking
makeGuess
to the Code Breaker andgiveClue
to the Code Master.Restricting access to methods ensures that only the intended players can interact with the zkApp, preventing intruders from disrupting the 1 vs 1 interactive game.
solutionHash
The solution must remain private; otherwise, the game loses its purpose. Therefore, whenever the Code Master provides a clue, they should enter the
secretCombination
as a method parameter.To maintain the integrity of the solution, the solution is hashed and stored on-chain when the game is first created.
Each time the Code Master calls the
giveClue
method, the entered private secret combination is salted, hashed, and compared against thesolutionHash
stored on-chain. This process ensures the integrity of the combination and helps prevent side-channel attacks.Note: Unlike player IDs, where hashing is used for data compression, here it is used to preserve the privacy of the on-chain state and to ensure the integrity of the values entered privately with each method call.
unseparatedGuess
- This state represents the Code Breaker's guess as a single field encoded in decimal.
- For example, if the guess is
4 5 2 3
, this state would be stored as a Field value of4523
.
- For example, if the guess is
- The Code Master will later retrieve this value and separate it into the four individual digits to compare against the solution.
serializedClue
This state is a single field representing a clue, which is packed as a serialized value. A clue consists of four digits, each of which can be either
0
,1
, or2
, meaning the clue digits fall within the range of a 2-bit number. These digits are combined and stored on-chain as an 8-bit field in decimal.This state demonstrates a bit-serialization technique to compact multiple small field elements into one.
Note: To interpret the clue, the Code Breaker must deserialize and separate the clue digits to meaningfully understand the outcome of their previous guess.
isSolved
This state is a Bool
that indicates whether the Code Breaker has successfully uncovered the solution.
It is crucial for determining the end of the game, signaling completion once the Code Breaker achieves 4 hits within the allowed maxAttempts
.
Mastermind Methods
initGame
Note: The init()
method is predefined in the base SmartContract
class, similar to a constructor.
- It is automatically called when you deploy your zkApp with the zkApp CLI for the first time.
- It is not called during contract upgrades or subsequent deployments.
- The base
init()
method initializes provable types likeField
,UInt8
to0
, and theBool
type toBool(false)
, as it's a wrapper around a field with a value of0
. - Note that you cannot pass arguments to the
init
method of aSmartContract
.
There are three variations for initializing a zkApp:
All state initialized as
0
(no state with non-zero value):- If you don't need to set any state to a non-zero value, there's no need to override
init()
or create a custom initialization method. - The base
init()
method will be automatically invoked when the zkApp is first deployed using the zkApp CLI.
- If you don't need to set any state to a non-zero value, there's no need to override
Initialize at least one state with a constant value:
Override the
init()
method to initialize your on-chain state with constant values.Include the base
init()
method's logic by callingsuper.init()
to set all state variables to0
.Then, set the specific state variables to constant values, such as
Field(10)
orBool(true)
.Example:
class HelloWorld extends SmartContract { @state(Field) x = State<Field>(); init() { super.init(); this.x.set(Field(10)); // Set initial state to a constant value } }
Initialize at least one state with a value dependent on an argument:
Create a separate zkApp method with the adequate name
Within this method, call
super.init()
to initialize all state variables to0
.Use the method's parameters to set specific state variables based on the caller's input.
Example:
class HelloWorld extends SmartContract { @state(Field) x = State<Field>(); @method async initWorld(myValue: Field) { super.init(); this.x.set(myValue); // Set initial state based on caller's input } }
Notes:
In the Mastermind zkApp, we used the third variation to initialize the game, as it allows the caller to set the value of
maxAttempts
.In variations
1
and2
, theinit()
method, whether default or overridden, is automatically executed when the zkApp is deployed. In contrast, the custom init method in the third variation must be called manually to initialize the states.Since the custom initialization method can be called by anyone at any time, refer to the Security Considerations to ensure it is implemented securely.
createGame
This method should be called after initializing the game and only once.
The method executes successfully when the following conditions are met:
The code master provides two arguments:
unseparatedSecretCombination
and asalt
.The
unseparatedSecretCombination
is split into an array of fields representing the four digits. An error is thrown if the number is not in the range of1000
to9999
.The separated digits are validated to ensure they are unique and non-zero, with errors thrown if they do not meet these criteria.
The secret combination is then hashed with the salt and stored on-chain as
solutionHash
.The caller's
PublicKey
is hashed and stored on-chain ascodemasterId
once the combination is validated.Finally, the
turnCount
is incremented, signaling that the game is ready for the code breaker tomakeGuess
.The first user to call this method with valid inputs will be designated as the code master.
Note: For simplicity, security checks in this method have been abstracted. For more details, please refer to the Security Considerations.
makeGuess
This method should be called directly after a game is created or when a clue is given for the previous guess.
There are a few restrictions on calling this method to maintain a consistent progression of the game:
- If the game
isSolved
, the method can be called, but it will throw an error. - If the code breaker exceeds the
maxAttempts
, the method can be called, but it will throw an error. - This method also enforces the correct sequence of player interactions by only allowing the code breaker to make a guess if the
turnCount
state isodd
. If any of these conditions are not met, the method can be called, but it will throw an error.
- If the game
Special handling is required when the method is called for the first time:
- The first player to call the method and make a guess will be registered as the code breaker for the remainder of the game.
- The Provable.if API is used to either set the current caller's
PublicKey
hash or fetch the registered code breaker ID.
Once the
makeGuess
method is called successfully for the first time and a code breaker ID is registered, the method will restrict any caller except the registered one.After all the preceding checks pass, the code breaker's guess combination is validated, stored on-chain, and the
turnCount
is incremented. This then awaits the code master to read the guess and provide a clue.
giveClue
Similar to the
makeGuess
method, there are a few restrictions on calling this method to maintain a consistent progression of the game:- The caller is restricted to be only the registered code master ID.
- The correct sequence is enforced by checking that
turnCount
is non-zero (to avoid colliding with thecreateGame
method call) and even. - If the game
isSolved
, this method is blocked and cannot be executed. - If the code breaker exceeds the
maxAttempts
, this method is blocked and cannot be executed.
After the preceding checks pass, the plain
unseparatedSecretCombination
input is separated into 4 digits, hashed along with the salt, and asserted against thesolutionHash
state to ensure the integrity of the secret.Next, the guess from the previous turn is fetched, separated, and compared against the secret combination digits to provide a clue:
- If the clue results in 4 hits (e.g.,
2 2 2 2
), the game is marked as solved, and theisSolved
state is set toBool(true)
. - The clue is then serialized into
4
2-bit Fields, packed as an 8-bit field in decimal, and stored on-chain. - Note that this technique requires the adversary to deserialize and correctly interpret the digits before making the next guess.
- If the clue results in 4 hits (e.g.,
Finally, the
turnCount
is incremented, making it odd and awaiting the code breaker to deserialize and read the clue before making a meaningful guess—assuming the game is not already solved or has not reached the maximum number of attempts.
Mina zkApp Structure
Introduction
By the definition of a smart contract:
Smart contracts are digital contracts stored on a blockchain that are automatically executed when predetermined terms and conditions are met.
zkApps are essentially smart contracts on the Mina blockchain that can undergo updates to their on-chain state through zero-knowledge proofs when certain
preconditions
are met.Specifically, a zkApp is an account on the Mina blockchain with a verification key and 8 storage states that can be updated following the successful verification of zero-knowledge proofs. These proofs dictate the logic for how these states are updated.
zkApp components
As demonstrated in the Mastermind zkApp, a zkApp primarily consists of a set of states (up to 8) and methods.
States
These are the 8 states associated with the zkApp account, stored on-chain.
All zkApp states are public.
A state creates a precondition that is checked when the proof is sent in a transaction to the blockchain to be verified.
- This process occurs under the hood when using the following API:
const currentState = this.num.get(); this.num.requireEquals(currentState);
- The first line of code retrieves the current state before the proof is generated.
- The second line creates a precondition that is verified when the proof is submitted in a transaction to the blockchain.
- This ensures that the transaction will fail if the value of the field has changed.
- This process occurs under the hood when using the following API:
Each state occupies 255 bits in size.
You can use other provable types like
Bool
,UInt8
, etc., but even if they appear smaller in size, they still occupy a full 255-bit field element.Keep in mind that a struct can take up more than one Field.
- For example, a
PublicKey
occupies two of the eight fields.
- For example, a
A state is defined as follows:
@state(Field) myState = State<Field>();
- Note: While the 8 on-chain states may be insufficient for some applications, there are workarounds using various techniques and APIs, which will be demonstrated in the upcoming advanced Mastermind Levels.
Methods
Interaction with a zkApp occurs through calling one or more of its methods.
A method call always generates a proof that must be verified on-chain.
A method can read on-chain data before generating a proof but can only trigger updates (write) after the transaction proof is successfully verified.
All method parameters are private, and you can include any number of parameters you need.
If we think of a method as a circuit:
- The circuit's private inputs are the method parameters.
- The circuit's public inputs are the on-chain states.
- As in any ZK circuit, the public inputs can be derived from or committed to by the private inputs. Additionally, state preconditions commit to the transaction proof, not necessarily to the private inputs of a method.
- The zkApp's on-chain states are fetched before the proof is generated.
- The method call also sets preconditions on the on-chain state:
- A precondition ensures that a specific condition (equality, greater than, less than, etc.) at the time the state was read for proof generation is still satisfied when the proof is verified on-chain.
- This guarantees correct execution and prevents race conditions during state updates.
Within a method, you can use o1js data types and primitives to define your custom logic.
- For more details, refer to this guide on methods.
A method call does not just submit a proof for verification but can also read data from the blockchain and perform actions like updating on-chain states, emitting events, and dispatching actions.
You can declare methods using the
@method
decorator as follows:@method async myMethod(secret: Field) {...}
A zkApp
init method
is quite an exception, for a comprehensive guide on initializing a zkApp, refer to the Mastermind initGame documentation.
Security Considerations
To ensure that a zkApp operates securely and avoids unintended behavior, it is crucial to implement comprehensive security measures that enforce trustless and consistent operation.
When developing a zkApp, various security checks should be incorporated, including access control mechanisms similar to modifiers
in Solidity, input validation, and specific safeguards related to zk-SNARK circuits.
Key security considerations include:
Controlling Method Invocation Patterns
- It's crucial to establish strict rules for the order and frequency of method calls to prevent unauthorized sequences or excessive invocation. This ensures that the zkApp’s logic is executed in a controlled and predictable manner, reducing the risk of exploitation.
Initialize: Must Be Called First and Only Once
For the initGame method, it's essential to ensure that this method is called immediately after deployment, with no other methods executed beforehand.
Additionally, we must enforce that this method is called only once.
- If the game is in progress and someone calls the method to reset the game, it could be catastrophic.
To restrict the sequence and frequency of calling this method, we use the following API:
const isInitialized = this.account.provedState.getAndRequireEquals(); isInitialized.assertFalse('The game has already been initialized!');
By asserting that
provedState
isfalse
, you ensure thatinitGame
cannot be called again after the zkApp is initially set up. Without this assertion, your zkApp could be reset by anyone calling the init method.It's also crucial that all other methods assert that
provedState
istrue
to ensure the zkApp has been properly initialized, asprovedState
becomestrue
afterinitGame
is invoked.
Enforce Method Call Once in a Specific Sequence
For the createGame
method, we need to ensure that a player can call this method only once after the game is initialized and before any other methods are executed.
- To enforce this restriction, we use an on-chain variable called
turnCount
.turnCount
is initialized to0
, and in other methods, every player action increments this count.- Therefore, we assert that
turnCount
iszero
to confirm that no other method was called beforecreateGame
.
turnCount.assertEquals(0, 'A mastermind game is already created!');
Securing Method Access with Authorization Controls
- It's crucial to have robust authorization mechanisms to restrict access to sensitive methods. This ensures that only verified and authorized entities can execute critical functions within the zkApp, protecting it from unauthorized use.
In the case of our Mastermind zkApp, it's essential to limit method access to the code master and the code breaker. Otherwise, anyone could access the zkApp and disrupt the game flow.
Generally, as seen in the codemasterId & codebreakerId states, the check involves storing the method caller ID, typically by hashing their PublicKey
, and asserting that the caller is authorized to execute the method.
For example:
// Generate codemaster ID
const computedCodemasterId = Poseidon.hash(
this.sender.getAndRequireSignature().toFields()
);
//! Restrict method access solely to the correct codemaster
this.codemasterId
.getAndRequireEquals()
.assertEquals(
computedCodemasterId,
'Only the codemaster of this game is allowed to give clue!'
);
Notes:
This logic works well in the game since it doesn't require much state. However, if we need to authorize a large number of users, it could become problematic due to the limited
8
states of storage available.To learn more about scaling data storage, including off-chain storage, actions/reducers, or other packing techniques, follow the next levels of this game and explore the relevant APIs in the o1js library.
Rigorous Input Validation and State Verification
Enforce stringent validation of all inputs and verify the integrity of the on-chain state before executing any dependent logic. This prevents invalid or malicious data from affecting the zkApp’s operations, maintaining overall security and correctness.
Validate inputs and on-chain state for correctness by checking value ranges, enforcing equality, and applying necessary conditions.
For example, in the case of the
maxAttempts
state, it’s useful to allow flexibility in setting the number of attempts. However, an unrestricted range could be manipulated—small values like0
to4
would favor the Code Master by giving fewer chances to the Code Breaker, while larger values would do the opposite. Therefore, it’s important to assert that themaxAttempts
state falls within a reasonable range, such as5
to20
, to ensure a balanced game.
For example:
maxAttempts.assertGreaterThanOrEqual(
UInt8.from(5),
'The minimum number of attempts allowed is 5!'
);
maxAttempts.assertLessThanOrEqual(
UInt8.from(15),
'The maximum number of attempts allowed is 15!'
);
Safeguarding Private Inputs in zk-SNARK Circuits
Protect sensitive input data by ensuring that it remains confidential within the zk-SNARK circuit. It's critical to verify inputs without exposing their values, thereby preventing data leaks or manipulation.
Protect the security of private inputs by securely committing them as state hashes, ensuring they cannot be manipulated or exposed.
For example:
- Since the combination in a Mastermind game is a 4-digit number, it could inadvertently disclose information about the
solutionHash
, as state updates are publicly stored on the Mina blockchain and could be easily brute-forced.
- Since the combination in a Mastermind game is a 4-digit number, it could inadvertently disclose information about the
To enhance security, a salt (random field) is introduced to the hash input:
hash(secret, salt)
. This adds approximately255
bits of security, making it astronomically difficult to uncover the original input through brute force.However, while the use of salt increases security, it also adds complexity in debugging, as it becomes more difficult to trace errors related to the hash. The hash now varies with each change in the secret or the salt, making it harder to pinpoint the source of issues.
Preventing Underconstrained Proofs
Prevent vulnerabilities by ensuring that all provable code within the zk-SNARK circuit is fully constrained. This avoids the creation of underconstrained proofs, which could otherwise lead to security breaches or unintended behavior.
It's crucial to properly constrain the provable code by using assertions.
- For example, the API
Field.equals(2)
is not the same asField.assertEquals(2)
. The former returns aBool
, while the latter adds a constraint on the equality and will cause proof generation and verification to fail if the equality check fails. - The same applies to other comparisons and checks, such as
greaterThan
,lessThan
, etc.
- For example, the API
Be cautious when using the
Provable.witness
API. Ensure that its output is consistently constrained, operating based on another field or variable.- Please refer to the API documentation here for more details.
Note that the security considerations documented here are specific to the Mastermind game. For more details on zkApp security considerations, please refer to this excellent guide on Security and zkApps.
Good practices
Project Structure
Unlike other zkDSLs, the
o1js
SDK handles both circuit writing for the zkApp as well as interacting with the blockchain, whether for deployment or interaction.At times, it can be confusing to distinguish between these functionalities within the zkApp. To address this, it’s a good practice to shift provable code (i.e., the code that defines the zk-SNARK circuits) to a separate file(s).
Separating the circuit code into its own file is beneficial for testing purposes. By isolating the main circuit "templates," you can unit test them separately, ensuring that the core logic is sound before integrating it into the larger zkApp. This modular approach improves maintainability and makes it easier to debug and verify the circuit logic independently.
This approach enhances readability and keeps the zkApp file less bloated, allowing it to focus more on the blockchain interaction logic. This makes the zkApp resemble a traditional smart contract, where the primary concern is handling transactions and state changes on the blockchain.
The MastermindZkApp
project is organized to enhance clarity, maintainability, and ease of testing. Key files include:
src/
├── index.ts
├── Mastermind.test.ts
├── Mastermind.ts
├── utils.test.ts
└── utils.ts
index.ts
: Serves as the entry point, importing and exporting all essential smart contract classes for the zkApp(s).utils.ts
: Contains reusable utility functions that act as standalonecircuit templates
. By isolating these logic components, the main zkApp code remains focused and uncluttered.utils.test.ts
: Provides unit tests for the functions inutils.ts
, ensuring each logic component works correctly before integration into the zkApp. This helps catch issues early and improves overall reliability.Mastermind.ts
: The core file where the game logic and blockchain interactions are defined. This ensures that the zkApp's mechanics and state management are clear and maintainable.Mastermind.test.ts
: Contains integration tests using alocalBlockchain
. These tests validate the zkApp’s behavior in various scenarios, ensuring it functions correctly and securely in a simulated environment.Note: Files that contain zkApp code and their test files typically start with an uppercase letter, such as
Mastermind.ts
. This convention aids in distinguishing and navigating the codebase more easily.
Unit and Integration Tests
It’s best practice to separate unit tests, which focus on testing the logic of individual zkApp components, from integration tests, which evaluate the zkApp as a whole, interacting with a
localBlockchain
environment.In both unit and integration tests, consider an adversarial environment. Test both the happy and unhappy paths, ensuring that all assumptions about trust and security within the zkApp are validated.
Integration tests serve as an additional check, not only for the zkApp’s overall functionality but also indirectly validating the utility components tested in unit tests.
Begin testing your zkApp on a
localBlockchain
as it is faster and requires less setup (e.g., keys, deployment). After thorough local testing, deploy it on devnet to interact with it in an environment similar to mainnet and track transactions using Mina Scan.For code examples:
- Refer to the Mastermind game integration tests for integration test examples.
- Refer to the Mastermind zkApp’s unit tests for examples of testing the logic components.
For more details, refer to the documentation on Testing zkApps Locally.
Notes
Integration tests not only evaluate the behavior and interactions of a zkApp but also validate that the provable code is consistent.
- For example, if you use the
.toBigInt()
method to convert aField
tobigint
within a zkApp method, the tests will fail and throw an error if the "circuit" is not provable. If nothing happens, it means the circuit is provable.
- For example, if you use the
When running integration tests, you can choose whether to generate proofs:
To generate proofs when sending a transaction to the local blockchain:
let proofsEnabled = true; if (proofsEnabled) await MastermindZkApp.compile(); // Set up the Mina local blockchain const Local = await Mina.LocalBlockchain({ proofsEnabled }); Mina.setActiveInstance(Local);
Note that when
proofsEnabled = true
, test execution will be slower since proof generation takes time.This provides a more realistic simulation of how transactions work on the Mina blockchain.
You don’t need to generate proofs for every test run; enable it occasionally for deeper checks, but keep it disabled for faster test execution.
Provability
In tests, you may notice that certain provable functions, like
combineDigits
, can be used as regular TypeScript functions, or some field states converted to JS/TS types such asbigint
.However, it's important to note that provable code outside a provable environment (e.g.,
zkApp
orzkProgram
) behaves like standard TypeScript code.This conversion is valid when moving from a provable to a non-provable environment, such as during testing, but not the other way around.
To migrate non-provable code into a provable environment (
zkApp
orzkProgram
), you must use theProvable.witness()
API and properly constrain the operation, as detailed in the documentation.Validating Provable Code:
To ensure that your provable code is indeed valid within a provable environment, you can run integration tests as setting up a Mina blockchain instance and executing your zkApp methods inherently validates their provability.
However, if you need to validate smaller components separately, the
constraintSystem
API can be utilized to check provability and view the specific rows/constraints for your code using the o1js API.For more details, refer to the constraintSystem documentation.
API Explanation
Benchmarking
In the Kimchi proof system, the circuit size is constrained by a limit of 2^16
or 65,536
rows.
Understanding the number of rows for each of your method circuits (i.e., the number of constraints in the underlying proof system) is crucial, as it serves as a good indicator of circuit size and the time required to generate proofs.
Additionally, the following APIs can be used as quick checks to ensure that your contract compiles without errors, significantly speeding up the iteration process.
analyzeMethods
Both
zkApp
andzkProgram
have a method calledanalyzeMethods
that returns an object, keyed by method name, with each entry containing:- rows: the size of the constraint system created by this method.
- digest: a digest of the method circuit.
- actions: the number of actions the method dispatches.
- gates: the constraint system, represented as an array of gates.
For example, you can check the
MastermindZkApp
method summaries as follows:import { MastermindZkApp } from './Mastermind.js'; console.log(await MastermindZkApp.analyzeMethods());
Provable.constraintSystem
You can effectively verify whether your provable code compiles without errors and streamline the development process by analyzing smaller functions or components within your zkApp/zkProgram.
The
Provable.constraintSystem
function, although similar toanalyzeMethods
, is specifically designed for benchmarking the logic within an individual callback. This enables you to profile and understand the constraints of specific components in your zkApp/zkProgram.It generates a circuit summary akin to analyzeMethods, but it specifically targets the execution of the provable callback and provides a detailed summary of its constraints.
Here’s a code snippet demonstrating its usage:
let boundCS = await Provable.constraintSystem(() => { const anyField = Provable.witness(Field, () => Field(1001)); const lowerBound = anyField.greaterThanOrEqual(1000); const upperBound = anyField.lessThanOrEqual(9999); lowerBound.and(upperBound).assertTrue(); }); console.log(boundCS);
The callback in
Provable.constraintSystem
simulates a circuit within the proof system environment. It's critical to use theProvable.witness
API to define circuit inputs for this quick-to-run provable code.When witnessing an input, as in
const anyField = Provable.witness(Field, () => Field(1001));
, the value serves as a placeholder for a circuit input argument. The actual value is not relevant to the benchmarks but is necessary to establish the circuit inputs.If no input is witnessed with
Provable.witness
, the summary will display0
constraints, leading to meaningless benchmarks.
Provable.witness
Based on the API JSDoc documentation:
Create a new witness. A witness, or variable, is a value provided as input by the prover. This offers a flexible way to introduce values from outside into the circuit. However, note that nothing about how the value was created is part of the proof—
Provable.witness
behaves exactly like user input. So, ensure that after receiving the witness, you make any necessary assertions associated with it.
When to Use
Provable.witness
is an API similar to the single arrow<--
in Circom.Provable.witness
follows the pattern:compute, then constrain
.Some operations are extremely difficult to model with pure constraints alone. In these cases, it may be necessary to shift an expensive or complicated computation outside the circuit and prove the integrity of that computation by adding adequate constraints.
In summary,
Provable.witness
is useful for optimizing circuit constraints or working around complex operations that are challenging to model when writing a circuit.
Security Warning
Provable.witness
can be a potential pitfall and lead to underconstraints.Underconstraints are a significant source of security bugs in zero-knowledge applications, so triple-check that the constraints are generated as expected!
How to Use
Provable.witness
is used by defining the type of the output and a callback that allows non-provable code and returns the same type defined earlier.Note that if you want to witness an array of provable elements, you need to use the
Provable.Array
API.For example:
// Witness the individual digits of the combination const digits = Provable.witness(Provable.Array(Field, 4), () => { const num = combination.toBigInt(); return [num / 1000n, (num / 100n) % 10n, (num / 10n) % 10n, num % 10n]; }); // Assert the correctness of the witnessed digit separation compressCombinationDigits(digits).assertEquals(combination);
Note that the type is inferred from the type entry, and there was no need to use
.map(Field)
.
Provable.witness
behaves like user input but can only return deterministic circuit values. In other words, it cannot replace a circuit input, as this input is only known when a user provides it.For example:
const myField = Provable.witness(Field, () => Field(10));
The snippet above is somewhat redundant because you could directly use
const myField = Field(10);
, but it becomes useful within theProvable.constraintSystem
API, where it simulates a circuit input to generate a circuit summary or benchmarks for provable code.Generally,
Provable.witness
is most useful when proving statements about another circuit variable for better efficiency and flexibility.For example, division is expensive when operating on field elements, so a workaround is:
@method async divideBy19(input: Field) { const result = Provable.witness(Field, () => { return input.toBigInt() / 19n; }); // This constraint asserts that `input` is divisible by 19 // and that `result` is correctly computed from `input` result.mul(19).assertEquals(input); }
The code snippet above, for example, costs only
1
constraint, whereas a similar computation with a division API would cost10
.
Provable.if
Provable.if
functions similarly to the ternary operator in JavaScript, allowing you to choose between two values based on aBoolean
condition.In the example below,
Provable.if
selectsField(10)
if theBool
condition istrue
, andField(15)
if it'sfalse
:const selector = Bool(true); const selected = Provable.if(selector, Field(10), Field(15));
The arms of
Provable.if
can be any value or function that returns a value.Unlike JavaScript's conditional statements,
Provable.if
does not bypass the execution of one branch based on the condition. Due to the deterministic nature of zk-SNARK circuits,Provable.if
evaluates both branches but only returns the value corresponding to theBool
condition.Caution: Since both branches are always executed, if either branch contains code that throws an error,
Provable.if
will trigger that error. This behavior contrasts with traditional JavaScript control flow, where only the executed branch of anif/else
statement is evaluated.
For more details, refer to the Control Flow section in the o1js documentation.
Field vs { UInt8, UInt32, UInt64 }
The
UInt
types impose size constraints on fields.- For example, the
UInt8
provable type restricts the field size to a single byte, allowing values in the range of0
to255
. - Any value assigned to
UInt8
that falls outside this range will trigger an error, ensuring that only valid values are used. - You can convert a
UInt
type back to aField
provable type:const byte = UInt8.from(12); const byteField = byte.value;
- Converting a
Field
to aUInt8
is possible but can be unsafe if the value exceeds theUInt8
range, leading to overflow:const smallField = Field(12); const byte = UInt8.Unsafe.fromField(smallField);
- For example, the
UInt
provable types come with specialized methods, such asdivMod
, which facilitate various operations. These methods are specifically tailored for theUInt
types, making them essential in cases where precise, bounded operations are required.Under the hood,
UInt
types utilize custom gates that are more efficient for range checks.UInt
types not only limit the field size but also offer optimized constraint handling for operations on small-sized fields.- For instance, in the constraintSystem example, a range check on a
Field
type consumes only56
rows, while performing a similar operation on aUInt32
would cost only10
rows.
let bench32 = await Provable.constraintSystem(() => { const anyUInt32 = Provable.witness(UInt32, () => UInt32.from(1001)); const lowerBound = anyUInt32.greaterThanOrEqual(UInt32.from(1000)); const upperBound = anyUInt32.lessThanOrEqual(UInt32.from(9999)); lowerBound.and(upperBound).assertTrue(); }); console.log('bound 32:', bench32);
For more details refer to the o1js documentation
How to Build & Test
How to build
npm run build
How to run tests
npm run test
npm run testw # watch mode
How to run coverage
npm run coverage