Skip to content

Writing a Circuit

Starting a fresh project and writing our first circuit

Here, we start from scratch, and explain how to set up a working project.

Initializing the Project

Run the following code in your home directory ~ to set up our project. The cargo add commands below will import the Binius crates that we need into our new project.

rustup update
cargo new my-circuit
cd my-circuit
 
cargo add binius-frontend --git https://github.com/IrreducibleOSS/binius64
cargo add binius-core --git https://github.com/IrreducibleOSS/binius64
cargo add binius-circuits --git https://github.com/IrreducibleOSS/binius64
cargo add sha2@0.10
cargo add rand@0.9 --no-default-features --features std

Building the Circuit

Now, in src/main.rs, begin by importing

use binius_frontend::CircuitBuilder;
use binius_circuits::sha256::Sha256;
use binius_core::{word::Word, verify::verify_constraints};
use sha2::{Digest, Sha256 as StdSha256};

Now, put the following lines in the body of fn main():

    let builder = CircuitBuilder::new();
 
    let content: Vec<_> = (0..4).map(|_| builder.add_witness()).collect();
    let nonce: Vec<_> = (0..4).map(|_| builder.add_witness()).collect(); 
    let commitment: [_; 4] = core::array::from_fn(|_| builder.add_inout());
 
    let message: Vec<_> = content.clone().into_iter().chain(nonce.clone()).collect();
    let len_bytes = builder.add_witness();
    let sha256 = Sha256::new(&builder, len_bytes, commitment, message);
    let circuit = builder.build();

The code above first creates a simple circuit. That circuit first declares content and nonce witness values. Each of these is 32 bytes, and so takes 4 wires to represent. It also declares commitment as input–output—i.e. public—wires. Finally, it wires all three of these into a sha256 gadget.

This already gives the circuit we wanted to build.

Populating Wire Values

Our next step will be to try inscribing actual values into our circuit's wires. Continue in fn main() by adding the following code:

    let mut witness = circuit.new_witness_filler();
    witness[len_bytes] = Word(64); // feed the circuit a wire containing the preimage length, in bytes.
    
    let mut content_bytes = [0u8; 32];
    content_bytes[..32].copy_from_slice(&b"A secret, exactly 32 bytes long."[..]);
    let nonce_bytes: [u8; 32] = rand::random();
    let mut message_bytes = [0u8; 64];
    message_bytes[..32].copy_from_slice(&content_bytes);
    message_bytes[32..].copy_from_slice(&nonce_bytes);
    sha256.populate_message(&mut witness, &message_bytes);
 
    let digest = StdSha256::digest(message_bytes);
    let mut digest_bytes = [0u8; 32];
    digest_bytes.copy_from_slice(&digest);
    sha256.populate_digest(&mut witness, digest_bytes);
 
    circuit.populate_wire_witness(&mut witness)?;

Above, we pick a sample content, as well as a fully random nonce, and populate those. We also populate the digest field of the circuit.

Checking Constraint Satisfaction

Finally, we can now check whether the values above we just populated actually satisfy the constraints imposed by the circuit.

Add the following further lines to fn main():

    let cs = circuit.constraint_system();
    let witness_vec = witness.into_value_vec();
    verify_constraints(cs, &witness_vec)?;
 
    println!("✓ the wire values you populated satisfy the circuit's constraints");
    
    Ok(())

To make this very last step work, you will also need to change the signature of fn main() to fn main() -> Result<(), Box<dyn std::error::Error>>.

Running Everything

To put everything together, run the following line in /my-circuit:

RUSTFLAGS="-C target-cpu=native" cargo run --release

You should get the following output:

✓ the wire values you populated satisfy the circuit's constraints

This very satisfaction is what we're about to generate and verify proofs about. We'll cover that in the next page.