Writing a 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.