pallet_messages/
lib.rs

1//! Stores messages for `IPFS` and `OnChain` Schema payload locations
2//!
3//! ## Quick Links
4//! - [Configuration: `Config`](Config)
5//! - [Extrinsics: `Call`](Call)
6//! - [Runtime API: `MessagesRuntimeApi`](../pallet_messages_runtime_api/trait.MessagesRuntimeApi.html)
7//! - [Custom RPC API: `MessagesApiServer`](../pallet_messages_rpc/trait.MessagesApiServer.html)
8//! - [Event Enum: `Event`](Event)
9//! - [Error Enum: `Error`](Error)
10#![doc = include_str!("../README.md")]
11// Substrate macros are tripping the clippy::expect_used lint.
12#![allow(clippy::expect_used)]
13// Ensure we're `no_std` when compiling for Wasm.
14#![cfg_attr(not(feature = "std"), no_std)]
15// Strong Documentation Lints
16#![deny(
17	rustdoc::broken_intra_doc_links,
18	rustdoc::missing_crate_level_docs,
19	rustdoc::invalid_codeblock_attributes,
20	missing_docs
21)]
22
23#[cfg(feature = "runtime-benchmarks")]
24mod benchmarking;
25#[cfg(test)]
26mod tests;
27
28pub mod weights;
29
30mod types;
31
32use core::{convert::TryInto, fmt::Debug};
33use frame_support::{ensure, pallet_prelude::Weight, traits::Get, BoundedVec};
34use sp_runtime::DispatchError;
35
36extern crate alloc;
37use alloc::vec::Vec;
38use common_primitives::{
39	messages::*,
40	msa::{
41		DelegatorId, MessageSourceId, MsaLookup, MsaValidator, ProviderId, SchemaGrantValidator,
42	},
43	schema::*,
44};
45use frame_support::dispatch::DispatchResult;
46use parity_scale_codec::Encode;
47
48#[cfg(feature = "runtime-benchmarks")]
49use common_primitives::benchmarks::{MsaBenchmarkHelper, SchemaBenchmarkHelper};
50
51pub use pallet::*;
52pub use types::*;
53pub use weights::*;
54
55use cid::Cid;
56use frame_system::pallet_prelude::*;
57
58#[frame_support::pallet]
59pub mod pallet {
60	use super::*;
61	use frame_support::pallet_prelude::*;
62
63	/// The current storage version.
64	pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
65
66	#[pallet::config]
67	pub trait Config: frame_system::Config {
68		/// The overarching event type.
69		#[allow(deprecated)]
70		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
71
72		/// Weight information for extrinsics in this pallet.
73		type WeightInfo: WeightInfo;
74
75		/// A type that will supply MSA related information
76		type MsaInfoProvider: MsaLookup + MsaValidator<AccountId = Self::AccountId>;
77
78		/// A type that will validate schema grants
79		type SchemaGrantValidator: SchemaGrantValidator<BlockNumberFor<Self>>;
80
81		/// A type that will supply schema related information.
82		type SchemaProvider: SchemaProvider<SchemaId>;
83
84		/// The maximum size of a message payload bytes.
85		#[pallet::constant]
86		type MessagesMaxPayloadSizeBytes: Get<u32> + Clone + Debug + MaxEncodedLen;
87
88		#[cfg(feature = "runtime-benchmarks")]
89		/// A set of helper functions for benchmarking.
90		type MsaBenchmarkHelper: MsaBenchmarkHelper<Self::AccountId>;
91
92		#[cfg(feature = "runtime-benchmarks")]
93		/// A set of helper functions for benchmarking.
94		type SchemaBenchmarkHelper: SchemaBenchmarkHelper;
95	}
96
97	#[pallet::pallet]
98	#[pallet::storage_version(STORAGE_VERSION)]
99	pub struct Pallet<T>(_);
100
101	/// A temporary storage for getting the index for messages
102	/// At the start of the next block this storage is set to 0
103	#[pallet::storage]
104	#[pallet::whitelist_storage]
105	pub(super) type BlockMessageIndex<T: Config> = StorageValue<_, MessageIndex, ValueQuery>;
106
107	#[pallet::storage]
108	pub(super) type MessagesV2<T: Config> = StorageNMap<
109		_,
110		(
111			storage::Key<Twox64Concat, BlockNumberFor<T>>,
112			storage::Key<Twox64Concat, SchemaId>,
113			storage::Key<Twox64Concat, MessageIndex>,
114		),
115		Message<T::MessagesMaxPayloadSizeBytes>,
116		OptionQuery,
117	>;
118
119	#[pallet::error]
120	pub enum Error<T> {
121		/// Deprecated: Too many messages are added to existing block
122		TooManyMessagesInBlock,
123
124		/// Message payload size is too large
125		ExceedsMaxMessagePayloadSizeBytes,
126
127		/// Type Conversion Overflow
128		TypeConversionOverflow,
129
130		/// Invalid Message Source Account
131		InvalidMessageSourceAccount,
132
133		/// Invalid SchemaId or Schema not found
134		InvalidSchemaId,
135
136		/// UnAuthorizedDelegate
137		UnAuthorizedDelegate,
138
139		/// Invalid payload location
140		InvalidPayloadLocation,
141
142		/// Unsupported CID version
143		UnsupportedCidVersion,
144
145		/// Invalid CID
146		InvalidCid,
147	}
148
149	#[pallet::event]
150	#[pallet::generate_deposit(pub(super) fn deposit_event)]
151	pub enum Event<T: Config> {
152		/// Deprecated: please use [`Event::MessagesInBlock`]
153		/// Messages are stored for a specified schema id and block number
154		MessagesStored {
155			/// The schema for these messages
156			schema_id: SchemaId,
157			/// The block number for these messages
158			block_number: BlockNumberFor<T>,
159		},
160		/// Messages stored in the current block
161		MessagesInBlock,
162	}
163
164	#[pallet::hooks]
165	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
166		fn on_initialize(_current: BlockNumberFor<T>) -> Weight {
167			<BlockMessageIndex<T>>::set(0u16);
168			// allocates 1 read and 1 write for any access of `MessageIndex` in every block
169			T::DbWeight::get().reads(1u64).saturating_add(T::DbWeight::get().writes(1u64))
170			// TODO: add retention policy execution GitHub Issue: #126 and #25
171		}
172	}
173
174	#[pallet::call]
175	impl<T: Config> Pallet<T> {
176		/// Adds a message for a resource hosted on IPFS. The input consists of
177		/// both a Base32-encoded [CID](https://docs.ipfs.tech/concepts/content-addressing/#version-1-v1)
178		/// as well as a 32-bit content length. The stored payload will contain the
179		/// CID encoded as binary, as well as the 32-bit message content length.
180		/// The actual message content will be on IPFS.
181		///
182		/// # Events
183		/// * [`Event::MessagesInBlock`] - Messages Stored in the block
184		///
185		/// # Errors
186		/// * [`Error::ExceedsMaxMessagePayloadSizeBytes`] - Payload is too large
187		/// * [`Error::InvalidSchemaId`] - Schema not found
188		/// * [`Error::InvalidPayloadLocation`] - The schema is not an IPFS payload location
189		/// * [`Error::InvalidMessageSourceAccount`] - Origin must be from an MSA
190		/// * [`Error::TypeConversionOverflow`] - Failed to add the message to storage as it is very full
191		/// * [`Error::UnsupportedCidVersion`] - CID version is not supported (V0)
192		/// * [`Error::InvalidCid`] - Unable to parse provided CID
193		///
194		#[pallet::call_index(0)]
195		#[pallet::weight(T::WeightInfo::add_ipfs_message())]
196		pub fn add_ipfs_message(
197			origin: OriginFor<T>,
198			#[pallet::compact] schema_id: SchemaId,
199			cid: Vec<u8>,
200			#[pallet::compact] payload_length: u32,
201		) -> DispatchResult {
202			let provider_key = ensure_signed(origin)?;
203			let cid_binary = Self::validate_cid(&cid)?;
204			let payload_tuple: OffchainPayloadType = (cid_binary, payload_length);
205			let bounded_payload: BoundedVec<u8, T::MessagesMaxPayloadSizeBytes> = payload_tuple
206				.encode()
207				.try_into()
208				.map_err(|_| Error::<T>::ExceedsMaxMessagePayloadSizeBytes)?;
209
210			if let Some(schema) = T::SchemaProvider::get_schema_info_by_id(schema_id) {
211				ensure!(
212					schema.payload_location == PayloadLocation::IPFS,
213					Error::<T>::InvalidPayloadLocation
214				);
215
216				let provider_msa_id = Self::find_msa_id(&provider_key)?;
217				let current_block = frame_system::Pallet::<T>::block_number();
218				if Self::add_message(
219					provider_msa_id,
220					None,
221					bounded_payload,
222					schema_id,
223					current_block,
224				)? {
225					Self::deposit_event(Event::MessagesInBlock);
226				}
227				Ok(())
228			} else {
229				Err(Error::<T>::InvalidSchemaId.into())
230			}
231		}
232
233		/// Add an on-chain message for a given schema id.
234		///
235		/// # Events
236		/// * [`Event::MessagesInBlock`] - In the next block
237		///
238		/// # Errors
239		/// * [`Error::ExceedsMaxMessagePayloadSizeBytes`] - Payload is too large
240		/// * [`Error::InvalidSchemaId`] - Schema not found
241		/// * [`Error::InvalidPayloadLocation`] - The schema is not an IPFS payload location
242		/// * [`Error::InvalidMessageSourceAccount`] - Origin must be from an MSA
243		/// * [`Error::UnAuthorizedDelegate`] - Trying to add a message without a proper delegation between the origin and the on_behalf_of MSA
244		/// * [`Error::TypeConversionOverflow`] - Failed to add the message to storage as it is very full
245		///
246		#[pallet::call_index(1)]
247		#[pallet::weight(T::WeightInfo::add_onchain_message(payload.len() as u32))]
248		pub fn add_onchain_message(
249			origin: OriginFor<T>,
250			on_behalf_of: Option<MessageSourceId>,
251			#[pallet::compact] schema_id: SchemaId,
252			payload: Vec<u8>,
253		) -> DispatchResult {
254			let provider_key = ensure_signed(origin)?;
255
256			let bounded_payload: BoundedVec<u8, T::MessagesMaxPayloadSizeBytes> =
257				payload.try_into().map_err(|_| Error::<T>::ExceedsMaxMessagePayloadSizeBytes)?;
258
259			if let Some(schema) = T::SchemaProvider::get_schema_info_by_id(schema_id) {
260				ensure!(
261					schema.payload_location == PayloadLocation::OnChain,
262					Error::<T>::InvalidPayloadLocation
263				);
264
265				let provider_msa_id = Self::find_msa_id(&provider_key)?;
266				let provider_id = ProviderId(provider_msa_id);
267
268				let current_block = frame_system::Pallet::<T>::block_number();
269				// On-chain messages either are sent from the user themselves, or on behalf of another MSA Id
270				let maybe_delegator = match on_behalf_of {
271					Some(delegator_msa_id) => {
272						let delegator_id = DelegatorId(delegator_msa_id);
273						T::SchemaGrantValidator::ensure_valid_schema_grant(
274							provider_id,
275							delegator_id,
276							schema_id,
277							current_block,
278						)
279						.map_err(|_| Error::<T>::UnAuthorizedDelegate)?;
280						delegator_id
281					},
282					None => DelegatorId(provider_msa_id), // Delegate is also the Provider
283				};
284
285				if Self::add_message(
286					provider_msa_id,
287					Some(maybe_delegator.into()),
288					bounded_payload,
289					schema_id,
290					current_block,
291				)? {
292					Self::deposit_event(Event::MessagesInBlock);
293				}
294
295				Ok(())
296			} else {
297				Err(Error::<T>::InvalidSchemaId.into())
298			}
299		}
300	}
301}
302
303impl<T: Config> Pallet<T> {
304	/// Stores a message for a given schema id.
305	/// returns true if it needs to emit an event
306	/// # Errors
307	/// * [`Error::TypeConversionOverflow`]
308	///
309	pub fn add_message(
310		provider_msa_id: MessageSourceId,
311		msa_id: Option<MessageSourceId>,
312		payload: BoundedVec<u8, T::MessagesMaxPayloadSizeBytes>,
313		schema_id: SchemaId,
314		current_block: BlockNumberFor<T>,
315	) -> Result<bool, DispatchError> {
316		let index = BlockMessageIndex::<T>::get();
317		let first = index == 0;
318		let msg = Message {
319			payload, // size is checked on top of extrinsic
320			provider_msa_id,
321			msa_id,
322		};
323
324		<MessagesV2<T>>::insert((current_block, schema_id, index), msg);
325		BlockMessageIndex::<T>::set(index.saturating_add(1));
326		Ok(first)
327	}
328
329	/// Resolve an MSA from an account key(key)
330	/// An MSA Id associated with the account key is returned, if one exists.
331	///
332	/// # Errors
333	/// * [`Error::InvalidMessageSourceAccount`]
334	///
335	pub fn find_msa_id(key: &T::AccountId) -> Result<MessageSourceId, DispatchError> {
336		Ok(T::MsaInfoProvider::ensure_valid_msa_key(key)
337			.map_err(|_| Error::<T>::InvalidMessageSourceAccount)?)
338	}
339
340	/// Gets a messages for a given schema-id and block-number.
341	///
342	/// Payload location is included to map to correct response (To avoid fetching the schema in this method)
343	///
344	/// Result is a vector of [`MessageResponse`].
345	///
346	pub fn get_messages_by_schema_and_block(
347		schema_id: SchemaId,
348		schema_payload_location: PayloadLocation,
349		block_number: BlockNumberFor<T>,
350	) -> Vec<MessageResponse> {
351		let block_number_value: u32 = block_number.try_into().unwrap_or_default();
352
353		match schema_payload_location {
354			PayloadLocation::Itemized | PayloadLocation::Paginated => Vec::new(),
355			_ => {
356				let mut messages: Vec<_> = <MessagesV2<T>>::iter_prefix((block_number, schema_id))
357					.map(|(index, msg)| {
358						msg.map_to_response(block_number_value, schema_payload_location, index)
359					})
360					.collect();
361				messages.sort_by(|a, b| a.index.cmp(&b.index));
362				messages
363			},
364		}
365	}
366
367	/// Validates a CID to conform to IPFS CIDv1 (or higher) formatting (does not validate decoded CID fields)
368	///
369	/// # Errors
370	/// * [`Error::UnsupportedCidVersion`] - CID version is not supported (V0)
371	/// * [`Error::InvalidCid`] - Unable to parse provided CID
372	///
373	pub fn validate_cid(in_cid: &[u8]) -> Result<Vec<u8>, DispatchError> {
374		// Decode SCALE encoded CID into string slice
375		let cid_str: &str = core::str::from_utf8(in_cid).map_err(|_| Error::<T>::InvalidCid)?;
376		ensure!(cid_str.len() > 2, Error::<T>::InvalidCid);
377		// starts_with handles Unicode multibyte characters safely
378		ensure!(!cid_str.starts_with("Qm"), Error::<T>::UnsupportedCidVersion);
379
380		// Assume it's a multibase-encoded string. Decode it to a byte array so we can parse the CID.
381		let cid_b = multibase::decode(cid_str).map_err(|_| Error::<T>::InvalidCid)?.1;
382		ensure!(Cid::read_bytes(&cid_b[..]).is_ok(), Error::<T>::InvalidCid);
383
384		Ok(cid_b)
385	}
386}