BattleZips
  • Awesome Circom
  • 🔬Theory
    • Prerequisite Knowledge
    • Resources
      • White Papers & PDF's
      • Blogs and Writeups
      • Videos
      • Important Entities
      • Communities
    • Proving Schemes
    • Primitives
      • Hash Functions
      • Public Key Cryptosystems
        • Note on L1 key registry → L2 hot key + callback to circuit-optimized hash functions
        • ECDSA & secp256k1
        • EdDSA
      • Merkle Trees
        • What is a Merkle Tree?
        • What is a merkle proof of inclusion?
        • zk-kit
        • Incremental Merkle Trees
        • Sparse Merkle Trees
        • Tree Arity (Binary, Quinary)
      • Semaphore
      • Arithmetic Circuits
  • 🏗️Development
    • Circom Language
      • Installation
      • IDE
      • Signals and Variables
      • Signal Assignment and Constraint Generation
      • Conditional Statements
      • Components and Templates
      • Circuit Compilation
      • Syntax
    • SnarkJS
      • Proving Schemes
      • Powers of Tau
      • ZK Keys
      • Zero Knowledge Proofs
      • On-Chain ZKP
      • Page 2
    • circomlib
      • Basic Math Constraints
      • Multiplexing
      • Hashing
      • EdDSA
      • circomlibjs
    • circom-tester
    • hardhat-circom
    • SHIELD
    • Circomspect
  • 🌆Ecosystem
    • Circom vs Other Solutions
      • Domain-Specific Languages
      • ZK Virtual Machines
      • ZK Ethereum Virtual Machines
    • Communities to Join
    • Recorded Content
    • Projects
  • 🛳️Examples
    • BattleZips V1
      • On the BattleZips Project
      • Docs holder
        • Join Game UML Sequence Diagram
        • Play Game UML Sequence Diagram
        • End Game UML Sequence Diagram
      • ZK Privacy Stack
      • Deploying Artifacts to Prod
      • Browser Client
    • RollupNC
      • Smart Contracts
      • Account/ State Tree
      • Transaction Tree
      • Layer 1 Deposits to Layer 2
      • Layer 2 Transacting
      • Layer 2 Withdrawals to Layer 1
Powered by GitBook
On this page
  • Conventions
  • Mux1
  • Intra-Circuit Incremental State Building
  1. Development
  2. circomlib

Multiplexing

MANDATORY use of the first party conditional signal selector. Do not skip!

PreviousBasic Math ConstraintsNextHashing

Last updated 2 years ago

For a detailed explanation on why you *need* to use multiplexers, visit the Circom Language > Conditional Statements section:

Conventions

Given N in a gadget MuxN, you can consider the multiplexer to have the following properties:

  • N selectors used to to toggle between signals

  • 2^n possible input signals

There will always be a single output signal. Given the overwhelming need for for general computation where higher order multiplexers are niche, the rest of the section will only consider the use of a 2 input multiplexer. As mentioned in the Conditional Statements section, use of higher order multiplexers could facilitate the need for something like a switch case.

Mux1

There has been a lot of use of multiplexers around this GitBook, so we will keep this example short and to the point. At the risk of beating a dead horse, we want to reiterate that MUX1 IS A REQUIREMENT FOR COMPUTING BINARY CONDITIONAL LOGIC IN CIRCOM.

The component provides a relatively simple interface for a developer to input two signals and return one or the other based on a boolean condition (selector). In order to avoid , a developer using the Circom DSL must entirely compute both branches to their resulting end signal value, then use the multiplexer to toggle between the two signals based on a specific case. The component is shown below:

template Mux1() {
    var i;
    signal input c[2];  // Constants
    signal input s;   // Selector
    signal output out;

    component mux = MultiMux1(1);

    for (i=0; i<2; i++) {
        mux.c[0][i] <== c[i];
    }

    s ==> mux.s;

    mux.out[0] ==> out;
}

This simple gadget can quickly be extended in your code base to facilitate most branching logical conclusions as is expected of a turing-complete programming language. For instance, BattleZips V1 makes use of a multiplexer to choose between the horizontal and vertical axis when compute whether a shot intersects with a ship placement. This is shown in the helper hitShip.circom component:

Consider the goal of having ships that can be placed either horizontally or vertically. Given the carrier ship (length 5, 0th index in 5-tuple) placed vertically on the board at (7, 7). This data can be represented as a 3-tuple (x, y, z) where the order inclusion of the ship 3-tuple in the board 5-tuple (see Examples > BattleZips V1) encodes the length of the ship.

As is shown in placeShip.circom or hitShip.circom, we compute both the horizontal and vertical collision and boundary checks. At /// MUX TO CHOOSE OUTPUT /// the z coordinate for the ship placement is used as the Mux1() selector, properly outputting the signal that passes constraints while discarding the signal that fails. Of course, we expect this property to hold in the reverse direction as well, constraining proof generation based on the z axis.

Multiplexed Logic Circuit Unit Test
it("Prove valid turn 5: horizontal muxxed miss with vertical hit", async () => {
    // demonstrate miss computation even if z=1 collision when z = 0
    const ships = [
        [1, 1, 1],
        [5, 1, 1],
        [4, 4, 1],
        [1, 7, 1],
        [3, 8, 0]
    ]
    const hash = await mimcSponge.multiHash(ships.flat())
    const shot = [2, 1]
    const hit = 0
    const witness = await shotCircuit.calculateWitness({
        ships,
        hash: F.toObject(hash),
        shot,
        hit
    })
    await shotCircuit.assertOut(witness, {})
})

Intra-Circuit Incremental State Building

"Intra-Circuit Incremental State Building" is quite verbose. Put more directly:

  • Iterative/ loop over data repeatedly with potentially different parameters each time

  • Mutate stored state each time such that a new loop's beginning picks up where previous loop left off

  • All computation is contained within a main component (top level ZK circuit)

template PlaceShip(n) {
.
.
.
    /// HORIZONTAL PLACEMENT COLLISION CHECK ///
    for (var i = 0; i < n; i++) {
        boardH[ship[0] + i][ship[1]] += 1;
        var cellVal = boardH[ship[0] + i][ship[1]];
        hCollision.in[i] <-- cellVal * (cellVal - 1) == 0;
    }
    /// VERTICAL PLACEMENT COLLISION CHECK ///
    for (var i = 0; i < n; i++) {
        boardV[ship[0]][ship[1] + i] += 1;
        var cellVal = boardV[ship[0]][ship[1] + i];
        vCollision.in[i] <-- cellVal * (cellVal - 1) == 0;
    }
    /// MUX TO CHOOSE CONSTRAINT ///
    collisionMux.c[0] <== hCollision.out;
    collisionMux.c[1] <== vCollision.out;
    collisionMux.s <== ship[2]; // z coordinate as selector for horizontal/ vertical
    collisionMux.out === expectedCollision.out; // expect 1 if all placements have binary values (no collisions, < 2)
    
    /// ##### INTRA-CIRCUIT STATE MUX HERE ###
    // numberify bitmap
    component toNumH = Bits2Num(100); // horizontal board Bits2Num
    component toNumV = Bits2Num(100); // vertical board Bits2Num
    for (var i = 0; i < 100; i++) {
        toNumH.in[i] <-- boardH[i \ 10][i % 10];
        toNumV.in[i] <-- boardV[i \ 10][i % 10];
    }
    // mux boards to get next state
    boardMux.c[0] <== toNumH.out;
    boardMux.c[1] <== toNumV.out;
    boardMux.s <== ship[2];
    boardOut <== boardMux.out;

Given the board placement computations above, the last "paragraph" of the circuit handles the conditional selection of the horizontal or vertical board signal as generated in a 10x10 variable array, encoded by Bits2Num. Using the multiplexer, we are able to witness a conditional commitment to some encoded piece of data, then work with that new state on the next loop/ increment.

pragma circom 2.0.3;

include "../../../node_modules/circomlib/circuits/mux1.circom";
include "../../../node_modules/circomlib/circuits/bitify.circom";


/*
    Determine whether or not a given ship uses a given x/y coordinate pair
    @param n - the length of the ship
*/
template HitShip(n) {

    signal input ship[3]; // x, y, z to hitscan from
    signal input shot[2]; // x, y, to hitscan with
    signal output hit; // 0 if not hit, 1 if hit

    /// HORIZONTAL CONSTRAINT ///
    var hHit = 0;
    for (var i = 0; i < n; i++) {
        var _x = (ship[0] + i == shot[0]);
        var _y = (ship[1] == shot[1]);
        hHit += 1 * (_x == 1 && _y == 1);
    }
    /// VERTICAL CONSTRAINT ///
    var vHit = 0;
    for (var i = 0; i < n; i++) {
        var _x = (ship[0] == shot[0]);
        var _y = (ship[1] + i == shot[1]);
        vHit += 1 * (_x == 1 && _y == 1);
    }

    /// MUX TO CHOOSE OUTPUT ///
    component mux = Mux1();
    mux.c[0] <-- hHit;
    mux.c[1] <-- vHit;
    mux.s <== ship[2];
    hit <== mux.out;
}

The code computes whether or not a ship placement is legal according to our own (arbitrarily defined to our needs) rules. In the BattleZip rule set, ignoring the concept of ship collisions, all cells covered by a ship placement must be within the 10x10 grid. Therefore, situations like the above diagram arise.

The BattleZips V1 circom-tester proves the intended functionality of using Mux1() in shot proofs for horizontal/ verital ship orientation:

Beyond the z axis orientation multiplexer, Mux1() is the interface by which conditional state updates computed in Zero Knowledge are communicated between iterations of the placeShip.circom component. Consider the where the two conditions are computed and then multiplexed:

🏗️
Conditional Statements
Mux1()
mux1.circom
non-quadratic expressions as cryptically explained in the official Circom docs
hitShip.circom
placeShip.circom
test/circuits.js unit testing
snippet of this component
Diagram of a case where multiplexing allows us to discard invalid state and choose valid state Z is 1, meaning the ship placement is valid. if Z were 0, the placement would extend off the board.