From 1431e1c34d2fce9f6469129b29f0bbc44d1f765c Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 23 Apr 2026 19:53:51 -0500 Subject: [PATCH] *Aligns proving flow with fixed 2x8 compact 16-proof build *Adds a developer path to rebuild local circuit artifacts from the local checkout and clears stale outputs so CLI and chain builds stay in sync. *Enforces the shipping 16-leaf aggregation shape, expands artifact validation, and fails fast when generated proofs or public inputs drift from the stable contract. *Refreshes runtime metadata to expose ZK tree state and proof APIs needed for Merkle-proof based verification. --- Cargo.lock | 21 +- Cargo.toml | 9 + README.md | 28 +- build.rs | 40 ++- src/bins.rs | 62 ++-- src/bins_consts.rs | 25 +- src/chain/quantus_subxt.rs | 683 +++++++++++++++++++++++++++++++------ src/cli/mod.rs | 208 +++++++++++ src/cli/wormhole.rs | 32 +- src/collect_rewards_lib.rs | 14 + src/quantus_metadata.scale | Bin 161568 -> 163813 bytes 11 files changed, 957 insertions(+), 165 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index add5cc9..7349c50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3840,8 +3840,7 @@ dependencies = [ [[package]] name = "qp-wormhole-aggregator" version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a23eea8e17c97632f5056d1fecdb83d2e997f79d823fb97c514823fce8580" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=compact-2x8-aggr#ab50dc303041f4a4fdd27bebba8aa380dc48723c" dependencies = [ "anyhow", "hex", @@ -3859,8 +3858,7 @@ dependencies = [ [[package]] name = "qp-wormhole-circuit" version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b72abd3357e1c486621431109d83a259076ed941fcc7f035353a629940c443d9" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=compact-2x8-aggr#ab50dc303041f4a4fdd27bebba8aa380dc48723c" dependencies = [ "anyhow", "hex", @@ -3872,8 +3870,7 @@ dependencies = [ [[package]] name = "qp-wormhole-circuit-builder" version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d27c981d34a35cb10ee96e16c1fb44fbb4c52f3e58a787ce089c21bf5469514" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=compact-2x8-aggr#ab50dc303041f4a4fdd27bebba8aa380dc48723c" dependencies = [ "anyhow", "clap", @@ -3886,8 +3883,7 @@ dependencies = [ [[package]] name = "qp-wormhole-inputs" version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69e54a0f450d349cb1169bbb743a7a846475a1210760308e38f55841e5aa5c0" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=compact-2x8-aggr#ab50dc303041f4a4fdd27bebba8aa380dc48723c" dependencies = [ "anyhow", ] @@ -3895,8 +3891,7 @@ dependencies = [ [[package]] name = "qp-wormhole-prover" version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68cee4d6a0317f89d1ec7a6c40a9592e5525c75b9cb21bd7d5d3b408301068bd" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=compact-2x8-aggr#ab50dc303041f4a4fdd27bebba8aa380dc48723c" dependencies = [ "anyhow", "qp-plonky2", @@ -3908,8 +3903,7 @@ dependencies = [ [[package]] name = "qp-wormhole-verifier" version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f7c0134a766c6624183d129abf12523247e38fc27860e3a9778f73321008ae" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=compact-2x8-aggr#ab50dc303041f4a4fdd27bebba8aa380dc48723c" dependencies = [ "anyhow", "qp-plonky2-verifier", @@ -3919,8 +3913,7 @@ dependencies = [ [[package]] name = "qp-zk-circuits-common" version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5e1d764560fd797f71defbf75a1618596945c53e33033ac42cc7c2e4689f6" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=compact-2x8-aggr#ab50dc303041f4a4fdd27bebba8aa380dc48723c" dependencies = [ "anyhow", "hex", diff --git a/Cargo.toml b/Cargo.toml index a7b8668..928a800 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,15 @@ tempfile = "3.8.1" serial_test = "3.1" qp-poseidon-core = "1.4.0" +[patch.crates-io] +qp-wormhole-aggregator = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "compact-2x8-aggr" } +qp-wormhole-circuit = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "compact-2x8-aggr" } +qp-wormhole-circuit-builder = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "compact-2x8-aggr" } +qp-wormhole-inputs = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "compact-2x8-aggr" } +qp-wormhole-prover = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "compact-2x8-aggr" } +qp-wormhole-verifier = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "compact-2x8-aggr" } +qp-zk-circuits-common = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "compact-2x8-aggr" } + # 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. diff --git a/README.md b/README.md index 3d4449a..bc1d2cd 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ quantus wormhole multiround \ ``` - `--num-proofs`: Number of proofs per round (1 to `num_leaf_proofs` from circuit config, default: 2). + The shipping layer-0 circuit capacity is fixed at 16 proofs. - `--rounds`: Number of rounds (default: 2). In intermediate rounds, exit accounts are the next round's wormhole addresses; in the final round, funds exit back to the wallet. - `--amount`: Total amount in planck to randomly partition across proofs (default: 100 DEV). - `--wallet`: Wallet name for funding (round 1) and final exit. @@ -295,11 +296,19 @@ quantus wormhole check-nullifier --secret 0x<64-hex-chars> --transfer-counts 0-5 #### `quantus developer build-circuits` -Build ZK circuit binaries from the `qp-zk-circuits` repository, then copy them to the CLI and chain directories. This is required whenever the circuit logic changes. +Build ZK circuit binaries from the local `qp-zk-circuits` checkout. This is required whenever the circuit logic changes. ```bash quantus developer build-circuits \ - --num-leaf-proofs 2 \ + --num-leaf-proofs 16 \ + --chain-path ../chain +``` + +Add `--num-layer0-proofs` only when you also want layer-1 artifacts: + +```bash +quantus developer build-circuits \ + --num-leaf-proofs 16 \ --num-layer0-proofs 2 \ --chain-path ../chain ``` @@ -308,24 +317,23 @@ Add `--skip-prover` when you only need verifier artifacts: ```bash quantus developer build-circuits \ - --num-leaf-proofs 2 \ - --num-layer0-proofs 2 \ + --num-leaf-proofs 16 \ --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. +- `--num-leaf-proofs`: Compatibility flag retained for the builder API. Production PR #129 output is fixed at `16`, so other values are rejected. +- `--num-layer0-proofs`: Number of layer-0 proofs per layer-1 aggregation. Omit this for normal layer-0 generation. - `--chain-path`: Path to the chain repo (default: `../chain`). -- `--skip-chain`: Skip copying binaries to the chain directory. +- `--skip-chain`: Skip clearing stale wormhole build outputs in the chain repo. - `--skip-prover`: Skip generating prover binaries. **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. +2. Calls the `qp-wormhole-circuit-builder` library directly to regenerate binary files in `generated-bins/` using the fixed shipping layer-0 output (`common.bin`, `verifier.bin`, `prover.bin`, `dummy_proof.bin`, `inner_*`, `outer_*`, `aggregated_common.bin`, `aggregated_verifier.bin`, `aggregated_prover.bin`, `aggregated_targets.bin`, and `config.json`, plus optional layer-1 files when requested). +3. Clears stale wormhole build outputs in the chain repo so the next chain build regenerates verifier artifacts from the same local `qp-zk-circuits` source. -After running, rebuild the chain (`cargo build --release` in the chain directory) so `include_bytes!()` picks up the new binaries. +After running, rebuild the chain so `include_bytes!()` picks up freshly regenerated verifier binaries from the same local dependency source. #### `quantus developer create-test-wallets` diff --git a/build.rs b/build.rs index d9bda18..d0d1237 100644 --- a/build.rs +++ b/build.rs @@ -38,6 +38,16 @@ fn print_bin_hash(dir: &Path, filename: &str) { } } +fn remove_existing_path(path: &Path) { + if let Ok(metadata) = std::fs::symlink_metadata(path) { + if metadata.file_type().is_symlink() || !metadata.is_dir() { + std::fs::remove_file(path).expect("Failed to remove existing file/symlink"); + } else { + std::fs::remove_dir_all(path).expect("Failed to remove existing directory"); + } + } +} + fn main() { // Allow skipping circuit generation for CI jobs that don't need it if env::var("SKIP_CIRCUIT_BUILD").is_ok() { @@ -55,6 +65,11 @@ fn main() { let num_leaf_proofs: usize = env::var("QP_NUM_LEAF_PROOFS") .map(|v| v.parse().expect("QP_NUM_LEAF_PROOFS must be a valid usize")) .unwrap_or(DEFAULT_NUM_LEAF_PROOFS); + if num_leaf_proofs != DEFAULT_NUM_LEAF_PROOFS { + panic!( + "PR #129 shipping layer-0 aggregation is fixed at 16 leaves. Set QP_NUM_LEAF_PROOFS=16 or unset it." + ); + } // Don't emit any rerun-if-changed directives - this forces the build script // to run on every build. Circuit generation is fast enough in release mode. @@ -66,13 +81,14 @@ fn main() { let start = Instant::now(); + remove_existing_path(&build_output_dir); std::fs::create_dir_all(&build_output_dir) .expect("Failed to create generated-bins directory in OUT_DIR"); qp_wormhole_circuit_builder::generate_all_circuit_binaries( &build_output_dir, true, - num_leaf_proofs, + DEFAULT_NUM_LEAF_PROOFS, None, ) .expect("Failed to generate circuit binaries"); @@ -88,13 +104,9 @@ fn main() { ); // Print hashes of generated binaries - print_bin_hash(&build_output_dir, "common.bin"); - print_bin_hash(&build_output_dir, "verifier.bin"); - print_bin_hash(&build_output_dir, "prover.bin"); - print_bin_hash(&build_output_dir, "dummy_proof.bin"); - print_bin_hash(&build_output_dir, "aggregated_common.bin"); - print_bin_hash(&build_output_dir, "aggregated_verifier.bin"); - print_bin_hash(&build_output_dir, "aggregated_prover.bin"); + for filename in REQUIRED_BIN_FILES { + print_bin_hash(&build_output_dir, filename); + } // Copy bins to project root for runtime access, but only during local source // builds — never during `cargo publish` verification (manifest_dir is inside @@ -111,16 +123,7 @@ fn main() { #[cfg(unix)] { use std::os::unix::fs::symlink; - // Remove any existing dir/file/symlink at destination. - if let Ok(meta) = std::fs::symlink_metadata(&project_bins) { - if meta.is_dir() { - std::fs::remove_dir_all(&project_bins) - .expect("Failed to remove existing generated-bins directory"); - } else { - std::fs::remove_file(&project_bins) - .expect("Failed to remove existing generated-bins file/symlink"); - } - } + remove_existing_path(&project_bins); if let Err(e) = symlink(&build_output_dir, &project_bins) { println!( "cargo:warning=[quantus-cli] Failed to symlink generated-bins ({}). Falling back to copy...", @@ -132,6 +135,7 @@ fn main() { } } + remove_existing_path(&project_bins); std::fs::create_dir_all(&project_bins).expect("Failed to create generated-bins directory"); let entries = std::fs::read_dir(&build_output_dir) .expect("Failed to read generated-bins directory in OUT_DIR"); diff --git a/src/bins.rs b/src/bins.rs index a526449..a987d2e 100644 --- a/src/bins.rs +++ b/src/bins.rs @@ -24,17 +24,6 @@ include!("bins_consts.rs"); pub const BINS_DIR_ENV: &str = "QUANTUS_BINS_DIR"; /// Files that must be present for all wormhole operations to succeed. -const REQUIRED_FILES: &[&str] = &[ - "prover.bin", - "verifier.bin", - "common.bin", - "aggregated_prover.bin", - "aggregated_verifier.bin", - "aggregated_common.bin", - "dummy_proof.bin", - "config.json", -]; - /// Resolve the path where circuit binaries should live. /// /// This never generates anything; see [`ensure_bins_dir`] for the full @@ -71,13 +60,13 @@ pub fn ensure_bins_dir() -> Result { return Ok(dir); } - let num_leaf_proofs = env_num_leaf_proofs(); + let num_leaf_proofs = env_num_leaf_proofs()?; generate(&dir, num_leaf_proofs)?; Ok(dir) } fn is_ready(dir: &Path) -> bool { - if !REQUIRED_FILES.iter().all(|f| dir.join(f).exists()) { + if !REQUIRED_BIN_FILES.iter().all(|f| dir.join(f).exists()) { return false; } match std::fs::read_to_string(dir.join(VERSION_MARKER)) { @@ -86,14 +75,44 @@ fn is_ready(dir: &Path) -> bool { } } -fn env_num_leaf_proofs() -> usize { - std::env::var("QP_NUM_LEAF_PROOFS") +fn env_num_leaf_proofs() -> Result { + let num_leaf_proofs = std::env::var("QP_NUM_LEAF_PROOFS") .ok() .and_then(|v| v.parse().ok()) - .unwrap_or(DEFAULT_NUM_LEAF_PROOFS) + .unwrap_or(DEFAULT_NUM_LEAF_PROOFS); + if num_leaf_proofs != DEFAULT_NUM_LEAF_PROOFS { + return Err(QuantusError::Generic( + "PR #129 shipping layer-0 aggregation is fixed at 16 leaves. Set QP_NUM_LEAF_PROOFS=16 or unset it.".to_string(), + )); + } + Ok(num_leaf_proofs) +} + +fn remove_existing_path(path: &Path) -> Result<()> { + if let Ok(metadata) = std::fs::symlink_metadata(path) { + if metadata.file_type().is_symlink() || !metadata.is_dir() { + std::fs::remove_file(path).map_err(|e| { + QuantusError::Generic(format!( + "Failed to remove existing bins file {}: {}", + path.display(), + e + )) + })?; + } else { + std::fs::remove_dir_all(path).map_err(|e| { + QuantusError::Generic(format!( + "Failed to remove existing bins directory {}: {}", + path.display(), + e + )) + })?; + } + } + Ok(()) } fn generate(dir: &Path, num_leaf_proofs: usize) -> Result<()> { + remove_existing_path(dir)?; std::fs::create_dir_all(dir).map_err(|e| { QuantusError::Generic(format!("Failed to create bins directory {}: {}", dir.display(), e)) })?; @@ -104,10 +123,13 @@ fn generate(dir: &Path, num_leaf_proofs: usize) -> Result<()> { log_print!(" num_leaf_proofs: {}", num_leaf_proofs); let start = std::time::Instant::now(); - qp_wormhole_circuit_builder::generate_all_circuit_binaries(dir, true, num_leaf_proofs, None) - .map_err(|e| { - QuantusError::Generic(format!("Failed to generate circuit binaries: {}", e)) - })?; + qp_wormhole_circuit_builder::generate_all_circuit_binaries( + dir, + true, + DEFAULT_NUM_LEAF_PROOFS, + None, + ) + .map_err(|e| QuantusError::Generic(format!("Failed to generate circuit binaries: {}", e)))?; std::fs::write(dir.join(VERSION_MARKER), env!("CARGO_PKG_VERSION")) .map_err(|e| QuantusError::Generic(format!("Failed to write version marker: {}", e)))?; diff --git a/src/bins_consts.rs b/src/bins_consts.rs index 068a8f4..bbd9b8d 100644 --- a/src/bins_consts.rs +++ b/src/bins_consts.rs @@ -1,8 +1,29 @@ /// Filename of the marker recording which CLI version produced the bins. /// When the CLI is upgraded this mismatches and the runtime regenerates. /// Shared by `build.rs` and `crate::bins` via `include!`. -const VERSION_MARKER: &str = ".quantus-cli-version"; +pub(crate) const VERSION_MARKER: &str = ".quantus-cli-version"; /// Number of leaf proofs aggregated into a single batch (default for both /// build-time generation and runtime lazy generation). -const DEFAULT_NUM_LEAF_PROOFS: usize = 16; +pub(crate) const DEFAULT_NUM_LEAF_PROOFS: usize = 16; + +/// Files required for local proving, aggregation, and verifier compatibility. +pub(crate) const REQUIRED_BIN_FILES: &[&str] = &[ + "common.bin", + "verifier.bin", + "prover.bin", + "dummy_proof.bin", + "inner_common.bin", + "inner_verifier.bin", + "inner_prover.bin", + "inner_targets.bin", + "outer_common.bin", + "outer_verifier.bin", + "outer_prover.bin", + "outer_targets.bin", + "aggregated_common.bin", + "aggregated_verifier.bin", + "aggregated_prover.bin", + "aggregated_targets.bin", + "config.json", +]; diff --git a/src/chain/quantus_subxt.rs b/src/chain/quantus_subxt.rs index bc15567..bebfa27 100644 --- a/src/chain/quantus_subxt.rs +++ b/src/chain/quantus_subxt.rs @@ -6,7 +6,7 @@ pub mod api { mod root_mod { pub use super::*; } - pub static PALLETS: [&str; 20usize] = [ + pub static PALLETS: [&str; 21usize] = [ "System", "Timestamp", "Balances", @@ -27,8 +27,9 @@ pub mod api { "AssetsHolder", "Multisig", "Wormhole", + "ZkTree", ]; - pub static RUNTIME_APIS: [&str; 11usize] = [ + pub static RUNTIME_APIS: [&str; 12usize] = [ "Core", "Metadata", "BlockBuilder", @@ -36,6 +37,7 @@ pub mod api { "OffchainWorkerApi", "SessionKeys", "QPoWApi", + "ZkTreeApi", "AccountNonceApi", "TransactionPaymentApi", "TransactionPaymentCallApi", @@ -89,6 +91,9 @@ pub mod api { pub fn q_po_w_api(&self) -> q_po_w_api::QPoWApi { q_po_w_api::QPoWApi } + pub fn zk_tree_api(&self) -> zk_tree_api::ZkTreeApi { + zk_tree_api::ZkTreeApi + } pub fn account_nonce_api(&self) -> account_nonce_api::AccountNonceApi { account_nonce_api::AccountNonceApi } @@ -142,10 +147,9 @@ pub mod api { "execute_block", types::ExecuteBlock { block }, [ - 81u8, 130u8, 143u8, 72u8, 156u8, 15u8, 28u8, 87u8, 117u8, 10u8, 192u8, - 249u8, 117u8, 214u8, 184u8, 13u8, 148u8, 224u8, 167u8, 170u8, 101u8, - 194u8, 229u8, 140u8, 199u8, 115u8, 73u8, 99u8, 183u8, 205u8, 98u8, - 33u8, + 180u8, 202u8, 143u8, 166u8, 125u8, 9u8, 195u8, 179u8, 198u8, 129u8, + 73u8, 45u8, 225u8, 244u8, 234u8, 234u8, 177u8, 61u8, 241u8, 26u8, 75u8, + 53u8, 218u8, 66u8, 56u8, 46u8, 81u8, 29u8, 120u8, 51u8, 249u8, 42u8, ], ) } @@ -162,9 +166,9 @@ pub mod api { "initialize_block", types::InitializeBlock { header }, [ - 112u8, 139u8, 92u8, 30u8, 37u8, 99u8, 47u8, 83u8, 221u8, 31u8, 204u8, - 129u8, 102u8, 92u8, 144u8, 80u8, 3u8, 98u8, 157u8, 5u8, 20u8, 31u8, - 110u8, 105u8, 86u8, 91u8, 173u8, 19u8, 140u8, 246u8, 60u8, 223u8, + 147u8, 82u8, 214u8, 164u8, 66u8, 79u8, 193u8, 216u8, 89u8, 29u8, 83u8, + 182u8, 99u8, 81u8, 244u8, 247u8, 88u8, 158u8, 251u8, 224u8, 6u8, 76u8, + 247u8, 87u8, 83u8, 98u8, 177u8, 188u8, 190u8, 219u8, 179u8, 107u8, ], ) } @@ -409,9 +413,9 @@ pub mod api { "finalize_block", types::FinalizeBlock {}, [ - 135u8, 81u8, 28u8, 123u8, 19u8, 171u8, 129u8, 82u8, 85u8, 96u8, 238u8, - 155u8, 211u8, 153u8, 243u8, 31u8, 189u8, 82u8, 91u8, 225u8, 78u8, 48u8, - 241u8, 236u8, 143u8, 65u8, 91u8, 167u8, 114u8, 146u8, 31u8, 197u8, + 183u8, 30u8, 223u8, 196u8, 0u8, 182u8, 94u8, 4u8, 106u8, 92u8, 190u8, + 77u8, 139u8, 95u8, 162u8, 171u8, 99u8, 168u8, 70u8, 165u8, 175u8, + 228u8, 97u8, 27u8, 25u8, 141u8, 244u8, 217u8, 5u8, 95u8, 49u8, 12u8, ], ) } @@ -449,10 +453,10 @@ pub mod api { "check_inherents", types::CheckInherents { block, data }, [ - 44u8, 230u8, 134u8, 154u8, 73u8, 173u8, 160u8, 231u8, 223u8, 148u8, - 247u8, 104u8, 214u8, 168u8, 43u8, 202u8, 204u8, 14u8, 148u8, 154u8, - 9u8, 103u8, 239u8, 45u8, 186u8, 21u8, 97u8, 136u8, 200u8, 108u8, 205u8, - 167u8, + 137u8, 14u8, 89u8, 87u8, 225u8, 80u8, 136u8, 255u8, 242u8, 41u8, 90u8, + 91u8, 140u8, 192u8, 112u8, 230u8, 46u8, 19u8, 149u8, 104u8, 153u8, + 122u8, 92u8, 215u8, 161u8, 88u8, 142u8, 248u8, 156u8, 159u8, 203u8, + 132u8, ], ) } @@ -633,10 +637,9 @@ pub mod api { "offchain_worker", types::OffchainWorker { header }, [ - 131u8, 199u8, 206u8, 86u8, 209u8, 109u8, 229u8, 152u8, 235u8, 155u8, - 35u8, 252u8, 70u8, 180u8, 47u8, 173u8, 84u8, 182u8, 176u8, 164u8, - 107u8, 88u8, 249u8, 181u8, 85u8, 174u8, 240u8, 226u8, 254u8, 189u8, - 167u8, 155u8, + 143u8, 60u8, 69u8, 66u8, 143u8, 216u8, 150u8, 58u8, 130u8, 32u8, 160u8, + 205u8, 92u8, 9u8, 114u8, 94u8, 74u8, 160u8, 105u8, 199u8, 246u8, 174u8, + 187u8, 111u8, 225u8, 121u8, 82u8, 29u8, 225u8, 115u8, 119u8, 94u8, ], ) } @@ -1177,6 +1180,175 @@ pub mod api { } } } + pub mod zk_tree_api { + use super::{root_mod, runtime_types}; + #[doc = " Runtime API for the ZK Tree pallet."] + #[doc = ""] + #[doc = " Provides methods to query the ZK Merkle tree state and generate proofs."] + pub struct ZkTreeApi; + impl ZkTreeApi { + #[doc = " Get the current root hash of the ZK tree."] + pub fn get_root( + &self, + ) -> ::subxt::ext::subxt_core::runtime_api::payload::StaticPayload< + types::GetRoot, + types::get_root::output::Output, + > { + ::subxt::ext::subxt_core::runtime_api::payload::StaticPayload::new_static( + "ZkTreeApi", + "get_root", + types::GetRoot {}, + [ + 198u8, 156u8, 112u8, 8u8, 86u8, 226u8, 81u8, 137u8, 207u8, 161u8, + 181u8, 99u8, 42u8, 221u8, 134u8, 50u8, 205u8, 191u8, 124u8, 165u8, + 174u8, 99u8, 193u8, 28u8, 46u8, 125u8, 2u8, 117u8, 173u8, 187u8, 243u8, + 73u8, + ], + ) + } + #[doc = " Get the current number of leaves in the tree."] + pub fn get_leaf_count( + &self, + ) -> ::subxt::ext::subxt_core::runtime_api::payload::StaticPayload< + types::GetLeafCount, + types::get_leaf_count::output::Output, + > { + ::subxt::ext::subxt_core::runtime_api::payload::StaticPayload::new_static( + "ZkTreeApi", + "get_leaf_count", + types::GetLeafCount {}, + [ + 1u8, 246u8, 191u8, 192u8, 156u8, 247u8, 209u8, 87u8, 187u8, 48u8, 98u8, + 144u8, 34u8, 200u8, 186u8, 202u8, 176u8, 64u8, 93u8, 165u8, 103u8, + 231u8, 77u8, 57u8, 52u8, 14u8, 213u8, 197u8, 118u8, 137u8, 64u8, 193u8, + ], + ) + } + #[doc = " Get the current depth of the tree."] + pub fn get_depth( + &self, + ) -> ::subxt::ext::subxt_core::runtime_api::payload::StaticPayload< + types::GetDepth, + types::get_depth::output::Output, + > { + ::subxt::ext::subxt_core::runtime_api::payload::StaticPayload::new_static( + "ZkTreeApi", + "get_depth", + types::GetDepth {}, + [ + 251u8, 48u8, 136u8, 161u8, 26u8, 3u8, 205u8, 80u8, 135u8, 48u8, 176u8, + 105u8, 254u8, 86u8, 79u8, 57u8, 70u8, 227u8, 26u8, 48u8, 228u8, 203u8, + 109u8, 59u8, 3u8, 204u8, 30u8, 40u8, 113u8, 62u8, 123u8, 47u8, + ], + ) + } + #[doc = " Get a Merkle proof for a leaf at the given index."] + #[doc = ""] + #[doc = " Returns `None` if the leaf index is out of bounds."] + pub fn get_merkle_proof( + &self, + leaf_index: types::get_merkle_proof::LeafIndex, + ) -> ::subxt::ext::subxt_core::runtime_api::payload::StaticPayload< + types::GetMerkleProof, + types::get_merkle_proof::output::Output, + > { + ::subxt::ext::subxt_core::runtime_api::payload::StaticPayload::new_static( + "ZkTreeApi", + "get_merkle_proof", + types::GetMerkleProof { leaf_index }, + [ + 74u8, 206u8, 166u8, 44u8, 235u8, 100u8, 174u8, 217u8, 16u8, 167u8, + 180u8, 174u8, 192u8, 121u8, 153u8, 163u8, 204u8, 104u8, 52u8, 198u8, + 44u8, 180u8, 144u8, 37u8, 216u8, 58u8, 175u8, 36u8, 50u8, 214u8, 66u8, + 131u8, + ], + ) + } + } + pub mod types { + use super::runtime_types; + pub mod get_root { + use super::runtime_types; + pub mod output { + use super::runtime_types; + pub type Output = [::core::primitive::u8; 32usize]; + } + } + #[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" + )] + pub struct GetRoot {} + pub mod get_leaf_count { + use super::runtime_types; + pub mod output { + use super::runtime_types; + pub type Output = ::core::primitive::u64; + } + } + #[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" + )] + pub struct GetLeafCount {} + pub mod get_depth { + use super::runtime_types; + pub mod output { + use super::runtime_types; + pub type Output = ::core::primitive::u8; + } + } + #[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" + )] + pub struct GetDepth {} + pub mod get_merkle_proof { + use super::runtime_types; + pub type LeafIndex = ::core::primitive::u64; + pub mod output { + use super::runtime_types; + pub type Output = + ::core::option::Option; + } + } + #[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" + )] + pub struct GetMerkleProof { + pub leaf_index: get_merkle_proof::LeafIndex, + } + } + } pub mod account_nonce_api { use super::{root_mod, runtime_types}; #[doc = " The API to query account nonce."] @@ -1885,6 +2057,9 @@ pub mod api { pub fn wormhole(&self) -> wormhole::storage::StorageApi { wormhole::storage::StorageApi } + pub fn zk_tree(&self) -> zk_tree::storage::StorageApi { + zk_tree::storage::StorageApi + } } pub struct TransactionApi; impl TransactionApi { @@ -1952,9 +2127,9 @@ pub mod api { .hash(); runtime_metadata_hash == [ - 95u8, 161u8, 29u8, 63u8, 6u8, 191u8, 169u8, 252u8, 9u8, 18u8, 126u8, 99u8, 29u8, - 6u8, 27u8, 29u8, 251u8, 189u8, 252u8, 166u8, 35u8, 141u8, 155u8, 35u8, 238u8, 85u8, - 244u8, 246u8, 49u8, 113u8, 167u8, 219u8, + 38u8, 70u8, 211u8, 247u8, 9u8, 225u8, 49u8, 109u8, 7u8, 110u8, 190u8, 92u8, 4u8, + 103u8, 25u8, 232u8, 138u8, 5u8, 214u8, 230u8, 238u8, 225u8, 70u8, 126u8, 86u8, + 133u8, 218u8, 42u8, 55u8, 63u8, 23u8, 207u8, ] } pub mod system { @@ -2695,6 +2870,10 @@ pub mod api { use super::runtime_types; pub type Digest = runtime_types::sp_runtime::generic::digest::Digest; } + pub mod zk_tree_root { + use super::runtime_types; + pub type ZkTreeRoot = ::subxt::ext::subxt_core::utils::H256; + } pub mod events { use super::runtime_types; pub type Events = ::subxt::ext::subxt_core::alloc::vec::Vec< @@ -3032,6 +3211,31 @@ pub mod api { ], ) } + #[doc = " ZK tree root for the current block."] + #[doc = ""] + #[doc = " Set by pallet-zk-tree during block finalization. This is included in the"] + #[doc = " block header as a dedicated field (not in digest) to ensure a fixed offset"] + #[doc = " for ZK circuit verification."] + pub fn zk_tree_root( + &self, + ) -> ::subxt::ext::subxt_core::storage::address::StaticAddress< + (), + types::zk_tree_root::ZkTreeRoot, + ::subxt::ext::subxt_core::utils::Yes, + ::subxt::ext::subxt_core::utils::Yes, + (), + > { + ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( + "System", + "ZkTreeRoot", + (), + [ + 20u8, 224u8, 59u8, 4u8, 237u8, 141u8, 176u8, 61u8, 156u8, 45u8, 84u8, + 60u8, 215u8, 14u8, 69u8, 234u8, 53u8, 157u8, 52u8, 242u8, 67u8, 227u8, + 117u8, 93u8, 77u8, 5u8, 210u8, 55u8, 12u8, 170u8, 57u8, 161u8, + ], + ) + } #[doc = " Events deposited for the current block."] #[doc = ""] #[doc = " NOTE: The item is unbound and should therefore never be read on chain."] @@ -3053,10 +3257,10 @@ pub mod api { "Events", (), [ - 114u8, 203u8, 75u8, 252u8, 183u8, 122u8, 99u8, 127u8, 226u8, 41u8, - 125u8, 202u8, 177u8, 177u8, 136u8, 66u8, 221u8, 204u8, 240u8, 189u8, - 146u8, 54u8, 190u8, 211u8, 240u8, 69u8, 233u8, 62u8, 78u8, 177u8, 97u8, - 8u8, + 255u8, 7u8, 244u8, 240u8, 243u8, 214u8, 236u8, 98u8, 179u8, 226u8, + 108u8, 68u8, 165u8, 184u8, 135u8, 230u8, 2u8, 88u8, 24u8, 37u8, 187u8, + 250u8, 198u8, 136u8, 211u8, 84u8, 158u8, 201u8, 156u8, 204u8, 143u8, + 208u8, ], ) } @@ -20104,12 +20308,15 @@ pub mod api { )] #[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 = "A native token transfer was recorded."] + #[doc = ""] + #[doc = "The `leaf_index` can be used to fetch Merkle proofs via the"] + #[doc = "`zkTrie_getMerkleProof` RPC for ZK circuit verification."] pub struct NativeTransferred { pub from: native_transferred::From, pub to: native_transferred::To, pub amount: native_transferred::Amount, pub transfer_count: native_transferred::TransferCount, - /// Index of this transfer in the ZK trie (for Merkle proof lookup) pub leaf_index: native_transferred::LeafIndex, } pub mod native_transferred { @@ -20131,13 +20338,16 @@ pub mod api { )] #[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 = "A non-native asset transfer was recorded."] + #[doc = ""] + #[doc = "The `leaf_index` can be used to fetch Merkle proofs via the"] + #[doc = "`zkTrie_getMerkleProof` RPC for ZK circuit verification."] pub struct AssetTransferred { pub asset_id: asset_transferred::AssetId, pub from: asset_transferred::From, pub to: asset_transferred::To, pub amount: asset_transferred::Amount, pub transfer_count: asset_transferred::TransferCount, - /// Index of this transfer in the ZK trie (for Merkle proof lookup) pub leaf_index: asset_transferred::LeafIndex, } pub mod asset_transferred { @@ -20184,12 +20394,6 @@ pub mod api { pub type UsedNullifiers = ::core::primitive::bool; pub type Param0 = [::core::primitive::u8; 32usize]; } - pub mod transfer_proof { - use super::runtime_types; - 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; pub type TransferCount = ::core::primitive::u64; @@ -20249,68 +20453,7 @@ pub mod api { ], ) } - #[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< - (), - types::transfer_proof::TransferProof, - (), - (), - ::subxt::ext::subxt_core::utils::Yes, - > { - ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( - "Wormhole", - "TransferProof", - (), - [ - 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 = ""] - #[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< - (), - types::transfer_proof::TransferProof, - ::subxt::ext::subxt_core::utils::Yes, - (), - (), - > { - ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( - "Wormhole", - "TransferProof", - (), - [ - 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 count for all wormhole transfers"] + #[doc = " Transfer count per recipient - used to generate unique leaf indices in the ZK trie."] pub fn transfer_count_iter( &self, ) -> ::subxt::ext::subxt_core::storage::address::StaticAddress< @@ -20332,7 +20475,7 @@ pub mod api { ], ) } - #[doc = " Transfer count for all wormhole transfers"] + #[doc = " Transfer count per recipient - used to generate unique leaf indices in the ZK trie."] pub fn transfer_count( &self, _0: types::transfer_count::Param0, @@ -20461,6 +20604,252 @@ pub mod api { } } } + pub mod zk_tree { + use super::{root_mod, runtime_types}; + #[doc = "The `Error` enum of this pallet."] + pub type Error = runtime_types::pallet_zk_tree::pallet::Error; + #[doc = "The `Event` enum of this pallet"] + pub type Event = runtime_types::pallet_zk_tree::pallet::Event; + pub mod events { + use super::runtime_types; + #[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 = "A new leaf was inserted into the tree."] + pub struct LeafInserted { + pub index: leaf_inserted::Index, + pub leaf_hash: leaf_inserted::LeafHash, + pub new_root: leaf_inserted::NewRoot, + } + pub mod leaf_inserted { + use super::runtime_types; + pub type Index = ::core::primitive::u64; + pub type LeafHash = [::core::primitive::u8; 32usize]; + pub type NewRoot = [::core::primitive::u8; 32usize]; + } + impl ::subxt::ext::subxt_core::events::StaticEvent for LeafInserted { + const PALLET: &'static str = "ZkTree"; + const EVENT: &'static str = "LeafInserted"; + } + #[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 = "Tree depth increased."] + pub struct TreeGrew { + pub new_depth: tree_grew::NewDepth, + } + pub mod tree_grew { + use super::runtime_types; + pub type NewDepth = ::core::primitive::u8; + } + impl ::subxt::ext::subxt_core::events::StaticEvent for TreeGrew { + const PALLET: &'static str = "ZkTree"; + const EVENT: &'static str = "TreeGrew"; + } + } + pub mod storage { + use super::runtime_types; + pub mod types { + use super::runtime_types; + pub mod leaves { + use super::runtime_types; + pub type Leaves = runtime_types::pallet_zk_tree::ZkLeaf< + ::subxt::ext::subxt_core::utils::AccountId32, + ::core::primitive::u32, + ::core::primitive::u128, + >; + pub type Param0 = ::core::primitive::u64; + } + pub mod nodes { + use super::runtime_types; + pub type Nodes = [::core::primitive::u8; 32usize]; + pub type Param0 = (::core::primitive::u8, ::core::primitive::u64); + } + pub mod leaf_count { + use super::runtime_types; + pub type LeafCount = ::core::primitive::u64; + } + pub mod depth { + use super::runtime_types; + pub type Depth = ::core::primitive::u8; + } + pub mod root { + use super::runtime_types; + pub type Root = [::core::primitive::u8; 32usize]; + } + } + pub struct StorageApi; + impl StorageApi { + #[doc = " Leaf data stored by index."] + pub fn leaves_iter( + &self, + ) -> ::subxt::ext::subxt_core::storage::address::StaticAddress< + (), + types::leaves::Leaves, + (), + (), + ::subxt::ext::subxt_core::utils::Yes, + > { + ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( + "ZkTree", + "Leaves", + (), + [ + 179u8, 219u8, 161u8, 103u8, 54u8, 178u8, 204u8, 221u8, 15u8, 0u8, + 103u8, 111u8, 19u8, 89u8, 167u8, 0u8, 200u8, 35u8, 149u8, 127u8, 230u8, + 183u8, 152u8, 151u8, 88u8, 171u8, 223u8, 134u8, 58u8, 122u8, 119u8, + 53u8, + ], + ) + } + #[doc = " Leaf data stored by index."] + pub fn leaves( + &self, + _0: types::leaves::Param0, + ) -> ::subxt::ext::subxt_core::storage::address::StaticAddress< + ::subxt::ext::subxt_core::storage::address::StaticStorageKey< + types::leaves::Param0, + >, + types::leaves::Leaves, + ::subxt::ext::subxt_core::utils::Yes, + (), + (), + > { + ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( + "ZkTree", + "Leaves", + ::subxt::ext::subxt_core::storage::address::StaticStorageKey::new(_0), + [ + 179u8, 219u8, 161u8, 103u8, 54u8, 178u8, 204u8, 221u8, 15u8, 0u8, + 103u8, 111u8, 19u8, 89u8, 167u8, 0u8, 200u8, 35u8, 149u8, 127u8, 230u8, + 183u8, 152u8, 151u8, 88u8, 171u8, 223u8, 134u8, 58u8, 122u8, 119u8, + 53u8, + ], + ) + } + #[doc = " Internal tree nodes: (level, index) -> hash."] + #[doc = " Level 0 is unused (leaves are hashed on-demand)."] + #[doc = " Level 1+ contains internal node hashes."] + pub fn nodes_iter( + &self, + ) -> ::subxt::ext::subxt_core::storage::address::StaticAddress< + (), + types::nodes::Nodes, + (), + (), + ::subxt::ext::subxt_core::utils::Yes, + > { + ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( + "ZkTree", + "Nodes", + (), + [ + 109u8, 135u8, 134u8, 82u8, 149u8, 113u8, 22u8, 231u8, 196u8, 37u8, + 27u8, 29u8, 86u8, 223u8, 101u8, 97u8, 89u8, 147u8, 86u8, 202u8, 170u8, + 24u8, 164u8, 137u8, 71u8, 46u8, 91u8, 69u8, 35u8, 251u8, 152u8, 144u8, + ], + ) + } + #[doc = " Internal tree nodes: (level, index) -> hash."] + #[doc = " Level 0 is unused (leaves are hashed on-demand)."] + #[doc = " Level 1+ contains internal node hashes."] + pub fn nodes( + &self, + _0: types::nodes::Param0, + ) -> ::subxt::ext::subxt_core::storage::address::StaticAddress< + ::subxt::ext::subxt_core::storage::address::StaticStorageKey< + types::nodes::Param0, + >, + types::nodes::Nodes, + ::subxt::ext::subxt_core::utils::Yes, + (), + (), + > { + ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( + "ZkTree", + "Nodes", + ::subxt::ext::subxt_core::storage::address::StaticStorageKey::new(_0), + [ + 109u8, 135u8, 134u8, 82u8, 149u8, 113u8, 22u8, 231u8, 196u8, 37u8, + 27u8, 29u8, 86u8, 223u8, 101u8, 97u8, 89u8, 147u8, 86u8, 202u8, 170u8, + 24u8, 164u8, 137u8, 71u8, 46u8, 91u8, 69u8, 35u8, 251u8, 152u8, 144u8, + ], + ) + } + #[doc = " Number of leaves in the tree."] + pub fn leaf_count( + &self, + ) -> ::subxt::ext::subxt_core::storage::address::StaticAddress< + (), + types::leaf_count::LeafCount, + ::subxt::ext::subxt_core::utils::Yes, + ::subxt::ext::subxt_core::utils::Yes, + (), + > { + ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( + "ZkTree", + "LeafCount", + (), + [ + 67u8, 168u8, 48u8, 81u8, 223u8, 41u8, 216u8, 165u8, 89u8, 116u8, 103u8, + 23u8, 83u8, 40u8, 238u8, 124u8, 63u8, 8u8, 247u8, 246u8, 11u8, 63u8, + 181u8, 30u8, 51u8, 181u8, 232u8, 236u8, 184u8, 121u8, 66u8, 250u8, + ], + ) + } + #[doc = " Current depth of the tree (0 = empty, 1 = up to 4 leaves, etc.)."] + pub fn depth( + &self, + ) -> ::subxt::ext::subxt_core::storage::address::StaticAddress< + (), + types::depth::Depth, + ::subxt::ext::subxt_core::utils::Yes, + ::subxt::ext::subxt_core::utils::Yes, + (), + > { + ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( + "ZkTree", + "Depth", + (), + [ + 15u8, 235u8, 244u8, 51u8, 6u8, 56u8, 103u8, 34u8, 29u8, 165u8, 154u8, + 31u8, 166u8, 174u8, 83u8, 59u8, 232u8, 175u8, 227u8, 168u8, 185u8, + 29u8, 71u8, 211u8, 162u8, 69u8, 81u8, 108u8, 247u8, 107u8, 215u8, 52u8, + ], + ) + } + #[doc = " Current root hash of the tree."] + pub fn root( + &self, + ) -> ::subxt::ext::subxt_core::storage::address::StaticAddress< + (), + types::root::Root, + ::subxt::ext::subxt_core::utils::Yes, + ::subxt::ext::subxt_core::utils::Yes, + (), + > { + ::subxt::ext::subxt_core::storage::address::StaticAddress::new_static( + "ZkTree", + "Root", + (), + [ + 240u8, 224u8, 214u8, 191u8, 99u8, 19u8, 153u8, 102u8, 246u8, 99u8, + 194u8, 80u8, 179u8, 162u8, 75u8, 8u8, 74u8, 143u8, 162u8, 61u8, 251u8, + 71u8, 235u8, 88u8, 14u8, 197u8, 34u8, 40u8, 136u8, 119u8, 201u8, 35u8, + ], + ) + } + } + } + } pub mod runtime_types { use super::runtime_types; pub mod bounded_collections { @@ -25701,6 +26090,9 @@ pub mod api { #[codec(index = 5)] #[doc = "Attempt to use a non-named function on a named task."] Named, + #[codec(index = 6)] + #[doc = "Periodic scheduling is not supported."] + PeriodicNotSupported, } #[derive( :: subxt :: ext :: subxt_core :: ext :: scale_decode :: DecodeAsType, @@ -26374,19 +26766,29 @@ pub mod api { #[doc = "The `Event` enum of this pallet"] pub enum Event { #[codec(index = 0)] + #[doc = "A native token transfer was recorded."] + #[doc = ""] + #[doc = "The `leaf_index` can be used to fetch Merkle proofs via the"] + #[doc = "`zkTrie_getMerkleProof` RPC for ZK circuit verification."] NativeTransferred { from: ::subxt::ext::subxt_core::utils::AccountId32, to: ::subxt::ext::subxt_core::utils::AccountId32, amount: ::core::primitive::u128, transfer_count: ::core::primitive::u64, + leaf_index: ::core::primitive::u64, }, #[codec(index = 1)] + #[doc = "A non-native asset transfer was recorded."] + #[doc = ""] + #[doc = "The `leaf_index` can be used to fetch Merkle proofs via the"] + #[doc = "`zkTrie_getMerkleProof` RPC for ZK circuit verification."] AssetTransferred { asset_id: ::core::primitive::u32, from: ::subxt::ext::subxt_core::utils::AccountId32, to: ::subxt::ext::subxt_core::utils::AccountId32, amount: ::core::primitive::u128, transfer_count: ::core::primitive::u64, + leaf_index: ::core::primitive::u64, }, #[codec(index = 2)] ProofVerified { @@ -26398,6 +26800,86 @@ pub mod api { } } } + pub mod pallet_zk_tree { + use super::runtime_types; + pub mod pallet { + use super::runtime_types; + #[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 = "The `Error` enum of this pallet."] + pub enum Error { + #[codec(index = 0)] + #[doc = "Leaf index out of bounds."] + LeafIndexOutOfBounds, + #[codec(index = 1)] + #[doc = "Leaf not found."] + LeafNotFound, + } + #[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 = "The `Event` enum of this pallet"] + pub enum Event { + #[codec(index = 0)] + #[doc = "A new leaf was inserted into the tree."] + LeafInserted { + index: ::core::primitive::u64, + leaf_hash: [::core::primitive::u8; 32usize], + new_root: [::core::primitive::u8; 32usize], + }, + #[codec(index = 1)] + #[doc = "Tree depth increased."] + TreeGrew { new_depth: ::core::primitive::u8 }, + } + } + #[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")] + pub struct ZkLeaf<_0, _1, _2> { + pub to: _0, + pub transfer_count: ::core::primitive::u64, + pub asset_id: _1, + pub amount: _2, + } + #[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")] + pub struct ZkMerkleProofRpc { + pub leaf_index: ::core::primitive::u64, + pub leaf_data: ::subxt::ext::subxt_core::alloc::vec::Vec<::core::primitive::u8>, + pub leaf_hash: [::core::primitive::u8; 32usize], + pub siblings: ::subxt::ext::subxt_core::alloc::vec::Vec< + [[::core::primitive::u8; 32usize]; 3usize], + >, + pub root: [::core::primitive::u8; 32usize], + pub depth: ::core::primitive::u8, + } + } pub mod primitive_types { use super::runtime_types; #[derive( @@ -26460,6 +26942,7 @@ pub mod api { pub number: _0, pub state_root: ::subxt::ext::subxt_core::utils::H256, pub extrinsics_root: ::subxt::ext::subxt_core::utils::H256, + pub zk_tree_root: ::subxt::ext::subxt_core::utils::H256, pub digest: runtime_types::sp_runtime::generic::digest::Digest, } } @@ -26645,6 +27128,8 @@ pub mod api { Multisig(runtime_types::pallet_multisig::pallet::Error), #[codec(index = 20)] Wormhole(runtime_types::pallet_wormhole::pallet::Error), + #[codec(index = 21)] + ZkTree(runtime_types::pallet_zk_tree::pallet::Error), } #[derive( :: subxt :: ext :: subxt_core :: ext :: scale_decode :: DecodeAsType, @@ -26692,6 +27177,8 @@ pub mod api { Multisig(runtime_types::pallet_multisig::pallet::Event), #[codec(index = 20)] Wormhole(runtime_types::pallet_wormhole::pallet::Event), + #[codec(index = 21)] + ZkTree(runtime_types::pallet_zk_tree::pallet::Event), } #[derive( :: subxt :: ext :: subxt_core :: ext :: scale_decode :: DecodeAsType, diff --git a/src/cli/mod.rs b/src/cli/mod.rs index c788e9b..9536e5e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -296,10 +296,205 @@ pub enum Commands { /// Developer subcommands #[derive(Subcommand, Debug)] pub enum DeveloperCommands { + /// Regenerate local wormhole circuit binaries from the qp-zk-circuits checkout + BuildCircuits { + /// Number of leaf proofs per layer-0 aggregation. + /// Shipping PR #129 output is fixed at 16. + #[arg(long, default_value_t = crate::bins::DEFAULT_NUM_LEAF_PROOFS)] + num_leaf_proofs: usize, + + /// Number of layer-0 proofs per layer-1 aggregation. + /// Omit this for normal layer-0 generation. + #[arg(long)] + num_layer0_proofs: Option, + + /// Path to the chain repo. + #[arg(long, default_value = "../chain")] + chain_path: String, + + /// Skip cleaning chain build outputs after regenerating CLI bins. + #[arg(long)] + skip_chain: bool, + + /// Skip prover binaries and only emit verifier-oriented artifacts. + #[arg(long)] + skip_prover: bool, + }, /// Create standard test wallets (crystal_alice, crystal_bob, crystal_charlie) CreateTestWallets, } +fn fixed_layer0_leaf_count_error(num_leaf_proofs: usize) -> crate::error::QuantusError { + crate::error::QuantusError::Generic(format!( + "PR #129 shipping layer-0 aggregation is fixed at {} leaves (got {}).", + crate::bins::DEFAULT_NUM_LEAF_PROOFS, + num_leaf_proofs + )) +} + +fn remove_existing_path(path: &std::path::Path) -> crate::error::Result<()> { + if let Ok(metadata) = std::fs::symlink_metadata(path) { + if metadata.file_type().is_symlink() || !metadata.is_dir() { + std::fs::remove_file(path).map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to remove {}: {}", + path.display(), + e + )) + })?; + } else { + std::fs::remove_dir_all(path).map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to remove {}: {}", + path.display(), + e + )) + })?; + } + } + Ok(()) +} + +fn clean_cli_generated_bins(repo_root: &std::path::Path) -> crate::error::Result<()> { + for relative in ["generated-bins", ".generated-bins", "target/generated-bins"] { + remove_existing_path(&repo_root.join(relative))?; + } + Ok(()) +} + +fn validate_generated_bins( + bins_dir: &std::path::Path, + skip_prover: bool, +) -> crate::error::Result<()> { + for filename in crate::bins::REQUIRED_BIN_FILES { + if skip_prover && filename.contains("prover.bin") { + continue; + } + if !bins_dir.join(filename).exists() { + return Err(crate::error::QuantusError::Generic(format!( + "Expected generated artifact missing: {}", + bins_dir.join(filename).display() + ))); + } + } + Ok(()) +} + +fn clean_chain_wormhole_build_outputs(chain_path: &std::path::Path) -> crate::error::Result { + let mut removed = 0usize; + for relative in ["target/debug/build", "target/release/build"] { + let build_dir = chain_path.join(relative); + if !build_dir.exists() { + continue; + } + for entry in std::fs::read_dir(&build_dir).map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to read {}: {}", + build_dir.display(), + e + )) + })? { + let entry = entry.map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to read {} entry: {}", + build_dir.display(), + e + )) + })?; + let file_name = entry.file_name(); + let file_name = file_name.to_string_lossy(); + if file_name.contains("wormhole") { + remove_existing_path(&entry.path())?; + removed += 1; + } + } + } + Ok(removed) +} + +fn handle_build_circuits( + num_leaf_proofs: usize, + num_layer0_proofs: Option, + chain_path: String, + skip_chain: bool, + skip_prover: bool, +) -> crate::error::Result<()> { + if num_leaf_proofs != crate::bins::DEFAULT_NUM_LEAF_PROOFS { + return Err(fixed_layer0_leaf_count_error(num_leaf_proofs)); + } + + let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let bins_dir = repo_root.join("generated-bins"); + + log_print!( + "🧪 {} Regenerating wormhole circuit binaries...", + "DEVELOPER".bright_magenta().bold() + ); + log_print!(" Output: {}", bins_dir.display()); + log_print!( + " num_leaf_proofs: {} (fixed shipping layer-0 output)", + crate::bins::DEFAULT_NUM_LEAF_PROOFS + ); + log_print!( + " num_layer0_proofs: {}", + num_layer0_proofs + .map(|value| value.to_string()) + .unwrap_or_else(|| "disabled".to_string()) + ); + log_print!(" include_prover: {}", !skip_prover); + + clean_cli_generated_bins(repo_root)?; + + qp_wormhole_circuit_builder::generate_all_circuit_binaries( + &bins_dir, + !skip_prover, + crate::bins::DEFAULT_NUM_LEAF_PROOFS, + num_layer0_proofs, + ) + .map_err(|e| { + crate::error::QuantusError::Generic(format!("Failed to generate circuit binaries: {}", e)) + })?; + + std::fs::write(bins_dir.join(crate::bins::VERSION_MARKER), env!("CARGO_PKG_VERSION")).map_err( + |e| { + crate::error::QuantusError::Generic(format!( + "Failed to write version marker to {}: {}", + bins_dir.join(crate::bins::VERSION_MARKER).display(), + e + )) + }, + )?; + + validate_generated_bins(&bins_dir, skip_prover)?; + log_success!("Generated circuit bins at {}", bins_dir.display()); + + if skip_prover { + log_print!( + " Warning: prover binaries were skipped, so this output is not suitable for local aggregation or multiround tests." + ); + } + + if !skip_chain { + let chain_path = std::path::PathBuf::from(&chain_path); + let chain_path = + if chain_path.is_absolute() { chain_path } else { repo_root.join(chain_path) }; + if !chain_path.join("Cargo.toml").exists() { + return Err(crate::error::QuantusError::Generic(format!( + "Chain path does not look like a repo root: {}", + chain_path.display() + ))); + } + let removed = clean_chain_wormhole_build_outputs(&chain_path)?; + log_print!( + " Chain sync: cleared {} stale wormhole build outputs under {} so the next chain build regenerates verifier artifacts from local qp-zk-circuits sources.", + removed, + chain_path.display() + ); + } + + Ok(()) +} + /// Execute a CLI command pub async fn execute_command( command: Commands, @@ -517,6 +712,19 @@ async fn handle_generic_call_command( /// Handle developer subcommands pub async fn handle_developer_command(command: DeveloperCommands) -> crate::error::Result<()> { match command { + DeveloperCommands::BuildCircuits { + num_leaf_proofs, + num_layer0_proofs, + chain_path, + skip_chain, + skip_prover, + } => handle_build_circuits( + num_leaf_proofs, + num_layer0_proofs, + chain_path, + skip_chain, + skip_prover, + ), DeveloperCommands::CreateTestWallets => { use crate::wallet::WalletManager; diff --git a/src/cli/wormhole.rs b/src/cli/wormhole.rs index 8e882c1..4750c5d 100644 --- a/src/cli/wormhole.rs +++ b/src/cli/wormhole.rs @@ -46,6 +46,23 @@ pub use crate::wormhole_lib::{ compute_output_amount, NATIVE_ASSET_ID, SCALE_DOWN_FACTOR, VOLUME_FEE_BPS, }; +pub(crate) const AGGREGATED_PUBLIC_INPUTS_LEN: usize = 344; +pub(crate) const AGGREGATED_PUBLIC_INPUTS_SEMANTIC_LEN: usize = 232; +pub(crate) const AGGREGATED_PUBLIC_INPUTS_ZERO_TAIL_LEN: usize = 112; + +fn ensure_stable_aggregated_public_input_len(len: usize) -> crate::error::Result<()> { + if len != AGGREGATED_PUBLIC_INPUTS_LEN { + return Err(crate::error::QuantusError::Generic(format!( + "Unexpected aggregated public input length: got {}, expected stable PR #129 length {} ({} semantic + {} zero-tail).", + len, + AGGREGATED_PUBLIC_INPUTS_LEN, + AGGREGATED_PUBLIC_INPUTS_SEMANTIC_LEN, + AGGREGATED_PUBLIC_INPUTS_ZERO_TAIL_LEN + ))); + } + Ok(()) +} + // ============================================================================ // ZK Tree Types (for 4-ary Poseidon Merkle proofs) // ============================================================================ @@ -712,7 +729,7 @@ pub enum WormholeCommands { }, /// Run a multi-round wormhole test: wallet -> wormhole -> ... -> wallet Multiround { - /// Number of proofs per round (default: 2, max: 8) + /// Number of proofs per round (default: 2, max: 16) #[arg(short, long, default_value = "2")] num_proofs: usize, @@ -1184,6 +1201,7 @@ async fn aggregate_proofs( .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()); + ensure_stable_aggregated_public_input_len(aggregated_proof.public_inputs.len())?; // Parse and display aggregated public inputs let aggregated_public_inputs = @@ -2620,6 +2638,13 @@ async fn parse_proof_file( log_print!("\nPublic inputs count: {}", proof.public_inputs.len()); log_verbose!("\nPublic inputs count: {}", proof.public_inputs.len()); + ensure_stable_aggregated_public_input_len(proof.public_inputs.len())?; + log_print!( + "Stable aggregated contract: {} semantic felts + {} zero-tail = {} total", + AGGREGATED_PUBLIC_INPUTS_SEMANTIC_LEN, + AGGREGATED_PUBLIC_INPUTS_ZERO_TAIL_LEN, + AGGREGATED_PUBLIC_INPUTS_LEN + ); // Try to parse as aggregated match qp_wormhole_verifier::parse_aggregated_public_inputs(&proof) { @@ -3242,6 +3267,7 @@ fn aggregate_proofs_to_file(proof_files: &[String], output_file: &str) -> crate: .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()); + ensure_stable_aggregated_public_input_len(proof.public_inputs.len())?; let proof_hex = hex::encode(proof.to_bytes()); std::fs::write(output_file, &proof_hex).map_err(|e| { @@ -3678,12 +3704,12 @@ mod tests { // the same JSON format that the upstream CircuitBinsConfig produces. // If the upstream adds/removes/renames fields, this test will catch it. let json = r#"{ - "num_leaf_proofs": 8, + "num_leaf_proofs": 16, "num_layer0_proofs": null }"#; let config: CircuitBinsConfig = serde_json::from_str(json).unwrap(); - assert_eq!(config.num_leaf_proofs, 8); + assert_eq!(config.num_leaf_proofs, 16); assert_eq!(config.num_layer0_proofs, None); } diff --git a/src/collect_rewards_lib.rs b/src/collect_rewards_lib.rs index eb601ed..2e66aff 100644 --- a/src/collect_rewards_lib.rs +++ b/src/collect_rewards_lib.rs @@ -820,6 +820,13 @@ fn aggregate_proof_bytes(proof_bytes_list: &[Vec], bins_dir: &Path) -> Resul let aggregated_proof = aggregator .aggregate() .map_err(|e| CollectRewardsError::from(format!("Aggregation failed: {}", e)))?; + if aggregated_proof.public_inputs.len() != crate::cli::wormhole::AGGREGATED_PUBLIC_INPUTS_LEN { + return Err(CollectRewardsError::from(format!( + "Unexpected aggregated public input length: got {}, expected {}", + aggregated_proof.public_inputs.len(), + crate::cli::wormhole::AGGREGATED_PUBLIC_INPUTS_LEN + ))); + } Ok(aggregated_proof.to_bytes()) } @@ -844,6 +851,13 @@ async fn submit_and_get_events( { qp_wormhole_verifier::D }, >::from_bytes(proof_bytes.clone(), &verifier.circuit_data.common) .map_err(|e| CollectRewardsError::from(format!("Failed to deserialize proof: {}", e)))?; + if proof.public_inputs.len() != crate::cli::wormhole::AGGREGATED_PUBLIC_INPUTS_LEN { + return Err(CollectRewardsError::from(format!( + "Unexpected aggregated public input length: got {}, expected {}", + proof.public_inputs.len(), + crate::cli::wormhole::AGGREGATED_PUBLIC_INPUTS_LEN + ))); + } verifier .verify(proof.clone()) diff --git a/src/quantus_metadata.scale b/src/quantus_metadata.scale index 74002ed6fa89073aa69d1f83c0223e40ce935997..745d333a87ff9c91127ff86b9bde5f9a86f0d9d5 100644 GIT binary patch delta 7554 zcmcgxeRLF6mVftE1v}7afCL&6^aT=0vq_s^f(;~*m;{M@bwbF33Q5sjA+_nQ?(|1U zSeY0d1V)3Pz#Dc1K}Clh7&l6&aa>eZQE^4ZbJ!V1&}C;iicXZ^;E_?By|1c6(qw1n zpWW@#r>pwaefQmW-~IUATf4tZKKJM3VF5L&q+IG8)-w#g83)&pjLR(1j`_*Xun&$c_j?J7|*{bT%;+mKq)dFfMNi*)vfL$u2@*@vG)8jMi zZr~*kLRLqPUsW3GHQlGS<$ygly-@zXO8jBLMiy<-A}ktK^hl!`W}1!zD!Y3g4rwaO zX$*$h0yW(1S6L_=L~?&H*c=NvNy^qTrYljcRb|m&vx?LDS*%tfEUbEiVV~-Akw4Tn zsU56gK!673RdlveWwD6r!?~9w9^ntMQaG zAsWi*)xzGG7G&%`S^X)M#y7Bsr}59K4_fCh*rEfL;Fw>VTAIP#r-% zFRl_rEdQ5O(jj#6|E`ikQ+0KyCGLL^{2g|8qA@wFzp184$#N|LTmF1Bn<_n`hNG&_ z4yl6-h(MNkq)kes$p#s7@iMJwIsvAutLyQMqJsvB{8Lnpx|)d@OqhqJN9LGJ@eVCO zSe&qD7Za9#XINb=LzZCVQ$x`vJJ@1w0&=Z*$jl*}<_)U~8W2BG2$Rrde)AwK8SSKm z&^$`;OJtkdO$vDFk6~G15rvBK;OgQjktmvnH&O`7pEZ_$pf~io=KY0iy(CR^~dCq#c6_)UO*F!NZ<2%+P zSrLC@JutkSTMzZHoL6pu(!%8f8&Jds@Q0W`RHX#Js059=iH3KQ4IkM_fbxb67-zh< z0Unc}g)5K1)Cn<~RoRB_u17R4v{PaON0X}f)NqXN{uIVmc2aQHsovtNM5wzMwPm%0 z>Mc6QT$9}HSQrbeRKws-5%xf`7NfdV^OiXHrAHuO%a2DcKEDf`&@R?b;}3Vi{NZcG z1{^5lT7I?*CJgUHp(zTpj&AQknhBdAD{Ui{7vrYL777j_P%R3wkuQ7{RqEda8KCiv zo4|E_7ln)(w2q)2A*4$@cknZtVAAj{g6Bp`wh5m3X*=TVb`D~=u*- zj-DZl^-!=?`S_9jkOn<`;ue^i{E}eTP03-yvjy&!;0XW4RiGU4eo*y{O~rYfs;ICJIsVreBO4LGwmcYmTj_!oT9R0 zQAmVO;SncMgp`FrUBx3wSsV;t0uq!!$Ql0nb|{)~7722f=wY=brbaM6pw*ynHLKBc zd^CqrIM1s&rm?epCkIc-Ict@(l$=jk=G@R_1Su}lYtATklRi^{kALhLO)pWNKLaKh z8-5CFDZOkyD7g~XY?3r!6*x9%FCY!s3rOZ7z{{S1F?{{cpgTDUhv*eblEs*hj+A)j zOh_Ld4hO?#ILt8KeF8#I;_+Y%4f-_i!eDf9EEEcg=tzdiTl>dZ#M`9$Vt!3u#W1(( z!6=LL)A8=NU^0y2^}9evvu4j=PWo&Yw4w>uKMD6kCNFypa;(b~p1&I$*9`604dbsF zlJ*Rt@{6JIg8g{DYY%)YPn5vV_q+<}yyqzx)6f6HQ!pHZ=XX!Re8}T-pN5-ZqBUv5 z({N{XrZiZK$l_Xym}qG+JAuwQl+J09q1k@RP&0a7jFOVR$ zJQi+MF?t6yy*_?W1H)v2H9hbwOuo5Vf($(6m73pAAhi*(P&i>I#j9%mYJTWhn39$7 zz^>}Pgn<-vnGib2b9fllJncEibS#m8&C%LaU)A(N0<5h**w~0DVhLaP9E=^6@DyF6 zn#Z1lTj&yr@81gPHx0~0=U*m)T|*{#Lr9XHESLC}GDtV-UxybZQ26t|fHqLYL*{>mY^3AXT$4?z|V3dT3Di=W)aK_|D)(?vJr16{_7Ha(s@e{ z%;6Ibp^5&s2gTSXMj4-V2p+hG_YRBq&N%NqgLv;WdEe(DdjxO%x7Xxt#0+>ulJ-e9 zzUyW5jEOJ9O&JF)TA6fPH*V5n-neD*lDUG2JaE`};$?W1V%Y6>*}M2_uRtya;Qw{M zJ!)AT`t1U-2IC{1f`|XA2Rr!-|AL_v2-3VmV@Vszby+4h|D z_^9;`qwyHrDbw>3?>qr%#v8{muH4WoL9U6)1uBR|`0V%b1V+CP`LxfXm2C<~E2njd@)C=mD`TOs~?evmKJ&sTL0L}k$d}`nW_*wE5A)wtY%lt zKpiCU`%gfnoGg=6uDk~s!%XK-=3kt^Fq+JhK7=BgEDJbb9`i_}<3p&s&N)gR9Qn=< z4N>QDElg5r{D)JJ?Hr##o+J+mlu5ElK9SG=9UMT@{?qRe+GX?GK0?hn`HGKB4BPn; z{0N-ePor0^JPqR>>rLmL)98^-AzvDAKMe(Gc`~@~5I}f=5-OgT%jysUmR9D7t`50<*D`i_@V;BQ$e<)=hvt~u6 z7@0JLX7|M0vzU81AyvlnAH)5_hAj{>pEOF(K|1Zgc%M+?V)-ko5c_~|AZM6%ROJKr zAe5E@wrDVM6Yl<+wBEEQtMtXtYF20Un$HdWoY2UWfUr6K&VI0WEHkxd&mqgvp01In zzf8J{3v3>%Ou~A4Q7pQs(bO~gg6tFHSgM$Xb}TB2?UmvTA}+JALijL8{Ke9y*rXId zp;3%jAVJ|vKLyucL+6uK89V|EFA6D$#0Fz@glvRrsC0hnQ^xfi1k|GjP_& z5=^#bsL8hAY_#xp!Ng70B6*D(iTR^81c!$~u7pmUV_VcLlfy`c#YJT}ER#-aRwr*b zkJ;k;&(OZt@yU3fzEOs`OY~+vxLP;sa>Xm4rdVQ~fy6SvMloqJ-|#u6m@fXC&mkMr z$d%7wYQYv+X8l%gljng(Q2yM&rluqSlSR5 z`V!_7Q$WXIR2gsWh1`xK=0NQ}AV(8e9UIE(n8oUt07MB6GW+i4L9LORke8rW4{7+2 zc)gCbsN#SOVK6awb7}wZkTO1^trYk*a$JTAL3~fV!k@)%U5>L-t|{>$bx^5Jj*I=C zkO_W?gn^RS$Vq{GG2Lb3}z>aXW&vfJ_4Xo3GG+ z4)d%FkPnymk_%8E%7e}0F)Hu8022_;{|X<;%O$v<_x8a}d_*5ikXt0!&!1m_n5Lo+ z{a`O|?t|Oq!xHS~Pri%^>dijP*T?zieNc;~+3gpR`8giC2rd}OpS+03Y9jyeA{0Rt zw|xzDv#&M+m&^Z6BT$5+L5)B{YUA=l{OU16U&C5yM2^)@tic`)U-1oO-H^K3YWHDt zMVu<{kg2018`xs(V6ALtmbK-rY0eWZ8s>{(qhJ+V=?{9FS)-;aer=7_b7QrllPJEr z*B`@7gEykJlLv8Xt66~2mnqoB!2$}q13u)T`h6@%1V5zrX{)fBbWRtIK~;~$!YUFs z3RH~YQ@~*nBL8CF&-mgScmlv_Z1@)HDHIr={Rx@@6Iku{m~TsrE#JfAfK8q3AK)m0 zt1o_l7qHK=>(9_2FQduQHcGRS??(d?0WE*qEwt8_wRG93Pal3$>*=6%00HCQfG!28 zg{JZXiEip33jnvRviQ~lu?X%W5b~=53^}Y}K-HKyTQzK{G>DDGiwq{VBa|unUuMTO zegyviLCd-EBrW1uk@2oXpS=zvY-9xe4xw_;PWQncK7J&1!2x6LNQyc8F#pLYsw2d| zJc{nbZUP@o??o8<$I&!<{85TFPl{{XJW9KexEvd$T9lAu#7DO%_A z>D0;B;Sahk-p?G}OZ&{p&_JLE^#zxV@6zedA^9>6%czzro_8b708k!ZL|dg}XRq^Xtju4ZLVt0 z!LCR$u@wMA=o8^BQ|TC9?xgFmkU8e0+i`;pQ|Uc;ME^XM-X>$3`=g6CB*mn5>9>N_ zbpGfq^aDn0ycCbY&YxdOC-F;n)3my3H0%C;zKNX}b~Z&< zUkf49kFvb0-TPIlE_$BW&et`>A6AbSF^}d|MI;!oZB2C>~aAkf#fhW0`nucF{>{s03fb}uS~9LMub${ zQibSq1F_@YlYn7ALLioStJ{1bPChGPY9JJC&t(O88w-hWQYg40(_DscQd~|K zNfE&vh>a`Xz$E3qs|-bWH-)v{`^1~>Xxo`0Q!YpE>c_)wxtN(Cn^je?XnRO?^&@SM z>}K)SsP%xP8BaIRVK6E`r5umSo`El7A_Cbh!h;;3ll2g zVLvitw)Jvj(22XqY%wEXzJaU9QZ3jh55{&!W&7JE7bW2ZnwcXN(-%9KV(o7g+MV9- zMDMa1H5v<}B{zs3Zv)fBi@*Wz#aDyq9>Ij7h1fT<_?t#n=BYB*h-WTQkj8<6i(~?e zXS)assKwEypbuX#1{p?&aC?6|upq1Du@Ui_fT6G9!Nh}!xzmV?KNq0?IJKSH{i^Oh ziQ%}yWK!R#s;EVDMSsL*kF`bP6sP##>u3Qd`DLB1fm22_NYeq%8Jk1&B1q@u1ID^A RT_HiQ@t-j|1EfBg{1?RJ=2QRx delta 5470 zcmb_geNYuwmcQq8LxA9j2>S4|QHbE60ug->Fe1Sbk%$T;#!-0k8tLJE(eFK#9gQ&= zCLxwlV%(!~n1n=KG3%(aWjbWnIBwSL8Z`-tRK=-9Lw2m4>;#RK_`z=6?CE|Fh49Dx zvsGQ)_ufADeEjaY=bU@5{t*58wdhE<8gRs2+8TKx5^jx!WV$;RN@0mnVuJ)(603oZ zZG*A&i#&)ij!u9k72@dfb5Or4uWBT{{wz#Q5Mq9z->(KLbce@Zr|P=uOrRScu$gqP zgCM;b)>GH7p&~vNg}`(a{0KS9ZWl9X?(?t`vZ(cW7&9>o<=7(c23f9v92BzPm#|(D zD01mvo`;`90VTh{pF(>21$YPxkqQ+SG;a(f+KW-dg3aUA-40hVf?OdW!|PLZM?mv> z?hU259UiA6;MJQ#G@{t#IEIp2U>lT#+V8hO!o)I!igHzdO!NC3f!g{am&5PR4?dhd zPl&QXVk!AWco>#Z<%^IHmGtt9Fg;{WVl1R+Zl9~#<8Z5%fr<3(i?A40(!6aj@8OmA zja_XT%P(QQ(1C&<21rG_P`M>V@vm5+8ArFlVF}$xSN;OyD7XVgj%&n};*9}a^Z2z| z2%?ArTfORVs(K^+b_a~f+l*kZQfu??5wZS_7?slKs}9lG7Fpy`w#S3+wW<#GxJ3?s zy$}i1zXRO#tDP_!g0yERIA%7PdfHeQZU{N!&i4Qj5DlXs5t3jcq{9ploB0>A9$S%W z-ho8QZ-tb^W|UX(46z--W-3#`N;H%2Wk{lzT44gH^j0g({7DPKgypKk@AdeFXfglW z=!aIA60_Z8+Ki&rVw#z_JIrtIAbz_|di!Vm>zS&5010PWQ zIhXk>VMO8_#sra`-f4T zH-{2U_DS@=`{9n9B!P?IeHc%P2YFXn45m@}L5ShFyXqh;fpj`_5FTQBYto+&!q1jX zlm=tKEPlL}No+KOoIHdsbvRvWh%VLS9mq}}LOx?S`HT>``QcFQ6IoRK4rJMLq|jE* z9kfh2A)0cL%MIjSf%z?z1V21^Q?bNR*0Si(A(-}H4vG|rGs9vI{qYbOQ%fXBC<^N9 zRgTzh%~S0U`g~qJaDP~ogr;BUfT<6aNicx}^IFa25|B{m@_O|lLvgjL<|?BxhhbXE zkRP$C$2nvmj-B0CMvlY07-bYZ49T`-5-1C`jjFR`&TIk7#%ga}9p|=X^t;0_X4H_s z?0RL?eHdopGKoGS7(eO$Oy0Yd62xlEgd0Lc#EO;1qi?~-60D|?M`0uV7sy_zts7dt96<7dn4+~Yqr zu$j6HSOhIJ>0Ou%+iBjrkivr|>xZ_6*Q|Nanr*c>+mDH6ZXowK-Wu$gUGKm`o^DCA zcftZ1+sV$kuM?jA5%1k0-h0Em+XnI8Yw_MxF4|1q+-nc<{$vG28<*dO=OpQX6h*C7 z?C6`{gUJ&*LK9?hB-F5r<5t5iixZX@Gh$$;L_4css-c{MeaNAcJ<8g*i%I!s$lxIQ z|Gs%ohun@gZ<#4Wp@svn?XPa*bc+7~rVi}oGnN5~>EZB}Iw*ad4Y>qfkm#ROw&kS{ z;UI_foO4jbA-(Aw6!6D52a~Wnv*ZYwON-N8Fd@7khszJiTv~y`J)hUF>N2E+(Jyfdh^uwD zT)A}d3&>(UB6}foOaY57w}>6Cfz`|%gkAyd=!J@mLOE)7oz9_lAgC(k%t{R~SB_CC z7hJ1?_D2-^Y9We^kzc|yk)umO%BG1jQ+uuymdU%W-L)CV{|rBXS$WR*YegrQxCLKB zQuIob!D=pTsrqZK4IQ-oYc5?>`uuB1uhxdFlRA8zxJ)sZ)on7di$*42uKI(nKoplW zCqXWTAkT?f+8~RQOvVLrv98M^7@8HNnm!JPP1MoHE4i7@_py^TnS)a(>Kc5>Lsoml zbyx~pY4vqTg=Tv0I!w=MmStsNQKRHiP*JV|8>7|2tgRPr=TeqCW`%AOt#tW1EFZm9 zhRhOAy{fC8z+;YpLx|lW5$ZQECv)!*_1omZ>bIGEhpOKen%egb{0`dbH#Z>lkpn~U z>KKk!N62#XgGV@&S_;~Z%i=hFe*-c`oUjHeIzV*Fv6MBMgZ->;At0R`)QhBl{T8Ni zD82qIWdHQ^5Rqqwi#!t&d1eR)_jll2b9RWmacpi5S8vv8#of1-D*}Zo#8GJ?=JFyJu5|!d<`z+A}hq;gjdK2>d;tBj-h8ja-SBklu*Il;}+img(IY^l>Cs7_%P0 zCKZLYa*_WJ3>c zzOhNz#%GcfN%(Wlq4DD|HR&`mup+-=W4WWr&F)*oM|dqD#2KS}9JWQkN#ojN+=dLv zg(;X$Z}JN+neDvzudvUW3^f8yu%cYnCr!me5Pg$}k8=fO#Fx!Z09V-Wi=IL;+q^n{xcK* zmW_Bi3s*4=f6T(UG9Oao=U`1lqZE`rr{sQ^Lr3P|Z(z5vc&^E*o&J=IZ^HrFm&dqw z(C2yRg(I}+5&VF07@3cY;!a2=0PqI^eWneav@Rd_Gdh-Dj6wnS%dFAj1vq50zNUAf;6CCy`#L!jE`* z8?Af-*RTQaJb@Z} zP>oZPIemER1ZT(+&P^gE0+fL(95W4h>-=J<;)qpVjIzCBU+CPWSR z(`SkTP*(6&iesIsG^kB^O2r26#;k06+LR~w7N*|oQm3RVn>O7a9^h|D4*gr;ax|#f z)!8|7(gNO$)u05z*GSb??UAl*8IZtPGN|S$F4a+2t$BRGfWNw)Zz`0ua!#0<)9cAr ztiHESsSCPX_ZTUB-Q!3Xm26(UrKb|~tA54naWyHX%zK0b68#$A9^4z<5*k)Czrxo> zZmm|~J0(R|Yc(GiT!ER4fywu&Ql(bYYlB)qS+D9^oyH;p^^O3))Zsd79&6k(N%5<- z=22a7oA*qrzmPHDIRjR0xo;L{?t5w}PQJI(Tz)0ZT8(PWnW0#SWhiyJ*UeV3We&Gl zzolpV%3RR(np5+v3t_`>&sfW9jcFR$LjX_LX>+;q%SWc0@*C_O05%Sb#A8v+rgJmTH957OKdgDw*mi#<6rJZ91jnkmD5yE76dhyleYd6ZN`Qs`~;*6 Wa=Wqkm$*iPE~DdV%mt}i7XJrTm@U8n