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
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    /// The current storage version.
45    const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
46
47    #[pallet::config]
48    // The pallet macro only accepts the full umbrella path for `frame_system::Config`
49    // in the supertrait and in the `RuntimeEvent` bound.
50    pub trait Config: polkadot_sdk::frame_system::Config + TypeInfo {
51        /// Current time source.
52        type Time: Time;
53        /// Datalog record data type.
54        type Record: Parameter + Default + MaxEncodedLen;
55        /// The overarching event type.
56        #[allow(deprecated)]
57        type RuntimeEvent: From<Event<Self>>
58            + IsType<<Self as polkadot_sdk::frame_system::Config>::RuntimeEvent>;
59        /// Data log window size
60        #[pallet::constant]
61        type WindowSize: Get<u64>;
62        /// Extrinsic weights
63        type WeightInfo: WeightInfo;
64    }
65
66    #[pallet::error]
67    pub enum Error<T> {
68        /// Data exceeds size limit
69        RecordTooBig,
70    }
71
72    #[pallet::event]
73    #[pallet::generate_deposit(pub (super) fn deposit_event)]
74    pub enum Event<T: Config> {
75        /// New data added.
76        NewRecord(T::AccountId, <T::Time as Time>::Moment, T::Record),
77        /// Account datalog erased.
78        Erased(T::AccountId),
79    }
80
81    #[pallet::hooks]
82    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
83
84    /// Ringbuffer start/end pointers
85    #[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    /// Ringbuffer items
96    #[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        /// Store new data into blockchain.
113        #[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            // remove previous version from storage
119            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        /// Clear account datalog.
136        #[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            // get the number of items in the ring buffer
145            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        /// Get account datalog as an ordered array
158        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        /// Add value to ring buffer, returning an index for insert slot
223        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        /// Returns the ring buffer item iterator
232        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            // old log should be empty
381            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}