pallet_robonomics_datalog/
lib.rs1#![cfg_attr(not(feature = "std"), no_std)]
20#![allow(clippy::type_complexity)]
21#![allow(clippy::unused_unit)]
22#![allow(clippy::from_over_into)]
23
24#[cfg(feature = "runtime-benchmarks")]
25mod benchmarking;
26pub mod weights;
27
28pub use pallet::*;
29pub use weights::WeightInfo;
30
31#[frame_support::pallet]
32#[allow(clippy::module_inception)]
33pub mod pallet {
34 use frame_support::{pallet_prelude::*, traits::Time};
35 use frame_system::pallet_prelude::*;
36 use parity_scale_codec::{Decode, Encode};
37 use sp_std::prelude::*;
38
39 use super::*;
40
41 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
43
44 #[pallet::config]
45 pub trait Config: frame_system::Config + TypeInfo {
46 type Time: Time;
48 type Record: Parameter + Default + MaxEncodedLen;
50 #[allow(deprecated)]
52 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
53 #[pallet::constant]
55 type WindowSize: Get<u64>;
56 type WeightInfo: WeightInfo;
58 }
59
60 #[pallet::error]
61 pub enum Error<T> {
62 RecordTooBig,
64 }
65
66 #[pallet::event]
67 #[pallet::generate_deposit(pub (super) fn deposit_event)]
68 pub enum Event<T: Config> {
69 NewRecord(T::AccountId, <T::Time as Time>::Moment, T::Record),
71 Erased(T::AccountId),
73 }
74
75 #[pallet::hooks]
76 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
77
78 #[pallet::storage]
80 #[pallet::getter(fn datalog_index)]
81 pub type DatalogIndex<T> = StorageMap<
82 _,
83 Twox64Concat,
84 <T as frame_system::Config>::AccountId,
85 RingBufferIndex,
86 ValueQuery,
87 >;
88
89 #[pallet::storage]
91 #[pallet::getter(fn datalog_item)]
92 pub type DatalogItem<T> = StorageMap<
93 _,
94 Twox64Concat,
95 (<T as frame_system::Config>::AccountId, u64),
96 RingBufferItem<T>,
97 ValueQuery,
98 >;
99
100 #[pallet::pallet]
101 #[pallet::storage_version(STORAGE_VERSION)]
102 pub struct Pallet<T>(PhantomData<T>);
103
104 #[pallet::call]
105 impl<T: Config> Pallet<T> {
106 #[pallet::weight(T::WeightInfo::record())]
108 #[pallet::call_index(0)]
109 pub fn record(origin: OriginFor<T>, record: T::Record) -> DispatchResultWithPostInfo {
110 let sender = ensure_signed(origin)?;
111
112 let now = T::Time::now();
114 let item = RingBufferItem(now, record);
115
116 DatalogIndex::<T>::mutate(&sender, |idx| {
117 let window_size = T::WindowSize::get();
118 let end = idx.add(window_size);
119
120 DatalogItem::<T>::insert((&sender, end), &item)
121 });
122
123 let (now, record) = item.split();
124
125 Self::deposit_event(Event::NewRecord(sender, now, record));
126 Ok(().into())
127 }
128
129 #[pallet::weight(T::WeightInfo::erase())]
131 #[pallet::call_index(1)]
132 pub fn erase(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
133 let sender = ensure_signed(origin)?;
134
135 let mut idx = DatalogIndex::<T>::take(&sender);
136
137 let window_size = T::WindowSize::get();
138 let _count = idx.count(window_size);
140
141 for start in idx.iter(window_size) {
142 DatalogItem::<T>::remove((&sender, start))
143 }
144
145 Self::deposit_event(Event::Erased(sender));
146 Ok(().into())
147 }
148 }
149
150 impl<T: Config> Pallet<T> {
151 pub fn data(account: &<T as frame_system::Config>::AccountId) -> Vec<RingBufferItem<T>> {
153 let mut idx = DatalogIndex::<T>::get(&account);
154 let window_size = T::WindowSize::get();
155
156 idx.iter(window_size)
157 .map(|i| DatalogItem::<T>::get((&account, i)))
158 .collect()
159 }
160 }
161
162 #[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Debug, PartialEq)]
163 pub struct RingBufferItem<T: Config>(
164 #[codec(compact)] <<T as Config>::Time as Time>::Moment,
165 <T as Config>::Record,
166 );
167
168 impl<T: Config> Default for RingBufferItem<T> {
169 fn default() -> Self {
170 Self(Default::default(), Default::default())
171 }
172 }
173
174 #[cfg(test)]
175 impl<T: Config> RingBufferItem<T> {
176 pub(crate) fn new(
177 now: <<T as Config>::Time as Time>::Moment,
178 record: <T as Config>::Record,
179 ) -> Self {
180 Self(now, record)
181 }
182 }
183
184 impl<T: Config> RingBufferItem<T> {
185 #[inline]
186 fn split(self) -> (<<T as Config>::Time as Time>::Moment, <T as Config>::Record) {
187 (self.0, self.1)
188 }
189 }
190
191 #[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen, Debug, PartialEq)]
192 pub struct RingBufferIndex {
193 #[codec(compact)]
194 pub(crate) start: u64,
195 #[codec(compact)]
196 pub(crate) end: u64,
197 }
198
199 impl RingBufferIndex {
200 #[inline]
201 pub(crate) fn count(&self, max: u64) -> u64 {
202 if self.start <= self.end {
203 self.end - self.start
204 } else {
205 max + self.end - self.start
206 }
207 }
208
209 #[inline]
210 fn next(val: &mut u64, max: u64) {
211 *val += 1;
212 if *val == max {
213 *val = 0
214 }
215 }
216 pub fn add(&mut self, max: u64) -> u64 {
218 let v = self.end;
219 Self::next(&mut self.end, max);
220 if self.start == self.end {
221 Self::next(&mut self.start, max);
222 }
223 v
224 }
225 fn iter(&mut self, max: u64) -> RingBufferIterator<'_> {
227 RingBufferIterator { inner: self, max }
228 }
229 }
230
231 struct RingBufferIterator<'a> {
232 inner: &'a mut RingBufferIndex,
233 max: u64,
234 }
235
236 impl Iterator for RingBufferIterator<'_> {
237 type Item = u64;
238 fn next(&mut self) -> Option<Self::Item> {
239 if self.inner.end == self.inner.start {
240 None
241 } else {
242 let u = self.inner.start;
243 RingBufferIndex::next(&mut self.inner.start, self.max);
244 Some(u)
245 }
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use frame_support::{assert_err, assert_ok, derive_impl, parameter_types, BoundedVec};
253
254 use sp_runtime::{BuildStorage, DispatchError};
255
256 use crate::{self as datalog, *};
257
258 type Block = frame_system::mocking::MockBlock<Runtime>;
259 type Item = RingBufferItem<Runtime>;
260
261 frame_support::construct_runtime!(
262 pub enum Runtime {
263 System: frame_system,
264 Timestamp: pallet_timestamp,
265 Datalog: datalog,
266 }
267 );
268
269 parameter_types! {
270 pub const BlockHashCount: u64 = 250;
271 }
272
273 #[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
274 impl frame_system::Config for Runtime {
275 type Block = Block;
276 }
277
278 impl pallet_timestamp::Config for Runtime {
279 type Moment = u64;
280 type OnTimestampSet = ();
281 type MinimumPeriod = ();
282 type WeightInfo = ();
283 }
284
285 const WINDOW: u64 = 20;
286 parameter_types! {
287 pub const WindowSize: u64 = WINDOW;
288 pub const MaximumMessageSize: u32 = 512;
289 }
290
291 impl Config for Runtime {
292 type Time = Timestamp;
293 type Record = BoundedVec<u8, MaximumMessageSize>;
294 type RuntimeEvent = RuntimeEvent;
295 type WindowSize = WindowSize;
296 type WeightInfo = weights::TestWeightInfo;
297 }
298
299 pub fn new_test_ext() -> sp_io::TestExternalities {
300 let storage = RuntimeGenesisConfig {
301 system: Default::default(),
302 }
303 .build_storage()
304 .unwrap();
305 storage.into()
306 }
307
308 #[test]
309 fn test_ringbuffer_index() {
310 let mut idx: RingBufferIndex = Default::default();
311 assert!(idx.start == idx.end);
312 assert!(idx.start == 0);
313
314 let i = idx.add(WINDOW);
315 assert_eq!(i, 0);
316 assert_eq!(idx.end, 1);
317
318 assert_eq!(idx.count(WINDOW), 1);
319
320 for _ in 0..WINDOW {
321 let _ = idx.add(WINDOW);
322 }
323 assert_eq!(idx.count(WINDOW), WINDOW - 1);
324 }
325
326 #[test]
327 fn test_store_data() {
328 new_test_ext().execute_with(|| {
329 let sender = 1;
330 let record = BoundedVec::try_from(b"datalog".to_vec()).unwrap();
331 assert_ok!(Datalog::record(
332 RuntimeOrigin::signed(sender),
333 record.clone()
334 ));
335 assert_eq!(Datalog::data(&sender), vec![Item::new(0, record)]);
336 })
337 }
338
339 #[test]
340 fn test_recycle_data() {
341 new_test_ext().execute_with(|| {
342 let sender = 1;
343
344 for i in 0..(WINDOW + 10) {
345 assert_ok!(Datalog::record(
346 RuntimeOrigin::signed(sender),
347 BoundedVec::try_from(i.to_be_bytes().to_vec()).unwrap()
348 ));
349 }
350
351 let data: Vec<_> = (11..(WINDOW + 10))
352 .map(|i| Item::new(0, BoundedVec::try_from(i.to_be_bytes().to_vec()).unwrap()))
353 .collect();
354
355 assert_eq!(Datalog::data(&sender), data);
356 let idx = Datalog::datalog_index(&sender);
357 assert_eq!(idx, RingBufferIndex { start: 11, end: 10 });
358 assert_eq!(idx.count(WINDOW), WINDOW - 1);
359 })
360 }
361
362 #[test]
363 fn test_erase_data() {
364 new_test_ext().execute_with(|| {
365 let sender = 1;
366 let record = BoundedVec::try_from(b"datalog".to_vec()).unwrap();
367 assert_ok!(Datalog::record(
368 RuntimeOrigin::signed(sender),
369 record.clone()
370 ));
371 assert_eq!(Datalog::data(&sender), vec![Item::new(0, record)]);
373 assert_eq!(
374 Datalog::datalog_index(&sender),
375 RingBufferIndex { start: 0, end: 1 }
376 );
377
378 assert_ok!(Datalog::erase(RuntimeOrigin::signed(sender)));
379 assert_eq!(Datalog::data(&sender), vec![]);
380
381 assert_eq!(
382 Datalog::datalog_index(&sender),
383 RingBufferIndex { start: 0, end: 0 }
384 );
385 })
386 }
387
388 #[test]
389 fn test_bad_origin() {
390 new_test_ext().execute_with(|| {
391 assert_err!(
392 Datalog::record(RuntimeOrigin::none(), Default::default()),
393 DispatchError::BadOrigin
394 );
395 })
396 }
397
398 pub fn hash2vec(ss58hash: &str) -> BoundedVec<u8, MaximumMessageSize> {
399 let ss58vec = bs58::decode(ss58hash)
400 .into_vec()
401 .expect("Couldn't decode from Base58");
402 BoundedVec::try_from(ss58vec).expect("Couldn't bound decoded Base58")
403 }
404
405 #[test]
406 fn test_store_ipfs_hashes() {
407 new_test_ext().execute_with(|| {
408 let sender = 1;
409 let record = hash2vec("QmWboFP8XeBtFMbNYK3Ne8Z3gKFBSR5iQzkKgeNgQz3dz4");
410
411 assert_ok!(Datalog::record(
412 RuntimeOrigin::signed(sender),
413 record.clone()
414 ));
415 assert_eq!(Datalog::data(&sender), vec![Item::new(0, record.clone())]);
416
417 let record2 = hash2vec("zdj7WWYAEceQ6ncfPZeRFjozov4dC7FaxU7SuMwzW4VuYBDta");
418
419 Timestamp::set_timestamp(100);
420 assert_ok!(Datalog::record(
421 RuntimeOrigin::signed(sender),
422 record2.clone()
423 ));
424 assert_eq!(
425 Datalog::data(&sender),
426 vec![
427 Item::new(0, record.clone()),
428 Item::new(100, record2.clone()),
429 ]
430 );
431 let record3 = hash2vec("QmWboFP8XeBtFMbNYK3Ne8Z3gKFBSR5iQzkKgeNgQz3dz2");
432
433 Timestamp::set_timestamp(200);
434 assert_ok!(Datalog::record(
435 RuntimeOrigin::signed(sender),
436 record3.clone()
437 ));
438 assert_eq!(
439 Datalog::data(&sender),
440 vec![
441 Item::new(0, record),
442 Item::new(100, record2),
443 Item::new(200, record3),
444 ]
445 );
446 })
447 }
448}