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 fc35560..f150b2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2049,6 +2049,7 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", + "rayon", "serde", ] @@ -3391,14 +3392,15 @@ dependencies = [ [[package]] name = "plonky2_maybe_rayon" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1e554181dc95243b8d9948ae7bae5759c7fb2502fed28f671f95ef38079406" +source = "git+https://github.com/Quantus-Network/qp-plonky2.git?branch=illuzen%2Fnew-rate#f46043c4a205de1c6cbb3bae4ea4213f9fb9d2bc" +dependencies = [ + "rayon", +] [[package]] name = "plonky2_util" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c32c137808ca984ab2458b612b7eb0462d853ee041a3136e83d54b96074c7610" +source = "git+https://github.com/Quantus-Network/qp-plonky2.git?branch=illuzen%2Fnew-rate#f46043c4a205de1c6cbb3bae4ea4213f9fb9d2bc" [[package]] name = "polkavm-common" @@ -3594,9 +3596,8 @@ dependencies = [ [[package]] name = "qp-dilithium-crypto" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c3fbdb9557a22cb876fc17c483d090e9556cbfdfc52beb0696143d4313d9" +version = "0.2.5" +source = "git+https://github.com/Quantus-Network/chain.git?branch=illuzen%2Fno-length-trie#50964f754abac3f1e0411f2e86859a41c3789ef1" dependencies = [ "log", "parity-scale-codec", @@ -3613,11 +3614,11 @@ dependencies = [ [[package]] name = "qp-plonky2" version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "593bccf15b8e2f9eb904ef4010f68b81ddcceb70aaf90116ce29ec09d7578dd4" +source = "git+https://github.com/Quantus-Network/qp-plonky2.git?branch=illuzen%2Fnew-rate#f46043c4a205de1c6cbb3bae4ea4213f9fb9d2bc" dependencies = [ "ahash", "anyhow", + "getrandom 0.2.17", "hashbrown 0.14.5", "itertools 0.11.0", "keccak-hash 0.8.0", @@ -3634,16 +3635,17 @@ dependencies = [ "qp-plonky2-verifier", "qp-poseidon-constants", "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "static_assertions", "unroll", + "web-time", ] [[package]] name = "qp-plonky2-core" version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d30fabfd90e359640f2371c8b3e9b377d215f7dcf4e61da1f38776c5b84540" +source = "git+https://github.com/Quantus-Network/qp-plonky2.git?branch=illuzen%2Fnew-rate#f46043c4a205de1c6cbb3bae4ea4213f9fb9d2bc" dependencies = [ "ahash", "anyhow", @@ -3668,8 +3670,7 @@ dependencies = [ [[package]] name = "qp-plonky2-field" version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c9f8259bf4f220b1d81001458cc6c09a1372f2b3e8dac2fb489a66230385c3" +source = "git+https://github.com/Quantus-Network/qp-plonky2.git?branch=illuzen%2Fnew-rate#f46043c4a205de1c6cbb3bae4ea4213f9fb9d2bc" dependencies = [ "anyhow", "itertools 0.11.0", @@ -3685,8 +3686,7 @@ dependencies = [ [[package]] name = "qp-plonky2-verifier" version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0eb89fd3cc40c4b25be95399635957d416406328169ba939db989c0444f364" +source = "git+https://github.com/Quantus-Network/qp-plonky2.git?branch=illuzen%2Fnew-rate#f46043c4a205de1c6cbb3bae4ea4213f9fb9d2bc" dependencies = [ "ahash", "anyhow", @@ -3710,9 +3710,8 @@ dependencies = [ [[package]] name = "qp-poseidon" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce4d91f0077b30ec888423fb7549bea6ee6fa6cbbbfe67eaeea38e101e0f630d" +version = "1.2.0" +source = "git+https://github.com/Quantus-Network/qp-poseidon.git?branch=illuzen%2Finjective-to-felts#df2963aae3f818466351810ff7b2061e75cd9836" dependencies = [ "log", "p3-field", @@ -3729,9 +3728,8 @@ dependencies = [ [[package]] name = "qp-poseidon-constants" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15005ef7ac88e170a9461990130619855284bd524e8ad059491ed291222d1553" +version = "1.1.0" +source = "git+https://github.com/Quantus-Network/qp-poseidon-constants.git?tag=v1.1.0#46916523e57f5412574fc0d9546032f70a66d23e" dependencies = [ "p3-field", "p3-goldilocks", @@ -3742,41 +3740,35 @@ dependencies = [ [[package]] name = "qp-poseidon-core" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcdc2b94d265b986711f5f44eea860c96c9e7b25aaa999f7aa024437c0dcd5f" +version = "1.2.0" +source = "git+https://github.com/Quantus-Network/qp-poseidon.git?branch=illuzen%2Finjective-to-felts#df2963aae3f818466351810ff7b2061e75cd9836" dependencies = [ "p3-field", "p3-goldilocks", "p3-poseidon2", "p3-symmetric", - "qp-plonky2", "qp-poseidon-constants", "rand_chacha 0.9.0", ] [[package]] name = "qp-rusty-crystals-dilithium" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d734438e080d69fa186dac23565dd261fa8146048af00ba8aea7467b429c104b" +version = "2.2.0" +source = "git+https://github.com/Quantus-Network/qp-rusty-crystals.git?branch=illuzen%2Fnew-poseidon-api#141d1e28b28419068f00d15b23de19c71d3adbfa" dependencies = [ "zeroize", ] [[package]] name = "qp-rusty-crystals-hdwallet" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91781bc0b96238c7e038a2d3157410388cb0458b05d42483851e79684e92f1a8" +version = "2.2.0" +source = "git+https://github.com/Quantus-Network/qp-rusty-crystals.git?branch=illuzen%2Fnew-poseidon-api#141d1e28b28419068f00d15b23de19c71d3adbfa" dependencies = [ "bip39", - "bs58", "getrandom 0.2.17", "hex", "hex-literal", "hmac 0.12.1", - "k256", "qp-poseidon-core", "qp-rusty-crystals-dilithium", "serde", @@ -3788,9 +3780,8 @@ dependencies = [ [[package]] name = "qp-wormhole-aggregator" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad3d3f37af4748e635f9197b2145cf4d218b97ad361e6b696724e3ddbb4e12a" +version = "1.1.1" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits.git?branch=illuzen%2Fnon-injectivity#9cda538d243f797a20bc7cb967719befeea411e6" dependencies = [ "anyhow", "hex", @@ -3803,14 +3794,12 @@ dependencies = [ "rayon", "serde", "serde_json", - "sha2 0.10.9", ] [[package]] name = "qp-wormhole-circuit" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7cdfba4fd293063a3e9eb964e2afb58673e9a7fd6d4edb0484783e0ed600927" +version = "1.1.1" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits.git?branch=illuzen%2Fnon-injectivity#9cda538d243f797a20bc7cb967719befeea411e6" dependencies = [ "anyhow", "hex", @@ -3819,20 +3808,31 @@ dependencies = [ "qp-zk-circuits-common", ] +[[package]] +name = "qp-wormhole-circuit-builder" +version = "1.1.1" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits.git?branch=illuzen%2Fnon-injectivity#9cda538d243f797a20bc7cb967719befeea411e6" +dependencies = [ + "anyhow", + "clap", + "qp-plonky2", + "qp-wormhole-aggregator", + "qp-wormhole-circuit", + "qp-zk-circuits-common", +] + [[package]] name = "qp-wormhole-inputs" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53ad195630b070fc8cd9d89c55a951abaae9694434793bc87f5ab3045ded7108" +version = "1.1.1" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits.git?branch=illuzen%2Fnon-injectivity#9cda538d243f797a20bc7cb967719befeea411e6" dependencies = [ "anyhow", ] [[package]] name = "qp-wormhole-prover" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d244e8514279f65d25f15ed5a6e6464905ac5276724a9233574696e11a461c3a" +version = "1.1.1" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits.git?branch=illuzen%2Fnon-injectivity#9cda538d243f797a20bc7cb967719befeea411e6" dependencies = [ "anyhow", "qp-plonky2", @@ -3843,9 +3843,8 @@ dependencies = [ [[package]] name = "qp-wormhole-verifier" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e95153853ceceeba61295ca5f1316d12bde37677b0c1e7f0539d815f627645" +version = "1.1.1" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits.git?branch=illuzen%2Fnon-injectivity#9cda538d243f797a20bc7cb967719befeea411e6" dependencies = [ "anyhow", "qp-plonky2-verifier", @@ -3854,13 +3853,13 @@ dependencies = [ [[package]] name = "qp-zk-circuits-common" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45c3d80adc2aecbcf27902569d3ec291f5f83e9d7d17ad12530f45102963faa" +version = "1.1.1" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits.git?branch=illuzen%2Fnon-injectivity#9cda538d243f797a20bc7cb967719befeea411e6" dependencies = [ "anyhow", "hex", "qp-plonky2", + "qp-poseidon-constants", "qp-poseidon-core", "qp-wormhole-inputs", "rand 0.8.5", @@ -3891,6 +3890,7 @@ dependencies = [ "qp-rusty-crystals-hdwallet", "qp-wormhole-aggregator", "qp-wormhole-circuit", + "qp-wormhole-circuit-builder", "qp-wormhole-inputs", "qp-wormhole-prover", "qp-wormhole-verifier", @@ -4327,9 +4327,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", diff --git a/Cargo.toml b/Cargo.toml index d673746..b67ed5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,11 +49,11 @@ rand = "0.9" aes-gcm = "0.10" # AES-256-GCM (quantum-safe with 256-bit keys) # Quantus crypto dependencies (aligned with chain) -qp-rusty-crystals-dilithium = { version = "2.1.0" } -qp-rusty-crystals-hdwallet = { version = "2.0.0" } -# Chain primitive (same branch as node); Cargo finds crate by name in repo -qp-dilithium-crypto = { version = "0.2.2", features = ["serde"] } -qp-poseidon = { version = "1.1.0" } +qp-rusty-crystals-dilithium = { git = "https://github.com/Quantus-Network/qp-rusty-crystals.git", branch = "illuzen/new-poseidon-api" } +qp-rusty-crystals-hdwallet = { git = "https://github.com/Quantus-Network/qp-rusty-crystals.git", branch = "illuzen/new-poseidon-api" } +# Chain primitive +qp-dilithium-crypto = { git = "https://github.com/Quantus-Network/chain.git", branch = "illuzen/no-length-trie" } +qp-poseidon = { git = "https://github.com/Quantus-Network/qp-poseidon.git", branch = "illuzen/injective-to-felts" } # HTTP client for Subsquid queries reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } @@ -77,14 +77,39 @@ subxt-metadata = "0.44" # ZK proof generation (aligned with chain) anyhow = "1.0" -qp-wormhole-circuit = { version = "1.0.7", default-features = false, features = ["std"] } -qp-wormhole-prover = { version = "1.0.7", default-features = false, features = ["std"] } -qp-wormhole-verifier = { version = "1.0.7", default-features = false, features = ["std"] } -qp-wormhole-aggregator = { version = "1.0.7", default-features = false, features = ["rayon", "std"] } -qp-wormhole-inputs = { version = "1.0.7", default-features = false, features = ["std"] } -qp-zk-circuits-common = { version = "1.0.7", default-features = false, features = ["std"] } -qp-plonky2 = { version = "1.1.3", default-features = false, features = ["rand", "std"] } +qp-wormhole-circuit = { git = "https://github.com/Quantus-Network/qp-zk-circuits.git", branch = "illuzen/non-injectivity", default-features = false, features = ["std"] } +qp-wormhole-circuit-builder = { git = "https://github.com/Quantus-Network/qp-zk-circuits.git", branch = "illuzen/non-injectivity" } +qp-wormhole-prover = { git = "https://github.com/Quantus-Network/qp-zk-circuits.git", branch = "illuzen/non-injectivity", default-features = false, features = ["std"] } +qp-wormhole-verifier = { git = "https://github.com/Quantus-Network/qp-zk-circuits.git", branch = "illuzen/non-injectivity", default-features = false, features = ["std"] } +qp-wormhole-aggregator = { git = "https://github.com/Quantus-Network/qp-zk-circuits.git", branch = "illuzen/non-injectivity", default-features = false, features = ["rayon", "std"] } +qp-wormhole-inputs = { git = "https://github.com/Quantus-Network/qp-zk-circuits.git", branch = "illuzen/non-injectivity", default-features = false, features = ["std"] } +qp-zk-circuits-common = { git = "https://github.com/Quantus-Network/qp-zk-circuits.git", branch = "illuzen/non-injectivity", default-features = false, features = ["std"] } +qp-plonky2 = { git = "https://github.com/Quantus-Network/qp-plonky2.git", branch = "illuzen/new-rate", default-features = false, features = ["rand", "std"] } + +[build-dependencies] +qp-wormhole-circuit-builder = { git = "https://github.com/Quantus-Network/qp-zk-circuits.git", branch = "illuzen/non-injectivity" } + +[patch.crates-io] +# Quantus dependencies +qp-poseidon = { git = "https://github.com/Quantus-Network/qp-poseidon.git", branch = "illuzen/injective-to-felts" } +qp-poseidon-core = { git = "https://github.com/Quantus-Network/qp-poseidon.git", branch = "illuzen/injective-to-felts" } +qp-poseidon-constants = { git = "https://github.com/Quantus-Network/qp-poseidon-constants.git", tag = "v1.1.0" } +qp-plonky2 = { git = "https://github.com/Quantus-Network/qp-plonky2.git", branch = "illuzen/new-rate" } +qp-plonky2-verifier = { git = "https://github.com/Quantus-Network/qp-plonky2.git", branch = "illuzen/new-rate" } +qp-plonky2-core = { git = "https://github.com/Quantus-Network/qp-plonky2.git", branch = "illuzen/new-rate" } +qp-plonky2-field = { git = "https://github.com/Quantus-Network/qp-plonky2.git", branch = "illuzen/new-rate" } +qp-rusty-crystals-dilithium = { git = "https://github.com/Quantus-Network/qp-rusty-crystals.git", branch = "illuzen/new-poseidon-api" } +qp-rusty-crystals-hdwallet = { git = "https://github.com/Quantus-Network/qp-rusty-crystals.git", branch = "illuzen/new-poseidon-api" } [dev-dependencies] tempfile = "3.8.1" serial_test = "3.1" + +# 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/client.rs b/src/chain/client.rs index 5d85f68..5b82fcd 100644 --- a/src/chain/client.rs +++ b/src/chain/client.rs @@ -7,7 +7,7 @@ use crate::{error::QuantusError, log_verbose}; use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; use qp_dilithium_crypto::types::DilithiumSignatureScheme; use qp_poseidon::PoseidonHasher; -use sp_core::{crypto::AccountId32, ByteArray}; +use sp_core::{crypto::AccountId32, ByteArray, Pair}; use sp_runtime::{traits::IdentifyAccount, MultiAddress}; use std::{sync::Arc, time::Duration}; use subxt::{ @@ -256,7 +256,7 @@ impl QuantusClient { impl subxt::tx::Signer for qp_dilithium_crypto::types::DilithiumPair { fn account_id(&self) -> ::AccountId { let resonance_public = - qp_dilithium_crypto::types::DilithiumPublic::from_slice(self.public.as_slice()) + qp_dilithium_crypto::types::DilithiumPublic::from_slice(self.public().as_ref()) .expect("Invalid public key"); ::into_account( resonance_public, 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 a42afc3..91f64b7 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, @@ -26,7 +30,7 @@ use qp_wormhole_prover::WormholeProver; use qp_zk_circuits_common::{ circuit::{C, D, F}, storage_proof::prepare_proof_for_circuit, - utils::{digest_felts_to_bytes, BytesDigest}, + utils::{digest_to_bytes, BytesDigest}, }; use rand::RngCore; use sp_core::crypto::{AccountId32, Ss58Codec}; @@ -52,95 +56,6 @@ pub const SCALE_DOWN_FACTOR: u128 = 10_000_000_000; /// 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, - pub aggregated_common: Option, - pub aggregated_verifier: Option, - pub dummy_proof: Option, -} - -/// 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, -} - -impl AggregationConfig { - /// Load config from the generated-bins directory - pub fn load_from_bins() -> crate::error::Result { - 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 { - 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 { @@ -691,6 +606,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, + + /// Read password from file + #[arg(long)] + password_file: Option, + + /// 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 +738,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`. @@ -825,11 +770,8 @@ fn show_wormhole_address(secret_hex: String) -> crate::error::Result<()> { qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret) .account_id; let unspendable_account_bytes_digest = - qp_zk_circuits_common::utils::digest_felts_to_bytes(unspendable_account); - let unspendable_account_bytes: [u8; 32] = unspendable_account_bytes_digest - .as_ref() - .try_into() - .expect("BytesDigest is always 32 bytes"); + qp_zk_circuits_common::utils::digest_to_bytes(unspendable_account); + let unspendable_account_bytes: [u8; 32] = *unspendable_account_bytes_digest; let account_id = sp_core::crypto::AccountId32::new(unspendable_account_bytes); let ss58_address = @@ -857,8 +799,7 @@ async fn aggregate_proofs( proof_files: Vec, output_file: String, ) -> crate::error::Result<()> { - use qp_wormhole_aggregator::aggregator::WormholeProofAggregator; - use qp_zk_circuits_common::aggregation::AggregationConfig as AggConfig; + use qp_wormhole_aggregator::aggregator::Layer0Aggregator; use std::path::Path; @@ -866,11 +807,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 { @@ -888,17 +830,17 @@ async fn aggregate_proofs( // This also generates the padding (dummy) proofs needed log_print!(" Loading aggregator and generating {} dummy proofs...", num_padding_proofs); - let aggr_config = AggConfig::new(agg_config.num_leaf_proofs); - let mut aggregator = WormholeProofAggregator::from_prebuilt_dir(bins_dir, aggr_config) - .map_err(|e| { - crate::error::QuantusError::Generic(format!( - "Failed to load aggregator from pre-built bins: {}", - e - )) - })?; + let mut aggregator = Layer0Aggregator::new(bins_dir).map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to load aggregator from pre-built bins: {}", + e + )) + })?; - log_verbose!("Aggregation config: num_leaf_proofs={}", aggregator.config.num_leaf_proofs); - let common_data = aggregator.leaf_circuit_data.common.clone(); + log_verbose!("Aggregation config: num_leaf_proofs={}", aggregator.batch_size()); + let common_data = aggregator.load_common_data(CircuitType::Leaf).map_err(|e| { + crate::error::QuantusError::Generic(format!("Failed to get common data: {}", e)) + })?; // Load and add proofs using helper function for (idx, proof_file) in proof_files.iter().enumerate() { @@ -930,15 +872,14 @@ async fn aggregate_proofs( log_print!(" Aggregation: {:.2}s", agg_elapsed.as_secs_f64()); // Parse and display aggregated public inputs - let aggregated_public_inputs = AggregatedPublicCircuitInputs::try_from_felts( - aggregated_proof.proof.public_inputs.as_slice(), - ) - .map_err(|e| { - crate::error::QuantusError::Generic(format!( - "Failed to parse aggregated public inputs: {}", - e - )) - })?; + let aggregated_public_inputs = + AggregatedPublicCircuitInputs::try_from_felts(aggregated_proof.public_inputs.as_slice()) + .map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to parse aggregated public inputs: {}", + e + )) + })?; log_verbose!("Aggregated public inputs: {:#?}", aggregated_public_inputs); @@ -966,18 +907,15 @@ async fn aggregate_proofs( // Verify the aggregated proof locally log_verbose!("Verifying aggregated proof locally..."); - aggregated_proof - .circuit_data - .verify(aggregated_proof.proof.clone()) - .map_err(|e| { - crate::error::QuantusError::Generic(format!( - "Aggregated proof verification failed: {}", - e - )) - })?; + aggregator.verify(aggregated_proof.clone()).map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Local aggregated proof verification failed: {}", + e + )) + })?; // Save aggregated proof using helper function - write_proof_file(&output_file, &aggregated_proof.proof.to_bytes()).map_err(|e| { + write_proof_file(&output_file, &aggregated_proof.to_bytes()).map_err(|e| { crate::error::QuantusError::Generic(format!("Failed to write proof: {}", e)) })?; @@ -1352,7 +1290,7 @@ fn load_multiround_wallet( fn print_multiround_config( config: &MultiroundConfig, wallet: &MultiroundWalletContext, - agg_config: &AggregationConfig, + num_leaf_proofs: usize, ) { use colored::Colorize; @@ -1367,7 +1305,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!(""); @@ -1714,7 +1652,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)?; @@ -1727,7 +1668,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!(""); @@ -1965,11 +1906,8 @@ async fn generate_proof( qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret) .account_id; let unspendable_account_bytes_digest = - qp_zk_circuits_common::utils::digest_felts_to_bytes(unspendable_account); - let unspendable_account_bytes: [u8; 32] = unspendable_account_bytes_digest - .as_ref() - .try_into() - .expect("BytesDigest is always 32 bytes"); + qp_zk_circuits_common::utils::digest_to_bytes(unspendable_account); + let unspendable_account_bytes: [u8; 32] = *unspendable_account_bytes_digest; let from_account = funding_account.clone(); let to_account = AccountId32::new(unspendable_account_bytes); @@ -1980,28 +1918,31 @@ 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::( - &( - 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); + // Build storage key using the new format: + // - Key suffix: Blake2_256(asset_id, transfer_count, from, to) - without amount + // - Value: Poseidon2(asset_id, transfer_count, from, to, amount) - the leaf_inputs_hash + + // 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 transfer_data_tuple = + (NATIVE_ASSET_ID, transfer_count, from_account.clone(), to_account.clone(), funding_amount); + let encoded_data = transfer_data_tuple.encode(); + let leaf_hash = qp_poseidon::PoseidonHasher::hash_storage::(&encoded_data); + + // 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); + + // Hash the key tuple with Blake2_256 and append + let key_tuple: TransferProofKey = (to_account.clone(), transfer_count); + let encoded_key = key_tuple.encode(); + let key_hash = sp_core::blake2_256(&encoded_key); + final_key.extend_from_slice(&key_hash); // Verify storage key exists let storage_api = client.storage().at(block_hash); @@ -2067,7 +2008,7 @@ async fn generate_proof( output_amount_1: output_assignment.output_amount_1, output_amount_2: output_assignment.output_amount_2, volume_fee_bps: VOLUME_FEE_BPS, - nullifier: digest_felts_to_bytes(Nullifier::from_preimage(secret, transfer_count).hash), + 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()) @@ -2478,13 +2419,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()); @@ -2630,7 +2570,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..."); @@ -2704,22 +2644,17 @@ 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::WormholeProofAggregator; - use qp_zk_circuits_common::aggregation::AggregationConfig as AggConfig; +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 aggr_config = AggConfig::new(agg_config.num_leaf_proofs); - let mut aggregator = WormholeProofAggregator::from_prebuilt_dir(bins_dir, aggr_config) - .map_err(|e| { - crate::error::QuantusError::Generic(format!("Failed to create aggregator: {}", e)) - })?; + let mut aggregator = Layer0Aggregator::new(bins_dir).map_err(|e| { + crate::error::QuantusError::Generic(format!("Failed to create aggregator: {}", e)) + })?; - let common_data = aggregator.leaf_circuit_data.common.clone(); + let common_data = aggregator.load_common_data(CircuitType::Leaf).map_err(|e| { + crate::error::QuantusError::Generic(format!("Failed to load common data: {}", e)) + })?; for proof_file in proof_files { let proof_bytes = read_hex_proof_file_to_bytes(proof_file)?; @@ -2736,13 +2671,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.proof.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)) })?; @@ -2750,6 +2685,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, + password_file: Option, + 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::() + .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 = 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, + 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::(&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::*; @@ -3000,22 +3773,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/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 37c25d2..e30cf68 100644 --- a/src/wallet/keystore.rs +++ b/src/wallet/keystore.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use sp_core::crypto::Ss58AddressFormat; use sp_core::{ crypto::{AccountId32, Ss58Codec}, - ByteArray, + ByteArray, Pair, }; // Quantum-safe encryption imports use aes_gcm::{ @@ -58,19 +58,16 @@ impl QuantumKeyPair { /// Convert to DilithiumPair for use with substrate-api-client pub fn to_resonance_pair(&self) -> Result { - // Convert our QuantumKeyPair to DilithiumPair using from_seed - // Use the private key as the seed - Ok(DilithiumPair { - public: self.public_key.as_slice().try_into().unwrap(), - secret: self.private_key.as_slice().try_into().unwrap(), - }) + // Convert our QuantumKeyPair to DilithiumPair using from_raw + Ok(DilithiumPair::from_raw(&self.public_key, &self.private_key) + .map_err(|_| crate::error::WalletError::KeyGeneration)?) } #[allow(dead_code)] pub fn from_resonance_pair(keypair: &DilithiumPair) -> Self { Self { - public_key: keypair.public.as_ref().to_vec(), - private_key: keypair.secret.as_ref().to_vec(), + public_key: keypair.public().as_ref().to_vec(), + private_key: keypair.secret_bytes().to_vec(), } } @@ -317,8 +314,8 @@ mod tests { let quantum_keypair = QuantumKeyPair::from_resonance_pair(&resonance_pair); // 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.as_ref().to_vec()); + assert_eq!(quantum_keypair.public_key, resonance_pair.public().as_ref().to_vec()); + assert_eq!(quantum_keypair.private_key.as_slice(), resonance_pair.secret_bytes()); } #[test] @@ -330,8 +327,8 @@ mod tests { quantum_keypair.to_resonance_pair().expect("Conversion should succeed"); // Verify round-trip conversion preserves data - assert_eq!(original_pair.public.as_ref(), converted_pair.public.as_ref()); - assert_eq!(original_pair.secret.as_ref(), converted_pair.secret.as_ref()); + assert_eq!(original_pair.public().as_ref(), converted_pair.public().as_ref()); + assert_eq!(original_pair.secret_bytes(), converted_pair.secret_bytes()); } /// Wallet address must match chain: same AccountId (Poseidon hash of Dilithium public) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 0837d2d..7ddec09 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -13,6 +13,7 @@ pub use keystore::{Keystore, QuantumKeyPair, WalletData}; use qp_rusty_crystals_hdwallet::{derive_key_from_mnemonic, generate_mnemonic, SensitiveBytes32}; use rand::{rng, RngCore}; use serde::{Deserialize, Serialize}; +use sp_core::Pair; use sp_runtime::traits::IdentifyAccount; /// Default derivation path for Quantus wallets: m/44'/189189'/0'/0'/0' @@ -773,8 +774,8 @@ mod tests { let (wallet_manager, _temp_dir) = create_test_wallet_manager().await; let test_mnemonic = "orchard answer curve patient visual flower maze noise retreat penalty cage small earth domain scan pitch bottom crunch theme club client swap slice raven"; - let expected_address = "qzoog56PJKvDwqo9GwkzRN74kxEgDEspxu5zVA62y18ttt3tG"; // default derivation path index 0 - let expected_address_no_derive = "qzofkFbmnEYLX6iHwqJ9uKYXFi7ypQwcBBMxcYYLVD17vGpsm"; + let expected_address = "qzm5QCox8Dp5A3oSXZZYHD8YoYgPz7enykZb6RPUropdCyN5h"; // default derivation path index 0 + let expected_address_no_derive = "qzopudncsY1FdVZhxPwdEWn9uDgv2yPNj1n2x3d9xpWboSqyK"; let imported_wallet = wallet_manager .import_wallet("imported-test-wallet", test_mnemonic, Some("import-password")) diff --git a/tests/wormhole_integration.rs b/tests/wormhole_integration.rs index 1353029..0fabdfa 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,8 @@ //! with valid parent hash linkage. We use batch transfers to ensure same-block proofs. use plonky2::plonk::{circuit_data::CircuitConfig, proof::ProofWithPublicInputs}; +use qp_poseidon::ToFelts; +use qp_wormhole_aggregator::aggregator::{AggregationBackend, Layer0Aggregator}; use qp_wormhole_circuit::{ inputs::{CircuitInputs, PrivateCircuitInputs}, nullifier::Nullifier, @@ -61,8 +63,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 +338,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::( + // 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::( &( NATIVE_ASSET_ID, transfer_data.transfer_count, @@ -341,16 +351,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 +469,7 @@ async fn generate_proof_from_transfer( async fn submit_single_proof_for_verification( quantus_client: &QuantusClient, proof_bytes: Vec, -) -> Result<(), String> { +) -> Result { println!(" Submitting single proof for on-chain verification..."); let verify_tx = quantus_node::api::tx().wormhole().verify_aggregated_proof(proof_bytes); @@ -477,7 +490,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 +498,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,27 +515,24 @@ fn aggregate_proofs( proof_contexts: Vec, num_leaf_proofs: usize, ) -> Result { - use qp_wormhole_aggregator::aggregator::WormholeProofAggregator; - use qp_zk_circuits_common::aggregation::AggregationConfig; - println!( " Aggregating {} proofs (num_leaf_proofs={})...", proof_contexts.len(), num_leaf_proofs, ); - let config = CircuitConfig::standard_recursion_zk_config(); - let aggregation_config = AggregationConfig::new(num_leaf_proofs); - - if proof_contexts.len() > aggregation_config.num_leaf_proofs { + if proof_contexts.len() > num_leaf_proofs { return Err(format!( "Too many proofs: {} provided, max {}", proof_contexts.len(), - aggregation_config.num_leaf_proofs, + num_leaf_proofs, )); } - let mut aggregator = WormholeProofAggregator::from_circuit_config(config, aggregation_config); + 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); @@ -543,23 +553,21 @@ 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.proof.public_inputs.as_slice(), - ) - .map_err(|e| format!("Failed to parse aggregated public inputs: {}", e))?; + let public_inputs = + AggregatedPublicCircuitInputs::try_from_felts(aggregated_proof.public_inputs.as_slice()) + .map_err(|e| format!("Failed to parse aggregated public inputs: {}", e))?; // Verify locally first println!(" Verifying aggregated proof locally..."); - aggregated_result - .circuit_data - .verify(aggregated_result.proof.clone()) + aggregator + .verify(aggregated_proof.clone()) .map_err(|e| format!("Local verification failed: {}", e))?; - let proof_bytes = aggregated_result.proof.to_bytes(); + let proof_bytes = aggregated_proof.to_bytes(); println!( " Aggregation complete! Size: {} bytes, {} nullifiers", proof_bytes.len(), @@ -573,7 +581,7 @@ fn aggregate_proofs( async fn submit_aggregated_proof_for_verification( quantus_client: &QuantusClient, proof_bytes: Vec, -) -> Result<(), String> { +) -> Result { println!(" Submitting aggregated proof for on-chain verification..."); let verify_tx = quantus_node::api::tx().wormhole().verify_aggregated_proof(proof_bytes); @@ -594,7 +602,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(); @@ -602,7 +610,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)); @@ -614,12 +622,20 @@ 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 { 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::PoseidonHasher::hash_variable_length(preimage.to_felts()); + SubxtAccountId::decode(&mut &author_bytes[..]).ok() + }, _ => None, }) } @@ -703,16 +719,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(); @@ -853,19 +864,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();