/home/runner/work/creditcoin/creditcoin/pallets/creditcoin/src/ocw/nonce.rs
Line | Count | Source (jump to first uncovered line) |
1 | | use crate::{Config, Pallet}; |
2 | | use alloc::vec::Vec; |
3 | | use codec::Encode; |
4 | | use sp_runtime::offchain::storage_lock::{StorageLock, Time}; |
5 | | use sp_runtime::offchain::Duration; |
6 | | |
7 | | const SYNCED_NONCE: &[u8] = b"creditcoin/OCW/nonce/nonce/"; |
8 | | const SYNCED_NONCE_LOCK: &[u8] = b"creditcoin/OCW/nonce/lock/"; |
9 | | const LOCK_DEADLINE: u64 = 50_000; |
10 | | |
11 | 18 | pub(super) fn lock_key<Id: Encode>(id: &Id) -> Vec<u8> { |
12 | 18 | id.using_encoded(|encoded_id| SYNCED_NONCE_LOCK.iter().chain(encoded_id).copied().collect()) |
13 | 18 | } |
14 | | |
15 | 21 | pub fn nonce_key<Id: Encode>(id: &Id) -> Vec<u8> { |
16 | 21 | id.using_encoded(|encoded_id| SYNCED_NONCE.iter().chain(encoded_id).copied().collect()) |
17 | 21 | } |
18 | | |
19 | | impl<T: Config> Pallet<T> { |
20 | 17 | pub(super) fn nonce_lock_new(key: &[u8]) -> StorageLock<'_, Time> { |
21 | 17 | StorageLock::<Time>::with_deadline(key, Duration::from_millis(LOCK_DEADLINE)) |
22 | 17 | } |
23 | | } |
24 | | |
25 | | #[cfg(test)] |
26 | | mod tests { |
27 | | use super::*; |
28 | | use crate::helpers::HexToAddress; |
29 | | use crate::mock::{roll_to, roll_to_with_ocw, ExtBuilder, Origin, Test}; |
30 | | use crate::ocw::errors::VerificationFailureCause as Cause; |
31 | | use crate::ocw::tasks::collect_coins::tests::mock_rpc_for_collect_coins; |
32 | | use crate::ocw::tasks::collect_coins::{testing_constants::CHAIN, tests::TX_HASH}; |
33 | | use crate::tests::generate_address_with_proof; |
34 | | use crate::types::{Address, AddressId}; |
35 | | use crate::Pallet as Creditcoin; |
36 | | use assert_matches::assert_matches; |
37 | | use codec::Decode; |
38 | | use core::sync::atomic::AtomicU64; |
39 | | use frame_support::assert_ok; |
40 | | use frame_system::Config as SystemConfig; |
41 | | use frame_system::Pallet as System; |
42 | | use sp_runtime::offchain::storage::StorageValueRef; |
43 | | use sp_runtime::offchain::testing::TestOffchainExt; |
44 | | use sp_runtime::traits::IdentifyAccount; |
45 | | use std::sync::atomic::Ordering; |
46 | | use std::sync::Arc; |
47 | | |
48 | 1 | #[test] |
49 | 1 | fn incremented_for_persisted_task() { |
50 | 1 | let mut ext = ExtBuilder::default(); |
51 | 1 | let pkey = ext.generate_authority(); |
52 | 1 | let acct = <Test as SystemConfig>::AccountId::from(pkey.into_account().0); |
53 | 1 | ext.build_offchain_and_execute_with_state(|state, _| { |
54 | 1 | mock_rpc_for_collect_coins(&state); |
55 | 1 | |
56 | 1 | let (acc, addr, sign, _) = generate_address_with_proof("collector"); |
57 | 1 | assert_ok!(Creditcoin::<Test>::register_address( |
58 | 1 | Origin::signed(acc.clone()), |
59 | 1 | CHAIN, |
60 | 1 | addr.clone(), |
61 | 1 | sign |
62 | 1 | )); |
63 | | |
64 | 1 | roll_to(1); |
65 | 1 | assert_ok!(Creditcoin::<Test>::request_collect_coins( |
66 | 1 | Origin::signed(acc), |
67 | 1 | addr, |
68 | 1 | TX_HASH.hex_to_address() |
69 | 1 | )); |
70 | 1 | roll_to_with_ocw(2); |
71 | 1 | |
72 | 1 | let key = &nonce_key(&acct); |
73 | 1 | let synced_nonce = StorageValueRef::persistent(key); |
74 | 1 | let synced_nonce: u64 = synced_nonce.get().unwrap().unwrap(); |
75 | 1 | assert_eq!(synced_nonce, 1u64); |
76 | 1 | let nonce = System::<Test>::account(acct).nonce; |
77 | 1 | assert_eq!(nonce, 1u64); |
78 | 1 | }); |
79 | 1 | } |
80 | | |
81 | 1 | #[test] |
82 | 1 | fn incremented_for_failed_task() { |
83 | 1 | let mut ext = ExtBuilder::default(); |
84 | 1 | let pkey = ext.generate_authority(); |
85 | 1 | let acct = <Test as SystemConfig>::AccountId::from(pkey.into_account().0); |
86 | 1 | ext.build_offchain_and_execute_with_state(|state, pool| { |
87 | 1 | mock_rpc_for_collect_coins(&state); |
88 | 1 | |
89 | 1 | let (acc, addr, sign, _) = generate_address_with_proof("collector"); |
90 | 1 | assert_ok!(Creditcoin::<Test>::register_address( |
91 | 1 | Origin::signed(acc.clone()), |
92 | 1 | CHAIN, |
93 | 1 | addr.clone(), |
94 | 1 | sign |
95 | 1 | )); |
96 | | |
97 | 1 | let mut fake = addr; |
98 | 1 | fake[0] = 0xff; |
99 | 1 | let address_id = AddressId::new::<Test>(&CHAIN, &fake); |
100 | 1 | let entry = Address { blockchain: CHAIN, value: fake.clone(), owner: acc.clone() }; |
101 | 1 | crate::Addresses::<Test>::insert(address_id, entry); |
102 | 1 | |
103 | 1 | roll_to(1); |
104 | 1 | assert_ok!(Creditcoin::<Test>::request_collect_coins( |
105 | 1 | Origin::signed(acc), |
106 | 1 | fake.clone(), |
107 | 1 | TX_HASH.hex_to_address() |
108 | 1 | )); |
109 | | |
110 | 1 | let deadline = Test::unverified_transfer_deadline(); |
111 | 1 | roll_to_with_ocw(2); |
112 | 1 | |
113 | 1 | let key = &nonce_key(&acct); |
114 | 1 | let synced_nonce = StorageValueRef::persistent(key); |
115 | 1 | let synced_nonce: u64 = synced_nonce.get().unwrap().unwrap(); |
116 | 1 | assert_eq!(synced_nonce, 1u64); |
117 | 1 | let nonce = System::<Test>::account(acct).nonce; |
118 | 1 | assert_eq!(nonce, 1u64); |
119 | | |
120 | 1 | let expected_collected_coins_id = crate::CollectedCoinsId::new::<crate::mock::Test>( |
121 | 1 | &CHAIN, |
122 | 1 | &TX_HASH.hex_to_address(), |
123 | 1 | ); |
124 | 1 | |
125 | 1 | let call = crate::Call::<crate::mock::Test>::fail_task { |
126 | 1 | task_id: expected_collected_coins_id.into(), |
127 | 1 | cause: Cause::IncorrectSender, |
128 | 1 | deadline, |
129 | 1 | }; |
130 | | |
131 | 1 | assert_matches!0 (pool.write().transactions.pop(), |
132 | 1 | Some(tx) => { |
133 | 1 | let tx = crate::mock::Extrinsic::decode(&mut &*tx).unwrap(); |
134 | 1 | assert_eq!(tx.call, crate::mock::Call::Creditcoin(call)); |
135 | | } |
136 | | ); |
137 | 1 | }); |
138 | 1 | } |
139 | | |
140 | 1 | #[test] |
141 | 1 | fn unique_per_account() { |
142 | 1 | let mut ext = ExtBuilder::default(); |
143 | 1 | let pkey = ext.generate_authority(); |
144 | 1 | let acct_1 = <Test as SystemConfig>::AccountId::from(pkey.into_account().0); |
145 | 1 | let pkey = ext.generate_authority(); |
146 | 1 | let acct_2 = <Test as SystemConfig>::AccountId::from(pkey.into_account().0); |
147 | 1 | assert!(nonce_key(&acct_1) != nonce_key(&acct_2)); |
148 | 1 | assert!(lock_key(&acct_1) != lock_key(&acct_2)); |
149 | 1 | } |
150 | | |
151 | 1 | #[test] |
152 | 1 | fn not_incremented_on_task_error() { |
153 | 1 | let mut ext = ExtBuilder::default(); |
154 | 1 | let pkey = ext.generate_authority(); |
155 | 1 | let acct = <Test as SystemConfig>::AccountId::from(pkey.into_account().0); |
156 | 1 | ext.build_offchain_and_execute_with_state(|_, pool| { |
157 | 1 | let (acc, addr, sign, _) = generate_address_with_proof("collector"); |
158 | 1 | assert_ok!(Creditcoin::<Test>::register_address( |
159 | 1 | Origin::signed(acc.clone()), |
160 | 1 | CHAIN, |
161 | 1 | addr.clone(), |
162 | 1 | sign |
163 | 1 | )); |
164 | | |
165 | 1 | roll_to(1); |
166 | 1 | assert_ok!(Creditcoin::<Test>::request_collect_coins( |
167 | 1 | Origin::signed(acc), |
168 | 1 | addr, |
169 | 1 | TX_HASH.hex_to_address() |
170 | 1 | )); |
171 | 1 | roll_to_with_ocw(2); |
172 | 1 | |
173 | 1 | let key = &nonce_key(&acct); |
174 | 1 | let synced_nonce = StorageValueRef::persistent(key); |
175 | 1 | let synced_nonce = synced_nonce.get::<u64>().unwrap(); |
176 | 1 | assert!(synced_nonce.is_none()); |
177 | 1 | let nonce = System::<Test>::account(acct).nonce; |
178 | 1 | assert_eq!(nonce, 0u64); |
179 | | |
180 | 1 | assert!(pool.write().transactions.is_empty()); |
181 | 1 | }); |
182 | 1 | } |
183 | | |
184 | 1 | #[test] |
185 | 1 | fn parallel_increment() { |
186 | 1 | let (offchain, _) = TestOffchainExt::new(); |
187 | 1 | const THREADS: u32 = 3; |
188 | 1 | let nonces = Arc::new(AtomicU64::new(0)); |
189 | 1 | |
190 | 3 | let handles = (0..THREADS).into_iter().map(|_| { |
191 | 3 | let offchain = offchain.clone(); |
192 | 3 | let nonces = nonces.clone(); |
193 | 3 | |
194 | 3 | std::thread::spawn(move || { |
195 | 3 | let mut ext_builder = ExtBuilder::default(); |
196 | 3 | let acct_pubkey = ext_builder.generate_authority(); |
197 | 3 | let acct = <Test as SystemConfig>::AccountId::from(acct_pubkey.into_account().0); |
198 | 3 | let expected_collected_coins_id = |
199 | 3 | crate::CollectedCoinsId::new::<Test>(&CHAIN, &[0]); |
200 | 3 | let (mut ext, pool) = ext_builder.build_with(offchain); |
201 | 3 | let execute = || { |
202 | 3 | crate::mock::roll_to(1); |
203 | 3 | let call = crate::Call::<Test>::fail_task { |
204 | 3 | task_id: expected_collected_coins_id.into(), |
205 | 3 | cause: Cause::AbiMismatch, |
206 | 3 | deadline: Test::unverified_transfer_deadline(), |
207 | 3 | }; |
208 | 3 | assert_ok!(crate::Pallet::<Test>::submit_txn_with_synced_nonce( |
209 | 3 | acct.clone(), |
210 | 3 | |_| call.clone(), |
211 | 3 | )); |
212 | | |
213 | 3 | assert_matches!0 (pool.write().transactions.pop(), |
214 | 3 | Some(tx) => { |
215 | 3 | let tx = crate::mock::Extrinsic::decode(&mut &*tx).unwrap(); |
216 | 3 | assert_eq!(tx.call, crate::mock::Call::Creditcoin(call)); |
217 | | } |
218 | | ); |
219 | | |
220 | 3 | let nonce = System::<Test>::account(acct).nonce; |
221 | 3 | nonces.fetch_add(nonce, Ordering::Relaxed); |
222 | 3 | }; |
223 | 3 | |
224 | 3 | ext.execute_with(execute); |
225 | 3 | }) |
226 | 3 | })1 ; |
227 | | |
228 | 4 | for h3 in handles { |
229 | 3 | h.join().expect("testing context is shared"); |
230 | 3 | } |
231 | | |
232 | 1 | let ext_builder = ExtBuilder::default(); |
233 | 1 | let (mut ext, _) = ext_builder.build_with(offchain); |
234 | 1 | ext.execute_with(|| { |
235 | 1 | let nonce_post_submition_sum = (THREADS) * (THREADS + 1) / 2; |
236 | 1 | assert_eq!(nonces.load(Ordering::Relaxed), nonce_post_submition_sum as u64); |
237 | 1 | }); |
238 | 1 | } |
239 | | |
240 | 1 | #[test] |
241 | 1 | fn lock_works() { |
242 | 1 | let (offchain, _) = TestOffchainExt::new(); |
243 | 1 | const THREADS: u32 = 2; |
244 | 1 | |
245 | 2 | let handles = (0..THREADS).map(|_| { |
246 | 2 | let offchain = offchain.clone(); |
247 | 2 | |
248 | 2 | std::thread::spawn(move || { |
249 | 2 | let mut ext_builder = ExtBuilder::default(); |
250 | 2 | let acct_pubkey = ext_builder.generate_authority(); |
251 | 2 | let acct = <Test as SystemConfig>::AccountId::from(acct_pubkey.into_account().0); |
252 | 2 | let (mut ext, _) = ext_builder.build_with(offchain); |
253 | 2 | |
254 | 2 | let execute = || { |
255 | 2 | crate::mock::roll_to(1); |
256 | 2 | |
257 | 2 | let key = lock_key(&acct); |
258 | 2 | let mut lock = Pallet::<Test>::nonce_lock_new(&key); |
259 | 2 | let guard = lock.try_lock(); |
260 | 2 | guard.map(|g| g.forget()1 ).or_else(|deadline| { |
261 | 1 | // failed to acq guard; move to active guard's deadline boundaries |
262 | 1 | sp_io::offchain::sleep_until(deadline); |
263 | 1 | //deadline still effective |
264 | 1 | lock.try_lock().map(|_| ()0 ) |
265 | 2 | }) |
266 | 2 | }; |
267 | 2 | |
268 | 2 | ext.execute_with(execute) |
269 | 2 | }) |
270 | 2 | }); |
271 | 1 | |
272 | 2 | if !handles.into_iter().any(|h| h.join().expect("thread joins").is_err())1 { |
273 | 0 | panic!("lock should block") |
274 | 1 | } |
275 | 1 | } |
276 | | |
277 | 1 | #[test] |
278 | 1 | fn nonce_lock_expires() { |
279 | 1 | let ext = ExtBuilder::default(); |
280 | 1 | ext.build_offchain_and_execute_with_state(|_, _| { |
281 | 1 | System::<Test>::set_block_number(1); |
282 | 1 | |
283 | 1 | let key = &b"lock_key"[..]; |
284 | 1 | let mut lock = Pallet::<Test>::nonce_lock_new(key); |
285 | 1 | let guard = lock.try_lock().expect("ok"); |
286 | 1 | guard.forget(); |
287 | 1 | let guard = lock.try_lock(); |
288 | 1 | let deadline = guard.map(|_| ()0 ).expect_err("deadline"); |
289 | 1 | // failed to acq guard; move past active guard's deadline boundary |
290 | 1 | sp_io::offchain::sleep_until(deadline.add(Duration::from_millis(LOCK_DEADLINE + 1))); |
291 | 1 | let g = lock.try_lock(); |
292 | 1 | assert!(g.is_ok()); |
293 | 1 | }); |
294 | 1 | } |
295 | | } |