1use crate::Config;
3use common_primitives::{
4 node::EIP712Encode,
5 schema::{IntentId, SchemaId},
6 signatures::get_eip712_encoding_prefix,
7 stateful_storage::{PageHash, PageId, PageNonce},
8 utils::to_abi_compatible_number,
9};
10use frame_support::pallet_prelude::*;
11use frame_system::pallet_prelude::*;
12use lazy_static::lazy_static;
13use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
14use scale_info::TypeInfo;
15use sp_core::bounded::BoundedVec;
16extern crate alloc;
17use alloc::{boxed::Box, collections::btree_map::BTreeMap, vec::Vec};
18use core::{
19 cmp::*,
20 fmt::{Debug, Formatter},
21 hash::{Hash, Hasher},
22};
23use frame_support::traits::Len;
24use sp_core::U256;
25use twox_hash::XxHash64;
26
27pub const STATEFUL_STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
29pub const PALLET_STORAGE_PREFIX: &[u8] = b"stateful-storage";
31pub const ITEMIZED_STORAGE_PREFIX: &[u8] = b"itemized";
33pub const PAGINATED_STORAGE_PREFIX: &[u8] = b"paginated";
35
36pub type ItemizedKey = (IntentId,);
38pub type PaginatedKey = (IntentId, PageId);
40pub type PaginatedPrefixKey = (IntentId,);
42pub type ItemizedPage<T> = Page<<T as Config>::MaxItemizedPageSizeBytes>;
44pub type PaginatedPage<T> = Page<<T as Config>::MaxPaginatedPageSizeBytes>;
46
47pub trait ItemizedOperations<T: Config> {
49 fn apply_item_actions(
51 &self,
52 schema_id: SchemaId,
53 actions: &[ItemAction<T::MaxItemizedBlobSizeBytes>],
54 ) -> Result<ItemizedPage<T>, PageError>;
55
56 fn try_parse(&self, include_header: bool) -> Result<ParsedItemPage, PageError>;
58}
59#[derive(
61 Clone, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo, MaxEncodedLen, PartialEq,
62)]
63#[scale_info(skip_type_params(DataSize))]
64#[codec(mel_bound(DataSize: MaxEncodedLen))]
65pub enum ItemAction<DataSize: Get<u32> + Clone + core::fmt::Debug + PartialEq> {
66 Add {
68 data: BoundedVec<u8, DataSize>,
70 },
71 Delete {
73 index: u16,
75 },
76}
77
78#[derive(Encode, Decode, PartialEq, MaxEncodedLen, Debug)]
81pub enum ItemHeader {
82 V1 {
84 payload_len: u16,
86 },
87 V2 {
89 schema_id: SchemaId,
91 payload_len: u16,
93 },
94}
95
96impl ItemHeader {
97 pub fn schema_id(&self) -> Option<SchemaId> {
99 match self {
100 ItemHeader::V1 { .. } => None,
102 ItemHeader::V2 { schema_id, .. } => Some(*schema_id),
103 }
104 }
105
106 pub fn payload_len(&self) -> u16 {
108 match self {
109 ItemHeader::V1 { payload_len } => *payload_len,
111 ItemHeader::V2 { payload_len, .. } => *payload_len,
112 }
113 }
114}
115
116#[derive(Debug, PartialEq)]
118pub enum PageError {
119 ErrorParsing(&'static str),
121 InvalidAction(&'static str),
123 ArithmeticOverflow,
125 PageSizeOverflow,
127 UnsupportedItemType,
129}
130
131#[derive(
135 Encode,
136 Decode,
137 DecodeWithMemTracking,
138 TypeInfo,
139 MaxEncodedLen,
140 PartialEq,
141 RuntimeDebugNoBound,
142 Clone,
143)]
144#[scale_info(skip_type_params(T))]
145pub struct ItemizedSignaturePayloadV2<T: Config> {
146 #[codec(compact)]
148 pub schema_id: SchemaId,
149
150 #[codec(compact)]
152 pub target_hash: PageHash,
153
154 pub expiration: BlockNumberFor<T>,
156
157 pub actions: BoundedVec<
159 ItemAction<<T as Config>::MaxItemizedBlobSizeBytes>,
160 <T as Config>::MaxItemizedActionsCount,
161 >,
162}
163
164impl<T: Config> EIP712Encode for ItemizedSignaturePayloadV2<T> {
165 fn encode_eip_712(&self, chain_id: u32) -> Box<[u8]> {
166 lazy_static! {
167 static ref MAIN_TYPE_HASH: [u8; 32] =
169 sp_io::hashing::keccak_256(b"ItemizedSignaturePayloadV2(uint16 schemaId,uint32 targetHash,uint32 expiration,ItemAction[] actions)ItemAction(string actionType,bytes data,uint16 index)");
170
171 static ref SUB_TYPE_HASH: [u8; 32] =
172 sp_io::hashing::keccak_256(b"ItemAction(string actionType,bytes data,uint16 index)");
173
174 static ref ITEM_ACTION_ADD: [u8; 32] = sp_io::hashing::keccak_256(b"Add");
175 static ref ITEM_ACTION_DELETE: [u8; 32] = sp_io::hashing::keccak_256(b"Delete");
176
177 static ref EMPTY_BYTES_HASH: [u8; 32] = sp_io::hashing::keccak_256([].as_slice());
178 }
179 let prefix_domain_separator: Box<[u8]> =
181 get_eip712_encoding_prefix("0xcccccccccccccccccccccccccccccccccccccccc", chain_id);
182 let coded_schema_id = to_abi_compatible_number(self.schema_id);
183 let coded_target_hash = to_abi_compatible_number(self.target_hash);
184 let expiration: U256 = self.expiration.into();
185 let coded_expiration = to_abi_compatible_number(expiration.as_u128());
186 let coded_actions = {
187 let values: Vec<u8> = self
188 .actions
189 .iter()
190 .flat_map(|a| match a {
191 ItemAction::Add { data } => sp_io::hashing::keccak_256(
192 &[
193 SUB_TYPE_HASH.as_slice(),
194 ITEM_ACTION_ADD.as_slice(),
195 &sp_io::hashing::keccak_256(data.as_slice()),
196 [0u8; 32].as_slice(),
197 ]
198 .concat(),
199 ),
200 ItemAction::Delete { index } => sp_io::hashing::keccak_256(
201 &[
202 SUB_TYPE_HASH.as_slice(),
203 ITEM_ACTION_DELETE.as_slice(),
204 EMPTY_BYTES_HASH.as_slice(),
205 to_abi_compatible_number(*index).as_slice(),
206 ]
207 .concat(),
208 ),
209 })
210 .collect();
211 sp_io::hashing::keccak_256(&values)
212 };
213 let message = sp_io::hashing::keccak_256(
214 &[
215 MAIN_TYPE_HASH.as_slice(),
216 &coded_schema_id,
217 &coded_target_hash,
218 &coded_expiration,
219 &coded_actions,
220 ]
221 .concat(),
222 );
223 let combined = [prefix_domain_separator.as_ref(), &message].concat();
224 combined.into_boxed_slice()
225 }
226}
227
228#[derive(
232 Encode,
233 Decode,
234 DecodeWithMemTracking,
235 TypeInfo,
236 MaxEncodedLen,
237 PartialEq,
238 RuntimeDebugNoBound,
239 Clone,
240)]
241#[scale_info(skip_type_params(T))]
242pub struct PaginatedUpsertSignaturePayloadV2<T: Config> {
243 #[codec(compact)]
245 pub schema_id: SchemaId,
246
247 #[codec(compact)]
249 pub page_id: PageId,
250
251 #[codec(compact)]
253 pub target_hash: PageHash,
254
255 pub expiration: BlockNumberFor<T>,
257
258 pub payload: BoundedVec<u8, <T as Config>::MaxPaginatedPageSizeBytes>,
260}
261
262impl<T: Config> EIP712Encode for PaginatedUpsertSignaturePayloadV2<T> {
263 fn encode_eip_712(&self, chain_id: u32) -> Box<[u8]> {
264 lazy_static! {
265 static ref MAIN_TYPE_HASH: [u8; 32] =
267 sp_io::hashing::keccak_256(b"PaginatedUpsertSignaturePayloadV2(uint16 schemaId,uint16 pageId,uint32 targetHash,uint32 expiration,bytes payload)");
268 }
269 let prefix_domain_separator: Box<[u8]> =
271 get_eip712_encoding_prefix("0xcccccccccccccccccccccccccccccccccccccccc", chain_id);
272 let coded_schema_id = to_abi_compatible_number(self.schema_id);
273 let coded_page_id = to_abi_compatible_number(self.page_id);
274 let coded_target_hash = to_abi_compatible_number(self.target_hash);
275 let expiration: U256 = self.expiration.into();
276 let coded_expiration = to_abi_compatible_number(expiration.as_u128());
277 let coded_payload = sp_io::hashing::keccak_256(self.payload.as_slice());
278 let message = sp_io::hashing::keccak_256(
279 &[
280 MAIN_TYPE_HASH.as_slice(),
281 &coded_schema_id,
282 &coded_page_id,
283 &coded_target_hash,
284 &coded_expiration,
285 &coded_payload,
286 ]
287 .concat(),
288 );
289 let combined = [prefix_domain_separator.as_ref(), &message].concat();
290 combined.into_boxed_slice()
291 }
292}
293
294#[derive(
298 Encode,
299 Decode,
300 DecodeWithMemTracking,
301 TypeInfo,
302 MaxEncodedLen,
303 PartialEq,
304 RuntimeDebugNoBound,
305 Clone,
306)]
307#[scale_info(skip_type_params(T))]
308pub struct PaginatedDeleteSignaturePayloadV2<T: Config> {
309 #[codec(compact)]
311 pub schema_id: SchemaId,
312
313 #[codec(compact)]
315 pub page_id: PageId,
316
317 #[codec(compact)]
319 pub target_hash: PageHash,
320
321 pub expiration: BlockNumberFor<T>,
323}
324
325impl<T: Config> EIP712Encode for PaginatedDeleteSignaturePayloadV2<T> {
326 fn encode_eip_712(&self, chain_id: u32) -> Box<[u8]> {
327 lazy_static! {
328 static ref MAIN_TYPE_HASH: [u8; 32] =
330 sp_io::hashing::keccak_256(b"PaginatedDeleteSignaturePayloadV2(uint16 schemaId,uint16 pageId,uint32 targetHash,uint32 expiration)");
331 }
332 let prefix_domain_separator: Box<[u8]> =
334 get_eip712_encoding_prefix("0xcccccccccccccccccccccccccccccccccccccccc", chain_id);
335 let coded_schema_id = to_abi_compatible_number(self.schema_id);
336 let coded_page_id = to_abi_compatible_number(self.page_id);
337 let coded_target_hash = to_abi_compatible_number(self.target_hash);
338 let expiration: U256 = self.expiration.into();
339 let coded_expiration = to_abi_compatible_number(expiration.as_u128());
340 let message = sp_io::hashing::keccak_256(
341 &[
342 MAIN_TYPE_HASH.as_slice(),
343 &coded_schema_id,
344 &coded_page_id,
345 &coded_target_hash,
346 &coded_expiration,
347 ]
348 .concat(),
349 );
350 let combined = [prefix_domain_separator.as_ref(), &message].concat();
351 combined.into_boxed_slice()
352 }
353}
354
355#[derive(
357 Encode,
358 Decode,
359 DecodeWithMemTracking,
360 TypeInfo,
361 MaxEncodedLen,
362 PartialEq,
363 RuntimeDebugNoBound,
364 Clone,
365)]
366#[scale_info(skip_type_params(T))]
367pub struct PaginatedDeleteSignaturePayloadV3<T: Config> {
368 #[codec(compact)]
370 pub intent_id: IntentId,
371
372 #[codec(compact)]
374 pub page_id: PageId,
375
376 #[codec(compact)]
378 pub target_hash: PageHash,
379
380 pub expiration: BlockNumberFor<T>,
382}
383
384impl<T: Config> EIP712Encode for PaginatedDeleteSignaturePayloadV3<T> {
385 fn encode_eip_712(&self, chain_id: u32) -> Box<[u8]> {
386 lazy_static! {
387 static ref MAIN_TYPE_HASH: [u8; 32] =
389 sp_io::hashing::keccak_256(b"PaginatedDeleteSignaturePayloadV3(uint16 intentId,uint16 pageId,uint32 targetHash,uint32 expiration)");
390 }
391 let prefix_domain_separator: Box<[u8]> =
393 get_eip712_encoding_prefix("0xcccccccccccccccccccccccccccccccccccccccc", chain_id);
394 let coded_intent_id = to_abi_compatible_number(self.intent_id);
395 let coded_page_id = to_abi_compatible_number(self.page_id);
396 let coded_target_hash = to_abi_compatible_number(self.target_hash);
397 let expiration: U256 = self.expiration.into();
398 let coded_expiration = to_abi_compatible_number(expiration.as_u128());
399 let message = sp_io::hashing::keccak_256(
400 &[
401 MAIN_TYPE_HASH.as_slice(),
402 &coded_intent_id,
403 &coded_page_id,
404 &coded_target_hash,
405 &coded_expiration,
406 ]
407 .concat(),
408 );
409 let combined = [prefix_domain_separator.as_ref(), &message].concat();
410 combined.into_boxed_slice()
411 }
412}
413
414#[derive(
416 Encode, Decode, DecodeWithMemTracking, Default, Clone, TypeInfo, MaxEncodedLen, Debug, PartialEq,
417)]
418#[repr(u8)]
419pub enum PageVersion {
420 #[codec(index = 1)]
423 V1,
424
425 #[codec(index = 2)]
427 #[default] V2,
429}
430
431#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Default)]
433#[scale_info(skip_type_params(PageDataSize))]
434#[codec(mel_bound(PageDataSize: MaxEncodedLen))]
435pub struct Page<PageDataSize: Get<u32>> {
436 pub page_version: PageVersion,
440 pub schema_id: Option<SchemaId>,
443 pub nonce: PageNonce,
445 pub data: BoundedVec<u8, PageDataSize>,
449}
450
451impl<PageDataSize: Get<u32>> Debug for Page<PageDataSize> {
452 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
453 write!(
454 f,
455 "Page<Size> {{ page_version: {:?}, schema_id: {:?}, nonce: {}, data: {} bytes }}",
456 self.page_version,
457 self.schema_id,
458 self.nonce,
459 self.data.len()
460 )
461 }
462}
463
464#[derive(Debug, PartialEq)]
466pub struct ParsedItem<'a> {
467 pub header: ItemHeader,
469 pub payload: &'a [u8],
471}
472
473#[derive(Debug, PartialEq)]
475pub struct ParsedItemPage<'a> {
476 pub page_size: usize,
478 pub items: BTreeMap<u16, ParsedItem<'a>>,
480}
481
482impl<PageDataSize: Get<u32>> Page<PageDataSize> {
483 pub fn is_empty(&self) -> bool {
485 self.data.is_empty()
486 }
487
488 pub fn get_hash(&self) -> PageHash {
490 if self.is_empty() {
491 return PageHash::default();
492 }
493 let mut hasher = XxHash64::with_seed(0);
494 self.hash(&mut hasher);
495 let value_bytes: [u8; 4] =
496 hasher.finish().to_be_bytes()[..4].try_into().expect("incorrect hash size");
497 PageHash::from_be_bytes(value_bytes)
498 }
499}
500
501impl<PageDataSize: Get<u32>> Hash for Page<PageDataSize> {
503 fn hash<H: Hasher>(&self, state: &mut H) {
504 state.write(&self.page_version.encode());
505 state.write(&self.schema_id.encode());
506 state.write(&self.nonce.encode());
507 state.write(&self.data[..]);
508 }
509}
510
511impl<PageDataSize: Get<u32>> PartialEq for Page<PageDataSize> {
513 fn eq(&self, other: &Self) -> bool {
514 self.page_version.eq(&other.page_version) &&
515 self.schema_id.eq(&other.schema_id) &&
516 self.nonce.eq(&other.nonce) &&
517 self.data.eq(&other.data)
518 }
519}
520
521impl<PageDataSize: Get<u32>> From<BoundedVec<u8, PageDataSize>> for Page<PageDataSize> {
526 fn from(bounded: BoundedVec<u8, PageDataSize>) -> Self {
527 Self {
528 page_version: PageVersion::default(),
529 schema_id: None,
530 nonce: PageNonce::default(),
531 data: bounded,
532 }
533 }
534}
535
536impl<T: Config> ItemizedOperations<T> for ItemizedPage<T> {
537 fn apply_item_actions(
540 &self,
541 schema_id: SchemaId,
542 actions: &[ItemAction<T::MaxItemizedBlobSizeBytes>],
543 ) -> Result<ItemizedPage<T>, PageError> {
544 let mut parsed = ItemizedOperations::<T>::try_parse(self, true)?;
545
546 let mut updated_page_buffer = Vec::with_capacity(parsed.page_size);
547 let mut add_buffer = Vec::new();
548
549 for action in actions {
550 match action {
551 ItemAction::Delete { index } => {
552 ensure!(
553 parsed.items.contains_key(index),
554 PageError::InvalidAction("item index is invalid")
555 );
556 parsed.items.remove(index);
557 },
558 ItemAction::Add { data } => {
559 let header = ItemHeader::V2 {
560 schema_id,
561 payload_len: data
562 .len()
563 .try_into()
564 .map_err(|_| PageError::InvalidAction("invalid payload size"))?,
565 };
566 add_buffer.extend_from_slice(&header.encode()[..]);
567 add_buffer.extend_from_slice(&data[..]);
568 },
569 }
570 }
571
572 for (_, slice) in parsed.items.iter() {
574 updated_page_buffer.extend_from_slice(slice.payload);
575 }
576 updated_page_buffer.append(&mut add_buffer);
577
578 Ok(ItemizedPage::<T>::from(
579 BoundedVec::try_from(updated_page_buffer).map_err(|_| PageError::PageSizeOverflow)?,
580 ))
581 }
582
583 fn try_parse(&self, include_header: bool) -> Result<ParsedItemPage, PageError> {
586 let mut count = 0u16;
587 let mut items = BTreeMap::new();
588 let mut page_slice = &self.data[..];
589
590 while page_slice.len() > 0 {
591 let item_slice = page_slice;
592 let header = <ItemHeader>::decode(&mut page_slice)
593 .map_err(|_| PageError::ErrorParsing("unable to decode item header"))?;
594 if let ItemHeader::V1 { .. } = header {
595 return Err(PageError::UnsupportedItemType);
596 }
597 ensure!(
598 page_slice.len() >= header.payload_len() as usize,
599 PageError::ErrorParsing("item payload exceeds page data")
600 );
601 let header_len = item_slice.len() - page_slice.len();
602
603 let payload: &[u8];
604 (payload, page_slice) = match include_header {
605 true => item_slice
606 .split_at_checked(header_len + header.payload_len() as usize)
607 .ok_or(PageError::ErrorParsing("item payload exceeds page data"))?,
608 false => page_slice
609 .split_at_checked(header.payload_len() as usize)
610 .ok_or(PageError::ErrorParsing("item payload exceeds page data"))?,
611 };
612
613 items.insert(count, ParsedItem { header, payload });
614 count = count.checked_add(1).ok_or(PageError::ArithmeticOverflow)?;
615 }
616
617 Ok(ParsedItemPage { page_size: self.data.len(), items })
618 }
619}