Skip to main content

quantus_cli/
wormhole_lib.rs

1//! Wormhole Library Functions
2//!
3//! This module provides library-friendly functions for wormhole proof generation
4//! that can be used by external crates (like quantus-sdk) without requiring
5//! a chain client connection.
6//!
7//! These functions handle the core cryptographic operations:
8//! - Computing leaf hashes for storage proof verification
9//! - Computing storage keys
10//! - Generating ZK proofs from raw inputs
11
12use codec::Encode;
13use qp_wormhole_circuit::{
14	inputs::{CircuitInputs, PrivateCircuitInputs},
15	nullifier::Nullifier,
16};
17use qp_wormhole_inputs::PublicCircuitInputs;
18use qp_wormhole_prover::WormholeProver;
19use qp_zk_circuits_common::{
20	storage_proof::prepare_proof_for_circuit,
21	utils::{digest_to_bytes, BytesDigest},
22};
23use sp_core::crypto::AccountId32;
24use std::path::Path;
25
26/// Native asset id for QTU token
27pub const NATIVE_ASSET_ID: u32 = 0;
28
29/// Scale down factor for quantizing amounts (10^10 to go from 12 to 2 decimal places)
30pub const SCALE_DOWN_FACTOR: u128 = 10_000_000_000;
31
32/// Volume fee rate in basis points (10 bps = 0.1%)
33pub const VOLUME_FEE_BPS: u32 = 10;
34
35/// Full transfer data type - used to compute the leaf_inputs_hash via Poseidon2.
36/// Order: (asset_id, transfer_count, from, to, amount)
37pub type TransferProofData = (u32, u64, AccountId32, AccountId32, u128);
38
39/// Storage key type - (wormhole_address, transfer_count)
40/// This is hashed with Blake2_256 to form the storage key suffix.
41pub type TransferProofKey = (AccountId32, u64);
42
43/// Result type for wormhole library operations
44pub type Result<T> = std::result::Result<T, WormholeLibError>;
45
46/// Error type for wormhole library operations
47#[derive(Debug, Clone)]
48pub struct WormholeLibError {
49	pub message: String,
50}
51
52impl std::fmt::Display for WormholeLibError {
53	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54		write!(f, "{}", self.message)
55	}
56}
57
58impl std::error::Error for WormholeLibError {}
59
60impl From<String> for WormholeLibError {
61	fn from(message: String) -> Self {
62		Self { message }
63	}
64}
65
66/// Input data for generating a wormhole proof.
67/// All fields are raw bytes - no chain client required.
68#[derive(Debug, Clone)]
69pub struct ProofGenerationInput {
70	/// 32-byte secret
71	pub secret: [u8; 32],
72	/// Transfer count (atomic counter per recipient)
73	pub transfer_count: u64,
74	/// Funding account (sender) as 32 bytes
75	pub funding_account: [u8; 32],
76	/// Wormhole address (recipient/unspendable account) as 32 bytes
77	pub wormhole_address: [u8; 32],
78	/// Funding amount in planck (12 decimals)
79	pub funding_amount: u128,
80	/// Block hash as 32 bytes
81	pub block_hash: [u8; 32],
82	/// Block number
83	pub block_number: u32,
84	/// Parent hash as 32 bytes
85	pub parent_hash: [u8; 32],
86	/// State root as 32 bytes
87	pub state_root: [u8; 32],
88	/// Extrinsics root as 32 bytes
89	pub extrinsics_root: [u8; 32],
90	/// SCALE-encoded digest (variable length, padded to 110 bytes internally)
91	pub digest: Vec<u8>,
92	/// Storage proof nodes (each node is a Vec<u8>)
93	pub proof_nodes: Vec<Vec<u8>>,
94	/// Exit account 1 as 32 bytes
95	pub exit_account_1: [u8; 32],
96	/// Exit account 2 as 32 bytes (use zeros for single output)
97	pub exit_account_2: [u8; 32],
98	/// Output amount 1 (quantized, 2 decimals)
99	pub output_amount_1: u32,
100	/// Output amount 2 (quantized, 2 decimals, 0 for single output)
101	pub output_amount_2: u32,
102	/// Volume fee in basis points
103	pub volume_fee_bps: u32,
104	/// Asset ID (0 for native token)
105	pub asset_id: u32,
106}
107
108/// Output from proof generation
109#[derive(Debug, Clone)]
110pub struct ProofGenerationOutput {
111	/// Generated proof as bytes
112	pub proof_bytes: Vec<u8>,
113	/// Nullifier as 32 bytes (available for callers who need it)
114	#[allow(dead_code)]
115	pub nullifier: [u8; 32],
116}
117
118/// Compute the leaf hash (leaf_inputs_hash) for storage proof verification.
119///
120/// This uses Poseidon2 hashing via `hash_storage` to match the chain's
121/// PoseidonStorageHasher behavior.
122///
123/// # Arguments
124/// * `asset_id` - Asset ID (0 for native token)
125/// * `transfer_count` - Atomic transfer counter
126/// * `funding_account` - Sender account as 32 bytes
127/// * `wormhole_address` - Recipient (unspendable) account as 32 bytes
128/// * `amount` - Transfer amount in planck
129///
130/// # Returns
131/// 32-byte leaf hash
132pub fn compute_leaf_hash(
133	asset_id: u32,
134	transfer_count: u64,
135	funding_account: &[u8; 32],
136	wormhole_address: &[u8; 32],
137	amount: u128,
138) -> [u8; 32] {
139	// Use AccountId32 to match the chain's type exactly
140	let from_account = AccountId32::new(*funding_account);
141	let to_account = AccountId32::new(*wormhole_address);
142
143	let transfer_data: TransferProofData =
144		(asset_id, transfer_count, from_account, to_account, amount);
145	let encoded_data = transfer_data.encode();
146
147	qp_poseidon::PoseidonHasher::hash_storage::<TransferProofData>(&encoded_data)
148}
149
150/// Compute the storage key for a transfer proof.
151///
152/// The storage key is: Twox128("Wormhole") || Twox128("TransferProof") ||
153/// Blake2_256(wormhole_address, transfer_count)
154///
155/// # Arguments
156/// * `wormhole_address` - The unspendable wormhole account as 32 bytes
157/// * `transfer_count` - The atomic transfer counter
158///
159/// # Returns
160/// Full storage key as bytes
161pub fn compute_storage_key(wormhole_address: &[u8; 32], transfer_count: u64) -> Vec<u8> {
162	let pallet_hash = sp_core::twox_128(b"Wormhole");
163	let storage_hash = sp_core::twox_128(b"TransferProof");
164
165	let mut final_key = Vec::with_capacity(32 + 32);
166	final_key.extend_from_slice(&pallet_hash);
167	final_key.extend_from_slice(&storage_hash);
168
169	// Hash the key tuple with Blake2_256
170	let to_account = AccountId32::new(*wormhole_address);
171	let key_tuple: TransferProofKey = (to_account, transfer_count);
172	let encoded_key = key_tuple.encode();
173	let key_hash = sp_core::blake2_256(&encoded_key);
174	final_key.extend_from_slice(&key_hash);
175
176	final_key
177}
178
179/// Compute the unspendable wormhole account from a secret.
180///
181/// # Arguments
182/// * `secret` - 32-byte secret
183///
184/// # Returns
185/// 32-byte wormhole account address
186pub fn compute_wormhole_address(secret: &[u8; 32]) -> Result<[u8; 32]> {
187	let secret_digest: BytesDigest = (*secret)
188		.try_into()
189		.map_err(|e| WormholeLibError::from(format!("Invalid secret: {:?}", e)))?;
190
191	let unspendable =
192		qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret_digest);
193
194	Ok(*digest_to_bytes(unspendable.account_id))
195}
196
197/// Compute the nullifier from secret and transfer count.
198///
199/// # Arguments
200/// * `secret` - 32-byte secret
201/// * `transfer_count` - Transfer counter
202///
203/// # Returns
204/// 32-byte nullifier
205#[allow(dead_code)]
206pub fn compute_nullifier(secret: &[u8; 32], transfer_count: u64) -> Result<[u8; 32]> {
207	let secret_digest: BytesDigest = (*secret)
208		.try_into()
209		.map_err(|e| WormholeLibError::from(format!("Invalid secret: {:?}", e)))?;
210
211	let nullifier = Nullifier::from_preimage(secret_digest, transfer_count);
212	Ok(*digest_to_bytes(nullifier.hash))
213}
214
215/// Quantize a funding amount from 12 decimal places to 2 decimal places.
216///
217/// # Arguments
218/// * `amount` - Amount in planck (12 decimals)
219///
220/// # Returns
221/// Quantized amount (2 decimals) as u32
222pub fn quantize_amount(amount: u128) -> Result<u32> {
223	let quantized = amount / SCALE_DOWN_FACTOR;
224	if quantized > u32::MAX as u128 {
225		return Err(WormholeLibError::from(format!(
226			"Quantized amount {} exceeds u32::MAX",
227			quantized
228		)));
229	}
230	Ok(quantized as u32)
231}
232
233/// Compute output amount after fee deduction.
234///
235/// output = input * (10000 - fee_bps) / 10000
236pub fn compute_output_amount(input_amount: u32, fee_bps: u32) -> u32 {
237	((input_amount as u64) * (10000 - fee_bps as u64) / 10000) as u32
238}
239
240/// Generate a wormhole proof from raw inputs.
241///
242/// This function takes all necessary data as raw bytes and generates a ZK proof.
243/// It does not require a chain client - all data must be pre-fetched.
244///
245/// # Arguments
246/// * `input` - All input data for proof generation
247/// * `prover_bin_path` - Path to prover.bin
248/// * `common_bin_path` - Path to common.bin
249///
250/// # Returns
251/// Proof bytes and nullifier
252pub fn generate_proof(
253	input: &ProofGenerationInput,
254	prover_bin_path: &Path,
255	common_bin_path: &Path,
256) -> Result<ProofGenerationOutput> {
257	// Convert secret to BytesDigest
258	let secret_digest: BytesDigest = input
259		.secret
260		.try_into()
261		.map_err(|e| WormholeLibError::from(format!("Invalid secret: {:?}", e)))?;
262
263	// Compute leaf hash for storage proof
264	let leaf_hash = compute_leaf_hash(
265		input.asset_id,
266		input.transfer_count,
267		&input.funding_account,
268		&input.wormhole_address,
269		input.funding_amount,
270	);
271
272	// Prepare storage proof
273	let processed_proof = prepare_proof_for_circuit(
274		input.proof_nodes.clone(),
275		hex::encode(input.state_root),
276		leaf_hash,
277	)
278	.map_err(|e| WormholeLibError::from(format!("Storage proof preparation failed: {}", e)))?;
279
280	// Quantize input amount
281	let input_amount_quantized = quantize_amount(input.funding_amount)?;
282
283	// Compute nullifier
284	let nullifier = Nullifier::from_preimage(secret_digest, input.transfer_count);
285	let nullifier_bytes = digest_to_bytes(nullifier.hash);
286
287	// Compute unspendable account
288	let unspendable =
289		qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret_digest);
290	let unspendable_bytes = digest_to_bytes(unspendable.account_id);
291
292	// Prepare digest (padded to 110 bytes)
293	const DIGEST_LOGS_SIZE: usize = 110;
294	let mut digest_padded = [0u8; DIGEST_LOGS_SIZE];
295	let copy_len = input.digest.len().min(DIGEST_LOGS_SIZE);
296	digest_padded[..copy_len].copy_from_slice(&input.digest[..copy_len]);
297
298	// Build circuit inputs
299	let private = PrivateCircuitInputs {
300		secret: secret_digest,
301		transfer_count: input.transfer_count,
302		funding_account: input
303			.funding_account
304			.as_slice()
305			.try_into()
306			.map_err(|e| WormholeLibError::from(format!("Invalid funding account: {:?}", e)))?,
307		storage_proof: processed_proof,
308		unspendable_account: unspendable_bytes,
309		parent_hash: input
310			.parent_hash
311			.as_slice()
312			.try_into()
313			.map_err(|e| WormholeLibError::from(format!("Invalid parent hash: {:?}", e)))?,
314		state_root: input
315			.state_root
316			.as_slice()
317			.try_into()
318			.map_err(|e| WormholeLibError::from(format!("Invalid state root: {:?}", e)))?,
319		extrinsics_root: input
320			.extrinsics_root
321			.as_slice()
322			.try_into()
323			.map_err(|e| WormholeLibError::from(format!("Invalid extrinsics root: {:?}", e)))?,
324		digest: digest_padded,
325		input_amount: input_amount_quantized,
326	};
327
328	let public = PublicCircuitInputs {
329		asset_id: input.asset_id,
330		output_amount_1: input.output_amount_1,
331		output_amount_2: input.output_amount_2,
332		volume_fee_bps: input.volume_fee_bps,
333		nullifier: nullifier_bytes,
334		exit_account_1: input
335			.exit_account_1
336			.as_slice()
337			.try_into()
338			.map_err(|e| WormholeLibError::from(format!("Invalid exit account 1: {:?}", e)))?,
339		exit_account_2: input
340			.exit_account_2
341			.as_slice()
342			.try_into()
343			.map_err(|e| WormholeLibError::from(format!("Invalid exit account 2: {:?}", e)))?,
344		block_hash: input
345			.block_hash
346			.as_slice()
347			.try_into()
348			.map_err(|e| WormholeLibError::from(format!("Invalid block hash: {:?}", e)))?,
349		block_number: input.block_number,
350	};
351
352	let circuit_inputs = CircuitInputs { public, private };
353
354	// Load prover from pre-built bins
355	let prover = WormholeProver::new_from_files(prover_bin_path, common_bin_path)
356		.map_err(|e| WormholeLibError::from(format!("Failed to load prover: {}", e)))?;
357
358	let prover_with_inputs = prover
359		.commit(&circuit_inputs)
360		.map_err(|e| WormholeLibError::from(format!("Failed to commit inputs: {}", e)))?;
361
362	let proof = prover_with_inputs
363		.prove()
364		.map_err(|e| WormholeLibError::from(format!("Proof generation failed: {}", e)))?;
365
366	Ok(ProofGenerationOutput { proof_bytes: proof.to_bytes(), nullifier: *nullifier_bytes })
367}
368
369#[cfg(test)]
370mod tests {
371	use super::*;
372
373	#[test]
374	fn test_quantize_amount() {
375		// 1 QTU = 10^12 planck -> should quantize to 100 (1.00 with 2 decimals)
376		let result = quantize_amount(1_000_000_000_000).unwrap();
377		assert_eq!(result, 100);
378
379		// 0.01 QTU = 10^10 planck -> should quantize to 1
380		let result = quantize_amount(10_000_000_000).unwrap();
381		assert_eq!(result, 1);
382	}
383
384	#[test]
385	fn test_compute_output_amount() {
386		// 100 input with 10 bps fee -> 99.9 -> 99
387		let result = compute_output_amount(100, 10);
388		assert_eq!(result, 99);
389
390		// 1000 input with 10 bps fee -> 999
391		let result = compute_output_amount(1000, 10);
392		assert_eq!(result, 999);
393	}
394
395	#[test]
396	fn test_storage_key_computation() {
397		// Just verify it doesn't panic and returns expected length
398		let wormhole_address = [0u8; 32];
399		let key = compute_storage_key(&wormhole_address, 1);
400		// 16 (pallet) + 16 (storage) + 32 (blake2_256) = 64 bytes
401		assert_eq!(key.len(), 64);
402	}
403}