Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
03dad4d
fix(wallet): stop accepting mnemonic and seed via CLI flags
illuzen Aug 4, 2026
253c791
fix(wallet): skip malformed files when listing wallets
illuzen Aug 4, 2026
d2ac243
fix(multisig): correlate MultisigCreated to creator and params
illuzen Aug 4, 2026
e6dd668
fix(wallet): reject raw --password CLI credentials
illuzen Aug 4, 2026
58b8780
fix(tx): fail when watched extrinsic is missing from block
illuzen Aug 4, 2026
3bc368c
fix(wallet): enforce owner-only keystore permissions
illuzen Aug 4, 2026
27d8a9f
fix(wallet): require restrictive password-file permissions
illuzen Aug 4, 2026
0460883
fix(batch): use utility.batch_all for atomic transfers
illuzen Aug 4, 2026
f111d9b
fix(wormhole): require persisted mnemonic for HD secrets
illuzen Aug 4, 2026
dfd96c0
fix(tx): stop unsafe nonce-bump retries on ambiguous errors
illuzen Aug 4, 2026
02c741a
fix(wallet): authenticate address metadata and fail closed on migration
illuzen Aug 4, 2026
bfd228e
fix(wallet): harden storage races and exclusive wallet creation
illuzen Aug 4, 2026
a8051b1
fix(client): verify Quantus runtime identity at connect time
illuzen Aug 4, 2026
304d6d5
fix(system): fail closed on invalid RPC token properties
illuzen Aug 4, 2026
5fc7920
fix(wormhole): bind transfer events to from amount and count
illuzen Aug 4, 2026
c5d3f2d
fix(update): verify release archive SHA-256 before install
illuzen Aug 4, 2026
eeaefa7
fix(wallet): refuse to persist wallets with embedded AES key material
illuzen Aug 4, 2026
95c1fc9
fix(wallet): return errors for malformed public keys
illuzen Aug 4, 2026
986c2cd
fix(client): redact WebSocket URL credentials in diagnostics
illuzen Aug 4, 2026
a710bf6
fix(wormhole): remove --secret argv and verify extrinsic failures
illuzen Aug 4, 2026
0bec0ea
fix(wallet): write exported mnemonics to a protected file
illuzen Aug 4, 2026
1e9e910
fix(multisig): deduplicate signers before predict and threshold
illuzen Aug 4, 2026
b7039e6
fix(storage): bound pagination against overflow and stuck cursors
illuzen Aug 4, 2026
5015a2e
fix(tx): bound transaction-status subscription waits
illuzen Aug 4, 2026
12d3ff6
fix(rewards): use checked addition for indexer transfer totals
illuzen Aug 4, 2026
bcdab11
fix(batch): enforce runtime batched_calls_limit for batch size
illuzen Aug 4, 2026
fb5b36f
fix(subsquid): harden exhaustive transfer queries and spent filtering
illuzen Aug 4, 2026
fbd38ed
fix(wormhole): zeroize proof-generation secrets after use
illuzen Aug 4, 2026
8cd2d4c
fix(wormhole): validate Merkle depth and prefer finalized snapshots
illuzen Aug 4, 2026
06997c2
fix(wallet): zeroize secret material after encrypt and decrypt
illuzen Aug 4, 2026
fdaee0c
fix(cli): validate amounts delays ranks and fallible address helpers
illuzen Aug 4, 2026
7f569f9
fix(bins): authenticate circuit artifacts and publish atomically
illuzen Aug 4, 2026
ccc244a
fix(cli): bound ranges and reject silent zero coercions
illuzen Aug 4, 2026
3d781aa
fix(wallet): require an explicit password when creating wallets
illuzen Aug 4, 2026
82867b5
chore: silence clippy -D warnings failures
illuzen Aug 4, 2026
e6f112a
style: apply rustfmt after clippy run
illuzen Aug 4, 2026
2a58798
fix(tx): don't abort finalization waits on 30s inactivity
illuzen Aug 4, 2026
ea9318e
ci: remove CodeQL workflow
illuzen Aug 4, 2026
3278801
fmt
illuzen Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 0 additions & 50 deletions .github/workflows/codeql.yml

This file was deleted.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ qp-zk-circuits-common = { version = "3.1.0", default-features = false, features
hex = "0.4"
qp-poseidon-core = "3.0.2"
qp-wormhole-circuit-builder = { version = "3.1.0" }
sha2 = "0.10"

[dev-dependencies]
qp-poseidon-core = "3.0.2"
Expand Down
109 changes: 65 additions & 44 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@
//! to manually run `quantus developer build-circuits`.
//!
//! Outputs are written to `OUT_DIR` (required by cargo) and, during local source
//! builds only, linked/copied to `generated-bins/` in the project root. When the
//! crate is consumed via `cargo install` or `cargo publish` verification, the
//! manifest lives under `~/.cargo/registry/src/` or `target/package/`
//! builds only, atomically published to `generated-bins/` in the project root.
//! When the crate is consumed via `cargo install` or `cargo publish` verification,
//! the manifest lives under `~/.cargo/registry/src/` or `target/package/`
//! respectively — locations the installed binary cannot reach — so the project
//! copy is skipped. Installed binaries regenerate the files on first run via
//! `crate::bins::ensure_bins_dir()`.
//!
//! Set `SKIP_CIRCUIT_BUILD=1` to skip circuit generation (useful for CI jobs
//! that don't need the circuits, like clippy/doc checks).

use std::{env, path::Path, time::Instant};
use sha2::{Digest, Sha256};
use std::{env, time::Instant};

include!("src/bins_consts.rs");
include!("src/bins_fs.rs");

/// Compute Poseidon2 hash of bytes and return hex string
fn poseidon_hex(data: &[u8]) -> String {
Expand All @@ -40,6 +42,59 @@ fn print_bin_hash(dir: &Path, filename: &str) {
}
}

const MANIFESTED_FILES: &[&str] = &[
"verifier.bin",
"common.bin",
"private_batch_prover.bin",
"private_batch_verifier.bin",
"private_batch_common.bin",
"public_batch_prover.bin",
"public_batch_verifier.bin",
"public_batch_common.bin",
"dummy_proof.bin",
"dummy_private_batch_proof.bin",
"config.json",
VERSION_MARKER,
];

fn file_sha256_hex(dir: &Path, filename: &str) -> String {
let data =
std::fs::read(dir.join(filename)).expect("Failed to read generated artifact for manifest");
let mut hasher = Sha256::new();
hasher.update(&data);
hex::encode(hasher.finalize())
}

fn json_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}

fn write_manifest(
dir: &Path,
pkg_version: &str,
num_leaf_proofs: usize,
num_private_batch_proofs: usize,
) {
let mut content = String::new();
content.push_str("{\n");
content.push_str(" \"manifest_version\": 1,\n");
content.push_str(&format!(" \"package_version\": \"{}\",\n", json_escape(pkg_version)));
content.push_str(&format!(" \"num_leaf_proofs\": {},\n", num_leaf_proofs));
content.push_str(&format!(" \"num_private_batch_proofs\": {},\n", num_private_batch_proofs));
content.push_str(" \"files\": {\n");
for (idx, filename) in MANIFESTED_FILES.iter().enumerate() {
let comma = if idx + 1 == MANIFESTED_FILES.len() { "" } else { "," };
content.push_str(&format!(
" \"{}\": \"{}\"{}\n",
json_escape(filename),
file_sha256_hex(dir, filename),
comma
));
}
content.push_str(" }\n}\n");
std::fs::write(dir.join(MANIFEST_FILE), content).expect("Failed to write artifact manifest");
}

fn main() {
// Allow skipping circuit generation for CI jobs that don't need it
if env::var("SKIP_CIRCUIT_BUILD").is_ok() {
Expand Down Expand Up @@ -95,6 +150,7 @@ fn main() {
let pkg_version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION not set");
std::fs::write(build_output_dir.join(VERSION_MARKER), &pkg_version)
.expect("Failed to write version marker");
write_manifest(&build_output_dir, &pkg_version, num_leaf_proofs, num_private_batch_proofs);

let elapsed = start.elapsed();
println!(
Expand All @@ -113,49 +169,14 @@ fn main() {
print_bin_hash(&build_output_dir, "public_batch_verifier.bin");
print_bin_hash(&build_output_dir, "public_batch_prover.bin");

// Copy bins to project root for runtime access, but only during local source
// builds — never during `cargo publish` verification (manifest_dir is inside
// `target/package/`) nor during `cargo install` (manifest_dir is inside
// `.cargo/registry/src/`). In those cases the installed binary can't see the
// project dir; runtime lazy-generation takes over instead.
// Atomically publish a real directory (not a symlink) for runtime access.
// Symlinks are refused by runtime `ensure_bins_dir` (#160699), and the old
// check/remove/symlink/copy sequence was racy (#160700).
let project_bins = Path::new(&manifest_dir).join("generated-bins");
let is_source_build =
!manifest_dir.contains("target/package/") && !manifest_dir.contains(".cargo/registry/src");
if is_source_build {
// Prefer a symlink to avoid copying large prover binaries on every build.
// If symlink creation fails (e.g. on filesystems without symlink support),
// fall back to copying and surface errors.
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
// Remove any existing dir/file/symlink at destination.
if let Ok(meta) = std::fs::symlink_metadata(&project_bins) {
if meta.is_dir() {
std::fs::remove_dir_all(&project_bins)
.expect("Failed to remove existing generated-bins directory");
} else {
std::fs::remove_file(&project_bins)
.expect("Failed to remove existing generated-bins file/symlink");
}
}
if let Err(e) = symlink(&build_output_dir, &project_bins) {
println!(
"cargo:warning=[quantus-cli] Failed to symlink generated-bins ({}). Falling back to copy...",
e
);
} else {
// Symlink created successfully; we're done.
return;
}
}

std::fs::create_dir_all(&project_bins).expect("Failed to create generated-bins directory");
let entries = std::fs::read_dir(&build_output_dir)
.expect("Failed to read generated-bins directory in OUT_DIR");
for entry in entries {
let entry = entry.expect("Failed to read generated-bins entry");
let dest = project_bins.join(entry.file_name());
std::fs::copy(entry.path(), dest).expect("Failed to copy generated-bins file");
}
publish_dir_atomically(&build_output_dir, &project_bins)
.unwrap_or_else(|e| panic!("Failed to publish generated-bins: {e}"));
}
}
4 changes: 2 additions & 2 deletions examples/basic_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ async fn main() -> Result<()> {
println!("🔗 Connected to Quantus node");

// 4. Load the wallet for transactions
let wallet_data = wallet_manager.load_wallet("lib_example_wallet", "example_password")?;
let keypair = wallet_data.keypair;
let mut wallet_data = wallet_manager.load_wallet("lib_example_wallet", "example_password")?;
let keypair = wallet_data.take_keypair();

// 5. Get account balance
let account_id = keypair.to_account_id_32();
Expand Down
6 changes: 3 additions & 3 deletions examples/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl WalletService {
let balance = self.get_wallet_balance(name, password).await?;

Ok(WalletInfo {
name: wallet_data.name,
name: wallet_data.name.clone(),
address: wallet_data.keypair.to_account_id_ss58check(),
balance,
created_at: chrono::Utc::now().to_rfc3339(), // Could be stored in wallet data
Expand Down Expand Up @@ -152,9 +152,9 @@ impl WalletService {
/// Private method to perform transfer
async fn perform_transfer(&self, request: &TransferRequest) -> Result<subxt::utils::H256> {
// Load sender wallet
let wallet_data =
let mut wallet_data =
self.wallet_manager.load_wallet(&request.from_wallet, &request.password)?;
let keypair = wallet_data.keypair;
let keypair = wallet_data.take_keypair();

// Parse recipient address
let to_account_id = AccountId32::from_ss58check(&request.to_address)
Expand Down
4 changes: 2 additions & 2 deletions examples/wallet_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ impl QuantusApp {
amount: u128,
) -> Result<String> {
// Load sender wallet
let wallet_data = self.wallet_manager.load_wallet(from_wallet, from_password)?;
let keypair = wallet_data.keypair;
let mut wallet_data = self.wallet_manager.load_wallet(from_wallet, from_password)?;
let keypair = wallet_data.take_keypair();

// Parse recipient address
let (to_account_id, _) = AccountId32::from_ss58check_with_version(to_address)
Expand Down
4 changes: 2 additions & 2 deletions examples/wormhole_sdk_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ async fn main() -> Result<()> {

// 1. wallet ----------------------------------------------------------------
let wm = WalletManager::new()?;
let wallet = wm.load_wallet(&args.funder, &args.password)?;
let funder_kp = wallet.keypair;
let mut wallet = wm.load_wallet(&args.funder, &args.password)?;
let funder_kp = wallet.take_keypair();
let funder_ss58 = funder_kp.to_account_id_ss58check();
println!(" wallet : {funder_ss58}");

Expand Down
2 changes: 2 additions & 0 deletions src/batch_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fn load_batch_verifier_from_files(
/// Load the private-batch verifier from `bins_dir`, applying batch profile checks
/// (not the leaf keccak256 pin).
pub fn load_private_batch_verifier(bins_dir: &Path) -> Result<WormholeVerifier> {
crate::bins::verify_manifest(bins_dir)?;
let config = CircuitBinsConfig::load(bins_dir).map_err(|e| {
QuantusError::Generic(format!(
"Failed to load circuit bins config from {}: {e}",
Expand All @@ -145,6 +146,7 @@ pub fn load_private_batch_verifier(bins_dir: &Path) -> Result<WormholeVerifier>
/// Load the public-batch verifier from `bins_dir`, applying batch profile checks
/// (not the leaf keccak256 pin).
pub fn load_public_batch_verifier(bins_dir: &Path) -> Result<WormholeVerifier> {
crate::bins::verify_manifest(bins_dir)?;
let config = CircuitBinsConfig::load(bins_dir).map_err(|e| {
QuantusError::Generic(format!(
"Failed to load circuit bins config from {}: {e}",
Expand Down
Loading
Loading