diff --git a/.gitignore b/.gitignore
index 628b5e4..e6b176b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
 /target
 .idea
 *.hex
-/generated-bins/prover.bin
+
+# Circuit binaries are generated at build time by build.rs
+/generated-bins/
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 2769c66..35331f4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3929,6 +3929,7 @@ dependencies = [
  "qp-dilithium-crypto",
  "qp-plonky2",
  "qp-poseidon",
+ "qp-poseidon-core",
  "qp-rusty-crystals-dilithium",
  "qp-rusty-crystals-hdwallet",
  "qp-wormhole-aggregator",
diff --git a/Cargo.toml b/Cargo.toml
index bd603e5..27908fe 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -76,6 +76,7 @@ subxt-metadata = "0.44"
 
 # ZK proof generation (aligned with chain)
 anyhow = "1.0"
+
 qp-wormhole-circuit = { version = "1.3.0", default-features = false, features = ["std"] }
 qp-wormhole-prover = { version = "1.3.0", default-features = false, features = ["std"] }
 qp-wormhole-verifier = { version = "1.3.0", default-features = false, features = ["std"] }
@@ -83,8 +84,22 @@ qp-wormhole-aggregator = { version = "1.3.0", default-features = false, features
 qp-wormhole-inputs = { version = "1.3.0", default-features = false, features = ["std"] }
 qp-zk-circuits-common = { version = "1.3.0", default-features = false, features = ["std"] }
 qp-plonky2 = { version = "1.1.6", default-features = false, features = ["rand", "std"] }
+qp-wormhole-circuit-builder = { version = "1.3.0" }
+
+
+[build-dependencies]
+qp-wormhole-circuit-builder = { version = "1.3.0" }
 
 [dev-dependencies]
 tempfile = "3.8.1"
 serial_test = "3.1"
-qp-wormhole-circuit-builder = { version = "1.2.1" }
+qp-poseidon-core = "1.4.0"
+
+# Optimize build scripts and their dependencies in dev mode.
+# This is critical for circuit generation which is CPU-intensive.
+# Without this, circuit generation takes ~10 minutes instead of ~30 seconds.
+[profile.dev.build-override]
+opt-level = 3
+
+[profile.release.build-override]
+opt-level = 3
diff --git a/README.md b/README.md
index bf20aba..7b09f97 100644
--- a/README.md
+++ b/README.md
@@ -180,7 +180,6 @@ quantus wormhole aggregate \
 ```
 
 - `--proofs`: One or more hex-encoded proof files. The number must not exceed `num_leaf_proofs` from the circuit config.
-- Before aggregation, the CLI verifies binary hashes from `generated-bins/config.json` to detect stale circuit binaries.
 - Displays timing for dummy proof generation and aggregation separately.
 
 #### `quantus wormhole verify-aggregated`
@@ -250,23 +249,31 @@ Build ZK circuit binaries from the `qp-zk-circuits` repository, then copy them t
 
 ```bash
 quantus developer build-circuits \
-  --branching-factor 2 \
-  --depth 1 \
-  --circuits-path ../qp-zk-circuits \
+  --num-leaf-proofs 2 \
+  --num-layer0-proofs 2 \
   --chain-path ../chain
 ```
 
-- `--branching-factor`: Number of proofs aggregated at each tree level.
-- `--depth`: Depth of the aggregation tree. Total leaf proofs = `branching_factor ^ depth`.
-- `--circuits-path`: Path to the `qp-zk-circuits` repo (default: `../qp-zk-circuits`).
+Add `--skip-prover` when you only need verifier artifacts:
+
+```bash
+quantus developer build-circuits \
+  --num-leaf-proofs 2 \
+  --num-layer0-proofs 2 \
+  --chain-path ../chain \
+  --skip-prover
+```
+
+- `--num-leaf-proofs`: Number of leaf proofs per layer-0 aggregation.
+- `--num-layer0-proofs`: Number of inner proofs per layer-1 aggregation.
 - `--chain-path`: Path to the chain repo (default: `../chain`).
 - `--skip-chain`: Skip copying binaries to the chain directory.
+- `--skip-prover`: Skip generating prover binaries.
 
-**What it does (4 steps):**
-1. Builds the `qp-wormhole-circuit-builder` binary.
-2. Runs the circuit builder to generate binary files in `generated-bins/` (includes `prover.bin`, `verifier.bin`, `common.bin`, `aggregated_verifier.bin`, `aggregated_common.bin`, `config.json` with SHA256 hashes).
-3. Copies binaries to the CLI's `generated-bins/` directory and touches the aggregator source to force recompilation.
-4. Copies chain-relevant binaries (`aggregated_common.bin`, `aggregated_verifier.bin`, `config.json`) to `chain/pallets/wormhole/` and touches the pallet source.
+**What it does (3 steps):**
+1. Clears stale artifacts from the CLI's `generated-bins/` directory.
+2. Calls the `qp-wormhole-circuit-builder` library directly to regenerate binary files in `generated-bins/` (`verifier.bin`, `common.bin`, `aggregated_verifier.bin`, `aggregated_common.bin`, `config.json`, plus prover binaries unless `--skip-prover` is set).
+3. Copies chain-relevant binaries (`aggregated_common.bin`, `aggregated_verifier.bin`, `config.json`) to `chain/pallets/wormhole/` and touches the pallet source.
 
 After running, rebuild the chain (`cargo build --release` in the chain directory) so `include_bytes!()` picks up the new binaries.
 
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..2be40c9
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,45 @@
+//! Build script for quantus-cli.
+//!
+//! Generates circuit binaries (prover, verifier, aggregator) at build time.
+//! This ensures the binaries are always consistent with the circuit crate version
+//! and eliminates the need to manually run `quantus developer build-circuits`.
+
+use std::{env, path::Path, time::Instant};
+
+fn main() {
+	let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
+	let output_dir = Path::new(&manifest_dir).join("generated-bins");
+
+	let num_leaf_proofs: usize = env::var("QP_NUM_LEAF_PROOFS")
+		.unwrap_or_else(|_| "16".to_string())
+		.parse()
+		.expect("QP_NUM_LEAF_PROOFS must be a valid usize");
+
+	// Rerun if the circuit builder crate changes
+	println!("cargo:rerun-if-changed=build.rs");
+
+	println!(
+		"cargo:warning=[quantus-cli] Generating ZK circuit binaries (num_leaf_proofs={})...",
+		num_leaf_proofs
+	);
+
+	let start = Instant::now();
+
+	// Create the output directory if it doesn't exist
+	std::fs::create_dir_all(&output_dir).expect("Failed to create generated-bins directory");
+
+	// Generate all circuit binaries (leaf + aggregated, WITH prover)
+	qp_wormhole_circuit_builder::generate_all_circuit_binaries(
+		&output_dir,
+		true, // include_prover = true (CLI needs prover for proof generation)
+		num_leaf_proofs,
+		None, // num_layer0_proofs - no layer-1 aggregation
+	)
+	.expect("Failed to generate circuit binaries");
+
+	let elapsed = start.elapsed();
+	println!(
+		"cargo:warning=[quantus-cli] ZK circuit binaries generated in {:.2}s",
+		elapsed.as_secs_f64()
+	);
+}
diff --git a/generated-bins/aggregated_common.bin b/generated-bins/aggregated_common.bin
deleted file mode 100644
index 1121006..0000000
Binary files a/generated-bins/aggregated_common.bin and /dev/null differ
diff --git a/generated-bins/aggregated_verifier.bin b/generated-bins/aggregated_verifier.bin
deleted file mode 100644
index 8e3e3a5..0000000
Binary files a/generated-bins/aggregated_verifier.bin and /dev/null differ
diff --git a/generated-bins/common.bin b/generated-bins/common.bin
deleted file mode 100644
index 15d8a64..0000000
Binary files a/generated-bins/common.bin and /dev/null differ
diff --git a/generated-bins/config.json b/generated-bins/config.json
deleted file mode 100644
index 2dced0c..0000000
--- a/generated-bins/config.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "num_leaf_proofs": 16,
-  "hashes": {
-    "common": "672689a87e8ed780337c0752ebc7fd1db6a63611fbd59b4ad0cbe4a4d97edcf2",
-    "verifier": "bb017485b12fb9c6d0b5c3db8b68f417bd3f75b2d5f3a2ea5fe12b6244233372",
-    "prover": "78c114c7290b04bac00551a590fd652f98194653b10ac4e11b0c0ddd5c7c0976",
-    "aggregated_common": "af4461081f6fb527d2b9ffb74479a133ed8b92cdd3554b46adc481a0dfc38b5d",
-    "aggregated_verifier": "90350437c8e0e2144ca849623ea0b58edd2decd7bdf6b728b32e1aa9d8f1e337",
-    "dummy_proof": "8c46fd19c4c3581016ba4adcf4f0735bd04bf09f8e94e9b0f092ea522c9d3ba9"
-  }
-}
\ No newline at end of file
diff --git a/generated-bins/dummy_proof.bin b/generated-bins/dummy_proof.bin
deleted file mode 100644
index 7802f3d..0000000
Binary files a/generated-bins/dummy_proof.bin and /dev/null differ
diff --git a/generated-bins/verifier.bin b/generated-bins/verifier.bin
deleted file mode 100644
index 21c3030..0000000
Binary files a/generated-bins/verifier.bin and /dev/null differ
diff --git a/src/chain/quantus_subxt.rs b/src/chain/quantus_subxt.rs
index 12c1c58..a7fa077 100644
--- a/src/chain/quantus_subxt.rs
+++ b/src/chain/quantus_subxt.rs
@@ -1432,9 +1432,10 @@ pub mod api {
 						"query_call_info",
 						types::QueryCallInfo { call, len },
 						[
-							59u8, 44u8, 99u8, 66u8, 11u8, 94u8, 211u8, 6u8, 243u8, 46u8, 231u8,
-							78u8, 147u8, 43u8, 186u8, 119u8, 84u8, 5u8, 35u8, 154u8, 58u8, 175u8,
-							180u8, 197u8, 176u8, 160u8, 53u8, 155u8, 145u8, 42u8, 23u8, 102u8,
+							218u8, 133u8, 148u8, 251u8, 115u8, 98u8, 41u8, 181u8, 220u8, 98u8,
+							96u8, 5u8, 83u8, 71u8, 198u8, 107u8, 19u8, 120u8, 188u8, 127u8, 50u8,
+							201u8, 132u8, 73u8, 13u8, 118u8, 124u8, 200u8, 48u8, 103u8, 188u8,
+							219u8,
 						],
 					)
 				}
@@ -1452,9 +1453,10 @@ pub mod api {
 						"query_call_fee_details",
 						types::QueryCallFeeDetails { call, len },
 						[
-							135u8, 58u8, 229u8, 112u8, 19u8, 128u8, 2u8, 114u8, 132u8, 7u8, 98u8,
-							170u8, 14u8, 148u8, 33u8, 57u8, 125u8, 107u8, 77u8, 66u8, 33u8, 10u8,
-							65u8, 28u8, 121u8, 233u8, 63u8, 109u8, 189u8, 9u8, 189u8, 122u8,
+							222u8, 82u8, 209u8, 219u8, 210u8, 175u8, 13u8, 207u8, 13u8, 17u8,
+							234u8, 166u8, 55u8, 25u8, 194u8, 92u8, 155u8, 69u8, 143u8, 95u8, 18u8,
+							181u8, 70u8, 191u8, 64u8, 119u8, 244u8, 116u8, 44u8, 172u8, 251u8,
+							66u8,
 						],
 					)
 				}
@@ -1957,9 +1959,9 @@ pub mod api {
 			.hash();
 		runtime_metadata_hash ==
 			[
-				235u8, 241u8, 231u8, 143u8, 238u8, 18u8, 134u8, 210u8, 142u8, 123u8, 23u8, 238u8,
-				7u8, 116u8, 199u8, 36u8, 101u8, 103u8, 107u8, 11u8, 141u8, 9u8, 176u8, 251u8,
-				185u8, 220u8, 173u8, 198u8, 201u8, 167u8, 78u8, 188u8,
+				9u8, 252u8, 101u8, 200u8, 161u8, 142u8, 241u8, 130u8, 224u8, 189u8, 72u8, 23u8,
+				15u8, 35u8, 16u8, 38u8, 233u8, 12u8, 177u8, 101u8, 41u8, 225u8, 185u8, 137u8,
+				187u8, 153u8, 77u8, 45u8, 214u8, 214u8, 215u8, 198u8,
 			]
 	}
 	pub mod system {
@@ -3058,9 +3060,9 @@ pub mod api {
 						"Events",
 						(),
 						[
-							16u8, 202u8, 95u8, 126u8, 59u8, 71u8, 99u8, 58u8, 52u8, 83u8, 147u8,
-							29u8, 27u8, 91u8, 11u8, 178u8, 100u8, 183u8, 107u8, 163u8, 243u8,
-							160u8, 155u8, 46u8, 55u8, 109u8, 210u8, 242u8, 84u8, 21u8, 0u8, 174u8,
+							219u8, 135u8, 2u8, 5u8, 35u8, 85u8, 207u8, 98u8, 136u8, 150u8, 109u8,
+							92u8, 64u8, 218u8, 201u8, 111u8, 25u8, 157u8, 42u8, 17u8, 112u8, 204u8,
+							180u8, 241u8, 138u8, 97u8, 146u8, 96u8, 223u8, 24u8, 107u8, 44u8,
 						],
 					)
 				}
@@ -5541,9 +5543,10 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							224u8, 201u8, 98u8, 222u8, 12u8, 81u8, 22u8, 130u8, 86u8, 67u8, 141u8,
-							83u8, 170u8, 107u8, 209u8, 158u8, 237u8, 186u8, 132u8, 61u8, 103u8,
-							153u8, 37u8, 72u8, 59u8, 69u8, 181u8, 245u8, 166u8, 244u8, 62u8, 119u8,
+							92u8, 222u8, 38u8, 218u8, 220u8, 120u8, 153u8, 82u8, 131u8, 21u8,
+							133u8, 192u8, 226u8, 24u8, 106u8, 70u8, 156u8, 171u8, 231u8, 175u8,
+							152u8, 51u8, 118u8, 15u8, 247u8, 6u8, 137u8, 153u8, 189u8, 181u8,
+							207u8, 118u8,
 						],
 					)
 				}
@@ -5566,9 +5569,9 @@ pub mod api {
 							weight,
 						},
 						[
-							171u8, 105u8, 36u8, 170u8, 167u8, 130u8, 207u8, 182u8, 35u8, 146u8,
-							100u8, 18u8, 244u8, 232u8, 92u8, 75u8, 128u8, 183u8, 56u8, 211u8, 8u8,
-							44u8, 56u8, 99u8, 239u8, 143u8, 82u8, 65u8, 6u8, 26u8, 231u8, 216u8,
+							18u8, 172u8, 114u8, 181u8, 239u8, 91u8, 227u8, 49u8, 11u8, 101u8, 81u8,
+							116u8, 159u8, 79u8, 57u8, 170u8, 121u8, 12u8, 231u8, 89u8, 163u8, 95u8,
+							8u8, 144u8, 157u8, 93u8, 162u8, 156u8, 198u8, 99u8, 4u8, 123u8,
 						],
 					)
 				}
@@ -5606,10 +5609,10 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							225u8, 229u8, 154u8, 137u8, 127u8, 113u8, 207u8, 149u8, 115u8, 222u8,
-							119u8, 97u8, 241u8, 115u8, 146u8, 82u8, 97u8, 105u8, 97u8, 45u8, 237u8,
-							82u8, 84u8, 145u8, 235u8, 153u8, 200u8, 108u8, 222u8, 27u8, 149u8,
-							202u8,
+							25u8, 155u8, 115u8, 44u8, 183u8, 108u8, 169u8, 74u8, 11u8, 123u8,
+							235u8, 102u8, 23u8, 199u8, 181u8, 104u8, 66u8, 183u8, 147u8, 133u8,
+							155u8, 30u8, 203u8, 92u8, 78u8, 234u8, 38u8, 168u8, 178u8, 73u8, 108u8,
+							50u8,
 						],
 					)
 				}
@@ -5989,22 +5992,6 @@ pub mod api {
 						],
 					)
 				}
-				#[doc = " Fixed point scale for calculations (default: 10^18)"]
-				pub fn fixed_u128_scale(
-					&self,
-				) -> ::subxt::ext::subxt_core::constants::address::StaticAddress<
-					::core::primitive::u128,
-				> {
-					::subxt::ext::subxt_core::constants::address::StaticAddress::new_static(
-						"QPoW",
-						"FixedU128Scale",
-						[
-							84u8, 157u8, 140u8, 4u8, 93u8, 57u8, 29u8, 133u8, 105u8, 200u8, 214u8,
-							27u8, 144u8, 208u8, 218u8, 160u8, 130u8, 109u8, 101u8, 54u8, 210u8,
-							136u8, 71u8, 63u8, 49u8, 237u8, 234u8, 15u8, 178u8, 98u8, 148u8, 156u8,
-						],
-					)
-				}
 			}
 		}
 	}
@@ -6075,6 +6062,27 @@ pub mod api {
 				const PALLET: &'static str = "MiningRewards";
 				const EVENT: &'static str = "TreasuryRewarded";
 			}
+			#[derive(
+				:: subxt :: ext :: subxt_core :: ext :: scale_decode :: DecodeAsType,
+				:: subxt :: ext :: subxt_core :: ext :: scale_encode :: EncodeAsType,
+				Debug,
+			)]
+			#[decode_as_type(crate_path = ":: subxt :: ext :: subxt_core :: ext :: scale_decode")]
+			#[encode_as_type(crate_path = ":: subxt :: ext :: subxt_core :: ext :: scale_encode")]
+			#[doc = "Miner reward was redirected to treasury due to mint failure"]
+			pub struct MinerRewardRedirected {
+				pub miner: miner_reward_redirected::Miner,
+				pub reward: miner_reward_redirected::Reward,
+			}
+			pub mod miner_reward_redirected {
+				use super::runtime_types;
+				pub type Miner = ::subxt::ext::subxt_core::utils::AccountId32;
+				pub type Reward = ::core::primitive::u128;
+			}
+			impl ::subxt::ext::subxt_core::events::StaticEvent for MinerRewardRedirected {
+				const PALLET: &'static str = "MiningRewards";
+				const EVENT: &'static str = "MinerRewardRedirected";
+			}
 		}
 		pub mod storage {
 			use super::runtime_types;
@@ -7001,9 +7009,10 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							59u8, 142u8, 42u8, 82u8, 31u8, 67u8, 95u8, 118u8, 24u8, 55u8, 82u8,
-							141u8, 2u8, 133u8, 213u8, 81u8, 84u8, 109u8, 6u8, 239u8, 46u8, 233u8,
-							125u8, 136u8, 15u8, 160u8, 240u8, 29u8, 238u8, 12u8, 114u8, 8u8,
+							166u8, 18u8, 39u8, 234u8, 251u8, 53u8, 21u8, 117u8, 134u8, 194u8,
+							163u8, 196u8, 144u8, 31u8, 132u8, 122u8, 44u8, 9u8, 134u8, 32u8, 222u8,
+							32u8, 173u8, 128u8, 182u8, 71u8, 87u8, 253u8, 109u8, 104u8, 222u8,
+							32u8,
 						],
 					)
 				}
@@ -7042,9 +7051,10 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							132u8, 98u8, 238u8, 26u8, 202u8, 171u8, 103u8, 75u8, 126u8, 49u8, 0u8,
-							201u8, 239u8, 60u8, 78u8, 218u8, 23u8, 147u8, 136u8, 131u8, 252u8,
-							205u8, 89u8, 30u8, 83u8, 236u8, 25u8, 10u8, 149u8, 147u8, 56u8, 207u8,
+							232u8, 133u8, 188u8, 74u8, 36u8, 170u8, 171u8, 99u8, 255u8, 226u8,
+							174u8, 26u8, 109u8, 166u8, 144u8, 41u8, 219u8, 85u8, 170u8, 155u8,
+							192u8, 22u8, 176u8, 97u8, 47u8, 17u8, 44u8, 223u8, 100u8, 65u8, 69u8,
+							35u8,
 						],
 					)
 				}
@@ -7080,10 +7090,10 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							123u8, 115u8, 44u8, 60u8, 131u8, 182u8, 245u8, 36u8, 29u8, 238u8,
-							226u8, 239u8, 76u8, 4u8, 170u8, 52u8, 245u8, 83u8, 217u8, 193u8, 100u8,
-							58u8, 144u8, 126u8, 232u8, 17u8, 219u8, 120u8, 99u8, 154u8, 255u8,
-							116u8,
+							128u8, 96u8, 130u8, 203u8, 5u8, 21u8, 127u8, 65u8, 92u8, 180u8, 92u8,
+							228u8, 4u8, 71u8, 196u8, 224u8, 121u8, 194u8, 193u8, 213u8, 150u8,
+							149u8, 253u8, 188u8, 121u8, 221u8, 209u8, 133u8, 46u8, 247u8, 221u8,
+							29u8,
 						],
 					)
 				}
@@ -7105,9 +7115,10 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							51u8, 123u8, 141u8, 168u8, 243u8, 27u8, 146u8, 86u8, 92u8, 47u8, 205u8,
-							24u8, 66u8, 124u8, 97u8, 95u8, 171u8, 35u8, 140u8, 188u8, 58u8, 138u8,
-							115u8, 200u8, 132u8, 253u8, 68u8, 16u8, 112u8, 132u8, 121u8, 68u8,
+							143u8, 66u8, 101u8, 79u8, 129u8, 219u8, 60u8, 16u8, 207u8, 159u8,
+							231u8, 137u8, 114u8, 222u8, 149u8, 153u8, 60u8, 133u8, 35u8, 124u8,
+							175u8, 66u8, 67u8, 97u8, 172u8, 207u8, 100u8, 13u8, 27u8, 169u8, 51u8,
+							128u8,
 						],
 					)
 				}
@@ -8102,9 +8113,9 @@ pub mod api {
 						"batch",
 						types::Batch { calls },
 						[
-							175u8, 251u8, 47u8, 139u8, 42u8, 213u8, 29u8, 121u8, 126u8, 160u8,
-							31u8, 182u8, 18u8, 35u8, 57u8, 165u8, 44u8, 5u8, 237u8, 46u8, 120u8,
-							177u8, 32u8, 135u8, 177u8, 20u8, 87u8, 229u8, 64u8, 13u8, 127u8, 38u8,
+							12u8, 35u8, 169u8, 238u8, 108u8, 124u8, 242u8, 241u8, 158u8, 144u8,
+							55u8, 181u8, 164u8, 80u8, 109u8, 149u8, 149u8, 89u8, 202u8, 4u8, 65u8,
+							16u8, 217u8, 49u8, 232u8, 146u8, 244u8, 123u8, 48u8, 8u8, 45u8, 101u8,
 						],
 					)
 				}
@@ -8134,10 +8145,9 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							163u8, 103u8, 246u8, 62u8, 75u8, 102u8, 59u8, 101u8, 118u8, 106u8,
-							209u8, 68u8, 186u8, 60u8, 209u8, 233u8, 79u8, 179u8, 41u8, 65u8, 154u8,
-							146u8, 145u8, 126u8, 183u8, 212u8, 239u8, 26u8, 76u8, 22u8, 239u8,
-							228u8,
+							58u8, 129u8, 55u8, 87u8, 210u8, 249u8, 143u8, 136u8, 81u8, 237u8, 43u8,
+							136u8, 156u8, 85u8, 92u8, 204u8, 228u8, 131u8, 218u8, 62u8, 54u8, 87u8,
+							20u8, 248u8, 249u8, 118u8, 83u8, 233u8, 174u8, 3u8, 69u8, 110u8,
 						],
 					)
 				}
@@ -8163,9 +8173,9 @@ pub mod api {
 						"batch_all",
 						types::BatchAll { calls },
 						[
-							150u8, 227u8, 254u8, 10u8, 63u8, 29u8, 209u8, 16u8, 78u8, 200u8, 2u8,
-							1u8, 45u8, 134u8, 126u8, 227u8, 213u8, 206u8, 28u8, 11u8, 248u8, 34u8,
-							208u8, 245u8, 115u8, 226u8, 51u8, 80u8, 157u8, 19u8, 214u8, 7u8,
+							160u8, 132u8, 211u8, 158u8, 79u8, 68u8, 196u8, 4u8, 17u8, 136u8, 198u8,
+							11u8, 217u8, 69u8, 52u8, 19u8, 244u8, 95u8, 1u8, 43u8, 47u8, 107u8,
+							71u8, 70u8, 129u8, 180u8, 96u8, 162u8, 243u8, 62u8, 255u8, 246u8,
 						],
 					)
 				}
@@ -8188,10 +8198,9 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							26u8, 167u8, 149u8, 52u8, 49u8, 181u8, 232u8, 189u8, 214u8, 103u8,
-							142u8, 80u8, 167u8, 149u8, 40u8, 66u8, 198u8, 220u8, 22u8, 169u8,
-							249u8, 194u8, 40u8, 241u8, 212u8, 216u8, 96u8, 42u8, 107u8, 229u8,
-							250u8, 169u8,
+							70u8, 236u8, 128u8, 245u8, 79u8, 164u8, 36u8, 254u8, 249u8, 100u8,
+							132u8, 160u8, 42u8, 245u8, 92u8, 122u8, 66u8, 71u8, 16u8, 194u8, 241u8,
+							243u8, 146u8, 27u8, 3u8, 164u8, 155u8, 37u8, 133u8, 93u8, 15u8, 255u8,
 						],
 					)
 				}
@@ -8217,9 +8226,10 @@ pub mod api {
 						"force_batch",
 						types::ForceBatch { calls },
 						[
-							214u8, 226u8, 85u8, 46u8, 75u8, 28u8, 254u8, 199u8, 162u8, 194u8,
-							192u8, 69u8, 103u8, 7u8, 126u8, 201u8, 242u8, 24u8, 85u8, 53u8, 69u8,
-							91u8, 182u8, 111u8, 205u8, 250u8, 109u8, 233u8, 52u8, 9u8, 171u8, 11u8,
+							201u8, 43u8, 241u8, 144u8, 76u8, 120u8, 232u8, 97u8, 84u8, 126u8,
+							227u8, 232u8, 69u8, 158u8, 222u8, 176u8, 144u8, 160u8, 104u8, 207u8,
+							5u8, 106u8, 72u8, 119u8, 162u8, 214u8, 219u8, 131u8, 207u8, 153u8,
+							24u8, 247u8,
 						],
 					)
 				}
@@ -8242,9 +8252,9 @@ pub mod api {
 							weight,
 						},
 						[
-							164u8, 25u8, 18u8, 51u8, 125u8, 4u8, 172u8, 80u8, 236u8, 99u8, 192u8,
-							159u8, 103u8, 231u8, 83u8, 188u8, 32u8, 54u8, 11u8, 101u8, 127u8, 14u8,
-							78u8, 134u8, 146u8, 112u8, 165u8, 108u8, 89u8, 10u8, 184u8, 131u8,
+							241u8, 184u8, 146u8, 128u8, 160u8, 33u8, 170u8, 130u8, 105u8, 26u8,
+							128u8, 181u8, 154u8, 95u8, 76u8, 32u8, 133u8, 8u8, 115u8, 144u8, 198u8,
+							25u8, 84u8, 96u8, 155u8, 30u8, 249u8, 235u8, 223u8, 158u8, 37u8, 13u8,
 						],
 					)
 				}
@@ -8284,10 +8294,9 @@ pub mod api {
 							fallback: ::subxt::ext::subxt_core::alloc::boxed::Box::new(fallback),
 						},
 						[
-							92u8, 81u8, 197u8, 222u8, 38u8, 170u8, 79u8, 51u8, 116u8, 62u8, 50u8,
-							58u8, 150u8, 250u8, 110u8, 71u8, 27u8, 159u8, 43u8, 203u8, 156u8,
-							187u8, 239u8, 196u8, 87u8, 161u8, 156u8, 23u8, 146u8, 234u8, 101u8,
-							3u8,
+							199u8, 6u8, 145u8, 140u8, 251u8, 79u8, 237u8, 173u8, 162u8, 41u8, 31u8,
+							94u8, 225u8, 34u8, 245u8, 153u8, 233u8, 225u8, 87u8, 190u8, 233u8,
+							191u8, 3u8, 25u8, 216u8, 212u8, 30u8, 180u8, 168u8, 145u8, 54u8, 150u8,
 						],
 					)
 				}
@@ -8310,9 +8319,10 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							130u8, 99u8, 92u8, 78u8, 42u8, 115u8, 255u8, 247u8, 224u8, 97u8, 137u8,
-							129u8, 227u8, 245u8, 204u8, 255u8, 128u8, 231u8, 43u8, 191u8, 72u8,
-							241u8, 154u8, 16u8, 23u8, 72u8, 130u8, 40u8, 219u8, 191u8, 118u8, 34u8,
+							222u8, 246u8, 25u8, 101u8, 155u8, 44u8, 93u8, 141u8, 239u8, 33u8,
+							186u8, 124u8, 253u8, 4u8, 203u8, 161u8, 102u8, 220u8, 158u8, 48u8,
+							81u8, 82u8, 9u8, 99u8, 50u8, 26u8, 210u8, 64u8, 165u8, 102u8, 227u8,
+							84u8,
 						],
 					)
 				}
@@ -13715,7 +13725,7 @@ pub mod api {
 				#[encode_as_type(
 					crate_path = ":: subxt :: ext :: subxt_core :: ext :: scale_encode"
 				)]
-				#[doc = "Set the treasury account. Root only."]
+				#[doc = "Set the treasury account. Root only. Zero address is rejected (funds would be locked)."]
 				pub struct SetTreasuryAccount {
 					pub account: set_treasury_account::Account,
 				}
@@ -13738,13 +13748,13 @@ pub mod api {
 				#[encode_as_type(
 					crate_path = ":: subxt :: ext :: subxt_core :: ext :: scale_encode"
 				)]
-				#[doc = "Set the treasury portion (0-100). Root only."]
+				#[doc = "Set the treasury portion (Permill, 0–100%). Root only."]
 				pub struct SetTreasuryPortion {
 					pub portion: set_treasury_portion::Portion,
 				}
 				pub mod set_treasury_portion {
 					use super::runtime_types;
-					pub type Portion = ::core::primitive::u8;
+					pub type Portion = runtime_types::sp_arithmetic::per_things::Permill;
 				}
 				impl ::subxt::ext::subxt_core::blocks::StaticExtrinsic for SetTreasuryPortion {
 					const PALLET: &'static str = "TreasuryPallet";
@@ -13753,7 +13763,7 @@ pub mod api {
 			}
 			pub struct TransactionApi;
 			impl TransactionApi {
-				#[doc = "Set the treasury account. Root only."]
+				#[doc = "Set the treasury account. Root only. Zero address is rejected (funds would be locked)."]
 				pub fn set_treasury_account(
 					&self,
 					account: types::set_treasury_account::Account,
@@ -13771,7 +13781,7 @@ pub mod api {
 						],
 					)
 				}
-				#[doc = "Set the treasury portion (0-100). Root only."]
+				#[doc = "Set the treasury portion (Permill, 0–100%). Root only."]
 				pub fn set_treasury_portion(
 					&self,
 					portion: types::set_treasury_portion::Portion,
@@ -13782,10 +13792,9 @@ pub mod api {
 						"set_treasury_portion",
 						types::SetTreasuryPortion { portion },
 						[
-							233u8, 84u8, 25u8, 145u8, 26u8, 255u8, 146u8, 109u8, 128u8, 122u8,
-							219u8, 78u8, 209u8, 193u8, 255u8, 120u8, 59u8, 121u8, 225u8, 15u8,
-							12u8, 203u8, 250u8, 6u8, 174u8, 6u8, 221u8, 153u8, 35u8, 240u8, 88u8,
-							204u8,
+							226u8, 74u8, 96u8, 96u8, 120u8, 14u8, 29u8, 33u8, 85u8, 192u8, 26u8,
+							67u8, 86u8, 203u8, 21u8, 96u8, 127u8, 87u8, 217u8, 185u8, 8u8, 68u8,
+							126u8, 227u8, 38u8, 172u8, 9u8, 97u8, 172u8, 27u8, 17u8, 199u8,
 						],
 					)
 				}
@@ -13825,7 +13834,7 @@ pub mod api {
 			}
 			pub mod treasury_portion_updated {
 				use super::runtime_types;
-				pub type NewPortion = ::core::primitive::u8;
+				pub type NewPortion = runtime_types::sp_arithmetic::per_things::Permill;
 			}
 			impl ::subxt::ext::subxt_core::events::StaticEvent for TreasuryPortionUpdated {
 				const PALLET: &'static str = "TreasuryPallet";
@@ -13842,7 +13851,7 @@ pub mod api {
 				}
 				pub mod treasury_portion {
 					use super::runtime_types;
-					pub type TreasuryPortion = ::core::primitive::u8;
+					pub type TreasuryPortion = runtime_types::sp_arithmetic::per_things::Permill;
 				}
 			}
 			pub struct StorageApi;
@@ -13869,14 +13878,15 @@ pub mod api {
 						],
 					)
 				}
-				#[doc = " The portion of mining rewards that goes to treasury (0-100)."]
+				#[doc = " The portion of mining rewards that goes to treasury (Permill, 0–100%)."]
+				#[doc = " Uses OptionQuery so genesis is required. Permill allows fine granularity (e.g. 33.3%)."]
 				pub fn treasury_portion(
 					&self,
 				) -> ::subxt::ext::subxt_core::storage::address::StaticAddress<
 					(),
 					types::treasury_portion::TreasuryPortion,
 					::subxt::ext::subxt_core::utils::Yes,
-					::subxt::ext::subxt_core::utils::Yes,
+					(),
 					(),
 				> {
 					::subxt::ext::subxt_core::storage::address::StaticAddress::new_static(
@@ -13884,9 +13894,9 @@ pub mod api {
 						"TreasuryPortion",
 						(),
 						[
-							102u8, 185u8, 214u8, 4u8, 191u8, 165u8, 131u8, 24u8, 160u8, 179u8,
-							59u8, 196u8, 73u8, 169u8, 17u8, 104u8, 66u8, 3u8, 202u8, 255u8, 195u8,
-							96u8, 65u8, 22u8, 145u8, 163u8, 6u8, 44u8, 47u8, 11u8, 42u8, 0u8,
+							27u8, 148u8, 61u8, 76u8, 110u8, 174u8, 202u8, 184u8, 62u8, 134u8,
+							238u8, 169u8, 40u8, 112u8, 83u8, 192u8, 156u8, 67u8, 1u8, 145u8, 11u8,
+							88u8, 249u8, 1u8, 37u8, 163u8, 238u8, 131u8, 242u8, 232u8, 20u8, 195u8,
 						],
 					)
 				}
@@ -14302,10 +14312,9 @@ pub mod api {
 							call: ::subxt::ext::subxt_core::alloc::boxed::Box::new(call),
 						},
 						[
-							138u8, 215u8, 99u8, 222u8, 139u8, 128u8, 44u8, 8u8, 70u8, 23u8, 174u8,
-							102u8, 142u8, 62u8, 176u8, 98u8, 216u8, 41u8, 138u8, 101u8, 201u8,
-							105u8, 99u8, 164u8, 144u8, 176u8, 94u8, 228u8, 191u8, 175u8, 145u8,
-							30u8,
+							0u8, 144u8, 16u8, 19u8, 207u8, 225u8, 184u8, 92u8, 212u8, 17u8, 156u8,
+							224u8, 97u8, 23u8, 96u8, 104u8, 207u8, 206u8, 163u8, 26u8, 91u8, 130u8,
+							113u8, 130u8, 171u8, 57u8, 255u8, 83u8, 240u8, 20u8, 199u8, 126u8,
 						],
 					)
 				}
@@ -20535,14 +20544,9 @@ pub mod api {
 				}
 				pub mod transfer_proof {
 					use super::runtime_types;
-					pub type TransferProof = ();
-					pub type Param0 = (
-						::core::primitive::u32,
-						::core::primitive::u64,
-						::subxt::ext::subxt_core::utils::AccountId32,
-						::subxt::ext::subxt_core::utils::AccountId32,
-						::core::primitive::u128,
-					);
+					pub type TransferProof = [::core::primitive::u8; 32usize];
+					pub type Param0 =
+						(::subxt::ext::subxt_core::utils::AccountId32, ::core::primitive::u64);
 				}
 				pub mod transfer_count {
 					use super::runtime_types;
@@ -20597,7 +20601,15 @@ pub mod api {
 						],
 					)
 				}
-				#[doc = " Transfer proofs for wormhole transfers (both native and assets)"]
+				#[doc = " Transfer proofs for wormhole transfers (both native and assets)."]
+				#[doc = ""]
+				#[doc = " Storage key: Twox128(\"Wormhole\") || Twox128(\"TransferProof\") || Blake2_256(to,"]
+				#[doc = " transfer_count) Storage value: leaf_inputs_hash (Poseidon2 hash of full transfer data)"]
+				#[doc = ""]
+				#[doc = " The key uses only (to, transfer_count) since transfer_count is atomic per recipient."]
+				#[doc = " The ZK circuit verifies that the leaf_inputs_hash in the value section matches"]
+				#[doc = " the Poseidon2 hash of all transfer details (asset_id, count, from, to, amount),"]
+				#[doc = " providing full 256-bit security."]
 				pub fn transfer_proof_iter(
 					&self,
 				) -> ::subxt::ext::subxt_core::storage::address::StaticAddress<
@@ -20612,21 +20624,27 @@ pub mod api {
 						"TransferProof",
 						(),
 						[
-							114u8, 93u8, 194u8, 180u8, 234u8, 160u8, 121u8, 23u8, 201u8, 229u8,
-							249u8, 211u8, 159u8, 188u8, 127u8, 128u8, 32u8, 26u8, 209u8, 25u8,
-							176u8, 192u8, 221u8, 140u8, 46u8, 26u8, 47u8, 223u8, 210u8, 37u8,
-							137u8, 90u8,
+							68u8, 118u8, 236u8, 167u8, 234u8, 90u8, 185u8, 84u8, 179u8, 175u8,
+							199u8, 180u8, 115u8, 225u8, 146u8, 36u8, 107u8, 176u8, 147u8, 154u8,
+							35u8, 40u8, 9u8, 62u8, 253u8, 6u8, 37u8, 253u8, 83u8, 206u8, 187u8,
+							55u8,
 						],
 					)
 				}
-				#[doc = " Transfer proofs for wormhole transfers (both native and assets)"]
+				#[doc = " Transfer proofs for wormhole transfers (both native and assets)."]
+				#[doc = ""]
+				#[doc = " Storage key: Twox128(\"Wormhole\") || Twox128(\"TransferProof\") || Blake2_256(to,"]
+				#[doc = " transfer_count) Storage value: leaf_inputs_hash (Poseidon2 hash of full transfer data)"]
+				#[doc = ""]
+				#[doc = " The key uses only (to, transfer_count) since transfer_count is atomic per recipient."]
+				#[doc = " The ZK circuit verifies that the leaf_inputs_hash in the value section matches"]
+				#[doc = " the Poseidon2 hash of all transfer details (asset_id, count, from, to, amount),"]
+				#[doc = " providing full 256-bit security."]
 				pub fn transfer_proof(
 					&self,
 					_0: types::transfer_proof::Param0,
 				) -> ::subxt::ext::subxt_core::storage::address::StaticAddress<
-					::subxt::ext::subxt_core::storage::address::StaticStorageKey<
-						types::transfer_proof::Param0,
-					>,
+					(),
 					types::transfer_proof::TransferProof,
 					::subxt::ext::subxt_core::utils::Yes,
 					(),
@@ -20635,12 +20653,12 @@ pub mod api {
 					::subxt::ext::subxt_core::storage::address::StaticAddress::new_static(
 						"Wormhole",
 						"TransferProof",
-						::subxt::ext::subxt_core::storage::address::StaticStorageKey::new(_0),
+						(),
 						[
-							114u8, 93u8, 194u8, 180u8, 234u8, 160u8, 121u8, 23u8, 201u8, 229u8,
-							249u8, 211u8, 159u8, 188u8, 127u8, 128u8, 32u8, 26u8, 209u8, 25u8,
-							176u8, 192u8, 221u8, 140u8, 46u8, 26u8, 47u8, 223u8, 210u8, 37u8,
-							137u8, 90u8,
+							68u8, 118u8, 236u8, 167u8, 234u8, 90u8, 185u8, 84u8, 179u8, 175u8,
+							199u8, 180u8, 115u8, 225u8, 146u8, 36u8, 107u8, 176u8, 147u8, 154u8,
+							35u8, 40u8, 9u8, 62u8, 253u8, 6u8, 37u8, 253u8, 83u8, 206u8, 187u8,
+							55u8,
 						],
 					)
 				}
@@ -23762,6 +23780,12 @@ pub mod api {
 					#[codec(index = 2)]
 					#[doc = "Rewards were sent to Treasury when no miner was specified"]
 					TreasuryRewarded { reward: ::core::primitive::u128 },
+					#[codec(index = 3)]
+					#[doc = "Miner reward was redirected to treasury due to mint failure"]
+					MinerRewardRedirected {
+						miner: ::subxt::ext::subxt_core::utils::AccountId32,
+						reward: ::core::primitive::u128,
+					},
 				}
 			}
 		}
@@ -26427,11 +26451,13 @@ pub mod api {
 				#[doc = "Contains a variant per dispatchable extrinsic that this pallet has."]
 				pub enum Call {
 					#[codec(index = 0)]
-					#[doc = "Set the treasury account. Root only."]
+					#[doc = "Set the treasury account. Root only. Zero address is rejected (funds would be locked)."]
 					set_treasury_account { account: ::subxt::ext::subxt_core::utils::AccountId32 },
 					#[codec(index = 1)]
-					#[doc = "Set the treasury portion (0-100). Root only."]
-					set_treasury_portion { portion: ::core::primitive::u8 },
+					#[doc = "Set the treasury portion (Permill, 0–100%). Root only."]
+					set_treasury_portion {
+						portion: runtime_types::sp_arithmetic::per_things::Permill,
+					},
 				}
 				#[derive(
 					:: subxt :: ext :: subxt_core :: ext :: scale_decode :: DecodeAsType,
@@ -26448,6 +26474,9 @@ pub mod api {
 				pub enum Error {
 					#[codec(index = 0)]
 					InvalidPortion,
+					#[codec(index = 1)]
+					#[doc = "Treasury account cannot be zero address (funds would be permanently locked)."]
+					InvalidTreasuryAccount,
 				}
 				#[derive(
 					:: subxt :: ext :: subxt_core :: ext :: scale_decode :: DecodeAsType,
@@ -26467,7 +26496,9 @@ pub mod api {
 						new_account: ::subxt::ext::subxt_core::utils::AccountId32,
 					},
 					#[codec(index = 1)]
-					TreasuryPortionUpdated { new_portion: ::core::primitive::u8 },
+					TreasuryPortionUpdated {
+						new_portion: runtime_types::sp_arithmetic::per_things::Permill,
+					},
 				}
 			}
 		}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 2d3e436..4a13545 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -287,25 +287,6 @@ pub enum Commands {
 pub enum DeveloperCommands {
 	/// Create standard test wallets (crystal_alice, crystal_bob, crystal_charlie)
 	CreateTestWallets,
-
-	/// Build wormhole circuit binaries and copy to CLI and chain directories
-	BuildCircuits {
-		/// Path to qp-zk-circuits repository (default: ../qp-zk-circuits)
-		#[arg(long, default_value = "../qp-zk-circuits")]
-		circuits_path: String,
-
-		/// Path to chain repository (default: ../chain)
-		#[arg(long, default_value = "../chain")]
-		chain_path: String,
-
-		/// Number of leaf proofs aggregated into a single proof
-		#[arg(long)]
-		num_leaf_proofs: usize,
-
-		/// Skip copying to chain directory
-		#[arg(long)]
-		skip_chain: bool,
-	},
 }
 
 /// Execute a CLI command
@@ -565,159 +546,7 @@ pub async fn handle_developer_command(command: DeveloperCommands) -> crate::erro
 
 			Ok(())
 		},
-		DeveloperCommands::BuildCircuits {
-			circuits_path,
-			chain_path,
-			num_leaf_proofs,
-			skip_chain,
-		} => build_wormhole_circuits(&circuits_path, &chain_path, num_leaf_proofs, skip_chain).await,
-	}
-}
-
-/// Build wormhole circuit binaries and copy them to the appropriate locations
-async fn build_wormhole_circuits(
-	circuits_path: &str,
-	chain_path: &str,
-	num_leaf_proofs: usize,
-	skip_chain: bool,
-) -> crate::error::Result<()> {
-	use std::{path::Path, process::Command};
-
-	log_print!("Building ZK circuit binaries (num_leaf_proofs={})", num_leaf_proofs);
-	log_print!("");
-
-	let circuits_dir = Path::new(circuits_path);
-	let chain_dir = Path::new(chain_path);
-
-	// Verify circuits directory exists
-	if !circuits_dir.exists() {
-		return Err(crate::error::QuantusError::Generic(format!(
-			"Circuits directory not found: {}",
-			circuits_path
-		)));
-	}
-
-	// Step 1: Build the circuit builder
-	log_print!("Step 1/4: Building circuit builder...");
-	let build_output = Command::new("cargo")
-		.args(["build", "--release", "-p", "qp-wormhole-circuit-builder"])
-		.current_dir(circuits_dir)
-		.output()
-		.map_err(|e| {
-			crate::error::QuantusError::Generic(format!("Failed to run cargo build: {}", e))
-		})?;
-
-	if !build_output.status.success() {
-		let stderr = String::from_utf8_lossy(&build_output.stderr);
-		return Err(crate::error::QuantusError::Generic(format!(
-			"Circuit builder compilation failed:\n{}",
-			stderr
-		)));
 	}
-	log_success!("   Done");
-
-	// Step 2: Run the circuit builder to generate binaries
-	log_print!("Step 2/4: Generating circuit binaries (this may take a while)...");
-	let builder_path = circuits_dir.join("target/release/qp-wormhole-circuit-builder");
-	let run_output = Command::new(&builder_path)
-		.args(["--num-leaf-proofs", &num_leaf_proofs.to_string()])
-		.current_dir(circuits_dir)
-		.output()
-		.map_err(|e| {
-			crate::error::QuantusError::Generic(format!("Failed to run circuit builder: {}", e))
-		})?;
-
-	if !run_output.status.success() {
-		let stderr = String::from_utf8_lossy(&run_output.stderr);
-		return Err(crate::error::QuantusError::Generic(format!(
-			"Circuit builder failed:\n{}",
-			stderr
-		)));
-	}
-	log_success!("   Done");
-
-	// Step 3: Copy binaries to CLI and touch aggregator to force recompile
-	log_print!("Step 3/4: Copying binaries to CLI...");
-	let source_bins = circuits_dir.join("generated-bins");
-	let cli_bins = Path::new("generated-bins");
-
-	let cli_bin_files = [
-		"common.bin",
-		"verifier.bin",
-		"prover.bin",
-		"dummy_proof.bin",
-		"aggregated_common.bin",
-		"aggregated_verifier.bin",
-		"config.json",
-	];
-
-	for file in &cli_bin_files {
-		let src = source_bins.join(file);
-		let dst = cli_bins.join(file);
-		std::fs::copy(&src, &dst).map_err(|e| {
-			crate::error::QuantusError::Generic(format!("Failed to copy {} to CLI: {}", file, e))
-		})?;
-		log_verbose!("   Copied {}", file);
-	}
-
-	// Touch aggregator lib.rs to force cargo to recompile it
-	let aggregator_lib = circuits_dir.join("wormhole/aggregator/src/lib.rs");
-	if aggregator_lib.exists() {
-		if let Ok(file) = std::fs::OpenOptions::new().write(true).open(&aggregator_lib) {
-			let _ = file.set_modified(std::time::SystemTime::now());
-		}
-	}
-	log_success!("   Done");
-
-	// Step 4: Copy binaries to chain directory (if not skipped)
-	if !skip_chain {
-		log_print!("Step 4/4: Copying binaries to chain...");
-
-		if !chain_dir.exists() {
-			log_error!("   Chain directory not found: {}", chain_path);
-			log_print!("   Use --skip-chain to skip this step");
-		} else {
-			let chain_bins = chain_dir.join("pallets/wormhole");
-
-			let chain_bin_files =
-				["aggregated_common.bin", "aggregated_verifier.bin", "config.json"];
-
-			for file in &chain_bin_files {
-				let src = source_bins.join(file);
-				let dst = chain_bins.join(file);
-				std::fs::copy(&src, &dst).map_err(|e| {
-					crate::error::QuantusError::Generic(format!(
-						"Failed to copy {} to chain: {}",
-						file, e
-					))
-				})?;
-				log_verbose!("   Copied {}", file);
-			}
-
-			// Touch pallet lib.rs to force cargo to recompile it
-			let pallet_lib = chain_bins.join("src/lib.rs");
-			if pallet_lib.exists() {
-				if let Ok(file) = std::fs::OpenOptions::new().write(true).open(&pallet_lib) {
-					let _ = file.set_modified(std::time::SystemTime::now());
-				}
-			}
-			log_success!("   Done");
-		}
-	} else {
-		log_print!("Step 4/4: Skipping chain copy (--skip-chain)");
-	}
-
-	log_print!("");
-	log_success!("Circuit build complete!");
-	log_print!("");
-	if !skip_chain {
-		log_print!("{}", "Next steps:".bright_blue().bold());
-		log_print!("  1. Rebuild chain: cd {} && cargo build --release", chain_path);
-		log_print!("  2. Restart the chain node");
-		log_print!("");
-	}
-
-	Ok(())
 }
 
 /// Handle compatibility check command
diff --git a/src/cli/treasury.rs b/src/cli/treasury.rs
index 99b66b6..638162d 100644
--- a/src/cli/treasury.rs
+++ b/src/cli/treasury.rs
@@ -42,9 +42,9 @@ async fn show_treasury_info(
 		crate::error::QuantusError::Generic("Treasury account not set in storage".to_string())
 	})?;
 
-	// Portion of mining rewards that goes to treasury (0–100)
+	// Portion of mining rewards that goes to treasury (Permill: parts per million)
 	let portion_addr = quantus_subxt::api::storage().treasury_pallet().treasury_portion();
-	let portion = storage_at.fetch(&portion_addr).await?.unwrap_or(0);
+	let portion = storage_at.fetch(&portion_addr).await?.map(|p| p.0).unwrap_or(0);
 
 	// Account balance
 	let account_storage = quantus_subxt::api::storage().system().account(treasury_account.clone());
@@ -61,7 +61,9 @@ async fn show_treasury_info(
 
 	let account_ss58 = treasury_account.to_quantus_ss58();
 	log_print!("📍 Account: {}", account_ss58.bright_yellow());
-	log_print!("📊 Reward portion: {}%", portion.to_string().bright_cyan());
+	// Permill is parts per million, so divide by 10000 to get percentage
+	let portion_percent = portion as f64 / 10000.0;
+	log_print!("📊 Reward portion: {:.2}%", portion_percent.to_string().bright_cyan());
 	log_print!("💰 Free: {}", formatted_free);
 	log_print!("💰 Reserved: {}", formatted_reserved);
 
diff --git a/src/cli/wormhole.rs b/src/cli/wormhole.rs
index c6b09d4..b3d96a9 100644
--- a/src/cli/wormhole.rs
+++ b/src/cli/wormhole.rs
@@ -17,6 +17,10 @@ use qp_rusty_crystals_hdwallet::{
 	derive_wormhole_from_mnemonic, generate_mnemonic, SensitiveBytes32, WormholePair,
 	QUANTUS_WORMHOLE_CHAIN_ID,
 };
+use qp_wormhole_aggregator::{
+	aggregator::{AggregationBackend, CircuitType},
+	config::CircuitBinsConfig,
+};
 use qp_wormhole_circuit::{
 	inputs::{CircuitInputs, ParseAggregatedPublicInputs, PrivateCircuitInputs},
 	nullifier::Nullifier,
@@ -42,110 +46,11 @@ use subxt::{
 	OnlineClient,
 };
 
-/// Native asset id
-pub const NATIVE_ASSET_ID: u32 = 0;
-
-/// Scale down factor for quantizing amounts (10^10 to go from 12 to 2 decimal places)
-pub const SCALE_DOWN_FACTOR: u128 = 10_000_000_000;
-
-/// Volume fee rate in basis points (10 bps = 0.1%)
-/// This must match the on-chain VolumeFeeRateBps configuration
-pub const VOLUME_FEE_BPS: u32 = 10;
-
-/// SHA256 hashes of circuit binary files for integrity verification.
-/// Must match the BinaryHashes struct in qp-wormhole-aggregator/src/config.rs
-#[derive(Debug, Clone, serde::Deserialize, Default)]
-pub struct BinaryHashes {
-	pub prover: Option<String>,
-	pub aggregated_common: Option<String>,
-	pub aggregated_verifier: Option<String>,
-	pub dummy_proof: Option<String>,
-}
-
-/// Aggregation config loaded from generated-bins/config.json.
-/// Must match the CircuitBinsConfig struct in qp-wormhole-aggregator/src/config.rs
-#[derive(Debug, Clone, serde::Deserialize)]
-pub struct AggregationConfig {
-	pub num_leaf_proofs: usize,
-	#[serde(default)]
-	pub hashes: Option<BinaryHashes>,
-}
-
-impl AggregationConfig {
-	/// Load config from the generated-bins directory
-	pub fn load_from_bins() -> crate::error::Result<Self> {
-		let config_path = Path::new("generated-bins/config.json");
-		let config_str = std::fs::read_to_string(config_path).map_err(|e| {
-			crate::error::QuantusError::Generic(format!(
-				"Failed to read aggregation config from {}: {}. Run 'quantus developer build-circuits' first.",
-				config_path.display(),
-				e
-			))
-		})?;
-		serde_json::from_str(&config_str).map_err(|e| {
-			crate::error::QuantusError::Generic(format!(
-				"Failed to parse aggregation config: {}",
-				e
-			))
-		})
-	}
-
-	/// Verify that the binary files in generated-bins match the stored hashes.
-	pub fn verify_binary_hashes(&self) -> crate::error::Result<()> {
-		use sha2::{Digest, Sha256};
-
-		let Some(ref stored_hashes) = self.hashes else {
-			log_verbose!("   No hashes in config.json, skipping binary verification");
-			return Ok(());
-		};
-
-		let bins_dir = Path::new("generated-bins");
-		let mut mismatches = Vec::new();
-
-		let hash_file = |filename: &str| -> Option<String> {
-			let path = bins_dir.join(filename);
-			std::fs::read(&path).ok().map(|bytes| {
-				let hash = Sha256::digest(&bytes);
-				hex::encode(hash)
-			})
-		};
-
-		let checks = [
-			("aggregated_common.bin", &stored_hashes.aggregated_common),
-			("aggregated_verifier.bin", &stored_hashes.aggregated_verifier),
-			("prover.bin", &stored_hashes.prover),
-			("dummy_proof.bin", &stored_hashes.dummy_proof),
-		];
-		for (filename, expected_hash) in checks {
-			if let Some(ref expected) = expected_hash {
-				if let Some(actual) = hash_file(filename) {
-					if expected != &actual {
-						mismatches.push(format!("{}...", filename));
-					}
-				}
-			}
-		}
-
-		if mismatches.is_empty() {
-			log_verbose!("   Binary hashes verified successfully");
-			Ok(())
-		} else {
-			Err(crate::error::QuantusError::Generic(format!(
-				"Binary hash mismatch detected! The circuit binaries do not match config.json.\n\
-				 This can happen if binaries were regenerated but the CLI wasn't rebuilt.\n\
-				 Mismatches:\n  {}\n\n\
-				 To fix: Run 'quantus developer build-circuits' and then 'cargo build --release'",
-				mismatches.join("\n  ")
-			)))
-		}
-	}
-}
-
-/// Compute output amount after fee deduction
-/// output = input * (10000 - fee_bps) / 10000
-pub fn compute_output_amount(input_amount: u32, fee_bps: u32) -> u32 {
-	((input_amount as u64) * (10000 - fee_bps as u64) / 10000) as u32
-}
+// Re-export constants and functions from wormhole_lib module for backward compatibility
+use crate::wormhole_lib;
+pub use crate::wormhole_lib::{
+	compute_output_amount, NATIVE_ASSET_ID, SCALE_DOWN_FACTOR, VOLUME_FEE_BPS,
+};
 
 /// Parse a hex-encoded secret string into a 32-byte array
 pub fn parse_secret_hex(secret_hex: &str) -> Result<[u8; 32], String> {
@@ -183,11 +88,7 @@ pub fn parse_exit_account(exit_account_str: &str) -> Result<[u8; 32], String> {
 /// Quantize a funding amount from 12 decimal places to 2 decimal places
 /// Returns an error if the quantized value doesn't fit in u32
 pub fn quantize_funding_amount(amount: u128) -> Result<u32, String> {
-	let quantized = amount / SCALE_DOWN_FACTOR;
-
-	quantized
-		.try_into()
-		.map_err(|_| format!("Funding amount {} too large after quantization", quantized))
+	wormhole_lib::quantize_amount(amount).map_err(|e| e.message)
 }
 
 /// Read and decode a hex-encoded proof file
@@ -691,6 +592,24 @@ pub enum WormholeCommands {
 		#[arg(short, long, default_value = "/tmp/wormhole_dissolve")]
 		output_dir: String,
 	},
+	/// Fuzz test the leaf verification by attempting invalid proofs
+	Fuzz {
+		/// Wallet name to use for funding
+		#[arg(short, long)]
+		wallet: String,
+
+		/// Password for the wallet
+		#[arg(short, long)]
+		password: Option<String>,
+
+		/// Read password from file
+		#[arg(long)]
+		password_file: Option<String>,
+
+		/// Amount in DEV to use for the test transfer (default: 1.0)
+		#[arg(short, long, default_value = "1.0")]
+		amount: f64,
+	},
 }
 
 pub async fn handle_wormhole_command(
@@ -805,10 +724,22 @@ pub async fn handle_wormhole_command(
 			)
 			.await
 		},
+		WormholeCommands::Fuzz { wallet, password, password_file, amount } => {
+			let amount_planck = (amount * 1_000_000_000_000.0) as u128;
+			let amount_aligned = (amount_planck / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR;
+			run_fuzz_test(wallet, password, password_file, amount_aligned, node_url).await
+		},
 	}
 }
 
-pub type TransferProofKey = (u32, u64, AccountId32, AccountId32, u128);
+/// Key for TransferProof storage - uniquely identifies a transfer.
+/// Uses (to, transfer_count) since transfer_count is atomic per recipient.
+/// This is hashed with Blake2_256 to form the storage key suffix.
+pub type TransferProofKey = (AccountId32, u64);
+
+/// Full transfer data including amount - used to compute the leaf_inputs_hash via Poseidon2.
+/// This is what the ZK circuit verifies.
+pub type TransferProofData = (u32, u64, AccountId32, AccountId32, u128);
 
 /// Derive and display the unspendable wormhole address from a secret.
 /// Users can then send funds to this address using `quantus send`.
@@ -865,11 +796,12 @@ async fn aggregate_proofs(
 
 	// Load config first to validate and calculate padding needs
 	let bins_dir = Path::new("generated-bins");
-	let agg_config = AggregationConfig::load_from_bins()?;
-
-	// Verify binary hashes match config.json to detect stale binaries
-	log_verbose!("Verifying circuit binary integrity...");
-	agg_config.verify_binary_hashes()?;
+	let agg_config = CircuitBinsConfig::load(bins_dir).map_err(|e| {
+		crate::error::QuantusError::Generic(format!(
+			"Failed to load circuit bins config from {:?}: {}",
+			bins_dir, e
+		))
+	})?;
 
 	// Validate number of proofs before doing expensive work
 	if proof_files.len() > agg_config.num_leaf_proofs {
@@ -1341,7 +1273,7 @@ fn load_multiround_wallet(
 fn print_multiround_config(
 	config: &MultiroundConfig,
 	wallet: &MultiroundWalletContext,
-	agg_config: &AggregationConfig,
+	num_leaf_proofs: usize,
 ) {
 	use colored::Colorize;
 
@@ -1356,7 +1288,7 @@ fn print_multiround_config(
 	);
 	log_print!("  Proofs per round: {}", config.num_proofs);
 	log_print!("  Rounds: {}", config.rounds);
-	log_print!("  Aggregation: num_leaf_proofs={}", agg_config.num_leaf_proofs);
+	log_print!("  Aggregation: num_leaf_proofs={}", num_leaf_proofs);
 	log_print!("  Output directory: {}", config.output_dir);
 	log_print!("  Keep files: {}", config.keep_files);
 	log_print!("");
@@ -1703,7 +1635,10 @@ async fn run_multiround(
 	log_print!("");
 
 	// Load aggregation config from generated-bins/config.json
-	let agg_config = AggregationConfig::load_from_bins()?;
+	let bins_dir = Path::new("generated-bins");
+	let agg_config = CircuitBinsConfig::load(bins_dir).map_err(|e| {
+		crate::error::QuantusError::Generic(format!("Failed to load aggregation config: {}", e))
+	})?;
 
 	// Validate parameters
 	validate_multiround_params(num_proofs, rounds, agg_config.num_leaf_proofs)?;
@@ -1716,7 +1651,7 @@ async fn run_multiround(
 		MultiroundConfig { num_proofs, rounds, amount, output_dir: output_dir.clone(), keep_files };
 
 	// Print configuration
-	print_multiround_config(&config, &wallet, &agg_config);
+	print_multiround_config(&config, &wallet, agg_config.num_leaf_proofs);
 	log_print!("  Dry run: {}", dry_run);
 	log_print!("");
 
@@ -1914,6 +1849,9 @@ async fn run_multiround(
 }
 
 /// Generate a wormhole proof with dual outputs (used for random partitioning in multiround)
+///
+/// This function fetches the necessary data from the chain and delegates to
+/// `wormhole_lib::generate_proof` for the actual proof generation.
 async fn generate_proof(
 	secret_hex: &str,
 	funding_amount: u128,
@@ -1924,44 +1862,28 @@ async fn generate_proof(
 	output_file: &str,
 	quantus_client: &QuantusClient,
 ) -> crate::error::Result<()> {
-	// Parse secret
-	let secret_array = parse_secret_hex(secret_hex).map_err(crate::error::QuantusError::Generic)?;
-	let secret: BytesDigest = secret_array.try_into().map_err(|e| {
-		crate::error::QuantusError::Generic(format!("Failed to convert secret: {:?}", e))
-	})?;
-
-	// Parse funding account
+	// Parse inputs
+	let secret = parse_secret_hex(secret_hex).map_err(crate::error::QuantusError::Generic)?;
 	let funding_account_bytes =
 		parse_exit_account(funding_account_str).map_err(crate::error::QuantusError::Generic)?;
-	let funding_account = AccountId32::new(funding_account_bytes);
 
-	// Parse block hash
-	let hash_bytes = hex::decode(block_hash_str.trim_start_matches("0x"))
-		.map_err(|e| crate::error::QuantusError::Generic(format!("Invalid block hash: {}", e)))?;
-	if hash_bytes.len() != 32 {
-		return Err(crate::error::QuantusError::Generic(format!(
-			"Block hash must be 32 bytes, got {}",
-			hash_bytes.len()
-		)));
-	}
-	let hash: [u8; 32] = hash_bytes.try_into().unwrap();
-	let block_hash = subxt::utils::H256::from(hash);
+	let block_hash_bytes: [u8; 32] = hex::decode(block_hash_str.trim_start_matches("0x"))
+		.map_err(|e| crate::error::QuantusError::Generic(format!("Invalid block hash: {}", e)))?
+		.try_into()
+		.map_err(|_| {
+			crate::error::QuantusError::Generic("Block hash must be 32 bytes".to_string())
+		})?;
 
-	let client = quantus_client.client();
+	// Compute wormhole address using wormhole_lib
+	let wormhole_address = wormhole_lib::compute_wormhole_address(&secret)
+		.map_err(|e| crate::error::QuantusError::Generic(e.message))?;
 
-	// Generate unspendable account from secret
-	let unspendable_account =
-		qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret)
-			.account_id;
-	let unspendable_account_bytes_digest =
-		qp_zk_circuits_common::utils::digest_to_bytes(unspendable_account);
-	let unspendable_account_bytes: [u8; 32] = unspendable_account_bytes_digest
-		.as_ref()
-		.try_into()
-		.expect("BytesDigest is always 32 bytes");
+	// Compute storage key using wormhole_lib
+	let storage_key = wormhole_lib::compute_storage_key(&wormhole_address, transfer_count);
 
-	let from_account = funding_account.clone();
-	let to_account = AccountId32::new(unspendable_account_bytes);
+	// Fetch data from chain
+	let block_hash = subxt::utils::H256::from(block_hash_bytes);
+	let client = quantus_client.client();
 
 	// Get block
 	let blocks =
@@ -1969,33 +1891,10 @@ async fn generate_proof(
 			crate::error::QuantusError::Generic(format!("Failed to get block: {}", e))
 		})?;
 
-	// Build storage key
-	let leaf_hash = qp_poseidon::PoseidonHasher::hash_storage::<TransferProofKey>(
-		&(
-			NATIVE_ASSET_ID,
-			transfer_count,
-			from_account.clone(),
-			to_account.clone(),
-			funding_amount,
-		)
-			.encode(),
-	);
-
-	let proof_address = quantus_node::api::storage().wormhole().transfer_proof((
-		NATIVE_ASSET_ID,
-		transfer_count,
-		SubxtAccountId(from_account.clone().into()),
-		SubxtAccountId(to_account.clone().into()),
-		funding_amount,
-	));
-
-	let mut final_key = proof_address.to_root_bytes();
-	final_key.extend_from_slice(&leaf_hash);
-
 	// Verify storage key exists
 	let storage_api = client.storage().at(block_hash);
 	let val = storage_api
-		.fetch_raw(final_key.clone())
+		.fetch_raw(storage_key.clone())
 		.await
 		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
 	if val.is_none() {
@@ -2004,85 +1903,58 @@ async fn generate_proof(
 		));
 	}
 
-	// Get storage proof
-	let proof_params = rpc_params![vec![to_hex(&final_key)], block_hash];
+	// Get storage proof from chain
+	let proof_params = rpc_params![vec![to_hex(&storage_key)], block_hash];
 	let read_proof: ReadProof<sp_core::H256> = quantus_client
 		.rpc_client()
 		.request("state_getReadProof", proof_params)
 		.await
 		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
 
+	// Extract header data
 	let header = blocks.header();
-	let state_root = BytesDigest::try_from(header.state_root.as_bytes())
-		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
-	let parent_hash = BytesDigest::try_from(header.parent_hash.as_bytes())
-		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
-	let extrinsics_root = BytesDigest::try_from(header.extrinsics_root.as_bytes())
-		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
-	let digest =
-		header.digest.encode().try_into().map_err(|_| {
-			crate::error::QuantusError::Generic("Failed to encode digest".to_string())
-		})?;
+	let parent_hash: [u8; 32] = header.parent_hash.0;
+	let state_root: [u8; 32] = header.state_root.0;
+	let extrinsics_root: [u8; 32] = header.extrinsics_root.0;
+	let digest = header.digest.encode();
 	let block_number = header.number;
 
-	// Prepare storage proof
-	let processed_storage_proof = prepare_proof_for_circuit(
-		read_proof.proof.iter().map(|proof| proof.0.clone()).collect(),
-		hex::encode(header.state_root.0),
-		leaf_hash,
-	)
-	.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
-
-	// Quantize input amount
-	let input_amount_quantized: u32 =
-		quantize_funding_amount(funding_amount).map_err(crate::error::QuantusError::Generic)?;
+	// Convert proof nodes
+	let proof_nodes: Vec<Vec<u8>> = read_proof.proof.iter().map(|p| p.0.clone()).collect();
 
-	// Use the output assignment directly (already quantized)
-	let inputs = CircuitInputs {
-		private: PrivateCircuitInputs {
-			secret,
-			transfer_count,
-			funding_account: BytesDigest::try_from(funding_account.as_ref() as &[u8])
-				.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?,
-			storage_proof: processed_storage_proof,
-			unspendable_account: unspendable_account_bytes_digest,
-			parent_hash,
-			state_root,
-			extrinsics_root,
-			digest,
-			input_amount: input_amount_quantized,
-		},
-		public: PublicCircuitInputs {
-			output_amount_1: output_assignment.output_amount_1,
-			output_amount_2: output_assignment.output_amount_2,
-			volume_fee_bps: VOLUME_FEE_BPS,
-			nullifier: digest_to_bytes(Nullifier::from_preimage(secret, transfer_count).hash),
-			exit_account_1: BytesDigest::try_from(output_assignment.exit_account_1.as_ref())
-				.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?,
-			exit_account_2: BytesDigest::try_from(output_assignment.exit_account_2.as_ref())
-				.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?,
-			block_hash: BytesDigest::try_from(block_hash.as_ref())
-				.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?,
-			block_number,
-			asset_id: NATIVE_ASSET_ID,
-		},
+	// Build ProofGenerationInput using wormhole_lib types
+	let input = wormhole_lib::ProofGenerationInput {
+		secret,
+		transfer_count,
+		funding_account: funding_account_bytes,
+		wormhole_address,
+		funding_amount,
+		block_hash: block_hash_bytes,
+		block_number,
+		parent_hash,
+		state_root,
+		extrinsics_root,
+		digest,
+		proof_nodes,
+		exit_account_1: output_assignment.exit_account_1,
+		exit_account_2: output_assignment.exit_account_2,
+		output_amount_1: output_assignment.output_amount_1,
+		output_amount_2: output_assignment.output_amount_2,
+		volume_fee_bps: VOLUME_FEE_BPS,
+		asset_id: NATIVE_ASSET_ID,
 	};
 
-	// Load prover from pre-built bins
+	// Generate proof using wormhole_lib
 	let bins_dir = Path::new("generated-bins");
-	let prover =
-		WormholeProver::new_from_files(&bins_dir.join("prover.bin"), &bins_dir.join("common.bin"))
-			.map_err(|e| {
-				crate::error::QuantusError::Generic(format!("Failed to load prover: {}", e))
-			})?;
-	let prover_next = prover
-		.commit(&inputs)
-		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
-	let proof: ProofWithPublicInputs<_, _, 2> = prover_next.prove().map_err(|e| {
-		crate::error::QuantusError::Generic(format!("Proof generation failed: {}", e))
-	})?;
+	let result = wormhole_lib::generate_proof(
+		&input,
+		&bins_dir.join("prover.bin"),
+		&bins_dir.join("common.bin"),
+	)
+	.map_err(|e| crate::error::QuantusError::Generic(e.message))?;
 
-	let proof_hex = hex::encode(proof.to_bytes());
+	// Write proof to file
+	let proof_hex = hex::encode(result.proof_bytes);
 	std::fs::write(output_file, proof_hex).map_err(|e| {
 		crate::error::QuantusError::Generic(format!("Failed to write proof: {}", e))
 	})?;
@@ -2467,13 +2339,12 @@ async fn run_dissolve(
 
 	// Load aggregation config
 	let bins_dir = std::path::Path::new("generated-bins");
-	let agg_config: AggregationConfig =
-		serde_json::from_reader(std::fs::File::open(bins_dir.join("config.json")).map_err(
-			|e| crate::error::QuantusError::Generic(format!("Failed to open config.json: {}", e)),
-		)?)
-		.map_err(|e| {
-			crate::error::QuantusError::Generic(format!("Failed to parse config.json: {}", e))
-		})?;
+	let agg_config = CircuitBinsConfig::load(bins_dir).map_err(|e| {
+		crate::error::QuantusError::Generic(format!(
+			"Failed to load aggregation circuit config: {}",
+			e
+		))
+	})?;
 
 	// === Layer 0: Initial funding ===
 	log_print!("{}", "Layer 0: Initial funding".bright_yellow());
@@ -2619,7 +2490,7 @@ async fn run_dissolve(
 			// Aggregate
 			log_print!("    Aggregating...");
 			let aggregated_file = format!("{}/batch{}_aggregated.hex", layer_dir, batch_idx);
-			aggregate_proofs_to_file(&proof_files, &aggregated_file, &agg_config)?;
+			aggregate_proofs_to_file(&proof_files, &aggregated_file)?;
 
 			// Verify on-chain
 			log_print!("    Verifying on-chain...");
@@ -2693,12 +2564,8 @@ async fn run_dissolve(
 }
 
 /// Helper to aggregate proof files and write the result
-fn aggregate_proofs_to_file(
-	proof_files: &[String],
-	output_file: &str,
-	_agg_config: &AggregationConfig,
-) -> crate::error::Result<()> {
-	use qp_wormhole_aggregator::aggregator::{AggregationBackend, CircuitType, Layer0Aggregator};
+fn aggregate_proofs_to_file(proof_files: &[String], output_file: &str) -> crate::error::Result<()> {
+	use qp_wormhole_aggregator::aggregator::Layer0Aggregator;
 
 	let bins_dir = std::path::Path::new("generated-bins");
 	let mut aggregator = Layer0Aggregator::new(bins_dir).map_err(|e| {
@@ -2706,7 +2573,7 @@ fn aggregate_proofs_to_file(
 	})?;
 
 	let common_data = aggregator.load_common_data(CircuitType::Leaf).map_err(|e| {
-		crate::error::QuantusError::Generic(format!("Failed to load leaf circuit data: {}", e))
+		crate::error::QuantusError::Generic(format!("Failed to load common data: {}", e))
 	})?;
 
 	for proof_file in proof_files {
@@ -2724,13 +2591,13 @@ fn aggregate_proofs_to_file(
 	}
 
 	let agg_start = std::time::Instant::now();
-	let result = aggregator
+	let proof = aggregator
 		.aggregate()
 		.map_err(|e| crate::error::QuantusError::Generic(format!("Aggregation failed: {}", e)))?;
 	let agg_elapsed = agg_start.elapsed();
 	log_print!("    Aggregation: {:.2}s", agg_elapsed.as_secs_f64());
 
-	let proof_hex = hex::encode(result.to_bytes());
+	let proof_hex = hex::encode(proof.to_bytes());
 	std::fs::write(output_file, &proof_hex).map_err(|e| {
 		crate::error::QuantusError::Generic(format!("Failed to write proof: {}", e))
 	})?;
@@ -2738,6 +2605,844 @@ fn aggregate_proofs_to_file(
 	Ok(())
 }
 
+/// Goldilocks field order (2^64 - 2^32 + 1)
+const GOLDILOCKS_ORDER: u64 = 0xFFFFFFFF00000001;
+
+/// Fuzz inputs: (amount, from, to, transfer_count, secret)
+type FuzzInputs = (u128, [u8; 32], [u8; 32], u64, [u8; 32]);
+
+/// Represents a fuzz test case
+struct FuzzCase {
+	name: &'static str,
+	description: &'static str,
+	/// Whether this case should pass (true) or fail (false)
+	/// Cases within quantization threshold should pass
+	expect_pass: bool,
+}
+
+/// Seeded random number generator for reproducible fuzz tests
+struct SeededRng {
+	state: u64,
+}
+
+impl SeededRng {
+	fn new(seed: u64) -> Self {
+		Self { state: seed }
+	}
+
+	fn next_u64(&mut self) -> u64 {
+		// Simple xorshift64 PRNG
+		self.state ^= self.state << 13;
+		self.state ^= self.state >> 7;
+		self.state ^= self.state << 17;
+		self.state
+	}
+
+	fn next_u128(&mut self) -> u128 {
+		let high = self.next_u64() as u128;
+		let low = self.next_u64() as u128;
+		(high << 64) | low
+	}
+}
+
+/// Add a u64 value to the first 8-byte chunk of an address (little-endian)
+fn add_to_address_chunk(addr: &mut [u8; 32], chunk_idx: usize, value: u64) {
+	let start = chunk_idx * 8;
+	let end = start + 8;
+	let current = u64::from_le_bytes(addr[start..end].try_into().unwrap());
+	let new_value = current.wrapping_add(value);
+	addr[start..end].copy_from_slice(&new_value.to_le_bytes());
+}
+
+/// Generate all fuzz cases with their fuzzed inputs
+#[allow(clippy::vec_init_then_push)]
+fn generate_fuzz_cases(
+	amount: u128,
+	from: [u8; 32],
+	to: [u8; 32],
+	count: u64,
+	secret: [u8; 32],
+	rng: &mut SeededRng,
+) -> Vec<(FuzzCase, FuzzInputs)> {
+	let random_u64 = rng.next_u64();
+	let random_u128 = rng.next_u128();
+	let mut random_addr = [0u8; 32];
+	for chunk in random_addr.chunks_mut(8) {
+		chunk.copy_from_slice(&rng.next_u64().to_le_bytes());
+	}
+	let mut random_secret = [0u8; 32];
+	for chunk in random_secret.chunks_mut(8) {
+		chunk.copy_from_slice(&rng.next_u64().to_le_bytes());
+	}
+
+	let mut cases = Vec::new();
+
+	// ============================================================
+	// AMOUNT FUZZING
+	// ============================================================
+
+	// Check if amount is at a quantization boundary
+	let amount_quantized = amount / SCALE_DOWN_FACTOR;
+	let amount_plus_one_quantized = (amount + 1) / SCALE_DOWN_FACTOR;
+	let amount_minus_one_quantized = amount.saturating_sub(1) / SCALE_DOWN_FACTOR;
+
+	// Amount + 1 planck - passes only if it stays in the same quantization bucket
+	let plus_one_same_bucket = amount_quantized == amount_plus_one_quantized;
+	cases.push((
+		FuzzCase {
+			name: "amount_plus_one_planck",
+			description: "Amount + 1 planck",
+			expect_pass: plus_one_same_bucket,
+		},
+		(amount + 1, from, to, count, secret),
+	));
+
+	// Amount - 1 planck - passes only if it stays in the same quantization bucket
+	let minus_one_same_bucket = amount_quantized == amount_minus_one_quantized;
+	cases.push((
+		FuzzCase {
+			name: "amount_minus_one_planck",
+			description: "Amount - 1 planck",
+			expect_pass: minus_one_same_bucket,
+		},
+		(amount.saturating_sub(1), from, to, count, secret),
+	));
+
+	// Amount + SCALE_DOWN_FACTOR (one quantized unit, should FAIL)
+	cases.push((
+		FuzzCase {
+			name: "amount_plus_one_quant_unit",
+			description: "Amount + 1 quantized unit",
+			expect_pass: false,
+		},
+		(amount + SCALE_DOWN_FACTOR, from, to, count, secret),
+	));
+
+	// Amount - SCALE_DOWN_FACTOR (one quantized unit, should FAIL)
+	cases.push((
+		FuzzCase {
+			name: "amount_minus_one_quant_unit",
+			description: "Amount - 1 quantized unit",
+			expect_pass: false,
+		},
+		(amount.saturating_sub(SCALE_DOWN_FACTOR), from, to, count, secret),
+	));
+
+	// Amount * 2 (should FAIL)
+	cases.push((
+		FuzzCase { name: "amount_doubled", description: "Amount * 2", expect_pass: false },
+		(amount * 2, from, to, count, secret),
+	));
+
+	// Amount = 0 (should FAIL)
+	cases.push((
+		FuzzCase { name: "amount_zero", description: "Amount = 0", expect_pass: false },
+		(0, from, to, count, secret),
+	));
+
+	// Amount + random large value (should FAIL)
+	cases.push((
+		FuzzCase {
+			name: "amount_plus_random",
+			description: "Amount + random u128",
+			expect_pass: false,
+		},
+		(amount.wrapping_add(random_u128), from, to, count, secret),
+	));
+
+	// Amount + Goldilocks order (overflow attack, should FAIL)
+	cases.push((
+		FuzzCase {
+			name: "amount_plus_goldilocks",
+			description: "Amount + Goldilocks field order",
+			expect_pass: false,
+		},
+		(amount.wrapping_add(GOLDILOCKS_ORDER as u128), from, to, count, secret),
+	));
+
+	// ============================================================
+	// TRANSFER COUNT FUZZING
+	// ============================================================
+
+	// Count + 1 (should FAIL)
+	cases.push((
+		FuzzCase { name: "count_plus_one", description: "Transfer count + 1", expect_pass: false },
+		(amount, from, to, count.wrapping_add(1), secret),
+	));
+
+	// Count - 1 with wrapping (0 -> u64::MAX, should FAIL)
+	cases.push((
+		FuzzCase {
+			name: "count_minus_one_wrap",
+			description: "Transfer count - 1 (wrapping)",
+			expect_pass: false,
+		},
+		(amount, from, to, count.wrapping_sub(1), secret),
+	));
+
+	// Count = u64::MAX (should FAIL)
+	cases.push((
+		FuzzCase {
+			name: "count_max",
+			description: "Transfer count = u64::MAX",
+			expect_pass: false,
+		},
+		(amount, from, to, u64::MAX, secret),
+	));
+
+	// Count + random (should FAIL)
+	cases.push((
+		FuzzCase {
+			name: "count_plus_random",
+			description: "Transfer count + random u64",
+			expect_pass: false,
+		},
+		(amount, from, to, count.wrapping_add(random_u64), secret),
+	));
+
+	// Count + Goldilocks order (overflow attack, should FAIL)
+	cases.push((
+		FuzzCase {
+			name: "count_plus_goldilocks",
+			description: "Transfer count + Goldilocks field order",
+			expect_pass: false,
+		},
+		(amount, from, to, count.wrapping_add(GOLDILOCKS_ORDER), secret),
+	));
+
+	// ============================================================
+	// FROM ADDRESS FUZZING
+	// ============================================================
+
+	// From: single bit flip (should FAIL)
+	let mut from_bit_flip = from;
+	from_bit_flip[0] ^= 0x01;
+	cases.push((
+		FuzzCase {
+			name: "from_single_bit_flip",
+			description: "From address single bit flip",
+			expect_pass: false,
+		},
+		(amount, from_bit_flip, to, count, secret),
+	));
+
+	// From: zeroed (should FAIL)
+	cases.push((
+		FuzzCase { name: "from_zeroed", description: "From address zeroed", expect_pass: false },
+		(amount, [0u8; 32], to, count, secret),
+	));
+
+	// From: add 1 to first chunk (should FAIL)
+	let mut from_plus_one = from;
+	add_to_address_chunk(&mut from_plus_one, 0, 1);
+	cases.push((
+		FuzzCase {
+			name: "from_plus_one",
+			description: "From address + 1 (first chunk)",
+			expect_pass: false,
+		},
+		(amount, from_plus_one, to, count, secret),
+	));
+
+	// From: random address (should FAIL)
+	cases.push((
+		FuzzCase { name: "from_random", description: "From address random", expect_pass: false },
+		(amount, random_addr, to, count, secret),
+	));
+
+	// From: add Goldilocks order to each chunk (should FAIL)
+	for chunk_idx in 0..4 {
+		let mut from_goldilocks = from;
+		add_to_address_chunk(&mut from_goldilocks, chunk_idx, GOLDILOCKS_ORDER);
+		cases.push((
+			FuzzCase {
+				name: match chunk_idx {
+					0 => "from_goldilocks_chunk0",
+					1 => "from_goldilocks_chunk1",
+					2 => "from_goldilocks_chunk2",
+					_ => "from_goldilocks_chunk3",
+				},
+				description: match chunk_idx {
+					0 => "From + Goldilocks order (chunk 0)",
+					1 => "From + Goldilocks order (chunk 1)",
+					2 => "From + Goldilocks order (chunk 2)",
+					_ => "From + Goldilocks order (chunk 3)",
+				},
+				expect_pass: false,
+			},
+			(amount, from_goldilocks, to, count, secret),
+		));
+	}
+
+	// ============================================================
+	// EXIT ACCOUNT FUZZING
+	// ============================================================
+	// NOTE: The exit_account is a PUBLIC INPUT that specifies where funds should go
+	// after proof verification. It is intentionally NOT validated against the storage
+	// proof's leaf hash. The security comes from:
+	// 1. The nullifier prevents double-spending
+	// 2. Only someone with the `secret` can create a valid proof
+	// 3. The exit_account being public means verifiers see where funds go
+	//
+	// The `to_account` IN THE LEAF HASH is the unspendable_account (derived from secret),
+	// which IS validated via the circuit's connect_hashes constraint.
+	//
+	// These tests confirm that exit_account can be set to any value (as designed).
+
+	// Exit account: single bit flip (SHOULD PASS - exit_account is not validated)
+	let mut exit_bit_flip = to;
+	exit_bit_flip[0] ^= 0x01;
+	cases.push((
+		FuzzCase {
+			name: "exit_single_bit_flip",
+			description: "Exit account single bit flip (not validated)",
+			expect_pass: true,
+		},
+		(amount, from, exit_bit_flip, count, secret),
+	));
+
+	// Exit account: zeroed (SHOULD PASS - exit_account is not validated)
+	cases.push((
+		FuzzCase {
+			name: "exit_zeroed",
+			description: "Exit account zeroed (not validated)",
+			expect_pass: true,
+		},
+		(amount, from, [0u8; 32], count, secret),
+	));
+
+	// Exit account: add 1 to first chunk (SHOULD PASS - exit_account is not validated)
+	let mut exit_plus_one = to;
+	add_to_address_chunk(&mut exit_plus_one, 0, 1);
+	cases.push((
+		FuzzCase {
+			name: "exit_plus_one",
+			description: "Exit account + 1 (not validated)",
+			expect_pass: true,
+		},
+		(amount, from, exit_plus_one, count, secret),
+	));
+
+	// Exit account: add Goldilocks order to each chunk (SHOULD PASS - not validated)
+	for chunk_idx in 0..4 {
+		let mut exit_goldilocks = to;
+		add_to_address_chunk(&mut exit_goldilocks, chunk_idx, GOLDILOCKS_ORDER);
+		cases.push((
+			FuzzCase {
+				name: match chunk_idx {
+					0 => "exit_goldilocks_chunk0",
+					1 => "exit_goldilocks_chunk1",
+					2 => "exit_goldilocks_chunk2",
+					_ => "exit_goldilocks_chunk3",
+				},
+				description: match chunk_idx {
+					0 => "Exit + Goldilocks (chunk 0, not validated)",
+					1 => "Exit + Goldilocks (chunk 1, not validated)",
+					2 => "Exit + Goldilocks (chunk 2, not validated)",
+					_ => "Exit + Goldilocks (chunk 3, not validated)",
+				},
+				expect_pass: true,
+			},
+			(amount, from, exit_goldilocks, count, secret),
+		));
+	}
+
+	// ============================================================
+	// SWAPPED/COMBINED
+	// ============================================================
+
+	// Swapped from/to (should FAIL)
+	cases.push((
+		FuzzCase {
+			name: "swapped_from_to",
+			description: "From and To addresses swapped",
+			expect_pass: false,
+		},
+		(amount, to, from, count, secret),
+	));
+
+	// All inputs fuzzed with random offsets (should FAIL)
+	let mut all_fuzzed_from = from;
+	let mut all_fuzzed_to = to;
+	all_fuzzed_from[31] ^= 0xFF;
+	all_fuzzed_to[31] ^= 0xFF;
+	cases.push((
+		FuzzCase {
+			name: "all_random_fuzzed",
+			description: "All inputs fuzzed with random values",
+			expect_pass: false,
+		},
+		(
+			amount.wrapping_add(random_u128),
+			all_fuzzed_from,
+			all_fuzzed_to,
+			count.wrapping_add(random_u64),
+			secret,
+		),
+	));
+
+	// All inputs + Goldilocks order (should FAIL)
+	let mut all_gold_from = from;
+	let mut all_gold_to = to;
+	add_to_address_chunk(&mut all_gold_from, 0, GOLDILOCKS_ORDER);
+	add_to_address_chunk(&mut all_gold_to, 0, GOLDILOCKS_ORDER);
+	cases.push((
+		FuzzCase {
+			name: "all_goldilocks_fuzzed",
+			description: "All inputs + Goldilocks order",
+			expect_pass: false,
+		},
+		(
+			amount.wrapping_add(GOLDILOCKS_ORDER as u128),
+			all_gold_from,
+			all_gold_to,
+			count.wrapping_add(GOLDILOCKS_ORDER),
+			secret,
+		),
+	));
+
+	// ============================================================
+	// SECRET FUZZING (tests unspendable_account validation)
+	// ============================================================
+	// The secret is used to derive the unspendable_account, which IS validated
+	// against the storage proof's leaf hash via the circuit's connect_hashes constraint.
+	// A wrong secret will derive a wrong unspendable_account, causing proof failure.
+
+	// Secret: single bit flip (should FAIL - wrong unspendable_account)
+	let mut secret_bit_flip = secret;
+	secret_bit_flip[0] ^= 0x01;
+	cases.push((
+		FuzzCase {
+			name: "secret_single_bit_flip",
+			description: "Secret single bit flip (wrong unspendable_account)",
+			expect_pass: false,
+		},
+		(amount, from, to, count, secret_bit_flip),
+	));
+
+	// Secret: zeroed (should FAIL)
+	cases.push((
+		FuzzCase { name: "secret_zeroed", description: "Secret zeroed", expect_pass: false },
+		(amount, from, to, count, [0u8; 32]),
+	));
+
+	// Secret: random (should FAIL)
+	cases.push((
+		FuzzCase { name: "secret_random", description: "Secret random value", expect_pass: false },
+		(amount, from, to, count, random_secret),
+	));
+
+	// Secret: add 1 to first byte (should FAIL)
+	let mut secret_plus_one = secret;
+	secret_plus_one[0] = secret_plus_one[0].wrapping_add(1);
+	cases.push((
+		FuzzCase {
+			name: "secret_plus_one",
+			description: "Secret + 1 (first byte)",
+			expect_pass: false,
+		},
+		(amount, from, to, count, secret_plus_one),
+	));
+
+	// Secret: add Goldilocks order to first chunk (overflow attack, should FAIL)
+	let mut secret_goldilocks = secret;
+	add_to_address_chunk(&mut secret_goldilocks, 0, GOLDILOCKS_ORDER);
+	cases.push((
+		FuzzCase {
+			name: "secret_plus_goldilocks",
+			description: "Secret + Goldilocks field order (chunk 0)",
+			expect_pass: false,
+		},
+		(amount, from, to, count, secret_goldilocks),
+	));
+
+	cases
+}
+
+/// Run fuzz tests on the leaf verification circuit.
+///
+/// This function:
+/// 1. Executes a real transfer to a wormhole address
+/// 2. Attempts to generate proofs with fuzzed inputs (wrong amount, wrong address, etc.)
+/// 3. Verifies that proof preparation fails for each fuzzed case (except within-threshold cases)
+/// 4. Reports which cases correctly passed/failed
+async fn run_fuzz_test(
+	wallet_name: String,
+	password: Option<String>,
+	password_file: Option<String>,
+	amount: u128,
+	node_url: &str,
+) -> crate::error::Result<()> {
+	use colored::Colorize;
+
+	log_print!("");
+	log_print!("==================================================");
+	log_print!("  Wormhole Fuzz Test");
+	log_print!("==================================================");
+	log_print!("");
+
+	// Use block timestamp as seed for reproducibility within same block
+	let seed = std::time::SystemTime::now()
+		.duration_since(std::time::UNIX_EPOCH)
+		.unwrap()
+		.as_secs();
+	log_print!("  Random seed: {}", seed);
+
+	// Load wallet
+	let wallet = load_multiround_wallet(&wallet_name, password, password_file)?;
+
+	// Connect to node
+	let quantus_client = QuantusClient::new(node_url)
+		.await
+		.map_err(|e| crate::error::QuantusError::Generic(format!("Failed to connect: {}", e)))?;
+
+	// Step 1: Generate a secret and derive wormhole address
+	log_print!("{}", "Step 1: Creating test transfer...".bright_yellow());
+
+	let mut secret_bytes = [0u8; 32];
+	rand::rng().fill_bytes(&mut secret_bytes);
+	let secret: BytesDigest = secret_bytes.try_into().map_err(|e| {
+		crate::error::QuantusError::Generic(format!("Failed to convert secret: {:?}", e))
+	})?;
+
+	let unspendable_account =
+		qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret)
+			.account_id;
+	let unspendable_account_bytes_digest =
+		qp_zk_circuits_common::utils::digest_to_bytes(unspendable_account);
+	let unspendable_account_bytes: [u8; 32] = *unspendable_account_bytes_digest;
+
+	let wormhole_address = SubxtAccountId(unspendable_account_bytes);
+
+	// Execute transfer
+	let transfer_tx = quantus_node::api::tx().balances().transfer_allow_death(
+		subxt::ext::subxt_core::utils::MultiAddress::Id(wormhole_address.clone()),
+		amount,
+	);
+
+	let quantum_keypair = QuantumKeyPair {
+		public_key: wallet.keypair.public_key.clone(),
+		private_key: wallet.keypair.private_key.clone(),
+	};
+
+	submit_transaction(
+		&quantus_client,
+		&quantum_keypair,
+		transfer_tx,
+		None,
+		ExecutionMode { finalized: false, wait_for_transaction: true },
+	)
+	.await
+	.map_err(|e| crate::error::QuantusError::Generic(format!("Transfer failed: {}", e)))?;
+
+	// Get block and event
+	let block = at_best_block(&quantus_client)
+		.await
+		.map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get block: {}", e)))?;
+	let block_hash = block.hash();
+	let events_api =
+		quantus_client.client().events().at(block_hash).await.map_err(|e| {
+			crate::error::QuantusError::Generic(format!("Failed to get events: {}", e))
+		})?;
+
+	let event = events_api
+		.find::<wormhole::events::NativeTransferred>()
+		.find(|e| if let Ok(evt) = e { evt.to.0 == unspendable_account_bytes } else { false })
+		.ok_or_else(|| crate::error::QuantusError::Generic("No transfer event found".to_string()))?
+		.map_err(|e| crate::error::QuantusError::Generic(format!("Event decode error: {}", e)))?;
+
+	let transfer_count = event.transfer_count;
+	let funding_account: [u8; 32] = wallet.keypair.to_account_id_32().into();
+
+	log_success!(
+		"  Transfer complete: {} to wormhole, transfer_count={}",
+		format_balance(amount),
+		transfer_count
+	);
+	log_print!("  Block: 0x{}", hex::encode(block_hash.0));
+
+	// Step 2: Test that correct inputs work (sanity check)
+	log_print!("");
+	log_print!("{}", "Step 2: Verifying correct inputs work...".bright_yellow());
+
+	// Step 2: Get block header data needed for proof generation
+	log_print!("{}", "Step 2: Fetching block header data...".bright_yellow());
+
+	let client = quantus_client.client();
+	let blocks =
+		client.blocks().at(block_hash).await.map_err(|e| {
+			crate::error::QuantusError::Generic(format!("Failed to get block: {}", e))
+		})?;
+	let header = blocks.header();
+
+	let state_root = BytesDigest::try_from(header.state_root.as_bytes())
+		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
+	let parent_hash = BytesDigest::try_from(header.parent_hash.as_bytes())
+		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
+	let extrinsics_root = BytesDigest::try_from(header.extrinsics_root.as_bytes())
+		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
+	let digest: [u8; 110] =
+		header.digest.encode().try_into().map_err(|_| {
+			crate::error::QuantusError::Generic("Failed to encode digest".to_string())
+		})?;
+	let block_number = header.number;
+	let state_root_hex = hex::encode(header.state_root.0);
+
+	// Build storage key for fetching proof
+	let pallet_hash = sp_core::twox_128(b"Wormhole");
+	let storage_hash = sp_core::twox_128(b"TransferProof");
+	let mut final_key = Vec::with_capacity(32 + 32);
+	final_key.extend_from_slice(&pallet_hash);
+	final_key.extend_from_slice(&storage_hash);
+	let key_tuple: TransferProofKey = (AccountId32::new(unspendable_account_bytes), transfer_count);
+	let encoded_key = key_tuple.encode();
+	let key_hash = sp_core::blake2_256(&encoded_key);
+	final_key.extend_from_slice(&key_hash);
+
+	// Fetch storage proof from RPC
+	let proof_params = rpc_params![vec![to_hex(&final_key)], block_hash];
+	let read_proof: ReadProof<sp_core::H256> = quantus_client
+		.rpc_client()
+		.request("state_getReadProof", proof_params)
+		.await
+		.map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
+
+	log_success!("  Block header and storage proof fetched");
+
+	// Prover bins directory
+	let bins_dir = Path::new("generated-bins");
+
+	// Step 3: Test that correct inputs work (sanity check with actual proof generation)
+	log_print!("");
+	log_print!("{}", "Step 3: Verifying correct inputs generate valid proof...".bright_yellow());
+
+	let correct_result = try_generate_fuzz_proof(
+		bins_dir,
+		&read_proof,
+		&state_root_hex,
+		block_hash,
+		secret,
+		amount,
+		funding_account,
+		unspendable_account_bytes,
+		transfer_count,
+		state_root,
+		parent_hash,
+		extrinsics_root,
+		digest,
+		block_number,
+	);
+
+	match correct_result {
+		Ok(_) => log_success!("  Correct inputs: PASSED (ZK proof generated successfully)"),
+		Err(e) => {
+			return Err(crate::error::QuantusError::Generic(format!(
+				"FATAL: Correct inputs failed proof generation: {}",
+				e
+			)));
+		},
+	}
+
+	// Step 4: Generate and run fuzz cases
+	log_print!("");
+	log_print!("{}", "Step 4: Running fuzz cases (generating ZK proofs)...".bright_yellow());
+	log_print!(
+		"  NOTE: Each case attempts actual ZK proof generation - this tests circuit constraints"
+	);
+	log_print!("");
+
+	let mut rng = SeededRng::new(seed);
+	let secret_bytes: [u8; 32] = secret.as_ref().try_into().expect("secret is 32 bytes");
+	let fuzz_cases = generate_fuzz_cases(
+		amount,
+		funding_account,
+		unspendable_account_bytes,
+		transfer_count,
+		secret_bytes,
+		&mut rng,
+	);
+
+	log_print!("  Total fuzz cases: {}", fuzz_cases.len());
+	log_print!("");
+
+	let mut passed = 0;
+	let mut failed = 0;
+
+	for (case, (fuzzed_amount, fuzzed_from, fuzzed_to, fuzzed_count, fuzzed_secret)) in &fuzz_cases
+	{
+		// Convert fuzzed_secret bytes to BytesDigest
+		let fuzzed_secret_digest: BytesDigest =
+			(*fuzzed_secret).try_into().expect("fuzzed_secret is 32 bytes");
+
+		// Use catch_unwind to handle panics from field overflow validation
+		let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+			try_generate_fuzz_proof(
+				bins_dir,
+				&read_proof,
+				&state_root_hex,
+				block_hash,
+				fuzzed_secret_digest,
+				*fuzzed_amount,
+				*fuzzed_from,
+				*fuzzed_to,
+				*fuzzed_count,
+				state_root,
+				parent_hash,
+				extrinsics_root,
+				digest,
+				block_number,
+			)
+		}));
+
+		let succeeded = match &result {
+			Ok(Ok(_)) => true,
+			Ok(Err(_)) => false,
+			Err(_) => false, // Panic counts as failure
+		};
+
+		if succeeded == case.expect_pass {
+			// Correct behavior
+			log_print!("  {} {}: {}", "OK".bright_green(), case.name, case.description);
+			passed += 1;
+		} else {
+			// Wrong behavior
+			let problem = if case.expect_pass {
+				match result {
+					Ok(Err(e)) => format!("Expected to pass but failed: {}", e),
+					Err(_) => "Expected to pass but panicked".to_string(),
+					_ => "Unknown error".to_string(),
+				}
+			} else {
+				"Expected circuit to reject but proof succeeded!".to_string()
+			};
+			log_print!(
+				"  {} {}: {} - {}",
+				"FAIL".bright_red(),
+				case.name,
+				case.description,
+				problem.red()
+			);
+			failed += 1;
+		}
+	}
+
+	// Summary
+	log_print!("");
+	log_print!("==================================================");
+	log_print!("  Fuzz Test Results");
+	log_print!("==================================================");
+	log_print!("");
+	log_print!("  Total cases: {}", fuzz_cases.len());
+	log_print!("  Passed: {} (correct behavior)", format!("{}", passed).bright_green());
+
+	if failed > 0 {
+		log_print!("  Failed: {} (incorrect behavior)", format!("{}", failed).bright_red());
+		log_print!("");
+		return Err(crate::error::QuantusError::Generic(format!(
+			"Fuzz test failed: {} cases had incorrect behavior",
+			failed
+		)));
+	}
+
+	log_print!("");
+	log_success!("All fuzz cases behaved correctly!");
+
+	Ok(())
+}
+
+/// Try to generate a ZK proof with the given (possibly fuzzed) inputs.
+/// This actually runs the ZK prover to test that circuit constraints reject invalid inputs.
+///
+/// Returns Ok(()) if proof generation succeeds, Err if it fails.
+#[allow(clippy::too_many_arguments)]
+fn try_generate_fuzz_proof(
+	bins_dir: &Path,
+	read_proof: &ReadProof<sp_core::H256>,
+	state_root_hex: &str,
+	block_hash: subxt::utils::H256,
+	secret: BytesDigest,
+	fuzzed_amount: u128,
+	fuzzed_from: [u8; 32],
+	fuzzed_to: [u8; 32],
+	fuzzed_count: u64,
+	state_root: BytesDigest,
+	parent_hash: BytesDigest,
+	extrinsics_root: BytesDigest,
+	digest: [u8; 110],
+	block_number: u32,
+) -> Result<(), String> {
+	use subxt::ext::codec::Encode;
+
+	// Compute the fuzzed leaf_inputs_hash
+	let from_account = AccountId32::new(fuzzed_from);
+	let to_account = AccountId32::new(fuzzed_to);
+	let transfer_data_tuple =
+		(NATIVE_ASSET_ID, fuzzed_count, from_account.clone(), to_account.clone(), fuzzed_amount);
+	let encoded_data = transfer_data_tuple.encode();
+	let fuzzed_leaf_hash =
+		qp_poseidon::PoseidonHasher::hash_storage::<TransferProofData>(&encoded_data);
+
+	// Prepare storage proof (now just logs warning on mismatch, doesn't fail)
+	let processed_storage_proof = prepare_proof_for_circuit(
+		read_proof.proof.iter().map(|proof| proof.0.clone()).collect(),
+		state_root_hex.to_string(),
+		fuzzed_leaf_hash,
+	)
+	.map_err(|e| e.to_string())?;
+
+	// Quantize input amount (may panic for invalid amounts, caught by caller)
+	let input_amount_quantized: u32 = quantize_funding_amount(fuzzed_amount)?;
+
+	// Compute output amount (single output for simplicity)
+	let output_amount = compute_output_amount(input_amount_quantized, VOLUME_FEE_BPS);
+
+	// Generate unspendable account from secret
+	let unspendable_account =
+		qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret)
+			.account_id;
+	let unspendable_account_bytes_digest =
+		qp_zk_circuits_common::utils::digest_to_bytes(unspendable_account);
+
+	// Build circuit inputs with fuzzed data
+	let inputs = CircuitInputs {
+		private: PrivateCircuitInputs {
+			secret,
+			transfer_count: fuzzed_count,
+			funding_account: BytesDigest::try_from(fuzzed_from.as_ref())
+				.map_err(|e| e.to_string())?,
+			storage_proof: processed_storage_proof,
+			unspendable_account: unspendable_account_bytes_digest,
+			parent_hash,
+			state_root,
+			extrinsics_root,
+			digest,
+			input_amount: input_amount_quantized,
+		},
+		public: PublicCircuitInputs {
+			output_amount_1: output_amount,
+			output_amount_2: 0,
+			volume_fee_bps: VOLUME_FEE_BPS,
+			nullifier: digest_to_bytes(Nullifier::from_preimage(secret, fuzzed_count).hash),
+			exit_account_1: BytesDigest::try_from(fuzzed_to.as_ref()).map_err(|e| e.to_string())?,
+			exit_account_2: BytesDigest::try_from([0u8; 32].as_ref()).map_err(|e| e.to_string())?,
+			block_hash: BytesDigest::try_from(block_hash.as_ref()).map_err(|e| e.to_string())?,
+			block_number,
+			asset_id: NATIVE_ASSET_ID,
+		},
+	};
+
+	// Load prover (need fresh instance for each proof since commit() takes ownership)
+	let prover =
+		WormholeProver::new_from_files(&bins_dir.join("prover.bin"), &bins_dir.join("common.bin"))
+			.map_err(|e| format!("Failed to load prover: {}", e))?;
+
+	// Try to generate the proof - this is where circuit constraints are checked
+	let prover_next = prover.commit(&inputs).map_err(|e| e.to_string())?;
+	let _proof: ProofWithPublicInputs<_, _, 2> =
+		prover_next.prove().map_err(|e| format!("Circuit rejected: {}", e))?;
+
+	Ok(())
+}
 #[cfg(test)]
 mod tests {
 	use super::*;
@@ -2806,7 +3511,7 @@ mod tests {
 		assert_eq!(quantize_funding_amount(max_valid).unwrap(), u32::MAX);
 		assert!(quantize_funding_amount(max_valid + SCALE_DOWN_FACTOR)
 			.unwrap_err()
-			.contains("too large"));
+			.contains("exceeds u32::MAX"));
 	}
 
 	#[test]
@@ -2988,22 +3693,12 @@ mod tests {
 		// If the upstream adds/removes/renames fields, this test will catch it.
 		let json = r#"{
 			"num_leaf_proofs": 8,
-			"hashes": {
-				"common": "aabbcc",
-				"verifier": "ddeeff",
-				"prover": "112233",
-				"aggregated_common": "445566",
-				"aggregated_verifier": "778899"
-			}
+			"num_layer0_proofs": null
 		}"#;
 
-		let config: AggregationConfig = serde_json::from_str(json).unwrap();
+		let config: CircuitBinsConfig = serde_json::from_str(json).unwrap();
 		assert_eq!(config.num_leaf_proofs, 8);
-
-		let hashes = config.hashes.unwrap();
-		assert_eq!(hashes.prover.as_deref(), Some("112233"));
-		assert_eq!(hashes.aggregated_common.as_deref(), Some("445566"));
-		assert_eq!(hashes.aggregated_verifier.as_deref(), Some("778899"));
+		assert_eq!(config.num_layer0_proofs, None);
 	}
 
 	fn mk_accounts(n: usize) -> Vec<[u8; 32]> {
diff --git a/src/lib.rs b/src/lib.rs
index a265e05..0111942 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -11,6 +11,7 @@ pub mod error;
 pub mod log;
 pub mod subsquid;
 pub mod wallet;
+pub mod wormhole_lib;
 
 // Re-export commonly used types and functions
 pub use error::{QuantusError as Error, Result};
@@ -38,6 +39,14 @@ pub use cli::multisig::{
 	ProposalStatus,
 };
 
+// Re-export wormhole library functions for SDK usage
+pub use wormhole_lib::{
+	compute_leaf_hash, compute_nullifier, compute_output_amount, compute_storage_key,
+	compute_wormhole_address, generate_proof as generate_wormhole_proof, quantize_amount,
+	ProofGenerationInput, ProofGenerationOutput, TransferProofData, TransferProofKey,
+	WormholeLibError, NATIVE_ASSET_ID, SCALE_DOWN_FACTOR, VOLUME_FEE_BPS,
+};
+
 /// Library version
 pub const VERSION: &str = env!("CARGO_PKG_VERSION");
 
diff --git a/src/main.rs b/src/main.rs
index c467064..847a91a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,6 +15,7 @@ mod error;
 mod log;
 mod subsquid;
 mod wallet;
+mod wormhole_lib;
 
 use cli::Commands;
 use error::QuantusError;
diff --git a/src/quantus_metadata.scale b/src/quantus_metadata.scale
index 468b29e..fb8eb49 100644
Binary files a/src/quantus_metadata.scale and b/src/quantus_metadata.scale differ
diff --git a/src/wallet/keystore.rs b/src/wallet/keystore.rs
index 250e29f..64096f4 100644
--- a/src/wallet/keystore.rs
+++ b/src/wallet/keystore.rs
@@ -58,8 +58,9 @@ impl QuantumKeyPair {
 
 	/// Convert to DilithiumPair for use with substrate-api-client
 	pub fn to_resonance_pair(&self) -> Result<DilithiumPair> {
-		DilithiumPair::from_raw(&self.public_key, &self.private_key)
-			.map_err(|_| WalletError::KeyGeneration.into())
+		// Convert our QuantumKeyPair to DilithiumPair using from_raw
+		Ok(DilithiumPair::from_raw(&self.public_key, &self.private_key)
+			.map_err(|_| crate::error::WalletError::KeyGeneration)?)
 	}
 
 	pub fn from_resonance_pair(keypair: &DilithiumPair) -> Self {
@@ -315,7 +316,7 @@ mod tests {
 
 		// Verify the conversion
 		assert_eq!(quantum_keypair.public_key, resonance_pair.public().as_ref().to_vec());
-		assert_eq!(quantum_keypair.private_key, resonance_pair.secret_bytes().to_vec());
+		assert_eq!(quantum_keypair.private_key.as_slice(), resonance_pair.secret_bytes());
 	}
 
 	#[test]
diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs
index d62e003..8557b7d 100644
--- a/src/wallet/mod.rs
+++ b/src/wallet/mod.rs
@@ -16,6 +16,7 @@ use qp_rusty_crystals_hdwallet::{
 };
 use rand::{rng, RngCore};
 use serde::{Deserialize, Serialize};
+
 use sp_runtime::traits::IdentifyAccount;
 
 /// Default derivation path for Quantus wallets: m/44'/189189'/0'/0'/0'
diff --git a/src/wormhole_lib.rs b/src/wormhole_lib.rs
new file mode 100644
index 0000000..a292031
--- /dev/null
+++ b/src/wormhole_lib.rs
@@ -0,0 +1,403 @@
+//! Wormhole Library Functions
+//!
+//! This module provides library-friendly functions for wormhole proof generation
+//! that can be used by external crates (like quantus-sdk) without requiring
+//! a chain client connection.
+//!
+//! These functions handle the core cryptographic operations:
+//! - Computing leaf hashes for storage proof verification
+//! - Computing storage keys
+//! - Generating ZK proofs from raw inputs
+
+use codec::Encode;
+use qp_wormhole_circuit::{
+	inputs::{CircuitInputs, PrivateCircuitInputs},
+	nullifier::Nullifier,
+};
+use qp_wormhole_inputs::PublicCircuitInputs;
+use qp_wormhole_prover::WormholeProver;
+use qp_zk_circuits_common::{
+	storage_proof::prepare_proof_for_circuit,
+	utils::{digest_to_bytes, BytesDigest},
+};
+use sp_core::crypto::AccountId32;
+use std::path::Path;
+
+/// Native asset id for QTU token
+pub const NATIVE_ASSET_ID: u32 = 0;
+
+/// Scale down factor for quantizing amounts (10^10 to go from 12 to 2 decimal places)
+pub const SCALE_DOWN_FACTOR: u128 = 10_000_000_000;
+
+/// Volume fee rate in basis points (10 bps = 0.1%)
+pub const VOLUME_FEE_BPS: u32 = 10;
+
+/// Full transfer data type - used to compute the leaf_inputs_hash via Poseidon2.
+/// Order: (asset_id, transfer_count, from, to, amount)
+pub type TransferProofData = (u32, u64, AccountId32, AccountId32, u128);
+
+/// Storage key type - (wormhole_address, transfer_count)
+/// This is hashed with Blake2_256 to form the storage key suffix.
+pub type TransferProofKey = (AccountId32, u64);
+
+/// Result type for wormhole library operations
+pub type Result<T> = std::result::Result<T, WormholeLibError>;
+
+/// Error type for wormhole library operations
+#[derive(Debug, Clone)]
+pub struct WormholeLibError {
+	pub message: String,
+}
+
+impl std::fmt::Display for WormholeLibError {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		write!(f, "{}", self.message)
+	}
+}
+
+impl std::error::Error for WormholeLibError {}
+
+impl From<String> for WormholeLibError {
+	fn from(message: String) -> Self {
+		Self { message }
+	}
+}
+
+/// Input data for generating a wormhole proof.
+/// All fields are raw bytes - no chain client required.
+#[derive(Debug, Clone)]
+pub struct ProofGenerationInput {
+	/// 32-byte secret
+	pub secret: [u8; 32],
+	/// Transfer count (atomic counter per recipient)
+	pub transfer_count: u64,
+	/// Funding account (sender) as 32 bytes
+	pub funding_account: [u8; 32],
+	/// Wormhole address (recipient/unspendable account) as 32 bytes
+	pub wormhole_address: [u8; 32],
+	/// Funding amount in planck (12 decimals)
+	pub funding_amount: u128,
+	/// Block hash as 32 bytes
+	pub block_hash: [u8; 32],
+	/// Block number
+	pub block_number: u32,
+	/// Parent hash as 32 bytes
+	pub parent_hash: [u8; 32],
+	/// State root as 32 bytes
+	pub state_root: [u8; 32],
+	/// Extrinsics root as 32 bytes
+	pub extrinsics_root: [u8; 32],
+	/// SCALE-encoded digest (variable length, padded to 110 bytes internally)
+	pub digest: Vec<u8>,
+	/// Storage proof nodes (each node is a Vec<u8>)
+	pub proof_nodes: Vec<Vec<u8>>,
+	/// Exit account 1 as 32 bytes
+	pub exit_account_1: [u8; 32],
+	/// Exit account 2 as 32 bytes (use zeros for single output)
+	pub exit_account_2: [u8; 32],
+	/// Output amount 1 (quantized, 2 decimals)
+	pub output_amount_1: u32,
+	/// Output amount 2 (quantized, 2 decimals, 0 for single output)
+	pub output_amount_2: u32,
+	/// Volume fee in basis points
+	pub volume_fee_bps: u32,
+	/// Asset ID (0 for native token)
+	pub asset_id: u32,
+}
+
+/// Output from proof generation
+#[derive(Debug, Clone)]
+pub struct ProofGenerationOutput {
+	/// Generated proof as bytes
+	pub proof_bytes: Vec<u8>,
+	/// Nullifier as 32 bytes (available for callers who need it)
+	#[allow(dead_code)]
+	pub nullifier: [u8; 32],
+}
+
+/// Compute the leaf hash (leaf_inputs_hash) for storage proof verification.
+///
+/// This uses Poseidon2 hashing via `hash_storage` to match the chain's
+/// PoseidonStorageHasher behavior.
+///
+/// # Arguments
+/// * `asset_id` - Asset ID (0 for native token)
+/// * `transfer_count` - Atomic transfer counter
+/// * `funding_account` - Sender account as 32 bytes
+/// * `wormhole_address` - Recipient (unspendable) account as 32 bytes
+/// * `amount` - Transfer amount in planck
+///
+/// # Returns
+/// 32-byte leaf hash
+pub fn compute_leaf_hash(
+	asset_id: u32,
+	transfer_count: u64,
+	funding_account: &[u8; 32],
+	wormhole_address: &[u8; 32],
+	amount: u128,
+) -> [u8; 32] {
+	// Use AccountId32 to match the chain's type exactly
+	let from_account = AccountId32::new(*funding_account);
+	let to_account = AccountId32::new(*wormhole_address);
+
+	let transfer_data: TransferProofData =
+		(asset_id, transfer_count, from_account, to_account, amount);
+	let encoded_data = transfer_data.encode();
+
+	qp_poseidon::PoseidonHasher::hash_storage::<TransferProofData>(&encoded_data)
+}
+
+/// Compute the storage key for a transfer proof.
+///
+/// The storage key is: Twox128("Wormhole") || Twox128("TransferProof") ||
+/// Blake2_256(wormhole_address, transfer_count)
+///
+/// # Arguments
+/// * `wormhole_address` - The unspendable wormhole account as 32 bytes
+/// * `transfer_count` - The atomic transfer counter
+///
+/// # Returns
+/// Full storage key as bytes
+pub fn compute_storage_key(wormhole_address: &[u8; 32], transfer_count: u64) -> Vec<u8> {
+	let pallet_hash = sp_core::twox_128(b"Wormhole");
+	let storage_hash = sp_core::twox_128(b"TransferProof");
+
+	let mut final_key = Vec::with_capacity(32 + 32);
+	final_key.extend_from_slice(&pallet_hash);
+	final_key.extend_from_slice(&storage_hash);
+
+	// Hash the key tuple with Blake2_256
+	let to_account = AccountId32::new(*wormhole_address);
+	let key_tuple: TransferProofKey = (to_account, transfer_count);
+	let encoded_key = key_tuple.encode();
+	let key_hash = sp_core::blake2_256(&encoded_key);
+	final_key.extend_from_slice(&key_hash);
+
+	final_key
+}
+
+/// Compute the unspendable wormhole account from a secret.
+///
+/// # Arguments
+/// * `secret` - 32-byte secret
+///
+/// # Returns
+/// 32-byte wormhole account address
+pub fn compute_wormhole_address(secret: &[u8; 32]) -> Result<[u8; 32]> {
+	let secret_digest: BytesDigest = (*secret)
+		.try_into()
+		.map_err(|e| WormholeLibError::from(format!("Invalid secret: {:?}", e)))?;
+
+	let unspendable =
+		qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret_digest);
+
+	Ok(*digest_to_bytes(unspendable.account_id))
+}
+
+/// Compute the nullifier from secret and transfer count.
+///
+/// # Arguments
+/// * `secret` - 32-byte secret
+/// * `transfer_count` - Transfer counter
+///
+/// # Returns
+/// 32-byte nullifier
+#[allow(dead_code)]
+pub fn compute_nullifier(secret: &[u8; 32], transfer_count: u64) -> Result<[u8; 32]> {
+	let secret_digest: BytesDigest = (*secret)
+		.try_into()
+		.map_err(|e| WormholeLibError::from(format!("Invalid secret: {:?}", e)))?;
+
+	let nullifier = Nullifier::from_preimage(secret_digest, transfer_count);
+	Ok(*digest_to_bytes(nullifier.hash))
+}
+
+/// Quantize a funding amount from 12 decimal places to 2 decimal places.
+///
+/// # Arguments
+/// * `amount` - Amount in planck (12 decimals)
+///
+/// # Returns
+/// Quantized amount (2 decimals) as u32
+pub fn quantize_amount(amount: u128) -> Result<u32> {
+	let quantized = amount / SCALE_DOWN_FACTOR;
+	if quantized > u32::MAX as u128 {
+		return Err(WormholeLibError::from(format!(
+			"Quantized amount {} exceeds u32::MAX",
+			quantized
+		)));
+	}
+	Ok(quantized as u32)
+}
+
+/// Compute output amount after fee deduction.
+///
+/// output = input * (10000 - fee_bps) / 10000
+pub fn compute_output_amount(input_amount: u32, fee_bps: u32) -> u32 {
+	((input_amount as u64) * (10000 - fee_bps as u64) / 10000) as u32
+}
+
+/// Generate a wormhole proof from raw inputs.
+///
+/// This function takes all necessary data as raw bytes and generates a ZK proof.
+/// It does not require a chain client - all data must be pre-fetched.
+///
+/// # Arguments
+/// * `input` - All input data for proof generation
+/// * `prover_bin_path` - Path to prover.bin
+/// * `common_bin_path` - Path to common.bin
+///
+/// # Returns
+/// Proof bytes and nullifier
+pub fn generate_proof(
+	input: &ProofGenerationInput,
+	prover_bin_path: &Path,
+	common_bin_path: &Path,
+) -> Result<ProofGenerationOutput> {
+	// Convert secret to BytesDigest
+	let secret_digest: BytesDigest = input
+		.secret
+		.try_into()
+		.map_err(|e| WormholeLibError::from(format!("Invalid secret: {:?}", e)))?;
+
+	// Compute leaf hash for storage proof
+	let leaf_hash = compute_leaf_hash(
+		input.asset_id,
+		input.transfer_count,
+		&input.funding_account,
+		&input.wormhole_address,
+		input.funding_amount,
+	);
+
+	// Prepare storage proof
+	let processed_proof = prepare_proof_for_circuit(
+		input.proof_nodes.clone(),
+		hex::encode(input.state_root),
+		leaf_hash,
+	)
+	.map_err(|e| WormholeLibError::from(format!("Storage proof preparation failed: {}", e)))?;
+
+	// Quantize input amount
+	let input_amount_quantized = quantize_amount(input.funding_amount)?;
+
+	// Compute nullifier
+	let nullifier = Nullifier::from_preimage(secret_digest, input.transfer_count);
+	let nullifier_bytes = digest_to_bytes(nullifier.hash);
+
+	// Compute unspendable account
+	let unspendable =
+		qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret_digest);
+	let unspendable_bytes = digest_to_bytes(unspendable.account_id);
+
+	// Prepare digest (padded to 110 bytes)
+	const DIGEST_LOGS_SIZE: usize = 110;
+	let mut digest_padded = [0u8; DIGEST_LOGS_SIZE];
+	let copy_len = input.digest.len().min(DIGEST_LOGS_SIZE);
+	digest_padded[..copy_len].copy_from_slice(&input.digest[..copy_len]);
+
+	// Build circuit inputs
+	let private = PrivateCircuitInputs {
+		secret: secret_digest,
+		transfer_count: input.transfer_count,
+		funding_account: input
+			.funding_account
+			.as_slice()
+			.try_into()
+			.map_err(|e| WormholeLibError::from(format!("Invalid funding account: {:?}", e)))?,
+		storage_proof: processed_proof,
+		unspendable_account: unspendable_bytes,
+		parent_hash: input
+			.parent_hash
+			.as_slice()
+			.try_into()
+			.map_err(|e| WormholeLibError::from(format!("Invalid parent hash: {:?}", e)))?,
+		state_root: input
+			.state_root
+			.as_slice()
+			.try_into()
+			.map_err(|e| WormholeLibError::from(format!("Invalid state root: {:?}", e)))?,
+		extrinsics_root: input
+			.extrinsics_root
+			.as_slice()
+			.try_into()
+			.map_err(|e| WormholeLibError::from(format!("Invalid extrinsics root: {:?}", e)))?,
+		digest: digest_padded,
+		input_amount: input_amount_quantized,
+	};
+
+	let public = PublicCircuitInputs {
+		asset_id: input.asset_id,
+		output_amount_1: input.output_amount_1,
+		output_amount_2: input.output_amount_2,
+		volume_fee_bps: input.volume_fee_bps,
+		nullifier: nullifier_bytes,
+		exit_account_1: input
+			.exit_account_1
+			.as_slice()
+			.try_into()
+			.map_err(|e| WormholeLibError::from(format!("Invalid exit account 1: {:?}", e)))?,
+		exit_account_2: input
+			.exit_account_2
+			.as_slice()
+			.try_into()
+			.map_err(|e| WormholeLibError::from(format!("Invalid exit account 2: {:?}", e)))?,
+		block_hash: input
+			.block_hash
+			.as_slice()
+			.try_into()
+			.map_err(|e| WormholeLibError::from(format!("Invalid block hash: {:?}", e)))?,
+		block_number: input.block_number,
+	};
+
+	let circuit_inputs = CircuitInputs { public, private };
+
+	// Load prover from pre-built bins
+	let prover = WormholeProver::new_from_files(prover_bin_path, common_bin_path)
+		.map_err(|e| WormholeLibError::from(format!("Failed to load prover: {}", e)))?;
+
+	let prover_with_inputs = prover
+		.commit(&circuit_inputs)
+		.map_err(|e| WormholeLibError::from(format!("Failed to commit inputs: {}", e)))?;
+
+	let proof = prover_with_inputs
+		.prove()
+		.map_err(|e| WormholeLibError::from(format!("Proof generation failed: {}", e)))?;
+
+	Ok(ProofGenerationOutput { proof_bytes: proof.to_bytes(), nullifier: *nullifier_bytes })
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+
+	#[test]
+	fn test_quantize_amount() {
+		// 1 QTU = 10^12 planck -> should quantize to 100 (1.00 with 2 decimals)
+		let result = quantize_amount(1_000_000_000_000).unwrap();
+		assert_eq!(result, 100);
+
+		// 0.01 QTU = 10^10 planck -> should quantize to 1
+		let result = quantize_amount(10_000_000_000).unwrap();
+		assert_eq!(result, 1);
+	}
+
+	#[test]
+	fn test_compute_output_amount() {
+		// 100 input with 10 bps fee -> 99.9 -> 99
+		let result = compute_output_amount(100, 10);
+		assert_eq!(result, 99);
+
+		// 1000 input with 10 bps fee -> 999
+		let result = compute_output_amount(1000, 10);
+		assert_eq!(result, 999);
+	}
+
+	#[test]
+	fn test_storage_key_computation() {
+		// Just verify it doesn't panic and returns expected length
+		let wormhole_address = [0u8; 32];
+		let key = compute_storage_key(&wormhole_address, 1);
+		// 16 (pallet) + 16 (storage) + 32 (blake2_256) = 64 bytes
+		assert_eq!(key.len(), 64);
+	}
+}
diff --git a/tests/wormhole_integration.rs b/tests/wormhole_integration.rs
index 60aa170..ec0fef7 100644
--- a/tests/wormhole_integration.rs
+++ b/tests/wormhole_integration.rs
@@ -3,7 +3,7 @@
 //! These tests require a local Quantus node running at ws://127.0.0.1:9944
 //! with funded developer accounts (crystal_alice, crystal_bob, crystal_charlie).
 //!
-//! Run with: `cargo test --test wormhole_integration -- --ignored --nocapture`
+//! Run with: `cargo test --release --test wormhole_integration -- --ignored --nocapture`
 //!
 //! The tests verify the full end-to-end flow:
 //! 1. Fund an unspendable account via wormhole transfer
@@ -15,6 +15,7 @@
 //! with valid parent hash linkage. We use batch transfers to ensure same-block proofs.
 
 use plonky2::plonk::{circuit_data::CircuitConfig, proof::ProofWithPublicInputs};
+use qp_wormhole_aggregator::aggregator::{AggregationBackend, Layer0Aggregator};
 use qp_wormhole_circuit::{
 	inputs::{CircuitInputs, PrivateCircuitInputs},
 	nullifier::Nullifier,
@@ -61,8 +62,14 @@ const SCALE_DOWN_FACTOR: u128 = 10_000_000_000;
 /// Volume fee rate in basis points (10 bps = 0.1%)
 const VOLUME_FEE_BPS: u32 = 10;
 
-/// Type alias for transfer proof storage key
-type TransferProofKey = (u32, u64, AccountId32, AccountId32, u128);
+/// Type alias for transfer proof storage key.
+/// Uses (to, transfer_count) since transfer_count is atomic per recipient.
+/// This is hashed with Blake2_256 to form the storage key suffix.
+type TransferProofKey = (AccountId32, u64);
+
+/// Full transfer data including amount - used to compute the leaf_inputs_hash via Poseidon2.
+/// This is what the ZK circuit verifies.
+type TransferProofData = (u32, u64, AccountId32, AccountId32, u128);
 
 /// Compute output amount after fee deduction
 fn compute_output_amount(input_amount: u32, fee_bps: u32) -> u32 {
@@ -330,7 +337,9 @@ async fn generate_proof_from_transfer(
 	let from_account = AccountId32::new(transfer_data.from_account.0);
 	let to_account = AccountId32::new(transfer_data.to_account.0);
 
-	let leaf_hash = qp_poseidon::PoseidonHasher::hash_storage::<TransferProofKey>(
+	// Compute leaf_inputs_hash (Poseidon2 hash of full transfer data including amount)
+	// This is what gets stored as the value and verified by the ZK circuit
+	let leaf_hash = qp_poseidon::PoseidonHasher::hash_storage::<TransferProofData>(
 		&(
 			NATIVE_ASSET_ID,
 			transfer_data.transfer_count,
@@ -341,16 +350,19 @@ async fn generate_proof_from_transfer(
 			.encode(),
 	);
 
-	let proof_address = quantus_node::api::storage().wormhole().transfer_proof((
-		NATIVE_ASSET_ID,
-		transfer_data.transfer_count,
-		transfer_data.from_account.clone(),
-		transfer_data.to_account.clone(),
-		transfer_data.amount,
-	));
+	// Build the storage key manually:
+	// Key = Twox128("Wormhole") || Twox128("TransferProof") || Blake2_256(to, transfer_count)
+	// Compute the prefix using Twox128 hashes
+	let pallet_hash = sp_core::twox_128(b"Wormhole");
+	let storage_hash = sp_core::twox_128(b"TransferProof");
+	let mut final_key = Vec::with_capacity(32 + 32); // prefix + blake2_256 hash
+	final_key.extend_from_slice(&pallet_hash);
+	final_key.extend_from_slice(&storage_hash);
 
-	let mut final_key = proof_address.to_root_bytes();
-	final_key.extend_from_slice(&leaf_hash);
+	// Hash the key tuple with Blake2_256 and append
+	let key_tuple: TransferProofKey = (to_account.clone(), transfer_data.transfer_count);
+	let key_hash = sp_core::blake2_256(&key_tuple.encode());
+	final_key.extend_from_slice(&key_hash);
 
 	let storage_api = client.storage().at(block_hash);
 	let val = storage_api
@@ -456,7 +468,7 @@ async fn generate_proof_from_transfer(
 async fn submit_single_proof_for_verification(
 	quantus_client: &QuantusClient,
 	proof_bytes: Vec<u8>,
-) -> Result<(), String> {
+) -> Result<subxt::utils::H256, String> {
 	println!("  Submitting single proof for on-chain verification...");
 
 	let verify_tx = quantus_node::api::tx().wormhole().verify_aggregated_proof(proof_bytes);
@@ -477,7 +489,7 @@ async fn submit_single_proof_for_verification(
 			TxStatus::InBestBlock(tx_in_block) => {
 				let block_hash = tx_in_block.block_hash();
 				println!("  ✅ Single proof verified on-chain! Block: {:?}", block_hash);
-				return Ok(());
+				return Ok(block_hash);
 			},
 			TxStatus::InFinalizedBlock(tx_in_block) => {
 				let block_hash = tx_in_block.block_hash();
@@ -485,7 +497,7 @@ async fn submit_single_proof_for_verification(
 					"  ✅ Single proof verified on-chain (finalized)! Block: {:?}",
 					block_hash
 				);
-				return Ok(());
+				return Ok(block_hash);
 			},
 			TxStatus::Error { message } | TxStatus::Invalid { message } => {
 				return Err(format!("Transaction failed: {}", message));
@@ -502,39 +514,25 @@ fn aggregate_proofs(
 	proof_contexts: Vec<ProofContext>,
 	num_leaf_proofs: usize,
 ) -> Result<AggregatedProofContext, String> {
-	use qp_wormhole_aggregator::aggregator::{AggregationBackend, Layer0Aggregator};
-	use std::sync::Once;
-
-	static BINS_INIT: Once = Once::new();
-	const TEST_BINS_DIR: &str = "test-generated-bins";
-
 	println!(
 		"  Aggregating {} proofs (num_leaf_proofs={})...",
 		proof_contexts.len(),
 		num_leaf_proofs,
 	);
 
-	BINS_INIT.call_once(|| {
-		qp_wormhole_circuit_builder::generate_all_circuit_binaries(
-			TEST_BINS_DIR,
-			true,
-			num_leaf_proofs,
-			None,
-		)
-		.expect("Failed to generate circuit binaries");
-	});
-
-	let mut aggregator = Layer0Aggregator::new(TEST_BINS_DIR)
-		.map_err(|e| format!("Failed to create aggregator: {}", e))?;
-
-	if proof_contexts.len() > aggregator.batch_size() {
+	if proof_contexts.len() > num_leaf_proofs {
 		return Err(format!(
 			"Too many proofs: {} provided, max {}",
 			proof_contexts.len(),
-			aggregator.batch_size(),
+			num_leaf_proofs,
 		));
 	}
 
+	let bins_dir = std::path::Path::new("generated-bins");
+
+	let mut aggregator = Layer0Aggregator::new(bins_dir)
+		.map_err(|e| format!("Failed to create aggregator: {}", e))?;
+
 	for (idx, ctx) in proof_contexts.into_iter().enumerate() {
 		println!("    Adding proof {} to aggregator...", idx + 1);
 		println!("      Public inputs:");
@@ -554,20 +552,20 @@ fn aggregate_proofs(
 	}
 
 	println!("  Running aggregation (this may take ~60s)...");
-	let aggregated_result =
+	let aggregated_proof =
 		aggregator.aggregate().map_err(|e| format!("Aggregation failed: {}", e))?;
 
 	use qp_wormhole_circuit::inputs::ParseAggregatedPublicInputs;
 	let public_inputs =
-		AggregatedPublicCircuitInputs::try_from_felts(aggregated_result.public_inputs.as_slice())
+		AggregatedPublicCircuitInputs::try_from_felts(aggregated_proof.public_inputs.as_slice())
 			.map_err(|e| format!("Failed to parse aggregated public inputs: {}", e))?;
 
 	println!("  Verifying aggregated proof locally...");
 	aggregator
-		.verify(aggregated_result.clone())
+		.verify(aggregated_proof.clone())
 		.map_err(|e| format!("Local verification failed: {}", e))?;
 
-	let proof_bytes = aggregated_result.to_bytes();
+	let proof_bytes = aggregated_proof.to_bytes();
 	println!(
 		"  Aggregation complete! Size: {} bytes, {} nullifiers",
 		proof_bytes.len(),
@@ -581,7 +579,7 @@ fn aggregate_proofs(
 async fn submit_aggregated_proof_for_verification(
 	quantus_client: &QuantusClient,
 	proof_bytes: Vec<u8>,
-) -> Result<(), String> {
+) -> Result<subxt::utils::H256, String> {
 	println!("  Submitting aggregated proof for on-chain verification...");
 
 	let verify_tx = quantus_node::api::tx().wormhole().verify_aggregated_proof(proof_bytes);
@@ -602,7 +600,7 @@ async fn submit_aggregated_proof_for_verification(
 			TxStatus::InBestBlock(tx_in_block) => {
 				let block_hash = tx_in_block.block_hash();
 				println!("  ✅ Aggregated proof verified on-chain! Block: {:?}", block_hash);
-				return Ok(());
+				return Ok(block_hash);
 			},
 			TxStatus::InFinalizedBlock(tx_in_block) => {
 				let block_hash = tx_in_block.block_hash();
@@ -610,7 +608,7 @@ async fn submit_aggregated_proof_for_verification(
 					"  ✅ Aggregated proof verified on-chain (finalized)! Block: {:?}",
 					block_hash
 				);
-				return Ok(());
+				return Ok(block_hash);
 			},
 			TxStatus::Error { message } | TxStatus::Invalid { message } => {
 				return Err(format!("Transaction failed: {}", message));
@@ -622,12 +620,19 @@ async fn submit_aggregated_proof_for_verification(
 	Err("Transaction stream ended unexpectedly".to_string())
 }
 
+const POW_ENGINE_ID: [u8; 4] = *b"pow_";
+
 fn author_from_header_digest(
 	header_digest: &subxt::config::substrate::Digest,
 ) -> Option<SubxtAccountId> {
 	header_digest.logs.iter().find_map(|item| match item {
-		subxt::config::substrate::DigestItem::PreRuntime(_engine_id, data) =>
-			SubxtAccountId::decode(&mut &data[..]).ok(),
+		subxt::config::substrate::DigestItem::PreRuntime(engine_id, data)
+			if *engine_id == POW_ENGINE_ID && data.len() == 32 =>
+		{
+			let preimage: [u8; 32] = data.as_slice().try_into().ok()?;
+			let author_bytes = qp_poseidon_core::rehash_to_bytes(&preimage);
+			SubxtAccountId::decode(&mut &author_bytes[..]).ok()
+		},
 		_ => None,
 	})
 }
@@ -711,16 +716,11 @@ async fn test_single_proof_on_chain_verification() {
 	// Submit for on-chain verification
 	println!("4. Verifying proof on-chain...");
 
-	// Submit extrinsic
-	submit_single_proof_for_verification(&quantus_client, proof_context.proof_bytes.clone())
-		.await
-		.expect("On-chain verification failed");
-
-	// Find the block that executed verification
-	let verify_block_hash = quantus_client
-		.get_latest_block()
-		.await
-		.expect("Failed to get latest block hash");
+	// Submit extrinsic and find the block that executed verification
+	let verify_block_hash =
+		submit_single_proof_for_verification(&quantus_client, proof_context.proof_bytes.clone())
+			.await
+			.expect("On-chain verification failed");
 
 	// Extract author from block digest
 	let verify_block = quantus_client.client().blocks().at(verify_block_hash).await.unwrap();
@@ -861,19 +861,13 @@ async fn test_aggregated_proof_on_chain_verification() {
 	println!("5. Verifying aggregated proof on-chain...");
 
 	// Submit aggregated verification
-	submit_aggregated_proof_for_verification(
+	let verify_block_hash = submit_aggregated_proof_for_verification(
 		&quantus_client,
 		aggregated_context.proof_bytes.clone(),
 	)
 	.await
 	.expect("On-chain aggregated verification failed");
 
-	// Identify verification block
-	let verify_block_hash = quantus_client
-		.get_latest_block()
-		.await
-		.expect("Failed to get latest block hash");
-
 	// Extract author
 	let verify_block = quantus_client.client().blocks().at(verify_block_hash).await.unwrap();
 	let header = verify_block.header();
