Skip to main content

topsoil_core/system/extensions/
check_nonce.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later WITH Classpath-exception-2.0
6
7extern crate alloc;
8
9use alloc::{vec, vec::Vec};
10
11use crate::system::Config;
12use codec::{Decode, DecodeWithMemTracking, Encode};
13use scale_info::TypeInfo;
14use subsoil::runtime::{
15	traits::{
16		AsSystemOriginSigner, CheckedAdd, DispatchInfoOf, Dispatchable, One, PostDispatchInfoOf,
17		TransactionExtension, ValidateResult, Zero,
18	},
19	transaction_validity::{
20		InvalidTransaction, TransactionLongevity, TransactionValidityError, ValidTransaction,
21	},
22	DispatchResult, Saturating,
23};
24use subsoil::weights::Weight;
25use topsoil_core::{dispatch::DispatchInfo, pallet_prelude::TransactionSource, DebugNoBound};
26
27/// Nonce check and increment to give replay protection for transactions.
28///
29/// # Transaction Validity
30///
31/// This extension affects `requires` and `provides` tags of validity, but DOES NOT
32/// set the `priority` field. Make sure that AT LEAST one of the transaction extension sets
33/// some kind of priority upon validating transactions.
34///
35/// The preparation step assumes that the nonce information has not changed since the validation
36/// step. This means that other extensions ahead of `CheckNonce` in the pipeline must not alter the
37/// nonce during their own preparation step, or else the transaction may be rejected during dispatch
38/// or lead to an inconsistent account state.
39#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
40#[scale_info(skip_type_params(T))]
41pub struct CheckNonce<T: Config>(#[codec(compact)] pub T::Nonce);
42
43/// For a valid transaction the provides and requires information related to the nonce.
44pub struct ValidNonceInfo {
45	/// The encoded `provides` used for this transaction.
46	pub provides: Vec<Vec<u8>>,
47	/// The encoded `requires` used for this transaction.
48	pub requires: Vec<Vec<u8>>,
49}
50
51impl<T: Config> CheckNonce<T> {
52	/// utility constructor. Used only in client/factory code.
53	pub fn from(nonce: T::Nonce) -> Self {
54		Self(nonce)
55	}
56
57	/// In transaction extension, validate nonce for account, on success returns provides and
58	/// requires.
59	pub fn validate_nonce_for_account(
60		who: &T::AccountId,
61		nonce: T::Nonce,
62	) -> Result<ValidNonceInfo, TransactionValidityError> {
63		let account = crate::system::Account::<T>::get(who);
64		if account.providers.is_zero() && account.sufficients.is_zero() {
65			// Nonce storage not paid for
66			return Err(InvalidTransaction::Payment.into());
67		}
68		if nonce < account.nonce {
69			return Err(InvalidTransaction::Stale.into());
70		}
71
72		let provides = vec![Encode::encode(&(who.clone(), nonce))];
73		let requires = if account.nonce < nonce {
74			vec![Encode::encode(&(who.clone(), nonce.saturating_sub(One::one())))]
75		} else {
76			vec![]
77		};
78
79		Ok(ValidNonceInfo { provides, requires })
80	}
81
82	/// In transaction extension, prepare nonce for account.
83	pub fn prepare_nonce_for_account(
84		who: &T::AccountId,
85		mut nonce: T::Nonce,
86	) -> Result<(), TransactionValidityError> {
87		let account = crate::system::Account::<T>::get(who);
88		if nonce > account.nonce {
89			return Err(InvalidTransaction::Future.into());
90		}
91		nonce = nonce.checked_add(&T::Nonce::one()).unwrap_or(T::Nonce::zero());
92		crate::system::Account::<T>::mutate(who, |account| account.nonce = nonce);
93		Ok(())
94	}
95}
96
97impl<T: Config> core::fmt::Debug for CheckNonce<T> {
98	#[cfg(feature = "std")]
99	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
100		write!(f, "CheckNonce({})", self.0)
101	}
102
103	#[cfg(not(feature = "std"))]
104	fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
105		Ok(())
106	}
107}
108
109/// Operation to perform from `validate` to `prepare` in [`CheckNonce`] transaction extension.
110#[derive(DebugNoBound)]
111pub enum Val<T: Config> {
112	/// Account and its nonce to check for.
113	CheckNonce(T::AccountId),
114	/// Weight to refund.
115	Refund(Weight),
116}
117
118/// Operation to perform from `prepare` to `post_dispatch_details` in [`CheckNonce`] transaction
119/// extension.
120#[derive(DebugNoBound)]
121pub enum Pre {
122	/// The transaction extension weight should not be refunded.
123	NonceChecked,
124	/// The transaction extension weight should be refunded.
125	Refund(Weight),
126}
127
128impl<T: Config> TransactionExtension<T::RuntimeCall> for CheckNonce<T>
129where
130	T::RuntimeCall: Dispatchable<Info = DispatchInfo>,
131	<T::RuntimeCall as Dispatchable>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
132{
133	const IDENTIFIER: &'static str = "CheckNonce";
134	type Implicit = ();
135	type Val = Val<T>;
136	type Pre = Pre;
137
138	fn weight(&self, _: &T::RuntimeCall) -> subsoil::weights::Weight {
139		<T::ExtensionsWeightInfo as super::WeightInfo>::check_nonce()
140	}
141
142	fn validate(
143		&self,
144		origin: <T as Config>::RuntimeOrigin,
145		call: &T::RuntimeCall,
146		_info: &DispatchInfoOf<T::RuntimeCall>,
147		_len: usize,
148		_self_implicit: Self::Implicit,
149		_inherited_implication: &impl Encode,
150		_source: TransactionSource,
151	) -> ValidateResult<Self::Val, T::RuntimeCall> {
152		let Some(who) = origin.as_system_origin_signer() else {
153			return Ok((Default::default(), Val::Refund(self.weight(call)), origin));
154		};
155		let ValidNonceInfo { provides, requires } = Self::validate_nonce_for_account(who, self.0)?;
156
157		let validity = ValidTransaction {
158			priority: 0,
159			requires,
160			provides,
161			longevity: TransactionLongevity::max_value(),
162			propagate: true,
163		};
164
165		Ok((validity, Val::CheckNonce(who.clone()), origin))
166	}
167
168	fn prepare(
169		self,
170		val: Self::Val,
171		_origin: &T::RuntimeOrigin,
172		_call: &T::RuntimeCall,
173		_info: &DispatchInfoOf<T::RuntimeCall>,
174		_len: usize,
175	) -> Result<Self::Pre, TransactionValidityError> {
176		let (who, nonce) = match val {
177			Val::CheckNonce(who) => (who, self.0),
178			Val::Refund(weight) => return Ok(Pre::Refund(weight)),
179		};
180		Self::prepare_nonce_for_account(&who, nonce).map(|_| Pre::NonceChecked)
181	}
182
183	fn post_dispatch_details(
184		pre: Self::Pre,
185		_info: &DispatchInfo,
186		_post_info: &PostDispatchInfoOf<T::RuntimeCall>,
187		_len: usize,
188		_result: &DispatchResult,
189	) -> Result<Weight, TransactionValidityError> {
190		match pre {
191			Pre::NonceChecked => Ok(Weight::zero()),
192			Pre::Refund(weight) => Ok(weight),
193		}
194	}
195}
196
197#[cfg(test)]
198mod tests {
199	use super::*;
200	use crate::system::mock::{new_test_ext, RuntimeCall, Test, CALL};
201	use subsoil::runtime::{
202		traits::{AsTransactionAuthorizedOrigin, DispatchTransaction, TxBaseImplication},
203		transaction_validity::TransactionSource::External,
204	};
205	use topsoil_core::{
206		assert_ok, assert_storage_noop, dispatch::GetDispatchInfo, traits::OriginTrait,
207	};
208
209	#[test]
210	fn signed_ext_check_nonce_works() {
211		new_test_ext().execute_with(|| {
212			crate::system::Account::<Test>::insert(
213				1,
214				crate::system::AccountInfo {
215					nonce: 1u64.into(),
216					consumers: 0,
217					providers: 1,
218					sufficients: 0,
219					data: 0,
220				},
221			);
222			let info = DispatchInfo::default();
223			let len = 0_usize;
224			// stale
225			assert_storage_noop!({
226				assert_eq!(
227					CheckNonce::<Test>(0u64.into())
228						.validate_only(Some(1).into(), CALL, &info, len, External, 0)
229						.unwrap_err(),
230					TransactionValidityError::Invalid(InvalidTransaction::Stale)
231				);
232				assert_eq!(
233					CheckNonce::<Test>(0u64.into())
234						.validate_and_prepare(Some(1).into(), CALL, &info, len, 0)
235						.unwrap_err(),
236					TransactionValidityError::Invalid(InvalidTransaction::Stale)
237				);
238			});
239			// correct
240			assert_ok!(CheckNonce::<Test>(1u64.into()).validate_only(
241				Some(1).into(),
242				CALL,
243				&info,
244				len,
245				External,
246				0,
247			));
248			assert_ok!(CheckNonce::<Test>(1u64.into()).validate_and_prepare(
249				Some(1).into(),
250				CALL,
251				&info,
252				len,
253				0,
254			));
255			// future
256			assert_ok!(CheckNonce::<Test>(5u64.into()).validate_only(
257				Some(1).into(),
258				CALL,
259				&info,
260				len,
261				External,
262				0,
263			));
264			assert_eq!(
265				CheckNonce::<Test>(5u64.into())
266					.validate_and_prepare(Some(1).into(), CALL, &info, len, 0)
267					.unwrap_err(),
268				TransactionValidityError::Invalid(InvalidTransaction::Future)
269			);
270		})
271	}
272
273	#[test]
274	fn signed_ext_check_nonce_requires_provider() {
275		new_test_ext().execute_with(|| {
276			crate::system::Account::<Test>::insert(
277				2,
278				crate::system::AccountInfo {
279					nonce: 1u64.into(),
280					consumers: 0,
281					providers: 1,
282					sufficients: 0,
283					data: 0,
284				},
285			);
286			crate::system::Account::<Test>::insert(
287				3,
288				crate::system::AccountInfo {
289					nonce: 1u64.into(),
290					consumers: 0,
291					providers: 0,
292					sufficients: 1,
293					data: 0,
294				},
295			);
296			let info = DispatchInfo::default();
297			let len = 0_usize;
298			// Both providers and sufficients zero
299			assert_storage_noop!({
300				assert_eq!(
301					CheckNonce::<Test>(1u64.into())
302						.validate_only(Some(1).into(), CALL, &info, len, External, 0)
303						.unwrap_err(),
304					TransactionValidityError::Invalid(InvalidTransaction::Payment)
305				);
306				assert_eq!(
307					CheckNonce::<Test>(1u64.into())
308						.validate_and_prepare(Some(1).into(), CALL, &info, len, 0)
309						.unwrap_err(),
310					TransactionValidityError::Invalid(InvalidTransaction::Payment)
311				);
312			});
313			// Non-zero providers
314			assert_ok!(CheckNonce::<Test>(1u64.into()).validate_only(
315				Some(2).into(),
316				CALL,
317				&info,
318				len,
319				External,
320				0,
321			));
322			assert_ok!(CheckNonce::<Test>(1u64.into()).validate_and_prepare(
323				Some(2).into(),
324				CALL,
325				&info,
326				len,
327				0,
328			));
329			// Non-zero sufficients
330			assert_ok!(CheckNonce::<Test>(1u64.into()).validate_only(
331				Some(3).into(),
332				CALL,
333				&info,
334				len,
335				External,
336				0,
337			));
338			assert_ok!(CheckNonce::<Test>(1u64.into()).validate_and_prepare(
339				Some(3).into(),
340				CALL,
341				&info,
342				len,
343				0,
344			));
345		})
346	}
347
348	#[test]
349	fn unsigned_check_nonce_works() {
350		new_test_ext().execute_with(|| {
351			let info = DispatchInfo::default();
352			let len = 0_usize;
353			let (_, val, origin) = CheckNonce::<Test>(1u64.into())
354				.validate(None.into(), CALL, &info, len, (), &TxBaseImplication(CALL), External)
355				.unwrap();
356			assert!(!origin.is_transaction_authorized());
357			assert_ok!(CheckNonce::<Test>(1u64.into()).prepare(val, &origin, CALL, &info, len));
358		})
359	}
360
361	#[test]
362	fn check_nonce_preserves_account_data() {
363		new_test_ext().execute_with(|| {
364			crate::system::Account::<Test>::insert(
365				1,
366				crate::system::AccountInfo {
367					nonce: 1u64.into(),
368					consumers: 0,
369					providers: 1,
370					sufficients: 0,
371					data: 0,
372				},
373			);
374			let info = DispatchInfo::default();
375			let len = 0_usize;
376			// run the validation step
377			let (_, val, origin) = CheckNonce::<Test>(1u64.into())
378				.validate(Some(1).into(), CALL, &info, len, (), &TxBaseImplication(CALL), External)
379				.unwrap();
380			// mutate `AccountData` for the caller
381			crate::system::Account::<Test>::mutate(1, |info| {
382				info.data = 42;
383			});
384			// run the preparation step
385			assert_ok!(CheckNonce::<Test>(1u64.into()).prepare(val, &origin, CALL, &info, len));
386			// only the nonce should be altered by the preparation step
387			let expected_info = crate::system::AccountInfo {
388				nonce: 2u64.into(),
389				consumers: 0,
390				providers: 1,
391				sufficients: 0,
392				data: 42,
393			};
394			assert_eq!(crate::system::Account::<Test>::get(1), expected_info);
395		})
396	}
397
398	#[test]
399	fn check_nonce_skipped_and_refund_for_other_origins() {
400		new_test_ext().execute_with(|| {
401			let ext = CheckNonce::<Test>(1u64.into());
402
403			let mut info = CALL.get_dispatch_info();
404			info.extension_weight = ext.weight(CALL);
405
406			// Ensure we test the refund.
407			assert!(info.extension_weight != Weight::zero());
408
409			let len = CALL.encoded_size();
410
411			let origin = crate::system::RawOrigin::Root.into();
412			let (pre, origin) = ext.validate_and_prepare(origin, CALL, &info, len, 0).unwrap();
413
414			assert!(origin.as_system_ref().unwrap().is_root());
415
416			let pd_res = Ok(());
417			let mut post_info = topsoil_core::dispatch::PostDispatchInfo {
418				actual_weight: Some(info.total_weight()),
419				pays_fee: Default::default(),
420			};
421
422			<CheckNonce<Test> as TransactionExtension<RuntimeCall>>::post_dispatch(
423				pre,
424				&info,
425				&mut post_info,
426				len,
427				&pd_res,
428			)
429			.unwrap();
430
431			assert_eq!(post_info.actual_weight, Some(info.call_weight));
432		})
433	}
434}