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 #[cfg_attr(feature = "std", derive(Debug, PartialEq))]
163 #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
164 pub struct RingBufferItem<T: Config>(
165 #[codec(compact)] <<T as Config>::Time as Time>::Moment,
166 <T as Config>::Record,
167 );
168
169 impl<T: Config> Default for RingBufferItem<T> {
170 fn default() -> Self {
171 Self(Default::default(), Default::default())
172 }
173 }
174
175 #[cfg(test)]
176 impl<T: Config> RingBufferItem<T> {
177 pub(crate) fn new(
178 now: <<T as Config>::Time as Time>::Moment,
179 record: <T as Config>::Record,
180 ) -> Self {
181 Self(now, record)
182 }
183 }
184
185 impl<T: Config> RingBufferItem<T> {
186 #[inline]
187 fn split(self) -> (<<T as Config>::Time as Time>::Moment, <T as Config>::Record) {
188 (self.0, self.1)
189 }
190 }
191
192 #[cfg_attr(feature = "std", derive(Debug, PartialEq))]
193 #[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]
194 pub struct RingBufferIndex {
195 #[codec(compact)]
196 pub(crate) start: u64,
197 #[codec(compact)]
198 pub(crate) end: u64,
199 }
200
201 impl RingBufferIndex {
202 #[inline]
203 pub(crate) fn count(&self, max: u64) -> u64 {
204 if self.start <= self.end {
205 self.end - self.start
206 } else {
207 max + self.end - self.start
208 }
209 }
210
211 #[inline]
212 fn next(val: &mut u64, max: u64) {
213 *val += 1;
214 if *val == max {
215 *val = 0
216 }
217 }
218 pub fn add(&mut self, max: u64) -> u64 {
220 let v = self.end;
221 Self::next(&mut self.end, max);
222 if self.start == self.end {
223 Self::next(&mut self.start, max);
224 }
225 v
226 }
227 fn iter(&mut self, max: u64) -> RingBufferIterator<'_> {
229 RingBufferIterator { inner: self, max }
230 }
231 }
232
233 struct RingBufferIterator<'a> {
234 inner: &'a mut RingBufferIndex,
235 max: u64,
236 }
237
238 impl Iterator for RingBufferIterator<'_> {
239 type Item = u64;
240 fn next(&mut self) -> Option<Self::Item> {
241 if self.inner.end == self.inner.start {
242 None
243 } else {
244 let u = self.inner.start;
245 RingBufferIndex::next(&mut self.inner.start, self.max);
246 Some(u)
247 }
248 }
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use frame_support::{assert_err, assert_ok, derive_impl, parameter_types, BoundedVec};
255
256 use sp_runtime::{BuildStorage, DispatchError};
257
258 use crate::{self as datalog, *};
259
260 type Block = frame_system::mocking::MockBlock<Runtime>;
261 type Item = RingBufferItem<Runtime>;
262
263 frame_support::construct_runtime!(
264 pub enum Runtime {
265 System: frame_system,
266 Timestamp: pallet_timestamp,
267 Datalog: datalog,
268 }
269 );
270
271 parameter_types! {
272 pub const BlockHashCount: u64 = 250;
273 }
274
275 #[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
276 impl frame_system::Config for Runtime {
277 type Block = Block;
278 }
279
280 impl pallet_timestamp::Config for Runtime {
281 type Moment = u64;
282 type OnTimestampSet = ();
283 type MinimumPeriod = ();
284 type WeightInfo = ();
285 }
286
287 const WINDOW: u64 = 20;
288 parameter_types! {
289 pub const WindowSize: u64 = WINDOW;
290 pub const MaximumMessageSize: u32 = 512;
291 }
292
293 impl Config for Runtime {
294 type Time = Timestamp;
295 type Record = BoundedVec<u8, MaximumMessageSize>;
296 type RuntimeEvent = RuntimeEvent;
297 type WindowSize = WindowSize;
298 type WeightInfo = ();
299 }
300
301 pub fn new_test_ext() -> sp_io::TestExternalities {
302 let storage = RuntimeGenesisConfig {
303 system: Default::default(),
304 }
305 .build_storage()
306 .unwrap();
307 storage.into()
308 }
309
310 #[test]
311 fn test_ringbuffer_index() {
312 let mut idx: RingBufferIndex = Default::default();
313 assert!(idx.start == idx.end);
314 assert!(idx.start == 0);
315
316 let i = idx.add(WINDOW);
317 assert_eq!(i, 0);
318 assert_eq!(idx.end, 1);
319
320 assert_eq!(idx.count(WINDOW), 1);
321
322 for _ in 0..WINDOW {
323 let _ = idx.add(WINDOW);
324 }
325 assert_eq!(idx.count(WINDOW), WINDOW - 1);
326 }
327
328 #[test]
329 fn test_store_data() {
330 new_test_ext().execute_with(|| {
331 let sender = 1;
332 let record = BoundedVec::try_from(b"datalog".to_vec()).unwrap();
333 assert_ok!(Datalog::record(
334 RuntimeOrigin::signed(sender),
335 record.clone()
336 ));
337 assert_eq!(Datalog::data(&sender), vec![Item::new(0, record)]);
338 })
339 }
340
341 #[test]
342 fn test_recycle_data() {
343 new_test_ext().execute_with(|| {
344 let sender = 1;
345
346 for i in 0..(WINDOW + 10) {
347 assert_ok!(Datalog::record(
348 RuntimeOrigin::signed(sender),
349 BoundedVec::try_from(i.to_be_bytes().to_vec()).unwrap()
350 ));
351 }
352
353 let data: Vec<_> = (11..(WINDOW + 10))
354 .map(|i| Item::new(0, BoundedVec::try_from(i.to_be_bytes().to_vec()).unwrap()))
355 .collect();
356
357 assert_eq!(Datalog::data(&sender), data);
358 let idx = Datalog::datalog_index(&sender);
359 assert_eq!(idx, RingBufferIndex { start: 11, end: 10 });
360 assert_eq!(idx.count(WINDOW), WINDOW - 1);
361 })
362 }
363
364 #[test]
365 fn test_erase_data() {
366 new_test_ext().execute_with(|| {
367 let sender = 1;
368 let record = BoundedVec::try_from(b"datalog".to_vec()).unwrap();
369 assert_ok!(Datalog::record(
370 RuntimeOrigin::signed(sender),
371 record.clone()
372 ));
373 assert_eq!(Datalog::data(&sender), vec![Item::new(0, record)]);
375 assert_eq!(
376 Datalog::datalog_index(&sender),
377 RingBufferIndex { start: 0, end: 1 }
378 );
379
380 assert_ok!(Datalog::erase(RuntimeOrigin::signed(sender)));
381 assert_eq!(Datalog::data(&sender), vec![]);
382
383 assert_eq!(
384 Datalog::datalog_index(&sender),
385 RingBufferIndex { start: 0, end: 0 }
386 );
387 })
388 }
389
390 #[test]
391 fn test_bad_origin() {
392 new_test_ext().execute_with(|| {
393 assert_err!(
394 Datalog::record(RuntimeOrigin::none(), Default::default()),
395 DispatchError::BadOrigin
396 );
397 })
398 }
399
400 pub fn hash2vec(ss58hash: &str) -> BoundedVec<u8, MaximumMessageSize> {
401 let ss58vec = bs58::decode(ss58hash)
402 .into_vec()
403 .expect("Couldn't decode from Base58");
404 BoundedVec::try_from(ss58vec).expect("Couldn't bound decoded Base58")
405 }
406
407 #[test]
408 fn test_store_ipfs_hashes() {
409 new_test_ext().execute_with(|| {
410 let sender = 1;
411 let record = hash2vec("QmWboFP8XeBtFMbNYK3Ne8Z3gKFBSR5iQzkKgeNgQz3dz4");
412
413 assert_ok!(Datalog::record(
414 RuntimeOrigin::signed(sender),
415 record.clone()
416 ));
417 assert_eq!(Datalog::data(&sender), vec![Item::new(0, record.clone())]);
418
419 let record2 = hash2vec("zdj7WWYAEceQ6ncfPZeRFjozov4dC7FaxU7SuMwzW4VuYBDta");
420
421 Timestamp::set_timestamp(100);
422 assert_ok!(Datalog::record(
423 RuntimeOrigin::signed(sender),
424 record2.clone()
425 ));
426 assert_eq!(
427 Datalog::data(&sender),
428 vec![
429 Item::new(0, record.clone()),
430 Item::new(100, record2.clone()),
431 ]
432 );
433 let record3 = hash2vec("QmWboFP8XeBtFMbNYK3Ne8Z3gKFBSR5iQzkKgeNgQz3dz2");
434
435 Timestamp::set_timestamp(200);
436 assert_ok!(Datalog::record(
437 RuntimeOrigin::signed(sender),
438 record3.clone()
439 ));
440 assert_eq!(
441 Datalog::data(&sender),
442 vec![
443 Item::new(0, record),
444 Item::new(100, record2),
445 Item::new(200, record3),
446 ]
447 );
448 })
449 }
450}