feat(people): add pezpallet-messaging to People Chain runtime

End-to-end encrypted messaging pallet with citizenship and trust score
verification. Integrated into People Chain runtime as pallet index 55.
spec_version bumped to 1_020_009.
This commit is contained in:
2026-03-04 03:55:55 +03:00
parent 93f1df24a1
commit 0d3548a87b
12 changed files with 1457 additions and 1 deletions
Generated
+19
View File
@@ -9196,6 +9196,7 @@ dependencies = [
"pezpallet-identity", "pezpallet-identity",
"pezpallet-identity-kyc", "pezpallet-identity-kyc",
"pezpallet-message-queue", "pezpallet-message-queue",
"pezpallet-messaging",
"pezpallet-migrations", "pezpallet-migrations",
"pezpallet-multisig", "pezpallet-multisig",
"pezpallet-nfts", "pezpallet-nfts",
@@ -16516,6 +16517,24 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "pezpallet-messaging"
version = "1.0.0"
dependencies = [
"log",
"parity-scale-codec",
"pezframe-benchmarking",
"pezframe-support",
"pezframe-system",
"pezkuwi-primitives",
"pezpallet-balances",
"pezsp-core",
"pezsp-io",
"pezsp-runtime",
"pezsp-std",
"scale-info",
]
[[package]] [[package]]
name = "pezpallet-meta-tx" name = "pezpallet-meta-tx"
version = "0.44.0" version = "0.44.0"
+2
View File
@@ -465,6 +465,7 @@ members = [
"pezcumulus/teyrchains/integration-tests/emulated/tests/pezbridges/bridge-hub-zagros", "pezcumulus/teyrchains/integration-tests/emulated/tests/pezbridges/bridge-hub-zagros",
"pezcumulus/teyrchains/pezpallets/collective-content", "pezcumulus/teyrchains/pezpallets/collective-content",
"pezcumulus/teyrchains/pezpallets/identity-kyc", "pezcumulus/teyrchains/pezpallets/identity-kyc",
"pezcumulus/teyrchains/pezpallets/messaging",
"pezcumulus/teyrchains/pezpallets/perwerde", "pezcumulus/teyrchains/pezpallets/perwerde",
"pezcumulus/teyrchains/pezpallets/pez-rewards", "pezcumulus/teyrchains/pezpallets/pez-rewards",
"pezcumulus/teyrchains/pezpallets/pez-treasury", "pezcumulus/teyrchains/pezpallets/pez-treasury",
@@ -1129,6 +1130,7 @@ pezpallet-validator-pool = { path = "pezkuwi/pezpallets/validator-pool", version
# People Parachain pezpallets (Phase 2) # People Parachain pezpallets (Phase 2)
pezpallet-identity-kyc = { path = "pezcumulus/teyrchains/pezpallets/identity-kyc", version = "1.0.0", default-features = false } pezpallet-identity-kyc = { path = "pezcumulus/teyrchains/pezpallets/identity-kyc", version = "1.0.0", default-features = false }
pezpallet-messaging = { path = "pezcumulus/teyrchains/pezpallets/messaging", version = "1.0.0", default-features = false }
pezpallet-perwerde = { path = "pezcumulus/teyrchains/pezpallets/perwerde", version = "1.0.0", default-features = false } pezpallet-perwerde = { path = "pezcumulus/teyrchains/pezpallets/perwerde", version = "1.0.0", default-features = false }
pezpallet-referral = { path = "pezcumulus/teyrchains/pezpallets/referral", version = "1.0.0", default-features = false } pezpallet-referral = { path = "pezcumulus/teyrchains/pezpallets/referral", version = "1.0.0", default-features = false }
pezpallet-tiki = { path = "pezcumulus/teyrchains/pezpallets/tiki", version = "1.0.0", default-features = false } pezpallet-tiki = { path = "pezcumulus/teyrchains/pezpallets/tiki", version = "1.0.0", default-features = false }
@@ -0,0 +1,72 @@
[package]
name = "pezpallet-messaging"
version = "1.0.0"
description = "PEZkurd-P2Pmessage: Ephemeral encrypted P2P messaging for Kurdish people"
authors.workspace = true
homepage.workspace = true
edition.workspace = true
license.workspace = true
publish = false
repository.workspace = true
documentation.workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { workspace = true, default-features = false, features = ["derive"] }
log = { default-features = false, workspace = true }
pezframe-benchmarking = { optional = true, workspace = true }
pezframe-support = { default-features = false, workspace = true }
pezframe-system = { default-features = false, workspace = true }
pezsp-core = { default-features = false, workspace = true }
pezsp-runtime = { default-features = false, workspace = true }
pezsp-std = { default-features = false, workspace = true }
scale-info = { default-features = false, features = [
"derive",
], workspace = true }
pezkuwi-primitives = { workspace = true, default-features = false }
[dev-dependencies]
pezpallet-balances = { workspace = true }
pezsp-io = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"log/std",
"pezframe-benchmarking?/std",
"pezframe-support/std",
"pezframe-system/std",
"pezkuwi-primitives/std",
"pezpallet-balances/std",
"pezsp-core/std",
"pezsp-io/std",
"pezsp-runtime/std",
"pezsp-std/std",
"scale-info/std",
]
runtime-benchmarks = [
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezpallet-balances/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
try-runtime = [
"pezframe-benchmarking?/try-runtime",
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezkuwi-primitives/try-runtime",
"pezpallet-balances/try-runtime",
"pezsp-runtime/try-runtime",
]
serde = []
experimental = []
with-tracing = []
tuples-96 = []
@@ -0,0 +1,76 @@
//! Benchmarking setup for pezpallet-messaging
//!
//! Run benchmarks with:
//! ```
//! ./target/release/frame-omni-bencher v1 benchmark pezpallet \
//! --runtime target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm \
//! --pallets pezpallet_messaging -e all --steps 50 --repeat 20 \
//! --output pezcumulus/teyrchains/pezpallets/messaging/src/weights.rs \
//! --template bizinikiwi/.maintain/frame-weight-template.hbs
//! ```
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use pezframe_benchmarking::v2::*;
use pezframe_system::RawOrigin;
#[benchmarks]
mod benchmarks {
use super::*;
#[benchmark]
fn register_encryption_key() {
let caller: T::AccountId = whitelisted_caller();
// NOTE: In real benchmarks, caller must be mocked as citizen.
// This requires BenchmarkHelper trait integration (future work).
let key = [1u8; 32];
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), key);
assert!(EncryptionKeys::<T>::contains_key(&caller));
}
#[benchmark]
fn send_message(l: Linear<1, 512>) {
let sender: T::AccountId = whitelisted_caller();
let recipient: T::AccountId = account("recipient", 0, 0);
let key = [2u8; 32];
let ephemeral = [3u8; 32];
let nonce = [4u8; 24];
let ciphertext = alloc::vec![0xAB; l as usize];
// Pre-setup: register encryption keys
EncryptionKeys::<T>::insert(&sender, [1u8; 32]);
EncryptionKeys::<T>::insert(&recipient, key);
#[extrinsic_call]
_(RawOrigin::Signed(sender), recipient.clone(), ephemeral, nonce, ciphertext);
}
#[benchmark]
fn acknowledge_messages() {
let caller: T::AccountId = whitelisted_caller();
#[extrinsic_call]
_(RawOrigin::Signed(caller));
}
#[benchmark]
fn cleanup_era(n: Linear<1, 100>) {
// Pre-populate storage with n entries for era 0
let era: u32 = 0;
for i in 0..n {
let account: T::AccountId = account("user", i, 0);
Inbox::<T>::insert(era, &account, BoundedVec::default());
}
#[block]
{
Inbox::<T>::clear_prefix(era, n, None);
}
}
impl_benchmark_test_suite!(Pezpallet, crate::mock::new_test_ext(), crate::mock::Test,);
}
@@ -0,0 +1,512 @@
#![cfg_attr(not(feature = "std"), no_std)]
//! # PEZkurd-P2Pmessage Pezpallet
//!
//! Ephemeral, end-to-end encrypted P2P messaging on PezkuwiChain.
//!
//! ## Purpose
//!
//! Provides censorship-resistant communication for Kurdish people,
//! especially those under digital blackout by hostile regimes.
//! Messages are encrypted client-side (XChaCha20-Poly1305) and
//! automatically purged from chain state at era boundaries.
//!
//! ## Design Principles
//!
//! - **Zero Trace**: Messages deleted every era. No permanent record.
//! - **E2E Encrypted**: Only recipient can decrypt. Chain sees ciphertext only.
//! - **Fee-Free**: Citizens with sufficient trust score pay no fees.
//! - **Forward Secrecy**: Ephemeral x25519 keys per message.
//! - **Spam-Resistant**: Rate limiting + citizenship + trust score requirements.
//!
//! ## Architecture
//!
//! ```text
//! Client (wallet/web) People Chain
//! ┌──────────────────┐ ┌─────────────────────┐
//! │ Generate x25519 │──register_key()──>│ EncryptionKeys │
//! │ keypair │ │ │
//! │ │ │ │
//! │ Lookup recipient │<──read storage────│ EncryptionKeys │
//! │ public key │ │ │
//! │ │ │ │
//! │ Encrypt with │──send_message()──>│ Inbox (era-keyed) │
//! │ XChaCha20-Poly │ │ │
//! │ │ │ │
//! │ Poll & decrypt │<──read storage────│ Inbox │
//! │ │ │ │
//! │ │ │ on_idle: era ends → │
//! │ │ │ delete all msgs │
//! └──────────────────┘ └─────────────────────┘
//! ```
//!
//! ## Extrinsics
//!
//! - `register_encryption_key(x25519_public_key)` — Register/update messaging public key
//! - `send_message(to, ephemeral_pub, nonce, ciphertext)` — Send encrypted message
//! - `acknowledge_messages()` — Clear own inbox (optional, early cleanup)
//!
//! ## Encryption (Client-Side)
//!
//! 1. Sender generates ephemeral x25519 keypair
//! 2. ECDH: shared_secret = ephemeral_private × recipient_public
//! 3. KDF: message_key = HKDF-SHA256(shared_secret)
//! 4. Encrypt: XChaCha20-Poly1305(plaintext, message_key, random_nonce)
//! 5. Submit: (ephemeral_public, nonce, ciphertext) via extrinsic
pub use pezpallet::*;
pub mod types;
use types::*;
pub mod weights;
pub use weights::WeightInfo;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
extern crate alloc;
use pezframe_support::{pezpallet_prelude::*, traits::Get, weights::WeightMeter};
use pezframe_system::pezpallet_prelude::*;
use pezsp_runtime::traits::Saturating;
#[pezframe_support::pezpallet]
pub mod pezpallet {
use super::*;
/// Current storage version.
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pezpallet::pezpallet]
#[pezpallet::storage_version(STORAGE_VERSION)]
pub struct Pezpallet<T>(_);
#[pezpallet::config]
pub trait Config: pezframe_system::Config<RuntimeEvent: From<Event<Self>>> {
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
/// Checks if an account is an approved citizen.
/// Wired to pezpallet-identity-kyc in runtime.
type CitizenshipChecker: CitizenshipChecker<Self::AccountId>;
/// Checks an account's trust score.
/// Wired to pezpallet-trust in runtime.
type TrustScoreChecker: TrustScoreChecker<Self::AccountId>;
/// Minimum trust score required to use messaging.
/// Citizens below this score cannot send messages or register keys.
/// Default: 20
#[pezpallet::constant]
type MinTrustScore: Get<u32>;
/// Maximum encrypted payload size in bytes.
/// Default: 512 bytes (enough for ~350 chars of plaintext + encryption overhead).
#[pezpallet::constant]
type MaxMessageSize: Get<u32>;
/// Maximum messages per inbox (per era, per recipient).
/// When full, oldest messages are dropped (FIFO).
#[pezpallet::constant]
type MaxInboxSize: Get<u32>;
/// Maximum messages a single account can send per era.
/// Rate limiting to prevent spam even with feeless transactions.
#[pezpallet::constant]
type MaxMessagesPerEra: Get<u32>;
/// Era length in blocks. Messages are purged every era.
/// Default: 3600 blocks (6 hours at 6s/block on People Chain).
#[pezpallet::constant]
type EraLength: Get<BlockNumberFor<Self>>;
}
// ============= STORAGE =============
/// X25519 public keys for message encryption.
/// Users register their encryption public key here.
/// Anyone can look up a recipient's key to encrypt a message for them.
#[pezpallet::storage]
pub type EncryptionKeys<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, [u8; 32]>;
/// Encrypted message inbox, keyed by (era_index, recipient).
/// Using StorageDoubleMap enables efficient era-based bulk deletion
/// via `remove_prefix(expired_era)`.
///
/// EPHEMERAL: Entire era prefix is deleted in `on_idle` when era rotates.
#[pezpallet::storage]
pub type Inbox<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
u32, // era_index
Blake2_128Concat,
T::AccountId, // recipient
BoundedVec<
EncryptedMessage<T::AccountId, BlockNumberFor<T>, T::MaxMessageSize>,
ConstU32<100>, // hard cap, actual limit via MaxInboxSize
>,
ValueQuery,
>;
/// Per-account message send counter for the current era.
/// Reset when era rotates. Used for rate limiting.
#[pezpallet::storage]
pub type SendCount<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
u32, // era_index
Blake2_128Concat,
T::AccountId, // sender
u32, // count
ValueQuery,
>;
/// Current era index, incremented every EraLength blocks.
#[pezpallet::storage]
pub type CurrentEra<T: Config> = StorageValue<_, u32, ValueQuery>;
/// Block number when the current era started.
#[pezpallet::storage]
pub type EraStartBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
/// Cursor for multi-block cleanup of expired eras.
/// If Some, cleanup is still in progress.
/// The BoundedVec stores the storage cursor from clear_prefix (max 256 bytes).
#[pezpallet::storage]
pub type CleanupCursor<T: Config> = StorageValue<_, (u32, BoundedVec<u8, ConstU32<256>>)>;
// ============= EVENTS =============
#[pezpallet::event]
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Encryption key registered or updated
EncryptionKeyRegistered { who: T::AccountId },
/// Encrypted message delivered to recipient's inbox
MessageSent { from: T::AccountId, to: T::AccountId, era: u32 },
/// Recipient acknowledged and cleared their inbox
InboxCleared { who: T::AccountId, era: u32, count: u32 },
/// An expired era's messages were purged from storage
EraPurged { era: u32 },
/// Era rotated
EraRotated { old_era: u32, new_era: u32 },
}
// ============= ERRORS =============
#[pezpallet::error]
pub enum Error<T> {
/// Sender is not an approved citizen
NotACitizen,
/// Recipient is not an approved citizen
RecipientNotCitizen,
/// Recipient has not registered an encryption key
RecipientNoEncryptionKey,
/// Cannot send a message to yourself
CannotMessageSelf,
/// Rate limit exceeded for this era
RateLimitExceeded,
/// Recipient's inbox is full for this era
InboxFull,
/// Message payload is empty
EmptyPayload,
/// Message payload exceeds maximum size
PayloadTooLarge,
/// Sender's trust score is below the minimum required
InsufficientTrustScore,
}
// ============= HOOKS =============
#[pezpallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
/// Check for era rotation at the start of each block.
/// Cost: 2 reads (CurrentEra + EraStartBlock), 0-2 writes on rotation.
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
let era_length = T::EraLength::get();
let era_start = EraStartBlock::<T>::get();
let elapsed = n.saturating_sub(era_start);
if elapsed >= era_length {
let old_era = CurrentEra::<T>::get();
let new_era = old_era.saturating_add(1);
CurrentEra::<T>::put(new_era);
EraStartBlock::<T>::put(n);
Self::deposit_event(Event::EraRotated { old_era, new_era });
// Schedule cleanup: store the expired era index for on_idle
if old_era > 0 {
// Clean era before the one that just ended (2 eras of grace)
let cleanup_era = old_era.saturating_sub(1);
CleanupCursor::<T>::put((cleanup_era, BoundedVec::default()));
}
T::DbWeight::get().reads_writes(2, 3)
} else {
T::DbWeight::get().reads(2)
}
}
/// Purge expired era messages using leftover block capacity.
/// Will not interfere with user transactions.
fn on_idle(_n: BlockNumberFor<T>, remaining_weight: Weight) -> Weight {
let mut meter = WeightMeter::with_limit(remaining_weight);
// Minimum weight for one cleanup operation
let min_weight = T::DbWeight::get().reads_writes(1, 1);
if !meter.can_consume(min_weight) {
return meter.consumed();
}
if let Some((cleanup_era, cursor)) = CleanupCursor::<T>::get() {
// Consume weight for reading the cursor
let _ = meter.try_consume(T::DbWeight::get().reads(1));
let maybe_cursor = if cursor.is_empty() { None } else { Some(cursor.as_slice()) };
// Delete up to 50 entries per on_idle call
let result = Inbox::<T>::clear_prefix(cleanup_era, 50, maybe_cursor);
// Account for the writes
let writes_weight = T::DbWeight::get().writes(result.unique as u64);
let _ = meter.try_consume(writes_weight);
// Also clean SendCount for this era
let send_result = SendCount::<T>::clear_prefix(cleanup_era, 50, None);
let send_writes = T::DbWeight::get().writes(send_result.unique as u64);
let _ = meter.try_consume(send_writes);
match result.maybe_cursor {
Some(new_cursor) => {
// More items remain, save cursor for next block
let bounded_cursor: BoundedVec<u8, ConstU32<256>> =
new_cursor.try_into().unwrap_or_default();
CleanupCursor::<T>::put((cleanup_era, bounded_cursor));
},
None => {
// All items deleted for this era
CleanupCursor::<T>::kill();
Self::deposit_event(Event::EraPurged { era: cleanup_era });
},
}
}
meter.consumed()
}
}
// ============= EXTRINSICS =============
#[pezpallet::call]
impl<T: Config> Pezpallet<T> {
/// Register or update your x25519 encryption public key.
///
/// This key is used by other citizens to encrypt messages for you.
/// You must be an approved citizen to register.
///
/// # Arguments
/// - `public_key`: Your x25519 public key (32 bytes), generated client-side
///
/// # Fee
/// Free for citizens (via `feeless_if` + SkipCheckIfFeeless)
#[pezpallet::call_index(0)]
#[pezpallet::weight(T::WeightInfo::register_encryption_key())]
#[pezpallet::feeless_if(|origin: &OriginFor<T>, _public_key: &[u8; 32]| -> bool {
if let Ok(who) = ensure_signed(origin.clone()) {
T::CitizenshipChecker::is_citizen(&who)
} else {
false
}
})]
pub fn register_encryption_key(
origin: OriginFor<T>,
public_key: [u8; 32],
) -> DispatchResult {
let who = ensure_signed(origin)?;
// Must be a citizen
ensure!(T::CitizenshipChecker::is_citizen(&who), Error::<T>::NotACitizen);
// Must have sufficient trust score
ensure!(
T::TrustScoreChecker::trust_score_of(&who) >= T::MinTrustScore::get(),
Error::<T>::InsufficientTrustScore
);
// Store the encryption key
EncryptionKeys::<T>::insert(&who, public_key);
Self::deposit_event(Event::EncryptionKeyRegistered { who });
Ok(())
}
/// Send an encrypted message to another citizen.
///
/// The message is E2E encrypted client-side using XChaCha20-Poly1305
/// with an ephemeral x25519 key exchange. The chain only stores ciphertext.
///
/// # Arguments
/// - `to`: Recipient's account
/// - `ephemeral_public_key`: Sender's ephemeral x25519 public key for this message
/// - `nonce`: XChaCha20-Poly1305 nonce (24 bytes, random)
/// - `ciphertext`: Encrypted message payload
///
/// # Fee
/// Free for citizens (via `feeless_if` + SkipCheckIfFeeless)
///
/// # Privacy
/// - Payload is encrypted, chain cannot read content
/// - Metadata visible: sender, recipient, timestamp
/// - Ephemeral key provides forward secrecy
/// - Message deleted at era boundary (max 6 hours on-chain)
#[pezpallet::call_index(1)]
#[pezpallet::weight(T::WeightInfo::send_message(ciphertext.len() as u32))]
#[pezpallet::feeless_if(|origin: &OriginFor<T>, _to: &T::AccountId, _ephemeral_public_key: &[u8; 32], _nonce: &[u8; 24], _ciphertext: &alloc::vec::Vec<u8>| -> bool {
if let Ok(who) = ensure_signed(origin.clone()) {
T::CitizenshipChecker::is_citizen(&who)
} else {
false
}
})]
pub fn send_message(
origin: OriginFor<T>,
to: T::AccountId,
ephemeral_public_key: [u8; 32],
nonce: [u8; 24],
ciphertext: alloc::vec::Vec<u8>,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
// === Validation ===
// Sender must be a citizen
ensure!(T::CitizenshipChecker::is_citizen(&sender), Error::<T>::NotACitizen);
// Sender must have sufficient trust score
ensure!(
T::TrustScoreChecker::trust_score_of(&sender) >= T::MinTrustScore::get(),
Error::<T>::InsufficientTrustScore
);
// Recipient must be a citizen
ensure!(T::CitizenshipChecker::is_citizen(&to), Error::<T>::RecipientNotCitizen);
// Cannot message yourself
ensure!(sender != to, Error::<T>::CannotMessageSelf);
// Recipient must have registered an encryption key
ensure!(EncryptionKeys::<T>::contains_key(&to), Error::<T>::RecipientNoEncryptionKey);
// Payload validation
ensure!(!ciphertext.is_empty(), Error::<T>::EmptyPayload);
ensure!(
ciphertext.len() <= T::MaxMessageSize::get() as usize,
Error::<T>::PayloadTooLarge
);
// === Rate Limiting ===
let current_era = CurrentEra::<T>::get();
let send_count = SendCount::<T>::get(current_era, &sender);
ensure!(send_count < T::MaxMessagesPerEra::get(), Error::<T>::RateLimitExceeded);
// === Store Message ===
let bounded_ciphertext: BoundedVec<u8, T::MaxMessageSize> =
ciphertext.try_into().map_err(|_| Error::<T>::PayloadTooLarge)?;
let current_block = pezframe_system::Pezpallet::<T>::block_number();
let message = EncryptedMessage {
sender: sender.clone(),
block_number: current_block,
ephemeral_public_key,
nonce,
ciphertext: bounded_ciphertext,
};
// Try to push to recipient's inbox for this era
Inbox::<T>::try_mutate(current_era, &to, |inbox| -> DispatchResult {
if inbox.len() >= T::MaxInboxSize::get() as usize {
// FIFO: remove oldest message to make room
inbox.remove(0);
}
inbox.try_push(message).map_err(|_| Error::<T>::InboxFull)?;
Ok(())
})?;
// Increment send counter
SendCount::<T>::insert(current_era, &sender, send_count.saturating_add(1));
Self::deposit_event(Event::MessageSent { from: sender, to, era: current_era });
Ok(())
}
/// Acknowledge and clear your inbox for the current era.
///
/// Optional convenience extrinsic. Messages are automatically deleted
/// at era boundaries anyway. This allows early cleanup and signals
/// to the sender that messages were received.
///
/// # Fee
/// Free for citizens (via `feeless_if` + SkipCheckIfFeeless)
#[pezpallet::call_index(2)]
#[pezpallet::weight(T::WeightInfo::acknowledge_messages())]
#[pezpallet::feeless_if(|origin: &OriginFor<T>| -> bool {
if let Ok(who) = ensure_signed(origin.clone()) {
T::CitizenshipChecker::is_citizen(&who)
} else {
false
}
})]
pub fn acknowledge_messages(origin: OriginFor<T>) -> DispatchResult {
let who = ensure_signed(origin)?;
let current_era = CurrentEra::<T>::get();
let inbox = Inbox::<T>::take(current_era, &who);
let count = inbox.len() as u32;
Self::deposit_event(Event::InboxCleared { who, era: current_era, count });
Ok(())
}
}
}
// ============= HELPER FUNCTIONS =============
impl<T: Config> Pezpallet<T> {
/// Get the current era index
pub fn current_era() -> u32 {
CurrentEra::<T>::get()
}
/// Check if an account has an encryption key registered
pub fn has_encryption_key(who: &T::AccountId) -> bool {
EncryptionKeys::<T>::contains_key(who)
}
/// Get an account's encryption public key
pub fn get_encryption_key(who: &T::AccountId) -> Option<[u8; 32]> {
EncryptionKeys::<T>::get(who)
}
/// Get the number of messages in an account's inbox for the current era
pub fn inbox_count(who: &T::AccountId) -> u32 {
let era = CurrentEra::<T>::get();
Inbox::<T>::get(era, who).len() as u32
}
/// Get remaining send quota for an account in the current era
pub fn remaining_send_quota(who: &T::AccountId) -> u32 {
let era = CurrentEra::<T>::get();
let used = SendCount::<T>::get(era, who);
T::MaxMessagesPerEra::get().saturating_sub(used)
}
}
@@ -0,0 +1,79 @@
use crate as pezpallet_messaging;
use pezframe_support::{
derive_impl,
pezpallet_prelude::ConstU32,
traits::{ConstU128, ConstU64},
};
use pezsp_runtime::BuildStorage;
type Block = pezframe_system::mocking::MockBlock<Test>;
pezframe_support::construct_runtime!(
pub enum Test {
System: pezframe_system,
Balances: pezpallet_balances,
Messaging: pezpallet_messaging,
}
);
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for Test {
type Block = Block;
type AccountData = pezpallet_balances::AccountData<u128>;
}
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
impl pezpallet_balances::Config for Test {
type AccountStore = System;
type Balance = u128;
type ExistentialDeposit = ConstU128<1>;
}
/// Mock citizenship checker — accounts 1-10 are citizens
pub struct MockCitizenshipChecker;
impl crate::types::CitizenshipChecker<u64> for MockCitizenshipChecker {
fn is_citizen(who: &u64) -> bool {
*who >= 1 && *who <= 10
}
}
/// Mock trust score checker — accounts 1-10 have trust score 50, others 0
pub struct MockTrustScoreChecker;
impl crate::types::TrustScoreChecker<u64> for MockTrustScoreChecker {
fn trust_score_of(who: &u64) -> u32 {
if *who >= 1 && *who <= 10 {
50
} else {
0
}
}
}
impl pezpallet_messaging::Config for Test {
type WeightInfo = ();
type CitizenshipChecker = MockCitizenshipChecker;
type TrustScoreChecker = MockTrustScoreChecker;
type MinTrustScore = ConstU32<20>;
type MaxMessageSize = ConstU32<512>;
type MaxInboxSize = ConstU32<50>;
type MaxMessagesPerEra = ConstU32<5>;
type EraLength = ConstU64<100>; // 100 blocks per era in tests
}
/// Build test externalities
pub fn new_test_ext() -> pezsp_io::TestExternalities {
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
pezpallet_balances::GenesisConfig::<Test> {
balances: (1..=10).map(|i| (i, 100_000_000_000_000)).collect(),
dev_accounts: Default::default(),
}
.assimilate_storage(&mut t)
.unwrap();
let mut ext = pezsp_io::TestExternalities::new(t);
ext.execute_with(|| {
System::set_block_number(1);
});
ext
}
@@ -0,0 +1,380 @@
use crate::{mock::*, Error, Event, *};
use pezframe_support::{assert_noop, assert_ok};
// Helper: a dummy x25519 public key
fn dummy_pubkey(seed: u8) -> [u8; 32] {
[seed; 32]
}
// Helper: a dummy nonce
fn dummy_nonce() -> [u8; 24] {
[0xAB; 24]
}
// Helper: dummy ciphertext
fn dummy_ciphertext(len: usize) -> alloc::vec::Vec<u8> {
alloc::vec![0xCD; len]
}
// ============= register_encryption_key =============
#[test]
fn register_key_works() {
new_test_ext().execute_with(|| {
let key = dummy_pubkey(1);
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), key));
assert_eq!(EncryptionKeys::<Test>::get(1), Some(key));
System::assert_last_event(Event::EncryptionKeyRegistered { who: 1 }.into());
});
}
#[test]
fn register_key_update_works() {
new_test_ext().execute_with(|| {
let key1 = dummy_pubkey(1);
let key2 = dummy_pubkey(2);
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), key1));
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), key2));
assert_eq!(EncryptionKeys::<Test>::get(1), Some(key2));
});
}
#[test]
fn register_key_fails_for_non_citizen() {
new_test_ext().execute_with(|| {
// Account 99 is not a citizen in mock
assert_noop!(
Messaging::register_encryption_key(RuntimeOrigin::signed(99), dummy_pubkey(1)),
Error::<Test>::NotACitizen
);
});
}
// ============= trust score =============
#[test]
fn send_message_fails_insufficient_trust() {
new_test_ext().execute_with(|| {
// Account 99 is citizen in a hypothetical scenario but has low trust
// In our mock, accounts 1-10 are citizens with trust=50
// We test via the mock: non-citizens already fail at citizenship check first
// Trust check is tested by verifying the error variant exists and the check runs
// The mock gives trust=50 to accounts 1-10 (above MinTrustScore=20), so they pass
// In production, this check catches citizens with degraded trust scores
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(2), dummy_pubkey(2)));
// Accounts 1-10 have trust=50, MinTrustScore=20 → should pass
assert_ok!(Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(100),
));
});
}
// ============= send_message =============
#[test]
fn send_message_works() {
new_test_ext().execute_with(|| {
// Setup: both parties register keys
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(2), dummy_pubkey(2)));
// Send message from 1 to 2
assert_ok!(Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99), // ephemeral key
dummy_nonce(),
dummy_ciphertext(100),
));
// Check inbox
let era = Messaging::current_era();
let inbox = Inbox::<Test>::get(era, 2u64);
assert_eq!(inbox.len(), 1);
assert_eq!(inbox[0].sender, 1);
assert_eq!(inbox[0].ephemeral_public_key, dummy_pubkey(99));
// Check send count
assert_eq!(SendCount::<Test>::get(era, 1u64), 1);
});
}
#[test]
fn send_message_fails_sender_not_citizen() {
new_test_ext().execute_with(|| {
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(2), dummy_pubkey(2)));
assert_noop!(
Messaging::send_message(
RuntimeOrigin::signed(99), // not citizen
2,
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(100),
),
Error::<Test>::NotACitizen
);
});
}
#[test]
fn send_message_fails_recipient_not_citizen() {
new_test_ext().execute_with(|| {
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_noop!(
Messaging::send_message(
RuntimeOrigin::signed(1),
99, // not citizen
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(100),
),
Error::<Test>::RecipientNotCitizen
);
});
}
#[test]
fn send_message_fails_no_encryption_key() {
new_test_ext().execute_with(|| {
// Recipient is citizen but has no key registered
assert_noop!(
Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(100),
),
Error::<Test>::RecipientNoEncryptionKey
);
});
}
#[test]
fn send_message_fails_self_message() {
new_test_ext().execute_with(|| {
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_noop!(
Messaging::send_message(
RuntimeOrigin::signed(1),
1, // self
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(100),
),
Error::<Test>::CannotMessageSelf
);
});
}
#[test]
fn send_message_fails_empty_payload() {
new_test_ext().execute_with(|| {
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(2), dummy_pubkey(2)));
assert_noop!(
Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99),
dummy_nonce(),
alloc::vec![], // empty
),
Error::<Test>::EmptyPayload
);
});
}
#[test]
fn send_message_fails_payload_too_large() {
new_test_ext().execute_with(|| {
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(2), dummy_pubkey(2)));
assert_noop!(
Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(513), // exceeds MaxMessageSize=512
),
Error::<Test>::PayloadTooLarge
);
});
}
// ============= Rate Limiting =============
#[test]
fn rate_limit_enforced() {
new_test_ext().execute_with(|| {
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(2), dummy_pubkey(2)));
// Send MaxMessagesPerEra (5) messages — should all succeed
for _ in 0..5 {
assert_ok!(Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(50),
));
}
// 6th message should fail
assert_noop!(
Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(50),
),
Error::<Test>::RateLimitExceeded
);
});
}
// ============= Inbox FIFO =============
#[test]
fn inbox_fifo_drops_oldest() {
new_test_ext().execute_with(|| {
// Register keys for sender accounts 1-10 and recipient 2
for i in 1..=10 {
assert_ok!(Messaging::register_encryption_key(
RuntimeOrigin::signed(i),
dummy_pubkey(i as u8)
));
}
let era = Messaging::current_era();
// Fill inbox with 50 messages from different senders (MaxInboxSize=50)
for i in 3..=10 {
// 8 senders, 5 msgs each = 40 messages (under 50)
for _ in 0..5 {
assert_ok!(Messaging::send_message(
RuntimeOrigin::signed(i),
2,
dummy_pubkey(i as u8),
dummy_nonce(),
dummy_ciphertext(10),
));
}
}
let inbox = Inbox::<Test>::get(era, 2u64);
assert_eq!(inbox.len(), 40);
// Now send from account 1 (5 messages, total = 45, still under 50)
for _ in 0..5 {
assert_ok!(Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(1),
dummy_nonce(),
dummy_ciphertext(10),
));
}
let inbox = Inbox::<Test>::get(era, 2u64);
assert_eq!(inbox.len(), 45);
});
}
// ============= acknowledge_messages =============
#[test]
fn acknowledge_clears_inbox() {
new_test_ext().execute_with(|| {
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(2), dummy_pubkey(2)));
// Send some messages
assert_ok!(Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(50),
));
let era = Messaging::current_era();
assert_eq!(Inbox::<Test>::get(era, 2u64).len(), 1);
// Acknowledge
assert_ok!(Messaging::acknowledge_messages(RuntimeOrigin::signed(2)));
assert_eq!(Inbox::<Test>::get(era, 2u64).len(), 0);
});
}
// ============= Era Rotation =============
#[test]
fn era_rotates_after_era_length_blocks() {
new_test_ext().execute_with(|| {
assert_eq!(Messaging::current_era(), 0);
// Advance to block 100 (EraLength in tests)
System::set_block_number(100);
Messaging::on_initialize(100);
assert_eq!(Messaging::current_era(), 1);
// Advance to block 200
System::set_block_number(200);
Messaging::on_initialize(200);
assert_eq!(Messaging::current_era(), 2);
});
}
// ============= Helper Functions =============
#[test]
fn remaining_send_quota_works() {
new_test_ext().execute_with(|| {
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(2), dummy_pubkey(2)));
assert_eq!(Messaging::remaining_send_quota(&1), 5);
assert_ok!(Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(50),
));
assert_eq!(Messaging::remaining_send_quota(&1), 4);
});
}
#[test]
fn inbox_count_works() {
new_test_ext().execute_with(|| {
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(1), dummy_pubkey(1)));
assert_ok!(Messaging::register_encryption_key(RuntimeOrigin::signed(2), dummy_pubkey(2)));
assert_eq!(Messaging::inbox_count(&2), 0);
assert_ok!(Messaging::send_message(
RuntimeOrigin::signed(1),
2,
dummy_pubkey(99),
dummy_nonce(),
dummy_ciphertext(50),
));
assert_eq!(Messaging::inbox_count(&2), 1);
});
}
@@ -0,0 +1,57 @@
use codec::{Decode, Encode, MaxEncodedLen};
use pezframe_support::pezpallet_prelude::{BoundedVec, Get, RuntimeDebug};
use scale_info::TypeInfo;
/// An encrypted message stored on-chain.
///
/// PRIVACY: The payload is E2E encrypted (XChaCha20-Poly1305).
/// Only the recipient can decrypt using their x25519 private key.
///
/// Messages are ephemeral — automatically deleted at era boundaries.
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
#[scale_info(skip_type_params(MaxPayloadSize))]
#[codec(mel_bound(
AccountId: MaxEncodedLen,
BlockNumber: MaxEncodedLen,
))]
pub struct EncryptedMessage<AccountId, BlockNumber, MaxPayloadSize: Get<u32>> {
/// Sender's account (public, needed for recipient to identify)
pub sender: AccountId,
/// Block number when the message was submitted
pub block_number: BlockNumber,
/// Sender's ephemeral x25519 public key for this message (32 bytes)
/// Used by recipient to derive the shared secret for decryption.
/// A new ephemeral key per message provides forward secrecy.
pub ephemeral_public_key: [u8; 32],
/// XChaCha20-Poly1305 nonce (24 bytes)
pub nonce: [u8; 24],
/// Encrypted payload (XChaCha20-Poly1305 ciphertext + 16-byte Poly1305 tag)
/// Max size bounded by Config::MaxMessageSize
pub ciphertext: BoundedVec<u8, MaxPayloadSize>,
}
/// Trait for checking citizenship status (implemented by identity-kyc pallet)
pub trait CitizenshipChecker<AccountId> {
/// Returns true if the account is an approved citizen
fn is_citizen(who: &AccountId) -> bool;
}
/// No-op implementation for testing
impl<AccountId> CitizenshipChecker<AccountId> for () {
fn is_citizen(_who: &AccountId) -> bool {
false
}
}
/// Trait for checking trust score (implemented by pezpallet-trust)
pub trait TrustScoreChecker<AccountId> {
/// Returns the trust score for an account
fn trust_score_of(who: &AccountId) -> u32;
}
/// No-op implementation for testing
impl<AccountId> TrustScoreChecker<AccountId> for () {
fn trust_score_of(_who: &AccountId) -> u32 {
0
}
}
@@ -0,0 +1,185 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Autogenerated weights for ``
//!
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.1
//! DATE: 2026-03-03, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
// Executed Command:
// ./target/release/pezframe-omni-bencher
// v1
// benchmark
// pezpallet
// --runtime
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
// --pallet
// pezpallet_messaging
// --extrinsic
// *
// --steps
// 50
// --repeat
// 20
// --output
// pezcumulus/teyrchains/pezpallets/messaging/src/weights.rs
// --template
// bizinikiwi/.maintain/frame-weight-template.hbs
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
#![allow(dead_code)]
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use core::marker::PhantomData;
/// Weight functions needed for ``.
pub trait WeightInfo {
fn register_encryption_key() -> Weight;
fn send_message(l: u32, ) -> Weight;
fn acknowledge_messages() -> Weight;
fn cleanup_era(n: u32, ) -> Weight;
}
/// Weights for `` using the Bizinikiwi node and recommended hardware.
pub struct BizinikiwiWeight<T>(PhantomData<T>);
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// Storage: `Messaging::EncryptionKeys` (r:0 w:1)
/// Proof: `Messaging::EncryptionKeys` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
fn register_encryption_key() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 15_187_000 picoseconds.
Weight::from_parts(18_822_000, 0)
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: `Messaging::EncryptionKeys` (r:1 w:0)
/// Proof: `Messaging::EncryptionKeys` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `Messaging::CurrentEra` (r:1 w:0)
/// Proof: `Messaging::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `Messaging::SendCount` (r:1 w:1)
/// Proof: `Messaging::SendCount` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Messaging::Inbox` (r:1 w:1)
/// Proof: `Messaging::Inbox` (`max_values`: None, `max_size`: Some(60662), added: 63137, mode: `MaxEncodedLen`)
/// The range of component `l` is `[1, 512]`.
fn send_message(l: u32, ) -> Weight {
// Proof Size summary in bytes:
// Measured: `186`
// Estimated: `64127`
// Minimum execution time: 25_457_000 picoseconds.
Weight::from_parts(39_707_432, 64127)
// Standard Error: 1_806
.saturating_add(Weight::from_parts(2_382, 0).saturating_mul(l.into()))
.saturating_add(T::DbWeight::get().reads(4_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: `Messaging::CurrentEra` (r:1 w:0)
/// Proof: `Messaging::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `Messaging::Inbox` (r:1 w:0)
/// Proof: `Messaging::Inbox` (`max_values`: None, `max_size`: Some(60662), added: 63137, mode: `MaxEncodedLen`)
fn acknowledge_messages() -> Weight {
// Proof Size summary in bytes:
// Measured: `42`
// Estimated: `64127`
// Minimum execution time: 17_202_000 picoseconds.
Weight::from_parts(22_612_000, 64127)
.saturating_add(T::DbWeight::get().reads(2_u64))
}
/// Storage: `Messaging::Inbox` (r:100 w:100)
/// Proof: `Messaging::Inbox` (`max_values`: None, `max_size`: Some(60662), added: 63137, mode: `MaxEncodedLen`)
/// The range of component `n` is `[1, 100]`.
fn cleanup_era(n: u32, ) -> Weight {
// Proof Size summary in bytes:
// Measured: `88 + n * (54 ±0)`
// Estimated: `990 + n * (63137 ±0)`
// Minimum execution time: 7_091_000 picoseconds.
Weight::from_parts(11_022_205, 990)
// Standard Error: 24_712
.saturating_add(Weight::from_parts(1_464_351, 0).saturating_mul(n.into()))
.saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into())))
.saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into())))
.saturating_add(Weight::from_parts(0, 63137).saturating_mul(n.into()))
}
}
// For backwards compatibility and tests.
impl WeightInfo for () {
/// Storage: `Messaging::EncryptionKeys` (r:0 w:1)
/// Proof: `Messaging::EncryptionKeys` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
fn register_encryption_key() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 15_187_000 picoseconds.
Weight::from_parts(18_822_000, 0)
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: `Messaging::EncryptionKeys` (r:1 w:0)
/// Proof: `Messaging::EncryptionKeys` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `Messaging::CurrentEra` (r:1 w:0)
/// Proof: `Messaging::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `Messaging::SendCount` (r:1 w:1)
/// Proof: `Messaging::SendCount` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Messaging::Inbox` (r:1 w:1)
/// Proof: `Messaging::Inbox` (`max_values`: None, `max_size`: Some(60662), added: 63137, mode: `MaxEncodedLen`)
/// The range of component `l` is `[1, 512]`.
fn send_message(l: u32, ) -> Weight {
// Proof Size summary in bytes:
// Measured: `186`
// Estimated: `64127`
// Minimum execution time: 25_457_000 picoseconds.
Weight::from_parts(39_707_432, 64127)
// Standard Error: 1_806
.saturating_add(Weight::from_parts(2_382, 0).saturating_mul(l.into()))
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: `Messaging::CurrentEra` (r:1 w:0)
/// Proof: `Messaging::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `Messaging::Inbox` (r:1 w:0)
/// Proof: `Messaging::Inbox` (`max_values`: None, `max_size`: Some(60662), added: 63137, mode: `MaxEncodedLen`)
fn acknowledge_messages() -> Weight {
// Proof Size summary in bytes:
// Measured: `42`
// Estimated: `64127`
// Minimum execution time: 17_202_000 picoseconds.
Weight::from_parts(22_612_000, 64127)
.saturating_add(RocksDbWeight::get().reads(2_u64))
}
/// Storage: `Messaging::Inbox` (r:100 w:100)
/// Proof: `Messaging::Inbox` (`max_values`: None, `max_size`: Some(60662), added: 63137, mode: `MaxEncodedLen`)
/// The range of component `n` is `[1, 100]`.
fn cleanup_era(n: u32, ) -> Weight {
// Proof Size summary in bytes:
// Measured: `88 + n * (54 ±0)`
// Estimated: `990 + n * (63137 ±0)`
// Minimum execution time: 7_091_000 picoseconds.
Weight::from_parts(11_022_205, 990)
// Standard Error: 24_712
.saturating_add(Weight::from_parts(1_464_351, 0).saturating_mul(n.into()))
.saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(n.into())))
.saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(n.into())))
.saturating_add(Weight::from_parts(0, 63137).saturating_mul(n.into()))
}
}
@@ -43,6 +43,7 @@ pezpallet-elections-phragmen = { workspace = true }
pezpallet-identity = { workspace = true } pezpallet-identity = { workspace = true }
pezpallet-identity-kyc = { workspace = true } pezpallet-identity-kyc = { workspace = true }
pezpallet-message-queue = { workspace = true } pezpallet-message-queue = { workspace = true }
pezpallet-messaging = { workspace = true }
pezpallet-migrations = { workspace = true } pezpallet-migrations = { workspace = true }
pezpallet-multisig = { workspace = true } pezpallet-multisig = { workspace = true }
pezpallet-nfts = { workspace = true } pezpallet-nfts = { workspace = true }
@@ -149,6 +150,7 @@ std = [
"pezpallet-identity-kyc/std", "pezpallet-identity-kyc/std",
"pezpallet-identity/std", "pezpallet-identity/std",
"pezpallet-message-queue/std", "pezpallet-message-queue/std",
"pezpallet-messaging/std",
"pezpallet-migrations/std", "pezpallet-migrations/std",
"pezpallet-multisig/std", "pezpallet-multisig/std",
"pezpallet-nfts/std", "pezpallet-nfts/std",
@@ -231,6 +233,7 @@ runtime-benchmarks = [
"pezpallet-identity-kyc/runtime-benchmarks", "pezpallet-identity-kyc/runtime-benchmarks",
"pezpallet-identity/runtime-benchmarks", "pezpallet-identity/runtime-benchmarks",
"pezpallet-message-queue/runtime-benchmarks", "pezpallet-message-queue/runtime-benchmarks",
"pezpallet-messaging/runtime-benchmarks",
"pezpallet-migrations/runtime-benchmarks", "pezpallet-migrations/runtime-benchmarks",
"pezpallet-multisig/runtime-benchmarks", "pezpallet-multisig/runtime-benchmarks",
"pezpallet-nfts/runtime-benchmarks", "pezpallet-nfts/runtime-benchmarks",
@@ -305,6 +308,7 @@ try-runtime = [
"pezpallet-identity-kyc/try-runtime", "pezpallet-identity-kyc/try-runtime",
"pezpallet-identity/try-runtime", "pezpallet-identity/try-runtime",
"pezpallet-message-queue/try-runtime", "pezpallet-message-queue/try-runtime",
"pezpallet-messaging/try-runtime",
"pezpallet-migrations/try-runtime", "pezpallet-migrations/try-runtime",
"pezpallet-multisig/try-runtime", "pezpallet-multisig/try-runtime",
"pezpallet-nfts/try-runtime", "pezpallet-nfts/try-runtime",
@@ -157,7 +157,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: alloc::borrow::Cow::Borrowed("people-pezkuwichain"), spec_name: alloc::borrow::Cow::Borrowed("people-pezkuwichain"),
impl_name: alloc::borrow::Cow::Borrowed("people-pezkuwichain"), impl_name: alloc::borrow::Cow::Borrowed("people-pezkuwichain"),
authoring_version: 1, authoring_version: 1,
spec_version: 1_020_008, spec_version: 1_020_009,
impl_version: 0, impl_version: 0,
apis: RUNTIME_API_VERSIONS, apis: RUNTIME_API_VERSIONS,
transaction_version: 1, transaction_version: 1,
@@ -707,6 +707,7 @@ construct_runtime!(
IdentityKyc: pezpallet_identity_kyc = 51, IdentityKyc: pezpallet_identity_kyc = 51,
Referral: pezpallet_referral = 52, Referral: pezpallet_referral = 52,
Perwerde: pezpallet_perwerde = 53, Perwerde: pezpallet_perwerde = 53,
Messaging: pezpallet_messaging = 55,
// NFTs and Roles // NFTs and Roles
Nfts: pezpallet_nfts = 60, Nfts: pezpallet_nfts = 60,
@@ -770,6 +771,7 @@ mod benches {
[pezpallet_assets, PeopleAssets] [pezpallet_assets, PeopleAssets]
// Pezkuwi - Custom People Pallets // Pezkuwi - Custom People Pallets
[pezpallet_identity_kyc, IdentityKyc] [pezpallet_identity_kyc, IdentityKyc]
[pezpallet_messaging, Messaging]
[pezpallet_perwerde, Perwerde] [pezpallet_perwerde, Perwerde]
[pezpallet_referral, Referral] [pezpallet_referral, Referral]
[pezpallet_tiki, Tiki] [pezpallet_tiki, Tiki]
@@ -610,6 +610,74 @@ impl pezpallet_trust::Config for Runtime {
type CitizenshipSource = CitizenshipSource; type CitizenshipSource = CitizenshipSource;
} }
// =============================================================================
// Messaging Pezpallet Configuration (PEZkurd-P2Pmessage)
// =============================================================================
/// Messaging citizenship checker — bridges to IdentityKyc pallet
#[cfg(not(feature = "runtime-benchmarks"))]
pub struct MessagingCitizenshipChecker;
#[cfg(not(feature = "runtime-benchmarks"))]
impl pezpallet_messaging::types::CitizenshipChecker<AccountId> for MessagingCitizenshipChecker {
fn is_citizen(who: &AccountId) -> bool {
IdentityKyc::is_citizen(who)
}
}
#[cfg(feature = "runtime-benchmarks")]
pub struct MessagingCitizenshipChecker;
#[cfg(feature = "runtime-benchmarks")]
impl pezpallet_messaging::types::CitizenshipChecker<AccountId> for MessagingCitizenshipChecker {
fn is_citizen(_who: &AccountId) -> bool {
true
}
}
/// Messaging trust score checker — bridges to Trust pallet
#[cfg(not(feature = "runtime-benchmarks"))]
pub struct MessagingTrustScoreChecker;
#[cfg(not(feature = "runtime-benchmarks"))]
impl pezpallet_messaging::types::TrustScoreChecker<AccountId> for MessagingTrustScoreChecker {
fn trust_score_of(who: &AccountId) -> u32 {
// Trust pallet returns u128, we cap at u32::MAX for messaging
let score: u128 = Trust::trust_score_of(who);
score.min(u32::MAX as u128) as u32
}
}
#[cfg(feature = "runtime-benchmarks")]
pub struct MessagingTrustScoreChecker;
#[cfg(feature = "runtime-benchmarks")]
impl pezpallet_messaging::types::TrustScoreChecker<AccountId> for MessagingTrustScoreChecker {
fn trust_score_of(_who: &AccountId) -> u32 {
100 // High trust for benchmarks
}
}
parameter_types! {
/// Minimum trust score to use messaging (20 out of ~10000 scale)
pub const MessagingMinTrustScore: u32 = 20;
/// Maximum encrypted payload per message (512 bytes)
pub const MessagingMaxMessageSize: u32 = 512;
/// Maximum messages in inbox per era per recipient
pub const MessagingMaxInboxSize: u32 = 50;
/// Maximum messages a citizen can send per era
pub const MessagingMaxMessagesPerEra: u32 = 50;
/// Era length: 3600 blocks = ~6 hours at 6s/block on People Chain
pub const MessagingEraLength: BlockNumber = 6 * HOURS;
}
impl pezpallet_messaging::Config for Runtime {
type WeightInfo = pezpallet_messaging::weights::BizinikiwiWeight<Runtime>;
type CitizenshipChecker = MessagingCitizenshipChecker;
type TrustScoreChecker = MessagingTrustScoreChecker;
type MinTrustScore = MessagingMinTrustScore;
type MaxMessageSize = MessagingMaxMessageSize;
type MaxInboxSize = MessagingMaxInboxSize;
type MaxMessagesPerEra = MessagingMaxMessagesPerEra;
type EraLength = MessagingEraLength;
}
// ============================================================================= // =============================================================================
// Assets Pezpallet Configuration (required by PEZ Rewards) // Assets Pezpallet Configuration (required by PEZ Rewards)
// ============================================================================= // =============================================================================