diff --git a/Cargo.lock b/Cargo.lock index 8c992bea..82087508 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8872,7 +8872,7 @@ dependencies = [ "env_logger 0.11.8", "log", "parity-scale-codec", - "qp-poseidon", + "qp-poseidon 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "qp-rusty-crystals-dilithium", "qp-rusty-crystals-hdwallet", "scale-info", diff --git a/Cargo.toml b/Cargo.toml index f57e846a..5f512c1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,18 +145,16 @@ sp-consensus-pow = { path = "./primitives/consensus/pow", default-features = fal sp-consensus-qpow = { path = "./primitives/consensus/qpow", default-features = false } # Quantus network dependencies -qp-poseidon = { version = "1.0.1", default-features = false } -qp-poseidon-core = { version = "1.0.1", default-features = false, features = ["p3"] } +qp-plonky2 = { version = "1.1.3", default-features = false } +qp-poseidon = { path = "../qp-poseidon/substrate", default-features = false } +qp-poseidon-core = { path = "../qp-poseidon-core/core", default-features = false, features = ["p2", "p3"] } qp-rusty-crystals-dilithium = { version = "2.0.0", default-features = false } qp-rusty-crystals-hdwallet = { version = "1.0.0" } -qp-wormhole-circuit = { version = "0.1.2", default-features = false } -qp-wormhole-circuit-builder = { version = "0.1.2", default-features = false } -qp-wormhole-verifier = { version = "0.1.2", default-features = false, features = [ - "no_random", -] } -qp-zk-circuits-common = { version = "0.1.2", default-features = false, features = [ - "no_random", -] } +qp-wormhole-circuit = { version = "0.1.5", default-features = false } +qp-wormhole-circuit-builder = { version = "0.1.5", default-features = false } +qp-wormhole-prover = { version = "0.1.5", default-features = false } +qp-wormhole-verifier = { version = "0.1.5", default-features = false } +qp-zk-circuits-common = { version = "0.1.5", default-features = false } # polkadot-sdk dependencies frame-benchmarking = { version = "41.0.0", default-features = false } diff --git a/pallets/wormhole/Cargo.toml b/pallets/wormhole/Cargo.toml index 54dba6aa..71781ac8 100644 --- a/pallets/wormhole/Cargo.toml +++ b/pallets/wormhole/Cargo.toml @@ -16,6 +16,8 @@ hex = { workspace = true, features = ["alloc"], optional = true } lazy_static.workspace = true log.workspace = true pallet-balances.workspace = true +qp-header = { workspace = true, features = ["serde"] } +qp-poseidon = { path = "../../../qp-poseidon/substrate", default-features = false } qp-wormhole.workspace = true qp-wormhole-circuit = { workspace = true, default-features = false } qp-wormhole-verifier = { workspace = true, default-features = false } diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 36457741..8f9f3538 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -2,8 +2,18 @@ extern crate alloc; +<<<<<<< Updated upstream use lazy_static::lazy_static; pub use pallet::*; +======= +use core::marker::PhantomData; + +use codec::{Decode, MaxEncodedLen}; +use frame_support::StorageHasher; +use lazy_static::lazy_static; +pub use pallet::*; +pub use qp_poseidon::{PoseidonHasher as PoseidonCore, ToFelts}; +>>>>>>> Stashed changes use qp_wormhole_verifier::WormholeVerifier; #[cfg(test)] @@ -29,9 +39,26 @@ pub fn get_wormhole_verifier() -> Result<&'static WormholeVerifier, &'static str WORMHOLE_VERIFIER.as_ref().ok_or("Wormhole verifier not available") } +// We use a generic struct so we can pass the specific Key type to the hasher +pub struct PoseidonStorageHasher(PhantomData); + +impl StorageHasher for PoseidonStorageHasher { + // We are lying here, but maybe it's ok because it's just metadata + const METADATA: StorageHasherIR = StorageHasherIR::Identity; + type Output = [u8; 32]; + + fn hash(x: &[u8]) -> Self::Output { + PoseidonCore::hash_storage::(x) + } + + fn max_len() -> usize { + 32 + } +} + #[frame_support::pallet] pub mod pallet { - use crate::WeightInfo; + use crate::{PoseidonStorageHasher, ToFelts, WeightInfo}; use alloc::vec::Vec; use codec::Decode; use frame_support::{ @@ -55,17 +82,45 @@ pub mod pallet { pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; + pub type TransferProofKey = ( + AssetIdOf, + ::TransferCount, + ::AccountId, + ::AccountId, + BalanceOf, + ); + #[pallet::pallet] pub struct Pallet(_); #[pallet::config] - pub trait Config: frame_system::Config { - /// Currency type used for minting tokens and handling wormhole transfers + pub trait Config: frame_system::Config + where + AssetIdOf: Default + From + Clone + ToFelts, + BalanceOf: Default + ToFelts, + AssetBalanceOf: Into> + From>, + ::AccountId: ToFelts, + { + /// Currency type used for native token transfers and minting type Currency: Mutate> + TransferProofs, Self::AccountId> + Unbalanced + Currency; + /// Assets type used for managing fungible assets + type Assets: fungibles::Inspect + + fungibles::Mutate + + fungibles::Create; + + /// Transfer count type used in storage + type TransferCount: Parameter + + MaxEncodedLen + + Default + + Saturating + + Copy + + sp_runtime::traits::One + + ToFelts; + /// Account ID used as the "from" account when creating transfer proofs for minted tokens #[pallet::constant] type MintingAccount: Get; @@ -81,10 +136,44 @@ pub mod pallet { pub(super) type UsedNullifiers = StorageMap<_, Blake2_128Concat, [u8; 32], bool, ValueQuery>; + /// Transfer proofs for wormhole transfers (both native and assets) + #[pallet::storage] + #[pallet::getter(fn transfer_proof)] + pub type TransferProof = StorageMap< + _, + PoseidonStorageHasher>, + TransferProofKey, + (), + OptionQuery, + >; + + /// Transfer count for all wormhole transfers + #[pallet::storage] + #[pallet::getter(fn transfer_count)] + pub type TransferCount = StorageValue<_, T::TransferCount, ValueQuery>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - ProofVerified { exit_amount: BalanceOf }, + pub enum Event + where + T::AccountId: ToFelts, + { + ProofVerified { + exit_amount: BalanceOf, + }, + NativeTransferred { + from: T::AccountId, + to: T::AccountId, + amount: BalanceOf, + transfer_count: T::TransferCount, + }, + AssetTransferred { + asset_id: AssetIdOf, + from: T::AccountId, + to: T::AccountId, + amount: AssetBalanceOf, + transfer_count: T::TransferCount, + }, } #[pallet::error] @@ -102,7 +191,10 @@ pub mod pallet { } #[pallet::call] - impl Pallet { + impl Pallet + where + ::AccountId: ToFelts, + { #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::verify_wormhole_proof())] pub fn verify_wormhole_proof( @@ -228,5 +320,107 @@ pub mod pallet { Ok(()) } + + /// Transfer native tokens and store proof for wormhole + #[pallet::call_index(1)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 2))] + pub fn transfer_native( + origin: OriginFor, + dest: AccountIdLookupOf, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + + // Prevent self-transfers + ensure!(source != dest, Error::::SelfTransfer); + + // Perform the transfer + >::transfer(&source, &dest, amount, Preservation::Expendable)?; + + // Store proof with asset_id = Default (0 for native) + Self::record_transfer(AssetIdOf::::default(), source, dest, amount)?; + + Ok(()) + } + + /// Transfer asset tokens and store proof for wormhole + #[pallet::call_index(2)] + #[pallet::weight(T::DbWeight::get().reads_writes(2, 2))] + pub fn transfer_asset( + origin: OriginFor, + asset_id: AssetIdOf, + dest: AccountIdLookupOf, + #[pallet::compact] amount: AssetBalanceOf, + ) -> DispatchResult { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + + // Prevent self-transfers + ensure!(source != dest, Error::::SelfTransfer); + + // Check if asset exists + ensure!( + >::asset_exists(asset_id.clone()), + Error::::AssetNotFound + ); + + // Perform the transfer + >::transfer( + asset_id.clone(), + &source, + &dest, + amount, + Preservation::Expendable, + )?; + + // Store proof + Self::record_transfer(asset_id, source, dest, amount.into())?; + + Ok(()) + } + } + + // Helper functions for recording transfer proofs + impl Pallet + where + ::AccountId: ToFelts, + { + /// Record a transfer proof + /// This should be called by transaction extensions or other runtime components + pub fn record_transfer( + asset_id: AssetIdOf, + from: T::AccountId, + to: T::AccountId, + amount: BalanceOf, + ) -> DispatchResult { + let current_count = TransferCount::::get(); + TransferProof::::insert( + (asset_id, current_count, from.clone(), to.clone(), amount), + (), + ); + TransferCount::::put(current_count.saturating_add(T::TransferCount::one())); + + Ok(()) + } + } + + // Implement the TransferProofRecorder trait for other pallets to use + impl qp_wormhole::TransferProofRecorder, BalanceOf> + for Pallet + where + T::AccountId: ToFelts, + { + type Error = DispatchError; + + fn record_transfer_proof( + asset_id: Option>, + from: T::AccountId, + to: T::AccountId, + amount: BalanceOf, + ) -> Result<(), Self::Error> { + let asset_id_value = asset_id.unwrap_or_default(); + Self::record_transfer(asset_id_value, from, to, amount) + } } } diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index a96fca74..cb1dbaa1 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -1,8 +1,18 @@ #[cfg(test)] mod wormhole_tests { - use crate::{get_wormhole_verifier, mock::*, weights, Config, Error, WeightInfo}; - use frame_support::{assert_noop, assert_ok, weights::WeightToFee}; - use qp_wormhole_circuit::inputs::PublicCircuitInputs; + use crate::{get_wormhole_verifier, mock::*, TransferProofKey}; + use codec::Encode; + use frame_support::{ + assert_ok, + traits::fungible::{Inspect, Mutate}, + }; + use plonky2::plonk::circuit_data::CircuitConfig; + use qp_poseidon::PoseidonHasher; + use qp_wormhole_circuit::{ + inputs::{CircuitInputs, PrivateCircuitInputs, PublicCircuitInputs}, + nullifier::Nullifier, + }; + use qp_wormhole_prover::WormholeProver; use qp_wormhole_verifier::ProofWithPublicInputs; use sp_runtime::Perbill; @@ -13,7 +23,138 @@ mod wormhole_tests { } #[test] - fn test_verifier_availability() { + fn test_wormhole_transfer_proof_generation() { + let alice = account_id(1); + let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret"); + let unspendable_account = + qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret) + .account_id; + let unspendable_account_bytes_digest = 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"); + let unspendable_account_id = AccountId::new(unspendable_account_bytes); + let exit_account_id = AccountId::new([42u8; 32]); + let funding_amount = 1_000_000_000_001u128; + + let mut ext = new_test_ext(); + + let (storage_key, state_root, leaf_hash, event_transfer_count, header) = + ext.execute_with(|| { + System::set_block_number(1); + + let pre_runtime_data = vec![ + 233, 182, 183, 107, 158, 1, 115, 19, 219, 126, 253, 86, 30, 208, 176, 70, 21, + 45, 180, 229, 9, 62, 91, 4, 6, 53, 245, 52, 48, 38, 123, 225, + ]; + let seal_data = vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 77, 142, + ]; + + System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); + System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); + + assert_ok!(Balances::mint_into(&alice, funding_amount)); + assert_ok!(Wormhole::transfer_native( + frame_system::RawOrigin::Signed(alice.clone()).into(), + unspendable_account_id.clone(), + funding_amount, + )); + + let event_transfer_count = 0u64; + + let leaf_hash = PoseidonHasher::hash_storage::>( + &( + 0u32, + event_transfer_count, + alice.clone(), + unspendable_account_id.clone(), + funding_amount, + ) + .encode(), + ); + + let proof_address = crate::pallet::TransferProof::::hashed_key_for(&( + 0u32, + event_transfer_count, + alice.clone(), + unspendable_account_id.clone(), + funding_amount, + )); + let mut storage_key = proof_address; + storage_key.extend_from_slice(&leaf_hash); + + let header = System::finalize(); + let state_root = *header.state_root(); + + (storage_key, state_root, leaf_hash, event_transfer_count, header) + }); + + use sp_state_machine::prove_read; + let proof = prove_read(ext.as_backend(), &[&storage_key]) + .expect("failed to generate storage proof"); + + let proof_nodes_vec: Vec> = proof.iter_nodes().map(|n| n.to_vec()).collect(); + + let processed_storage_proof = + prepare_proof_for_circuit(proof_nodes_vec, hex::encode(&state_root), leaf_hash) + .expect("failed to prepare proof for circuit"); + + let parent_hash = *header.parent_hash(); + let extrinsics_root = *header.extrinsics_root(); + let digest = header.digest().encode(); + let digest_array: [u8; 110] = digest.try_into().expect("digest should be 110 bytes"); + let block_number: u32 = (*header.number()).try_into().expect("block number fits in u32"); + + let block_hash = header.hash(); + + let circuit_inputs = CircuitInputs { + private: PrivateCircuitInputs { + secret, + storage_proof: processed_storage_proof, + transfer_count: event_transfer_count, + funding_account: BytesDigest::try_from(alice.as_ref() as &[u8]) + .expect("account is 32 bytes"), + unspendable_account: Digest::from(unspendable_account).into(), + state_root: BytesDigest::try_from(state_root.as_ref()) + .expect("state root is 32 bytes"), + extrinsics_root: BytesDigest::try_from(extrinsics_root.as_ref()) + .expect("extrinsics root is 32 bytes"), + digest: digest_array, + }, + public: PublicCircuitInputs { + asset_id: 0u32, + funding_amount, + nullifier: Nullifier::from_preimage(secret, event_transfer_count).hash.into(), + exit_account: BytesDigest::try_from(exit_account_id.as_ref() as &[u8]) + .expect("account is 32 bytes"), + block_hash: BytesDigest::try_from(block_hash.as_ref()) + .expect("block hash is 32 bytes"), + parent_hash: BytesDigest::try_from(parent_hash.as_ref()) + .expect("parent hash is 32 bytes"), + block_number, + }, + }; + + let proof = generate_proof(circuit_inputs); + + let public_inputs = + PublicCircuitInputs::try_from(&proof).expect("failed to parse public inputs"); + + assert_eq!(public_inputs.funding_amount, funding_amount); + assert_eq!( + public_inputs.exit_account, + BytesDigest::try_from(exit_account_id.as_ref() as &[u8]).unwrap() + ); + + let verifier = get_wormhole_verifier().expect("verifier should be available"); + verifier.verify(proof.clone()).expect("proof should verify"); + + let proof_bytes = proof.to_bytes(); + new_test_ext().execute_with(|| { let verifier = get_wormhole_verifier(); assert!(verifier.is_ok(), "Verifier should be available in tests");