Skip to content

Commit 7d81d3c

Browse files
critesjoshclaude
andcommitted
feat(docs): integrate recursive verification tutorial into docs/examples
Add the recursive verification example code from aztec-examples repo into docs/examples/ so it can be validated by CI and won't go stale. Update the tutorial to use #include_code macros to pull code from the example files. Changes: - Add vanilla Noir circuit (hello_circuit) to docs/examples/circuits/ - Add recursive_verification_contract to docs/examples/contracts/ - Add TypeScript examples for proof generation and deployment - Update bootstrap.sh to compile vanilla circuits - Update ts/bootstrap.sh to handle link: dependencies for bb.js and noir-noir_js - Update tutorial to use #include_code macros for circuit, contract, and TS code Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 4b2de34 commit 7d81d3c

File tree

14 files changed

+438
-177
lines changed

14 files changed

+438
-177
lines changed

docs/Nargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ members = [
33
"examples/contracts/counter_contract",
44
"examples/contracts/bob_token_contract",
55
"examples/contracts/nft",
6-
"examples/contracts/nft_bridge"
6+
"examples/contracts/nft_bridge",
7+
"examples/contracts/recursive_verification_contract"
78
]

docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md

Lines changed: 7 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This is called "recursive" verification because the proof is verified inside an
1212
:::
1313

1414
:::tip Full Working Example
15-
The complete code for this tutorial is available at [aztec-examples/recursive_verification](https://github.com/AztecProtocol/aztec-examples/tree/v3.0.0-devnet.6-patch.1/recursive_verification). Clone it to follow along or use it as a reference.
15+
The complete code for this tutorial is available in the [docs/examples](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/docs/examples) directory. Clone it to follow along or use it as a reference.
1616
:::
1717

1818
## Prerequisites
@@ -131,16 +131,7 @@ circuit/
131131

132132
Replace the contents of `circuit/src/main.nr` with:
133133

134-
```rust
135-
fn main(x: u64, y: pub u64) {
136-
assert(x != y);
137-
}
138-
139-
#[test]
140-
fn test_main() {
141-
main(1, 2);
142-
}
143-
```
134+
#include_code circuit /docs/examples/circuits/hello_circuit/src/main.nr rust
144135

145136
This is intentionally minimal to focus on the verification pattern. In production, you would replace `assert(x != y)` with meaningful computations like:
146137

@@ -170,14 +161,7 @@ For example, you could create a zkpassport proof demonstrating that you are over
170161

171162
Update `circuit/Nargo.toml` (see [Noir crates and packages](https://noir-lang.org/docs/noir/modules_packages_crates/crates_and_packages) for more details):
172163

173-
```toml
174-
[package]
175-
name = "hello_circuit"
176-
type = "bin"
177-
authors = ["[YOUR_NAME]"]
178-
179-
[dependencies]
180-
```
164+
#include_code circuit_nargo_toml /docs/examples/circuits/hello_circuit/Nargo.toml toml
181165

182166
**Note**: This is a vanilla Noir circuit, not an Aztec contract. It has `type = "bin"` (binary) and no Aztec dependencies. The circuit is compiled with `nargo`, not `aztec compile`. This distinction is important—you can verify proofs from _any_ Noir circuit inside Aztec contracts.
183167

@@ -267,70 +251,7 @@ bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages
267251

268252
Replace the contents of `contract/src/main.nr` with:
269253

270-
```rust
271-
use aztec::macros::aztec;
272-
273-
#[aztec]
274-
pub contract ValueNotEqual {
275-
use aztec::{
276-
macros::{functions::{external, initializer, internal, only_self, view}, storage::storage},
277-
oracle::debug_log::debug_log_format,
278-
protocol::{address::AztecAddress, traits::ToField},
279-
state_vars::{Map, PublicImmutable, PublicMutable},
280-
};
281-
use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof};
282-
283-
#[storage]
284-
struct Storage<Context> {
285-
counters: Map<AztecAddress, PublicMutable<Field, Context>, Context>,
286-
vk_hash: PublicImmutable<Field, Context>,
287-
}
288-
289-
#[initializer]
290-
#[external("public")]
291-
fn constructor(headstart: Field, owner: AztecAddress, vk_hash: Field) {
292-
self.storage.counters.at(owner).write(headstart);
293-
self.storage.vk_hash.initialize(vk_hash);
294-
}
295-
296-
#[external("private")]
297-
fn increment(
298-
owner: AztecAddress,
299-
verification_key: UltraHonkVerificationKey,
300-
proof: UltraHonkZKProof,
301-
public_inputs: [Field; 1],
302-
) {
303-
debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]);
304-
305-
// Read the stored VK hash - this is readable from private context
306-
// because PublicImmutable values are committed at deployment
307-
let vk_hash = self.storage.vk_hash.read();
308-
309-
// Verify the proof - this is the core operation
310-
// The function checks:
311-
// 1. The VK hashes to the stored vk_hash
312-
// 2. The proof is valid for the given VK and public inputs
313-
verify_honk_proof(verification_key, proof, public_inputs, vk_hash);
314-
315-
// If we reach here, the proof is valid
316-
// Enqueue a public function call to update state
317-
self.enqueue_self._increment_public(owner);
318-
}
319-
320-
#[only_self]
321-
#[external("public")]
322-
fn _increment_public(owner: AztecAddress) {
323-
let current = self.storage.counters.at(owner).read();
324-
self.storage.counters.at(owner).write(current + 1);
325-
}
326-
327-
#[view]
328-
#[external("public")]
329-
fn get_counter(owner: AztecAddress) -> Field {
330-
self.storage.counters.at(owner).read()
331-
}
332-
}
333-
```
254+
#include_code full_contract /docs/examples/contracts/recursive_verification_contract/src/main.nr rust
334255

335256
### Storage Variables Explained
336257

@@ -539,80 +460,7 @@ The proof generation script executes the circuit offchain and produces the proof
539460

540461
Create `scripts/generate_data.ts`:
541462

542-
```typescript
543-
import { Noir } from "@aztec/noir-noir_js";
544-
import circuitJson from "../circuit/target/hello_circuit.json" with { type: "json" };
545-
import { Barretenberg, UltraHonkBackend, deflattenFields } from "@aztec/bb.js";
546-
import fs from "fs";
547-
import { exit } from "process";
548-
549-
// Step 1: Initialize Barretenberg API (the proving system backend)
550-
// Barretenberg is the C++ library that implements UltraHonk
551-
// threads: 1 uses single-threaded mode (increase for faster proofs on multi-core machines)
552-
const barretenbergAPI = await Barretenberg.new({ threads: 1 });
553-
554-
// Step 2: Create Noir circuit instance from compiled bytecode
555-
// This loads the circuit definition so we can execute it
556-
const helloWorld = new Noir(circuitJson as any);
557-
558-
// Step 3: Execute circuit with inputs to generate witness
559-
// The witness is all intermediate values computed during circuit execution
560-
// x=1 (private), y=2 (public) - proves that 1 != 2
561-
const { witness: mainWitness } = await helloWorld.execute({ x: 1, y: 2 });
562-
563-
// Step 4: Create UltraHonk backend with circuit bytecode
564-
// The backend handles proof generation and verification
565-
const mainBackend = new UltraHonkBackend(circuitJson.bytecode, barretenbergAPI);
566-
567-
// Step 5: Generate proof targeting the noir-recursive verifier
568-
// verifierTarget: 'noir-recursive' creates a proof format suitable for
569-
// verification inside another Noir circuit (which is what Aztec contracts are)
570-
const mainProofData = await mainBackend.generateProof(mainWitness, {
571-
verifierTarget: "noir-recursive",
572-
});
573-
574-
// Step 6: Verify proof locally before saving
575-
// This catches errors early - if verification fails here, it will fail onchain too
576-
const isValid = await mainBackend.verifyProof(mainProofData, {
577-
verifierTarget: "noir-recursive",
578-
});
579-
console.log(`Proof verification: ${isValid ? "SUCCESS" : "FAILED"}`);
580-
581-
// Step 7: Generate recursive artifacts for onchain use
582-
// This converts the proof and VK into field element arrays that can be
583-
// passed to the Aztec contract
584-
const recursiveArtifacts = await mainBackend.generateRecursiveProofArtifacts(
585-
mainProofData.proof,
586-
mainProofData.publicInputs.length,
587-
);
588-
589-
// Step 8: Convert proof to field elements if needed
590-
// Some versions return empty proofAsFields, requiring manual conversion
591-
let proofAsFields = recursiveArtifacts.proofAsFields;
592-
if (proofAsFields.length === 0) {
593-
console.log("Using deflattenFields to convert proof...");
594-
proofAsFields = deflattenFields(mainProofData.proof).map((f) => f.toString());
595-
}
596-
597-
const vkAsFields = recursiveArtifacts.vkAsFields;
598-
599-
console.log(`VK size: ${vkAsFields.length}`); // Should be 115
600-
console.log(`Proof size: ${proofAsFields.length}`); // Should be 508
601-
console.log(`Public inputs: ${mainProofData.publicInputs.length}`); // Should be 1
602-
603-
// Step 9: Save all data to JSON for contract interaction
604-
const data = {
605-
vkAsFields: vkAsFields, // 115 field elements - the verification key
606-
vkHash: recursiveArtifacts.vkHash, // Hash of VK - stored in contract
607-
proofAsFields: proofAsFields, // 508 field elements - the proof
608-
publicInputs: mainProofData.publicInputs.map((p: string) => p.toString()),
609-
};
610-
611-
fs.writeFileSync("data.json", JSON.stringify(data, null, 2));
612-
await barretenbergAPI.destroy();
613-
console.log("Done");
614-
exit();
615-
```
463+
#include_code generate_data /docs/examples/ts/recursive_verification/scripts/generate_data.ts typescript
616464

617465
### Understanding the Proof Generation Pipeline
618466

@@ -900,22 +748,7 @@ This single line triggers a complex flow:
900748

901749
Create `scripts/sponsored_fpc.ts`:
902750

903-
```typescript
904-
import { getContractInstanceFromInstantiationParams } from "@aztec/aztec.js/contracts";
905-
import { Fr } from "@aztec/aztec.js/fields";
906-
import { SponsoredFPCContract } from "@aztec/noir-contracts.js/SponsoredFPC";
907-
908-
const SPONSORED_FPC_SALT = new Fr(BigInt(0));
909-
910-
export async function getSponsoredFPCInstance() {
911-
return await getContractInstanceFromInstantiationParams(
912-
SponsoredFPCContract.artifact,
913-
{
914-
salt: SPONSORED_FPC_SALT,
915-
},
916-
);
917-
}
918-
```
751+
#include_code sponsored_fpc /docs/examples/ts/recursive_verification/scripts/sponsored_fpc.ts typescript
919752

920753
This utility computes the address of the pre-deployed sponsored FPC contract. The salt ensures we get the same address every time. For more information about fee payment options, see [Paying Fees](../../aztec-js/how_to_pay_fees.md).
921754

@@ -955,7 +788,7 @@ The counter starts at 10 (set during deployment), and after successful proof ver
955788

956789
## Quick Reference
957790

958-
If you want to run all commands at once, or if you're starting fresh, here's the complete workflow. You can also clone the [full working example](https://github.com/AztecProtocol/aztec-examples/tree/main/recursive_verification) to get started quickly.
791+
If you want to run all commands at once, or if you're starting fresh, here's the complete workflow. You can also reference the [full working example](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/docs/examples) in the main repository.
959792

960793
```bash
961794
# Install dependencies (after creating package.json and tsconfig.json)

docs/examples/bootstrap.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,27 @@ export TRANSPILER=${TRANSPILER:-"$REPO_ROOT/avm-transpiler/target/release/avm-tr
1010
export STRIP_AZTEC_NR_PREFIX=${STRIP_AZTEC_NR_PREFIX:-"$REPO_ROOT/noir-projects/noir-contracts/scripts/strip_aztec_nr_prefix.sh"}
1111
export BB_HASH=${BB_HASH:-$("$REPO_ROOT/barretenberg/cpp/bootstrap.sh" hash)}
1212

13+
function compile-circuits {
14+
echo_header "Compiling vanilla Noir circuits"
15+
local CIRCUITS_DIR="$REPO_ROOT/docs/examples/circuits"
16+
17+
if [ ! -d "$CIRCUITS_DIR" ]; then
18+
echo_stderr "No circuits directory found at $CIRCUITS_DIR"
19+
return 0
20+
fi
21+
22+
if [ ! -f "$CIRCUITS_DIR/Nargo.toml" ]; then
23+
echo_stderr "No workspace Nargo.toml found in $CIRCUITS_DIR"
24+
return 0
25+
fi
26+
27+
# Compile all circuits in the workspace
28+
echo_stderr "Compiling circuits workspace..."
29+
(cd "$CIRCUITS_DIR" && $NARGO compile --workspace)
30+
31+
echo_stderr "Vanilla circuits compiled"
32+
}
33+
1334
function compile {
1435
echo_header "Compiling example contracts"
1536
# Use noir-contracts bootstrap with DOCS_WORKING_DIR pointing to parent (docs/)
@@ -154,6 +175,7 @@ function post_failure_comment_for_steps {
154175

155176
case "$cmd" in
156177
"")
178+
run_step "Compile (Noir circuits)" compile-circuits
157179
run_step "Compile (Noir contracts)" compile
158180
run_step "Compile (Solidity)" compile-solidity
159181
run_step "TypeScript validation" validate-ts
@@ -167,6 +189,9 @@ case "$cmd" in
167189
fi
168190
fi
169191
;;
192+
compile-circuits)
193+
compile-circuits
194+
;;
170195
compile-solidity)
171196
compile-solidity
172197
;;

docs/examples/circuits/Nargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[workspace]
2+
members = [
3+
"hello_circuit"
4+
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# docs:start:circuit_nargo_toml
2+
[package]
3+
name = "hello_circuit"
4+
type = "bin"
5+
authors = [""]
6+
7+
[dependencies]
8+
# docs:end:circuit_nargo_toml
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// docs:start:circuit
2+
fn main(x: u64, y: pub u64) {
3+
assert(x != y);
4+
}
5+
6+
#[test]
7+
fn test_main() {
8+
main(1, 2);
9+
}
10+
// docs:end:circuit
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# docs:start:nargo_toml
2+
[package]
3+
name = "recursive_verification_contract"
4+
type = "contract"
5+
authors = [""]
6+
compiler_version = ">=0.25.0"
7+
8+
[dependencies]
9+
aztec = { path = "../../../../noir-projects/aztec-nr/aztec" }
10+
bb_proof_verification = { path = "../../../../barretenberg/noir/bb_proof_verification" }
11+
# docs:end:nargo_toml
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// docs:start:full_contract
2+
// docs:start:contract
3+
use aztec::macros::aztec;
4+
5+
#[aztec]
6+
pub contract ValueNotEqual {
7+
// docs:end:contract
8+
// docs:start:imports
9+
use aztec::{
10+
macros::{functions::{external, initializer, only_self, view}, storage::storage},
11+
oracle::debug_log::debug_log_format,
12+
protocol::{address::AztecAddress, traits::ToField},
13+
state_vars::{Map, PublicImmutable, PublicMutable},
14+
};
15+
use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof};
16+
// docs:end:imports
17+
18+
// docs:start:storage
19+
#[storage]
20+
struct Storage<Context> {
21+
counters: Map<AztecAddress, PublicMutable<Field, Context>, Context>,
22+
vk_hash: PublicImmutable<Field, Context>,
23+
}
24+
// docs:end:storage
25+
26+
// docs:start:constructor
27+
#[initializer]
28+
#[external("public")]
29+
fn constructor(headstart: Field, owner: AztecAddress, vk_hash: Field) {
30+
self.storage.counters.at(owner).write(headstart);
31+
self.storage.vk_hash.initialize(vk_hash);
32+
}
33+
// docs:end:constructor
34+
35+
// docs:start:increment
36+
#[external("private")]
37+
fn increment(
38+
owner: AztecAddress,
39+
verification_key: UltraHonkVerificationKey,
40+
proof: UltraHonkZKProof,
41+
public_inputs: [Field; 1],
42+
) {
43+
debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]);
44+
45+
// Read the stored VK hash - this is readable from private context
46+
// because PublicImmutable values are committed at deployment
47+
let vk_hash = self.storage.vk_hash.read();
48+
49+
// Verify the proof - this is the core operation
50+
// The function checks:
51+
// 1. The VK hashes to the stored vk_hash
52+
// 2. The proof is valid for the given VK and public inputs
53+
verify_honk_proof(verification_key, proof, public_inputs, vk_hash);
54+
55+
// If we reach here, the proof is valid
56+
// Enqueue a public function call to update state
57+
self.enqueue_self._increment_public(owner);
58+
}
59+
// docs:end:increment
60+
61+
// docs:start:increment_public
62+
#[only_self]
63+
#[external("public")]
64+
fn _increment_public(owner: AztecAddress) {
65+
let current = self.storage.counters.at(owner).read();
66+
self.storage.counters.at(owner).write(current + 1);
67+
}
68+
// docs:end:increment_public
69+
70+
// docs:start:get_counter
71+
#[view]
72+
#[external("public")]
73+
fn get_counter(owner: AztecAddress) -> Field {
74+
self.storage.counters.at(owner).read()
75+
}
76+
// docs:end:get_counter
77+
}
78+
// docs:end:full_contract

0 commit comments

Comments
 (0)