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