pallet_robonomics_datalog/
lib.rs

1///////////////////////////////////////////////////////////////////////////////
2//
3//  Copyright 2018-2025 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    #[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        /// Add value to ring buffer, returning an index for insert slot
219        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        /// Returns the ring buffer item iterator
228        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            // old log should be empty
374            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}