npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mina-mastermind

v0.1.0

Published

![alt text](./mastermind-board.png)

Downloads

4

Readme

Mina zkApp: Mina Mastermind

alt text

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 a Code 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 and 1 blow.
        • The hit is 5 in the first position.
        • The blow is 9 in the fourth position.
  • 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, with maxAttempts 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 and giveClue methods until the Code Breaker either uncovers the secret combination or fails by exceeding the allowed maxAttempts, 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 the turnCount 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 the PublicKey, 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 and giveClue 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 the solutionHash 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 of 4523.
  • 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, or 2, 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 like Field, UInt8 to 0, and the Bool type to Bool(false), as it's a wrapper around a field with a value of 0.
  • Note that you cannot pass arguments to the init method of a SmartContract.

There are three variations for initializing a zkApp:

  1. 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.
  2. 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 calling super.init() to set all state variables to 0.

    • Then, set the specific state variables to constant values, such as Field(10) or Bool(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
        }
      }
  3. 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 to 0.

    • 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 and 2, the init() 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 a salt.

    • 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 of 1000 to 9999.

    • 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 as codemasterId once the combination is validated.

    • Finally, the turnCount is incremented, signaling that the game is ready for the code breaker to makeGuess.

    • 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 is odd. If any of these conditions are not met, the method can be called, but it will throw an error.
  • 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 the createGame 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 the solutionHash 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 the isSolved state is set to Bool(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.
  • 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.
  • 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.
  • 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 is false, you ensure that initGame 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 is true to ensure the zkApp has been properly initialized, as provedState becomes true after initGame 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 to 0, and in other methods, every player action increments this count.
    • Therefore, we assert that turnCount is zero to confirm that no other method was called before createGame.
    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 like 0 to 4 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 the maxAttempts state falls within a reasonable range, such as 5 to 20, 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.
  • To enhance security, a salt (random field) is introduced to the hash input: hash(secret, salt). This adds approximately 255 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 as Field.assertEquals(2). The former returns a Bool, 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.
  • 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 standalone circuit templates. By isolating these logic components, the main zkApp code remains focused and uncluttered.

  • utils.test.ts: Provides unit tests for the functions in utils.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 a localBlockchain. 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 a Field to bigint 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.
  • 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 as bigint.

  • However, it's important to note that provable code outside a provable environment (e.g., zkApp or zkProgram) 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 or zkProgram), you must use the Provable.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 and zkProgram have a method called analyzeMethods 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 to analyzeMethods, 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 the Provable.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 display 0 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 the Provable.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 cost 10.

Provable.if

  • Provable.if functions similarly to the ternary operator in JavaScript, allowing you to choose between two values based on a Boolean condition.

  • In the example below, Provable.if selects Field(10) if the Bool condition is true, and Field(15) if it's false:

    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 the Bool 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 an if/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 of 0 to 255.
    • 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 a Field provable type:
      const byte = UInt8.from(12);
      const byteField = byte.value;
    • Converting a Field to a UInt8 is possible but can be unsafe if the value exceeds the UInt8 range, leading to overflow:
      const smallField = Field(12);
      const byte = UInt8.Unsafe.fromField(smallField);
  • UInt provable types come with specialized methods, such as divMod, which facilitate various operations. These methods are specifically tailored for the UInt 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 only 56 rows, while performing a similar operation on a UInt32 would cost only 10 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

License

Apache-2.0