pallet_robonomics_datalog/
lib.rs

1///////////////////////////////////////////////////////////////////////////////
2//
3//  Copyright 2018-2026 Robonomics Network <research@robonomics.network>
4//
5//  Licensed under the Apache License, Version 2.0 (the "License");
6//  you may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at
8//
9//      http://www.apache.org/licenses/LICENSE-2.0
10//
11//  Unless required by applicable law or agreed to in writing, software
12//  distributed under the License is distributed on an "AS IS" BASIS,
13//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14//  See the License for the specific language governing permissions and
15//  limitations under the License.
16//
17///////////////////////////////////////////////////////////////////////////////
18//! Simple Robonomics datalog runtime module. This can be compiled with `#[no_std]`, ready for Wasm.
19#![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    /// The current storage version.
42    const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
43
44    #[pallet::config]
45    pub trait Config: frame_system::Config + TypeInfo {
46        /// Current time source.
47        type Time: Time;
48        /// Datalog record data type.
49        type Record: Parameter + Default + MaxEncodedLen;
50        /// The overarching event type.
51        #[allow(deprecated)]
52        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
53        /// Data log window size
54        #[pallet::constant]
55        type WindowSize: Get<u64>;
56        /// Extrinsic weights
57        type WeightInfo: WeightInfo;
58    }
59
60    #[pallet::error]
61    pub enum Error<T> {
62        /// Data exceeds size limit
63        RecordTooBig,
64    }
65
66    #[pallet::event]
67    #[pallet::generate_deposit(pub (super) fn deposit_event)]
68    pub enum Event<T: Config> {
69        /// New data added.
70        NewRecord(T::AccountId, <T::Time as Time>::Moment, T::Record),
71        /// Account datalog erased.
72        Erased(T::AccountId),
73    }
74
75    #[pallet::hooks]
76    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
77
78    /// Ringbuffer start/end pointers
79    #[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    /// Ringbuffer items
90    #[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        /// Store new data into blockchain.
107        #[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            // remove previous version from storage
113            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        /// Clear account datalog.
130        #[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            // get the number of items in the ring buffer
139            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        /// Get account datalog as an ordered array
152        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        /// Add value to ring buffer, returning an index for insert slot
217        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        /// Returns the ring buffer item iterator
226        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            // old log should be empty
372            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}