pallet_passkey/
lib.rs

1//! Using Passkeys to execute transactions
2//!
3//! ## Quick Links
4//! - [Configuration: `Config`](Config)
5//! - [Extrinsics: `Call`](Call)
6//! - [Event Enum: `Event`](Event)
7//! - [Error Enum: `Error`](Error)
8#![doc = include_str!("../README.md")]
9// Substrate macros are tripping the clippy::expect_used lint.
10#![allow(clippy::expect_used)]
11#![cfg_attr(not(feature = "std"), no_std)]
12// Strong Documentation Lints
13#![deny(
14	rustdoc::broken_intra_doc_links,
15	rustdoc::missing_crate_level_docs,
16	rustdoc::invalid_codeblock_attributes,
17	missing_docs
18)]
19// allowing deprecated until moving to Extrinsic V5 structure
20#![allow(deprecated)]
21use common_primitives::node::EIP712Encode;
22use common_runtime::{
23	extensions::check_nonce::{prepare_nonce, validate_nonce},
24	signature::check_signature,
25};
26use frame_support::{
27	dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo, RawOrigin},
28	pallet_prelude::*,
29	traits::Contains,
30};
31use frame_system::pallet_prelude::*;
32use pallet_transaction_payment::OnChargeTransaction;
33use sp_runtime::{
34	generic::Era,
35	traits::{AsTransactionAuthorizedOrigin, Convert, Dispatchable, TxBaseImplication, Zero},
36	transaction_validity::{TransactionValidity, TransactionValidityError},
37	AccountId32, MultiSignature,
38};
39extern crate alloc;
40use alloc::vec;
41
42/// Type aliases used for interaction with `OnChargeTransaction`.
43pub(crate) type OnChargeTransactionOf<T> =
44	<T as pallet_transaction_payment::Config>::OnChargeTransaction;
45
46/// Balance type alias.
47pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
48
49#[cfg(any(feature = "runtime-benchmarks", test))]
50mod test_common;
51
52#[cfg(test)]
53mod mock;
54#[cfg(test)]
55mod tests;
56#[cfg(test)]
57mod tests_v2;
58
59pub mod weights;
60pub use weights::*;
61
62#[cfg(feature = "runtime-benchmarks")]
63use frame_support::traits::tokens::fungible::Mutate;
64use frame_system::CheckWeight;
65use sp_runtime::traits::{DispatchTransaction, TransactionExtension};
66
67#[cfg(feature = "runtime-benchmarks")]
68mod benchmarking;
69
70/// defines all new types for this pallet
71pub mod types;
72pub use types::*;
73
74pub use module::*;
75
76#[frame_support::pallet]
77pub mod module {
78
79	use super::*;
80
81	/// the storage version for this pallet
82	pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
83
84	#[pallet::config]
85	pub trait Config:
86		frame_system::Config + pallet_transaction_payment::Config + Send + Sync
87	{
88		/// The overarching event type.
89		#[allow(deprecated)]
90		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
91
92		/// The overarching call type.
93		type RuntimeCall: Parameter
94			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
95			+ GetDispatchInfo
96			+ From<frame_system::Call<Self>>
97			+ IsType<<Self as frame_system::Config>::RuntimeCall>
98			+ From<Call<Self>>;
99
100		/// Weight information for extrinsics in this pallet.
101		type WeightInfo: WeightInfo;
102
103		/// AccountId truncated to 32 bytes
104		type ConvertIntoAccountId32: Convert<Self::AccountId, AccountId32>;
105
106		/// Filters the inner calls for passkey which is set in runtime
107		type PasskeyCallFilter: Contains<<Self as Config>::RuntimeCall>;
108
109		/// Helper Currency method for benchmarking
110		#[cfg(feature = "runtime-benchmarks")]
111		type Currency: Mutate<Self::AccountId>;
112	}
113
114	#[pallet::error]
115	pub enum Error<T> {
116		/// InvalidAccountSignature
117		InvalidAccountSignature,
118	}
119
120	#[pallet::event]
121	#[pallet::generate_deposit(fn deposit_event)]
122	pub enum Event<T: Config> {
123		/// When a passkey transaction is successfully executed
124		TransactionExecutionSuccess {
125			/// transaction account id
126			account_id: T::AccountId,
127		},
128	}
129
130	#[pallet::pallet]
131	#[pallet::storage_version(STORAGE_VERSION)]
132	pub struct Pallet<T>(_);
133
134	#[pallet::call]
135	impl<T: Config> Pallet<T> {
136		/// Proxies an extrinsic call by changing the origin to `account_id` inside the payload.
137		/// Since this is an unsigned extrinsic all the verification checks are performed inside
138		/// `validate_unsigned` and `pre_dispatch` hooks.
139		#[deprecated(since = "1.15.2", note = "Use proxy_v2 instead")]
140		#[pallet::call_index(0)]
141		#[pallet::weight({
142			let dispatch_info = payload.passkey_call.call.get_dispatch_info();
143			let overhead = <T as Config>::WeightInfo::pre_dispatch();
144			let total = overhead.saturating_add(dispatch_info.call_weight);
145			(total, dispatch_info.class)
146		})]
147		#[allow(clippy::useless_conversion)]
148		pub fn proxy(
149			origin: OriginFor<T>,
150			payload: PasskeyPayload<T>,
151		) -> DispatchResultWithPostInfo {
152			Self::proxy_v2(origin, payload.into())
153		}
154
155		/// Proxies an extrinsic call by changing the origin to `account_id` inside the payload.
156		/// Since this is an unsigned extrinsic all the verification checks are performed inside
157		/// `validate_unsigned` and `pre_dispatch` hooks.
158		#[pallet::call_index(1)]
159		#[pallet::weight({
160			let dispatch_info = payload.passkey_call.call.get_dispatch_info();
161			let overhead = <T as Config>::WeightInfo::pre_dispatch();
162			let total = overhead.saturating_add(dispatch_info.call_weight);
163			(total, dispatch_info.class)
164		})]
165		#[allow(clippy::useless_conversion)]
166		pub fn proxy_v2(
167			origin: OriginFor<T>,
168			payload: PasskeyPayloadV2<T>,
169		) -> DispatchResultWithPostInfo {
170			ensure_none(origin)?;
171			let transaction_account_id = payload.passkey_call.account_id.clone();
172			let main_origin = T::RuntimeOrigin::from(frame_system::RawOrigin::Signed(
173				transaction_account_id.clone(),
174			));
175			let result = payload.passkey_call.call.dispatch(main_origin);
176			if let Ok(_inner) = result {
177				// all post-dispatch logic should be included in here
178				Self::deposit_event(Event::TransactionExecutionSuccess {
179					account_id: transaction_account_id,
180				});
181			}
182			result
183		}
184	}
185
186	#[pallet::validate_unsigned]
187	impl<T: Config> ValidateUnsigned for Pallet<T>
188	where
189		BalanceOf<T>: Send + Sync + From<u64>,
190		<T as frame_system::Config>::RuntimeCall:
191			From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
192		<T as frame_system::Config>::RuntimeOrigin: AsTransactionAuthorizedOrigin,
193	{
194		type Call = Call<T>;
195
196		/// Validating the regular checks of an extrinsic plus verifying the P256 Passkey signature
197		/// The majority of these checks are the same as `SignedExtra` list in defined in runtime
198		fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
199			let valid_tx = ValidTransaction::default();
200			let (payload, is_legacy_call) = Self::filter_valid_calls(call)?;
201
202			let frame_system_validity =
203				FrameSystemChecks(payload.passkey_call.account_id.clone(), call.clone())
204					.validate()?;
205			let nonce_validity = PasskeyNonceCheck::new(payload.passkey_call.clone()).validate()?;
206			let weight_validity =
207				PasskeyWeightCheck::new(payload.passkey_call.account_id.clone(), call.clone())
208					.validate()?;
209			let tx_payment_validity = ChargeTransactionPayment::<T>(
210				payload.passkey_call.account_id.clone(),
211				call.clone(),
212			)
213			.validate()?;
214			// this is the last since it is the heaviest
215			let signature_validity =
216				PasskeySignatureCheck::new(payload.clone(), is_legacy_call).validate()?;
217
218			let valid_tx = valid_tx
219				.combine_with(frame_system_validity)
220				.combine_with(nonce_validity)
221				.combine_with(weight_validity)
222				.combine_with(tx_payment_validity)
223				.combine_with(signature_validity);
224			Ok(valid_tx)
225		}
226
227		/// Checking and executing a list of operations pre_dispatch
228		/// The majority of these checks are the same as `SignedExtra` list in defined in runtime
229		fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
230			let (payload, is_legacy_call) = Self::filter_valid_calls(call)?;
231			FrameSystemChecks(payload.passkey_call.account_id.clone(), call.clone())
232				.pre_dispatch()?;
233			PasskeyNonceCheck::new(payload.passkey_call.clone()).pre_dispatch()?;
234			PasskeyWeightCheck::new(payload.passkey_call.account_id.clone(), call.clone())
235				.pre_dispatch()?;
236			ChargeTransactionPayment::<T>(payload.passkey_call.account_id.clone(), call.clone())
237				.pre_dispatch()?;
238			// this is the last since it is the heaviest
239			PasskeySignatureCheck::new(payload.clone(), is_legacy_call).pre_dispatch()
240		}
241	}
242}
243
244impl<T: Config> Pallet<T>
245where
246	BalanceOf<T>: Send + Sync + From<u64>,
247	<T as frame_system::Config>::RuntimeCall:
248		From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
249{
250	/// Filtering the valid calls and extracting the Payload V2 from inside the call and returning if this
251	/// is a legacy call or not
252	fn filter_valid_calls(
253		call: &Call<T>,
254	) -> Result<(PasskeyPayloadV2<T>, bool), TransactionValidityError> {
255		match call {
256			Call::proxy { payload }
257				if T::PasskeyCallFilter::contains(&payload.clone().passkey_call.call) =>
258				Ok((payload.clone().into(), true)),
259			Call::proxy_v2 { payload }
260				if T::PasskeyCallFilter::contains(&payload.clone().passkey_call.call) =>
261				Ok((payload.clone(), false)),
262			_ => Err(InvalidTransaction::Call.into()),
263		}
264	}
265}
266
267/// Passkey specific nonce check which is a wrapper around `CheckNonce` extension
268#[derive(Encode, Decode, Clone, TypeInfo)]
269#[scale_info(skip_type_params(T))]
270struct PasskeyNonceCheck<T: Config>(pub PasskeyCallV2<T>);
271
272impl<T: Config> PasskeyNonceCheck<T>
273where
274	<T as frame_system::Config>::RuntimeCall: Dispatchable<Info = DispatchInfo>,
275{
276	pub fn new(passkey_call: PasskeyCallV2<T>) -> Self {
277		Self(passkey_call)
278	}
279
280	pub fn validate(&self) -> TransactionValidity {
281		let who = self.0.account_id.clone();
282		let nonce = self.0.account_nonce;
283		let (nonce_validity, _) = validate_nonce::<T>(&who, nonce)?;
284		Ok(nonce_validity)
285	}
286
287	pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
288		let who = self.0.account_id.clone();
289		let nonce = self.0.account_nonce;
290		let some_call: &<T as Config>::RuntimeCall = &self.0.call;
291		let info = &some_call.get_dispatch_info();
292		prepare_nonce::<T>(&who, nonce, info.pays_fee)?;
293		Ok(())
294	}
295}
296
297/// Passkey signatures check which verifies 2 signatures
298/// 1. Account signature of the P256 public key
299/// 2. Passkey P256 signature of the account public key
300#[derive(Encode, Decode, Clone, TypeInfo)]
301#[scale_info(skip_type_params(T))]
302struct PasskeySignatureCheck<T: Config> {
303	payload: PasskeyPayloadV2<T>,
304	is_legacy_payload: bool,
305}
306
307impl<T: Config> PasskeySignatureCheck<T> {
308	pub fn new(passkey_payload: PasskeyPayloadV2<T>, is_legacy_payload: bool) -> Self {
309		Self { payload: passkey_payload, is_legacy_payload }
310	}
311
312	pub fn validate(&self) -> TransactionValidity {
313		// checking account signature to verify ownership of the account used
314		let signed_data = self.payload.passkey_public_key.clone();
315		let signature = self.payload.account_ownership_proof.clone();
316		let signer = &self.payload.passkey_call.account_id;
317
318		Self::check_account_signature(signer, &signed_data, &signature)
319			.map_err(|_e| TransactionValidityError::Invalid(InvalidTransaction::BadSigner))?;
320
321		// checking the passkey signature to ensure access to the passkey
322		let p256_signed_data = match self.is_legacy_payload {
323			true => PasskeyPayload::from(self.payload.clone()).passkey_call.encode(),
324			false => self.payload.passkey_call.encode(),
325		};
326		let p256_signature = self.payload.verifiable_passkey_signature.clone();
327		let p256_signer = self.payload.passkey_public_key.clone();
328
329		p256_signature
330			.try_verify(&p256_signed_data, &p256_signer)
331			.map_err(|e| match e {
332				PasskeyVerificationError::InvalidProof =>
333					TransactionValidityError::Invalid(InvalidTransaction::BadSigner),
334				_ => TransactionValidityError::Invalid(InvalidTransaction::Custom(e.into())),
335			})?;
336
337		Ok(ValidTransaction::default())
338	}
339
340	pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
341		let _ = self.validate()?;
342		Ok(())
343	}
344
345	/// Check the signature on passkey public key by the account id
346	/// Returns Ok(()) if the signature is valid
347	/// Returns Err(InvalidAccountSignature) if the signature is invalid
348	/// # Arguments
349	/// * `signer` - The account id of the signer
350	/// * `signed_data` - The signed data
351	/// * `signature` - The signature
352	/// # Return
353	/// * `Ok(())` if the signature is valid
354	/// * `Err(InvalidAccountSignature)` if the signature is invalid
355	fn check_account_signature<P>(
356		signer: &T::AccountId,
357		payload: &P,
358		signature: &MultiSignature,
359	) -> DispatchResult
360	where
361		P: Encode + EIP712Encode,
362	{
363		let key = T::ConvertIntoAccountId32::convert((*signer).clone());
364
365		if !check_signature(signature, key, payload) {
366			return Err(Error::<T>::InvalidAccountSignature.into());
367		}
368
369		Ok(())
370	}
371}
372
373/// Passkey related tx payment
374#[derive(Encode, Decode, Clone, TypeInfo)]
375#[scale_info(skip_type_params(T))]
376pub struct ChargeTransactionPayment<T: Config>(pub T::AccountId, pub Call<T>);
377
378impl<T: Config> ChargeTransactionPayment<T>
379where
380	BalanceOf<T>: Send + Sync + From<u64>,
381	<T as frame_system::Config>::RuntimeCall:
382		From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
383	<T as frame_system::Config>::RuntimeOrigin: AsTransactionAuthorizedOrigin,
384{
385	/// Validates the transaction fee paid with tokens.
386	pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
387		let info = &self.1.get_dispatch_info();
388		let len = self.1.using_encoded(|c| c.len());
389		let runtime_call: <T as frame_system::Config>::RuntimeCall =
390			<T as frame_system::Config>::RuntimeCall::from(self.1.clone());
391
392		let raw_origin = RawOrigin::from(Some(self.0.clone()));
393		let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
394		pallet_transaction_payment::ChargeTransactionPayment::<T>::from(Zero::zero())
395			.validate_and_prepare(who, &runtime_call, info, len, 4)?;
396		Ok(())
397	}
398
399	/// Validates the transaction fee paid with tokens.
400	pub fn validate(&self) -> TransactionValidity {
401		let info = &self.1.get_dispatch_info();
402		let len = self.1.using_encoded(|c| c.len());
403		let runtime_call: <T as frame_system::Config>::RuntimeCall =
404			<T as frame_system::Config>::RuntimeCall::from(self.1.clone());
405		let raw_origin = RawOrigin::from(Some(self.0.clone()));
406		let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
407
408		let (res, _, _) =
409			pallet_transaction_payment::ChargeTransactionPayment::<T>::from(Zero::zero())
410				.validate(
411					who,
412					&runtime_call,
413					info,
414					len,
415					(),
416					&TxBaseImplication(runtime_call.clone()), // implication
417					TransactionSource::External,
418				)?;
419		Ok(res)
420	}
421}
422
423/// Frame system related checks
424#[derive(Encode, Decode, Clone, TypeInfo)]
425#[scale_info(skip_type_params(T))]
426pub struct FrameSystemChecks<T: Config + Send + Sync>(pub T::AccountId, pub Call<T>);
427
428impl<T: Config + Send + Sync> FrameSystemChecks<T>
429where
430	<T as frame_system::Config>::RuntimeCall:
431		From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
432{
433	/// Validates the transaction fee paid with tokens.
434	pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
435		let info = &self.1.get_dispatch_info();
436		let len = self.1.using_encoded(|c| c.len());
437		let runtime_call: <T as frame_system::Config>::RuntimeCall =
438			<T as frame_system::Config>::RuntimeCall::from(self.1.clone());
439
440		let raw_origin = RawOrigin::from(Some(self.0.clone()));
441		let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
442
443		let non_zero_sender_check = frame_system::CheckNonZeroSender::<T>::new();
444		let spec_version_check = frame_system::CheckSpecVersion::<T>::new();
445		let tx_version_check = frame_system::CheckTxVersion::<T>::new();
446		let genesis_hash_check = frame_system::CheckGenesis::<T>::new();
447		let era_check = frame_system::CheckEra::<T>::from(Era::immortal());
448
449		// currently (in stable2412) these implement the default version of `prepare`, which always returns Ok(...)
450		// val, origin, call, info, len (?)
451		non_zero_sender_check.prepare((), &who, &runtime_call, info, len)?;
452		spec_version_check.prepare((), &who, &runtime_call, info, len)?;
453		tx_version_check.prepare((), &who, &runtime_call, info, len)?;
454		genesis_hash_check.prepare((), &who, &runtime_call, info, len)?;
455		era_check.prepare((), &who, &runtime_call, info, len)
456	}
457
458	/// Validates the transaction fee paid with tokens.
459	pub fn validate(&self) -> TransactionValidity {
460		let info = &self.1.get_dispatch_info();
461		let len = self.1.using_encoded(|c| c.len());
462		let runtime_call: <T as frame_system::Config>::RuntimeCall =
463			<T as frame_system::Config>::RuntimeCall::from(self.1.clone());
464		let implication = TxBaseImplication(runtime_call.clone());
465		let raw_origin = RawOrigin::from(Some(self.0.clone()));
466		let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
467
468		let non_zero_sender_check = frame_system::CheckNonZeroSender::<T>::new();
469		let spec_version_check = frame_system::CheckSpecVersion::<T>::new();
470		let tx_version_check = frame_system::CheckTxVersion::<T>::new();
471		let genesis_hash_check = frame_system::CheckGenesis::<T>::new();
472		let era_check = frame_system::CheckEra::<T>::from(Era::immortal());
473
474		let (non_zero_sender_validity, _, origin) = non_zero_sender_check.validate(
475			who.clone(),
476			&runtime_call,
477			info,
478			len,
479			(),
480			&implication,
481			TransactionSource::External,
482		)?;
483
484		let (spec_version_validity, _, origin) = spec_version_check.validate(
485			origin,
486			&runtime_call,
487			info,
488			len,
489			spec_version_check.implicit()?,
490			&implication,
491			TransactionSource::External,
492		)?;
493
494		let (tx_version_validity, _, origin) = tx_version_check.validate(
495			origin,
496			&runtime_call,
497			info,
498			len,
499			tx_version_check.implicit()?,
500			&implication,
501			TransactionSource::External,
502		)?;
503
504		let (genesis_hash_validity, _, origin) = genesis_hash_check.validate(
505			origin,
506			&runtime_call,
507			info,
508			len,
509			genesis_hash_check.implicit()?,
510			&implication,
511			TransactionSource::External,
512		)?;
513
514		let (era_validity, _, _) = era_check.validate(
515			origin,
516			&runtime_call,
517			info,
518			len,
519			era_check.implicit()?,
520			&implication,
521			TransactionSource::External,
522		)?;
523
524		Ok(non_zero_sender_validity
525			.combine_with(spec_version_validity)
526			.combine_with(tx_version_validity)
527			.combine_with(genesis_hash_validity)
528			.combine_with(era_validity))
529	}
530}
531
532/// Block resource (weight) limit check.
533#[derive(Encode, Decode, Clone, TypeInfo)]
534#[scale_info(skip_type_params(T))]
535pub struct PasskeyWeightCheck<T: Config>(pub T::AccountId, pub Call<T>);
536
537impl<T: Config> PasskeyWeightCheck<T>
538where
539	<T as frame_system::Config>::RuntimeCall:
540		From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
541{
542	/// creating a new instance
543	pub fn new(account_id: T::AccountId, call: Call<T>) -> Self {
544		Self(account_id, call)
545	}
546
547	/// Validate the transaction
548	pub fn validate(&self) -> TransactionValidity {
549		let info = &self.1.get_dispatch_info();
550		let len = self.1.using_encoded(|c| c.len());
551		let runtime_call: <T as frame_system::Config>::RuntimeCall =
552			<T as frame_system::Config>::RuntimeCall::from(self.1.clone());
553		let implication = TxBaseImplication(runtime_call.clone());
554		let raw_origin = RawOrigin::from(Some(self.0.clone()));
555		let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
556
557		let check_weight = CheckWeight::<T>::new();
558		let (result, _, _) = check_weight.validate(
559			who,
560			&runtime_call,
561			info,
562			len,
563			(),
564			&implication,
565			TransactionSource::External,
566		)?;
567		Ok(result)
568	}
569
570	/// Pre-dispatch transaction checks
571	pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
572		let info = &self.1.get_dispatch_info();
573		let len = self.1.using_encoded(|c| c.len());
574		let runtime_call: <T as frame_system::Config>::RuntimeCall =
575			<T as frame_system::Config>::RuntimeCall::from(self.1.clone());
576
577		CheckWeight::<T>::bare_validate_and_prepare(&runtime_call, info, len)
578	}
579}