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