Skip to main content

river_core/room_state/
ban.rs

1use crate::room_state::member::{AuthorizedMember, MemberId, MembersV1};
2use crate::room_state::member_info::MemberInfoV1;
3use crate::room_state::ChatRoomParametersV1;
4use crate::util::{sign_struct, verify_struct};
5use crate::ChatRoomStateV1;
6use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
7use freenet_scaffold::util::{fast_hash, FastHash};
8use freenet_scaffold::ComposableState;
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeSet, HashMap, HashSet};
11use std::fmt;
12use std::hash::{Hash, Hasher};
13use std::time::SystemTime;
14
15/// Represents a collection of user bans in a chat room
16///
17/// This structure maintains a list of authorized bans and provides methods
18/// to verify, summarize, and apply changes to the ban list while ensuring
19/// all bans are valid according to room rules.
20///
21/// # Ban validity & the un-ban DoS (#411)
22///
23/// A ban is only honored while its banner is the OWNER or a CURRENT,
24/// signature-validated member: [`Self::ban_is_enforcing`] gates on that, and
25/// [`crate::room_state::ChatRoomStateV1::post_apply_cleanup`] sweeps any ban
26/// whose banner is neither. A member who is a banner is exempt from
27/// inactivity-pruning while they hold a retained ban, so a moderator's bans do
28/// not vanish. The `max_user_bans` FIFO is enforced there too, evicting inert
29/// (currently-unauthorized) bans before enforcing ones, and
30/// [`ComposableState::apply_delta`] bounds a single delta to `max_user_bans`
31/// new bans so a forged flood cannot make signature verification unbounded.
32///
33/// KNOWN, ACCEPTED, self-limiting residuals (Ian confirmed, #411 round 3 D):
34/// 1. A current member can flood bans against ABSENT targets (each such ban is
35///    "enforcing" only because its banner is a member).
36/// 2. Slot-squatting: because of the item-B pruning exemption, a current member
37///    can make themselves PERMANENTLY exempt from inactivity-pruning with a
38///    single junk ban against an absent target — that ban is "enforcing", so it
39///    survives both the non-member-banner sweep and the `max_user_bans`
40///    eviction, and inactivity-prune can no longer reclaim their member slot.
41/// Both are pre-existing/emergent and self-limiting the same way: the flooder /
42/// squatter is a current member, identifiable on every junk ban, and only an
43/// explicit OWNER ban reclaims the slot — banning them makes their bans inert
44/// and sweeps them (freeing the cap and making them prunable again).
45/// Inactivity-prune alone cannot. No code fix; documented deliberately.
46#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
47pub struct BansV1(pub Vec<AuthorizedUserBan>);
48
49/// Validation errors that can occur with bans.
50///
51/// Since #410, ban ENFORCEMENT authority (owner / ancestor / deputy) is no
52/// longer decided in `verify` — it is recomputed from converged state in
53/// `ChatRoomStateV1::post_apply_cleanup`. The former invite-chain /
54/// excess-count validation variants were removed with that change; the only
55/// remaining `verify`-time rejection is an orphaned ban whose banner was
56/// themselves banned.
57#[derive(Debug, Clone, PartialEq)]
58pub enum BanValidationError {
59    /// The banning member is not in the current member list AND was themselves
60    /// banned — an orphaned ban.
61    BannerNotFound(MemberId),
62}
63
64impl fmt::Display for BanValidationError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            BanValidationError::BannerNotFound(id) => {
68                write!(f, "Banning member not found in member list: {:?}", id)
69            }
70        }
71    }
72}
73
74impl BansV1 {
75    /// Validates the per-ban orphan constraints and returns a map of invalid
76    /// bans with errors. Does NOT enforce the `max_user_bans` ceiling — that is
77    /// a whole-collection concern applied as a hard count check in `verify` and
78    /// enforced (inert-first) in `ChatRoomStateV1::post_apply_cleanup` (#410).
79    fn get_invalid_bans(
80        &self,
81        parent_state: &ChatRoomStateV1,
82        parameters: &ChatRoomParametersV1,
83    ) -> HashMap<BanId, BanValidationError> {
84        let member_map = parent_state.members.members_by_member_id();
85        let mut invalid_bans = HashMap::new();
86        let banned_user_ids: HashSet<MemberId> = self.0.iter().map(|b| b.ban.banned_user).collect();
87
88        // Validate each ban
89        for ban in &self.0 {
90            self.validate_single_ban(
91                ban,
92                &member_map,
93                parameters,
94                &mut invalid_bans,
95                &banned_user_ids,
96            );
97        }
98
99        invalid_bans
100    }
101
102    /// Validates a single ban and adds any validation errors to the invalid_bans map.
103    ///
104    /// Note (#410): this does NOT reject a ban merely because the banner is not
105    /// a current ancestor of the target. Authority to ENFORCE a ban (owner /
106    /// ancestor / deputy, including retroactive deputy revocation) is evaluated
107    /// at enforcement time in [`crate::room_state::member::MembersV1::banned_member_ids`]
108    /// (run from `post_apply_cleanup`), NOT here. A ban whose banner has no
109    /// current authority (for example a revoked deputy) is INERT — it removes
110    /// nobody — but must still pass `verify`: a legitimately converged state (a
111    /// previously-banned user who rejoined after their deputy was revoked)
112    /// would otherwise fail validation and break convergence. Keeping ban
113    /// authority out of `verify` is exactly what makes `verify` stable across
114    /// deputy-state changes. Ban SIGNATURES are still verified in `verify`, and
115    /// the orphaned-ban check (banner was themselves banned) is retained.
116    fn validate_single_ban(
117        &self,
118        ban: &AuthorizedUserBan,
119        member_map: &HashMap<MemberId, &AuthorizedMember>,
120        parameters: &ChatRoomParametersV1,
121        invalid_bans: &mut HashMap<BanId, BanValidationError>,
122        banned_user_ids: &HashSet<MemberId>,
123    ) {
124        // If the banned member is no longer present they were already removed
125        // (e.g. by this ban, a cascade, or an inactivity prune); nothing left
126        // to enforce, so the ban is valid.
127        if !member_map.contains_key(&ban.ban.banned_user) {
128            return;
129        }
130
131        // Owner bans are always valid.
132        if ban.banned_by == parameters.owner_id() {
133            return;
134        }
135
136        // If the banner is not a current member, distinguish an orphaned ban
137        // (the banner was themselves banned) from a still-valid ban by a member
138        // who was merely pruned for inactivity. If the banner IS a current
139        // member the ban is accepted regardless of the banner's current
140        // ancestor/deputy authority (see the doc comment above) — enforcement
141        // decides who is actually removed.
142        if !member_map.contains_key(&ban.banned_by) && banned_user_ids.contains(&ban.banned_by) {
143            // Banner was banned — this ban is orphaned.
144            invalid_bans.insert(ban.id(), BanValidationError::BannerNotFound(ban.banned_by));
145        }
146    }
147
148    /// Per-ban validity + signature checks, EXCLUDING the `max_user_bans`
149    /// ceiling. `apply_delta` uses this because it defers cap enforcement to
150    /// `ChatRoomStateV1::post_apply_cleanup` (where the converged member_info is
151    /// available to evict inert bans before enforcing ones); `verify` layers the
152    /// hard count ceiling on top. See #410 review round 1.
153    fn verify_excluding_cap(
154        &self,
155        parent_state: &ChatRoomStateV1,
156        parameters: &ChatRoomParametersV1,
157    ) -> Result<(), String> {
158        let invalid_bans = self.get_invalid_bans(parent_state, parameters);
159        if !invalid_bans.is_empty() {
160            let error_messages: Vec<String> = invalid_bans
161                .iter()
162                .map(|(id, error)| format!("{:?}: {}", id, error))
163                .collect();
164            return Err(format!("Invalid bans: {}", error_messages.join(", ")));
165        }
166
167        let members_by_id = parent_state.members.members_by_member_id();
168        let owner_vk = parameters.owner;
169        let owner_id = parameters.owner_id();
170
171        // Verify signatures for all bans.
172        for ban in &self.0 {
173            if ban.banned_by == owner_id {
174                ban.verify_signature(&owner_vk)
175                    .map_err(|e| format!("Invalid ban signature: {}", e))?;
176            } else if let Some(banning_member) = members_by_id.get(&ban.banned_by) {
177                ban.verify_signature(&banning_member.member.member_vk)
178                    .map_err(|e| format!("Invalid ban signature: {}", e))?;
179            } else {
180                // Banning member not in current members list. This can happen when:
181                // 1. During merge when bans are applied before the members delta
182                // 2. The banner was pruned for inactivity (no recent messages)
183                // In both cases, skip signature verification — we can't verify
184                // without the banner's key, and the signature was verified when
185                // the ban was first created. post_apply_cleanup will remove
186                // truly orphaned bans (where the banner was banned, not pruned).
187            }
188        }
189
190        Ok(())
191    }
192
193    /// Re-verify `ban`'s signature against the banner's CURRENT converged key
194    /// (#411 round 4 item A). Returns `true` iff the banner is the owner or a
195    /// current member AND the stored signature verifies against that banner's
196    /// current verifying key; `false` for a non-member banner (unverifiable) or a
197    /// signature that does not match — a forged/replayed ban.
198    ///
199    /// This closes the SAME-DELTA replay bypass. Field order applies `bans`
200    /// before `members`, so `verify` SKIPS the signature check for a banner that
201    /// is absent from the parent state at bans-apply time (see
202    /// `verify_excluding_cap`). A single delta that re-adds a pruned deputy via
203    /// their PUBLIC, replayable `AuthorizedMember` AND carries a garbage-signature
204    /// ban attributed to that deputy would otherwise have the ban ENFORCED by
205    /// `post_apply_cleanup` once the deputy is a current member (the retained
206    /// deputy grant authorizes it) — a ban forged without the deputy's private
207    /// key. ENFORCEMENT re-checks the signature against the converged key, so the
208    /// apply-time skip no longer matters. It can never false-reject a genuine
209    /// ban: a `MemberId` is the hash of its verifying key, so a current member's
210    /// key is exactly the one that produced their id, and a ban legitimately
211    /// signed by that member always verifies.
212    ///
213    /// `verify` itself is deliberately NOT tightened (that would strand the
214    /// Official room's migration PUT, which carries legitimately sig-skipped
215    /// non-member-banner bans); the check lives only in enforcement.
216    pub fn ban_signature_matches_current_key(
217        ban: &AuthorizedUserBan,
218        members_by_id: &HashMap<MemberId, &AuthorizedMember>,
219        owner_id: MemberId,
220        owner_vk: &VerifyingKey,
221    ) -> bool {
222        let banner = ban.banned_by;
223        let vk = if banner == owner_id {
224            *owner_vk
225        } else if let Some(member) = members_by_id.get(&banner) {
226            member.member.member_vk
227        } else {
228            // Non-member banner: unverifiable here (key unavailable). Treated as
229            // not-matching so callers classify it inert / sweep it.
230            return false;
231        };
232        ban.verify_signature(&vk).is_ok()
233    }
234
235    /// THE single definition of the `max_user_bans` eviction (#411 round 7).
236    ///
237    /// Two sites need to know which bans survive the cap: `post_apply_cleanup`
238    /// step 0-cap, which stores the result, and
239    /// `DirectMessagesV1::apply_delta`, which needs the resulting
240    /// enforced-ban set to decide which held DMs the caps may rank
241    /// (freenet/river#675). They MUST agree exactly — a DM swept at apply time
242    /// against a different surviving ban set than step 6 uses is data loss.
243    /// One function rather than two matching copies is what makes that hold by
244    /// construction; two copies of an almost-identical predicate is precisely
245    /// what drifted in freenet/river#671 and in #411 round 4.
246    ///
247    /// Evicts inert-before-enforcing, then oldest-before-newest, then by ban
248    /// id, and restores the canonical `(banned_at, id)` stored order. A no-op
249    /// when already within the cap. `sort_by_cached_key` computes
250    /// `ban_is_enforcing` at most once per ban (#411 round 3 C).
251    pub fn enforce_user_ban_cap(
252        bans: &mut Vec<AuthorizedUserBan>,
253        max_bans: usize,
254        members_by_id: &HashMap<MemberId, &AuthorizedMember>,
255        member_info: &MemberInfoV1,
256        owner_id: MemberId,
257        owner_vk: &VerifyingKey,
258    ) {
259        if bans.len() <= max_bans {
260            return;
261        }
262        bans.sort_by_cached_key(|ban| {
263            (
264                Self::ban_is_enforcing(ban, members_by_id, member_info, owner_id, owner_vk),
265                ban.ban.banned_at,
266                ban.id(),
267            )
268        });
269        let to_remove = bans.len() - max_bans;
270        bans.drain(0..to_remove);
271        bans.sort_by(|a, b| {
272            a.ban
273                .banned_at
274                .cmp(&b.ban.banned_at)
275                .then_with(|| a.id().cmp(&b.id()))
276        });
277    }
278
279    /// Whether `ban` is currently ENFORCING (worth keeping under `max_user_bans`
280    /// pressure) rather than INERT (evicted first). Pure function of the
281    /// converged `(members + member_info)` state (#410 review round 1).
282    ///
283    /// - The banner must be the owner or a current member AND the ban's signature
284    ///   must verify against that banner's CURRENT key
285    ///   ([`Self::ban_signature_matches_current_key`], #411 round 4 A). A
286    ///   non-member banner, or a forged/replayed ban whose signature does not
287    ///   match the converged key, is inert.
288    /// - If the target is a **current member**, the ban is enforcing iff its
289    ///   banner is currently authorized to ban it
290    ///   ([`MembersV1::is_ban_authorized`]). A forged ban on a present member,
291    ///   or a revoked-deputy ban whose target rejoined, is inert.
292    /// - If the target is **absent** (already removed/pruned), keep the ban (its
293    ///   banner is a signature-verified owner/member): a real enforcing ban.
294    pub fn ban_is_enforcing(
295        ban: &AuthorizedUserBan,
296        members_by_id: &HashMap<MemberId, &AuthorizedMember>,
297        member_info: &MemberInfoV1,
298        owner_id: MemberId,
299        owner_vk: &VerifyingKey,
300    ) -> bool {
301        let banner = ban.banned_by;
302        // A ban can only enforce while its banner is the owner or a current
303        // member (#411 round 3). A non-member banner ID — a stale/pruned deputy,
304        // or a forged one — must NOT make a ban enforcing, and such bans are
305        // swept from state entirely in `post_apply_cleanup`.
306        if banner != owner_id && !members_by_id.contains_key(&banner) {
307            return false;
308        }
309        // Re-verify the signature against the banner's CURRENT converged key
310        // (#411 round 4 A). Closes the same-delta pruned-deputy-replay bypass:
311        // `verify` skipped this at apply time because the banner was absent then.
312        if !Self::ban_signature_matches_current_key(ban, members_by_id, owner_id, owner_vk) {
313            return false;
314        }
315        let target = ban.ban.banned_user;
316        if members_by_id.contains_key(&target) {
317            MembersV1::is_ban_authorized(banner, target, members_by_id, member_info, owner_id)
318        } else {
319            // Target already removed and the banner is a signature-verified owner
320            // or current member (checked above): a real enforcing ban worth keeping.
321            true
322        }
323    }
324}
325
326impl ComposableState for BansV1 {
327    type ParentState = ChatRoomStateV1;
328    // BTreeSet (not HashSet) so the ciborium-serialized summary bytes are
329    // deterministic: freenet-core byte-compares `summarize_state` output for
330    // staleness, and a HashSet iterates in a per-process-random order, making
331    // two identical ban sets summarize to different bytes → spurious
332    // anti-entropy heals. See `.claude/rules/contract-summary-determinism.md`
333    // and freenet/freenet-core#4857.
334    type Summary = BTreeSet<BanId>;
335    type Delta = Vec<AuthorizedUserBan>;
336    type Parameters = ChatRoomParametersV1;
337
338    /// Verifies that all bans in the collection are valid:
339    /// - per-ban orphan constraints hold and all signatures are valid
340    ///   (`verify_excluding_cap`), AND
341    /// - the number of bans does not exceed `max_user_bans` (the hard ceiling
342    ///   on stored state; a legitimately-produced state is already ≤ the cap
343    ///   because `post_apply_cleanup` evicts inert-first down to it).
344    fn verify(
345        &self,
346        parent_state: &Self::ParentState,
347        parameters: &Self::Parameters,
348    ) -> Result<(), String> {
349        self.verify_excluding_cap(parent_state, parameters)?;
350
351        if self.0.len() > parent_state.configuration.configuration.max_user_bans {
352            return Err(format!(
353                "Number of bans ({}) exceeds the maximum allowed ({})",
354                self.0.len(),
355                parent_state.configuration.configuration.max_user_bans
356            ));
357        }
358
359        Ok(())
360    }
361
362    /// Creates a summary of the current ban state
363    ///
364    /// Returns a set of all ban IDs currently in the collection
365    fn summarize(
366        &self,
367        _parent_state: &Self::ParentState,
368        _parameters: &Self::Parameters,
369    ) -> Self::Summary {
370        self.0.iter().map(|ban| ban.id()).collect()
371    }
372
373    /// Computes the difference between current ban state and old state
374    ///
375    /// Returns a vector of bans that exist in the current state but not in the old state,
376    /// or None if there are no differences
377    fn delta(
378        &self,
379        _parent_state: &Self::ParentState,
380        _parameters: &Self::Parameters,
381        old_state_summary: &Self::Summary,
382    ) -> Option<Self::Delta> {
383        // Identify bans in self.0 that are not in old_state_summary
384        let delta = self
385            .0
386            .iter()
387            .filter(|ban| !old_state_summary.contains(&ban.id()))
388            .cloned()
389            .collect::<Vec<_>>();
390        if delta.is_empty() {
391            None
392        } else {
393            Some(delta)
394        }
395    }
396
397    /// Applies changes from a delta to the current ban state.
398    ///
399    /// This method:
400    /// - Checks for duplicate bans
401    /// - Verifies all new bans are valid (per-ban + signatures), EXCLUDING the
402    ///   `max_user_bans` ceiling
403    /// - Adds the new bans to the collection
404    ///
405    /// It deliberately does NOT enforce `max_user_bans` here. The cap is
406    /// enforced in `ChatRoomStateV1::post_apply_cleanup`, which evicts INERT
407    /// (currently-unauthorized) bans before enforcing ones — using the converged
408    /// `member_info`, which is not yet available at this point in the field-apply
409    /// order. Capping here (by oldest, as before) would let a flood of
410    /// forged/inert bans evict the real moderator bans and un-ban spammers
411    /// (#410 review round 1). The transient over-cap set is capped by
412    /// post_apply_cleanup at the end of the same whole-state apply; `verify`
413    /// still rejects any stored state that is over the cap.
414    ///
415    /// Returns an error if any ban in the delta is invalid or already exists.
416    fn apply_delta(
417        &mut self,
418        parent_state: &Self::ParentState,
419        parameters: &Self::Parameters,
420        delta: &Option<Self::Delta>,
421    ) -> Result<(), String> {
422        if let Some(delta) = delta {
423            // Bound the number of NEW bans a single delta may carry (#411 round
424            // 3 item C). Since post_apply_cleanup caps stored state at
425            // `max_user_bans`, ANY legitimate peer's ban list — and therefore
426            // any legitimate delta (a summary-diff against another peer) — holds
427            // at most `max_user_bans` bans. A larger delta can only be a forged
428            // flood; rejecting it bounds the O(N) signature-verification work
429            // below (round 2 removed the pre-verify cap that used to do this).
430            // Deterministic across peers, so it does not affect convergence: a
431            // legitimate delta is never over the bound, and a flood is rejected
432            // identically everywhere.
433            //
434            // Transient skew during a `max_user_bans` REDUCTION: config applies
435            // before bans in field order, so a peer that has already applied the
436            // lower (monotonic) cap may briefly reject a ban-only delta from a
437            // peer still lagging on the old higher cap. Self-heals once the
438            // config converges (the lagging peer re-derives a within-bound delta).
439            let max_bans = parent_state.configuration.configuration.max_user_bans;
440            if delta.len() > max_bans {
441                return Err(format!(
442                    "Ban delta of {} exceeds max_user_bans ({}); refusing to process a flood",
443                    delta.len(),
444                    max_bans
445                ));
446            }
447
448            // Check for duplicate bans
449            let existing_ban_ids: std::collections::HashSet<_> =
450                self.0.iter().map(|ban| ban.id()).collect();
451            for new_ban in delta {
452                if existing_ban_ids.contains(&new_ban.id()) {
453                    return Err(format!("Duplicate ban detected: {:?}", new_ban.id()));
454                }
455            }
456
457            // Create a temporary BansV1 with the new bans and validate WITHOUT
458            // the max-cap ceiling (deferred to post_apply_cleanup).
459            let mut temp_bans = self.clone();
460            temp_bans.0.extend(delta.iter().cloned());
461            if let Err(e) = temp_bans.verify_excluding_cap(parent_state, parameters) {
462                return Err(format!("Invalid delta: {}", e));
463            }
464            self.0 = temp_bans.0;
465        }
466
467        // Sort for deterministic ordering (CRDT convergence requirement)
468        self.0.sort_by(|a, b| {
469            a.ban
470                .banned_at
471                .cmp(&b.ban.banned_at)
472                .then_with(|| a.id().cmp(&b.id()))
473        });
474
475        Ok(())
476    }
477}
478
479/// A user ban with authorization proof
480///
481/// Contains the ban details, the ID of the member who created the ban,
482/// and a cryptographic signature proving the ban's authenticity
483#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
484pub struct AuthorizedUserBan {
485    pub ban: UserBan,
486    pub banned_by: MemberId,
487    pub signature: Signature,
488}
489
490impl Eq for AuthorizedUserBan {}
491
492impl Hash for AuthorizedUserBan {
493    fn hash<H: Hasher>(&self, state: &mut H) {
494        self.signature.to_bytes().hash(state);
495    }
496}
497
498impl AuthorizedUserBan {
499    /// Creates a new authorized ban
500    ///
501    /// Signs the ban with the provided signing key and verifies that the
502    /// banned_by ID matches the public key derived from the signing key
503    pub fn new(ban: UserBan, banned_by: MemberId, banner_signing_key: &SigningKey) -> Self {
504        assert_eq!(
505            MemberId::from(banner_signing_key.verifying_key()),
506            banned_by
507        );
508
509        let signature = sign_struct(&ban, banner_signing_key);
510
511        Self {
512            ban,
513            banned_by,
514            signature,
515        }
516    }
517
518    /// Create an AuthorizedUserBan with a pre-computed signature.
519    /// Use this when signing is done externally (e.g., via delegate).
520    pub fn with_signature(ban: UserBan, banned_by: MemberId, signature: Signature) -> Self {
521        Self {
522            ban,
523            banned_by,
524            signature,
525        }
526    }
527
528    /// Verifies that the ban's signature is valid
529    ///
530    /// Checks that the signature was created by the key corresponding to the provided verifying key
531    pub fn verify_signature(&self, banner_verifying_key: &VerifyingKey) -> Result<(), String> {
532        verify_struct(&self.ban, &self.signature, banner_verifying_key)
533            .map_err(|e| format!("Invalid ban signature: {}", e))
534    }
535
536    /// Generates a unique identifier for this ban based on its signature
537    pub fn id(&self) -> BanId {
538        BanId(fast_hash(&self.signature.to_bytes()))
539    }
540}
541
542/// Contains the core information about a user ban
543///
544/// Includes the room owner's ID, the time of the ban, and the ID of the banned user
545#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
546pub struct UserBan {
547    pub owner_member_id: MemberId,
548    pub banned_at: SystemTime,
549    pub banned_user: MemberId,
550}
551
552/// A unique identifier for a ban
553///
554/// Created from a hash of the ban's signature to ensure uniqueness
555#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Hash, Debug, Ord, PartialOrd)]
556pub struct BanId(pub FastHash);
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use crate::room_state::configuration::AuthorizedConfigurationV1;
562    use crate::room_state::member::{AuthorizedMember, Member, MembersV1};
563    use ed25519_dalek::SigningKey;
564    use std::time::Duration;
565
566    fn create_test_chat_room_state() -> ChatRoomStateV1 {
567        // Create a minimal ChatRoomStateV1 for testing
568        ChatRoomStateV1 {
569            configuration: AuthorizedConfigurationV1::default(),
570            bans: Default::default(),
571            members: MembersV1::default(),
572            member_info: Default::default(),
573            secrets: Default::default(),
574            recent_messages: Default::default(),
575            upgrade: Default::default(),
576            ..Default::default()
577        }
578    }
579
580    fn create_test_parameters() -> ChatRoomParametersV1 {
581        // Create minimal ChatRoomParametersV1 for testing
582        let owner_key = SigningKey::generate(&mut rand::thread_rng());
583        ChatRoomParametersV1 {
584            owner: owner_key.verifying_key(),
585        }
586    }
587
588    #[test]
589    fn test_bans_verify() {
590        let mut state = create_test_chat_room_state();
591        let params = create_test_parameters();
592
593        // Create some test members
594        let owner_key = SigningKey::generate(&mut rand::thread_rng());
595        let owner_id: MemberId = owner_key.verifying_key().into();
596        let member1_key = SigningKey::generate(&mut rand::thread_rng());
597        let member1_id: MemberId = member1_key.verifying_key().into();
598        let member2_key = SigningKey::generate(&mut rand::thread_rng());
599        let member2_id: MemberId = member2_key.verifying_key().into();
600
601        // Add members to the room_state
602        state.members.members.push(AuthorizedMember::new(
603            Member {
604                owner_member_id: owner_id,
605                invited_by: owner_id,
606                member_vk: owner_key.verifying_key(),
607            },
608            &owner_key,
609        ));
610        state.members.members.push(AuthorizedMember::new(
611            Member {
612                owner_member_id: owner_id,
613                invited_by: owner_id,
614                member_vk: member1_key.verifying_key(),
615            },
616            &owner_key,
617        ));
618        state.members.members.push(AuthorizedMember::new(
619            Member {
620                owner_member_id: owner_id,
621                invited_by: member1_id,
622                member_vk: member2_key.verifying_key(),
623            },
624            &member1_key,
625        ));
626
627        // Update the configuration to allow bans
628        state.configuration.configuration.max_user_bans = 5;
629
630        // Test 1: Valid ban by owner
631        let ban1 = AuthorizedUserBan::new(
632            UserBan {
633                owner_member_id: owner_id,
634                banned_at: SystemTime::now(),
635                banned_user: member1_id,
636            },
637            owner_id,
638            &owner_key,
639        );
640
641        let bans = BansV1(vec![ban1]);
642        assert!(
643            bans.verify(&state, &params).is_ok(),
644            "Valid ban should be verified successfully: {:?}",
645            bans.verify(&state, &params).err()
646        );
647
648        // Test 2: Exceeding max_user_bans
649        let mut many_bans = Vec::new();
650        for _ in 0..6 {
651            many_bans.push(AuthorizedUserBan::new(
652                UserBan {
653                    owner_member_id: owner_id,
654                    banned_at: SystemTime::now(),
655                    banned_user: member1_id,
656                },
657                owner_id,
658                &owner_key,
659            ));
660        }
661        let too_many_bans = BansV1(many_bans);
662        assert!(
663            too_many_bans.verify(&state, &params).is_err(),
664            "Exceeding max_user_bans should fail verification"
665        );
666
667        // Test 3: Ban by pruned member (not in member list, not banned) — valid
668        // With message-based lifecycle, banners not in the members list who
669        // are not themselves banned are considered pruned for inactivity.
670        let pruned_key = SigningKey::generate(&mut rand::thread_rng());
671        let pruned_id: MemberId = pruned_key.verifying_key().into();
672        let pruned_ban = AuthorizedUserBan::new(
673            UserBan {
674                owner_member_id: owner_id,
675                banned_at: SystemTime::now(),
676                banned_user: member2_id,
677            },
678            pruned_id,
679            &pruned_key,
680        );
681
682        let pruned_bans = BansV1(vec![pruned_ban]);
683        assert!(
684            pruned_bans.verify(&state, &params).is_ok(),
685            "Ban by pruned (non-banned) member should pass verification: {:?}",
686            pruned_bans.verify(&state, &params).err()
687        );
688
689        // Test 3b: Orphaned ban (banner was banned) — invalid
690        let orphaned_key = SigningKey::generate(&mut rand::thread_rng());
691        let orphaned_id: MemberId = orphaned_key.verifying_key().into();
692        let orphaned_ban = AuthorizedUserBan::new(
693            UserBan {
694                owner_member_id: owner_id,
695                banned_at: SystemTime::now(),
696                banned_user: member2_id,
697            },
698            orphaned_id,
699            &orphaned_key,
700        );
701        // A ban targeting the orphaned member makes them "banned" (not just pruned)
702        let ban_of_orphaned = AuthorizedUserBan::new(
703            UserBan {
704                owner_member_id: owner_id,
705                banned_at: SystemTime::now(),
706                banned_user: orphaned_id,
707            },
708            owner_id,
709            &owner_key,
710        );
711        let orphaned_bans = BansV1(vec![orphaned_ban, ban_of_orphaned]);
712        assert!(
713            orphaned_bans.verify(&state, &params).is_err(),
714            "Orphaned ban (banner was banned) should fail verification"
715        );
716
717        // Test 4: Valid ban by non-owner member
718        let ban_by_member = AuthorizedUserBan::new(
719            UserBan {
720                owner_member_id: owner_id,
721                banned_at: SystemTime::now(),
722                banned_user: member2_id,
723            },
724            member1_id,
725            &member1_key,
726        );
727
728        let member_bans = BansV1(vec![ban_by_member]);
729        assert!(
730            member_bans.verify(&state, &params).is_ok(),
731            "Valid ban by non-owner member should pass verification"
732        );
733    }
734
735    #[test]
736    fn test_bans_summarize() {
737        let state = create_test_chat_room_state();
738        let params = create_test_parameters();
739
740        let key = SigningKey::generate(&mut rand::thread_rng());
741        let id: MemberId = key.verifying_key().into();
742
743        let ban1 = AuthorizedUserBan::new(
744            UserBan {
745                owner_member_id: id,
746                banned_at: SystemTime::now(),
747                banned_user: id,
748            },
749            id,
750            &key,
751        );
752
753        let ban2 = AuthorizedUserBan::new(
754            UserBan {
755                owner_member_id: id,
756                banned_at: SystemTime::now() + Duration::from_secs(1),
757                banned_user: id,
758            },
759            id,
760            &key,
761        );
762
763        let bans = BansV1(vec![ban1.clone(), ban2.clone()]);
764        let summary = bans.summarize(&state, &params);
765
766        assert_eq!(summary.len(), 2);
767        assert!(summary.contains(&ban1.id()));
768        assert!(summary.contains(&ban2.id()));
769    }
770
771    #[test]
772    fn test_bans_delta() {
773        let state = create_test_chat_room_state();
774        let params = create_test_parameters();
775
776        let key = SigningKey::generate(&mut rand::thread_rng());
777        let id: MemberId = key.verifying_key().into();
778
779        let ban1 = AuthorizedUserBan::new(
780            UserBan {
781                owner_member_id: id,
782                banned_at: SystemTime::now(),
783                banned_user: id,
784            },
785            id,
786            &key,
787        );
788
789        let ban2 = AuthorizedUserBan::new(
790            UserBan {
791                owner_member_id: id,
792                banned_at: SystemTime::now() + Duration::from_secs(1),
793                banned_user: id,
794            },
795            id,
796            &key,
797        );
798
799        let bans = BansV1(vec![ban1.clone(), ban2.clone()]);
800
801        // Test 1: Empty old summary
802        let empty_summary = BTreeSet::new();
803        let delta = bans.delta(&state, &params, &empty_summary);
804        assert_eq!(delta, Some(vec![ban1.clone(), ban2.clone()]));
805
806        // Test 2: Partial old summary
807        let partial_summary: BTreeSet<BanId> = vec![ban1.id()].into_iter().collect();
808        let delta = bans.delta(&state, &params, &partial_summary);
809        assert_eq!(delta, Some(vec![ban2.clone()]));
810
811        // Test 3: Full old summary
812        let full_summary: BTreeSet<BanId> = vec![ban1.id(), ban2.id()].into_iter().collect();
813        let delta = bans.delta(&state, &params, &full_summary);
814        assert_eq!(delta, None);
815    }
816
817    #[test]
818    fn test_bans_apply_delta() {
819        let mut state = create_test_chat_room_state();
820        let params = create_test_parameters();
821
822        let owner_key = SigningKey::generate(&mut rand::thread_rng());
823        let owner_id: MemberId = owner_key.verifying_key().into();
824        let member_key = SigningKey::generate(&mut rand::thread_rng());
825        let member_id: MemberId = member_key.verifying_key().into();
826
827        // Add members to the room_state
828        state.members.members.push(AuthorizedMember::new(
829            Member {
830                owner_member_id: owner_id,
831                invited_by: owner_id,
832                member_vk: owner_key.verifying_key(),
833            },
834            &owner_key,
835        ));
836        state.members.members.push(AuthorizedMember::new(
837            Member {
838                owner_member_id: owner_id,
839                invited_by: owner_id,
840                member_vk: member_key.verifying_key(),
841            },
842            &owner_key,
843        ));
844
845        // Update the configuration to allow bans
846        state.configuration.configuration.max_user_bans = 5;
847
848        let mut bans = BansV1::default();
849
850        let new_ban = AuthorizedUserBan::new(
851            UserBan {
852                owner_member_id: owner_id,
853                banned_at: SystemTime::now(),
854                banned_user: member_id,
855            },
856            owner_id,
857            &owner_key,
858        );
859
860        // Test 1: Apply valid delta
861        let delta = vec![new_ban.clone()];
862        assert!(
863            bans.apply_delta(&state, &params, &Some(delta.clone()))
864                .is_ok(),
865            "Valid delta should be applied successfully: {:?}",
866            bans.apply_delta(&state, &params, &Some(delta)).err()
867        );
868        assert_eq!(
869            bans.0.len(),
870            1,
871            "Bans should contain one ban after applying delta"
872        );
873        assert_eq!(bans.0[0], new_ban, "Applied ban should match the new ban");
874
875        // Test 2: A delta that pushes past max_user_bans NO LONGER caps here.
876        // The cap moved to `ChatRoomStateV1::post_apply_cleanup` so it can evict
877        // INERT-before-enforcing bans using the converged member_info (#410
878        // review round 1). `apply_delta` accumulates all valid bans; the
879        // whole-state cleanup enforces the ceiling. (State-level capping +
880        // inert-first eviction are covered by the DoS test in
881        // `common/tests/deputy_ban_test.rs`.)
882        let mut many_bans = Vec::new();
883        for i in 0..5 {
884            many_bans.push(AuthorizedUserBan::new(
885                UserBan {
886                    owner_member_id: owner_id,
887                    banned_at: SystemTime::now() + Duration::from_secs(i as u64 + 10),
888                    banned_user: member_id,
889                },
890                owner_id,
891                &owner_key,
892            ));
893        }
894        let delta_exceeding_max = Some(many_bans.clone());
895        assert!(
896            bans.apply_delta(&state, &params, &delta_exceeding_max)
897                .is_ok(),
898            "Applying more bans should succeed: {:?}",
899            bans.apply_delta(&state, &params, &delta_exceeding_max)
900                .err()
901        );
902        assert_eq!(
903            bans.0.len(),
904            6,
905            "apply_delta accumulates without capping (cap is enforced in post_apply_cleanup)"
906        );
907        assert!(
908            bans.0.contains(&new_ban),
909            "apply_delta must NOT drop the oldest ban — evicting is post_apply_cleanup's job"
910        );
911
912        // Test 3: Apply invalid delta (duplicate ban) - use one of the bans still in the list
913        let existing_ban = many_bans.last().unwrap().clone();
914        let invalid_delta = Some(vec![existing_ban]);
915        assert!(
916            bans.apply_delta(&state, &params, &invalid_delta).is_err(),
917            "Applying duplicate ban should fail: {:?}",
918            bans.apply_delta(&state, &params, &invalid_delta).ok()
919        );
920        assert_eq!(
921            bans.0.len(),
922            6,
923            "State should not change after applying duplicate ban"
924        );
925
926        // Test 4: More valid bans keep accumulating (still no cap at this level).
927        let mut additional_bans = Vec::new();
928        for i in 0..2 {
929            additional_bans.push(AuthorizedUserBan::new(
930                UserBan {
931                    owner_member_id: owner_id,
932                    banned_at: SystemTime::now() + Duration::from_secs(i as u64 + 100),
933                    banned_user: member_id,
934                },
935                owner_id,
936                &owner_key,
937            ));
938        }
939        assert!(
940            bans.apply_delta(&state, &params, &Some(additional_bans))
941                .is_ok(),
942            "Applying more bans should succeed: {:?}",
943            bans.apply_delta(&state, &params, &Some(Vec::new())).err()
944        );
945        assert_eq!(
946            bans.0.len(),
947            8,
948            "apply_delta keeps accumulating; the cap is enforced by post_apply_cleanup"
949        );
950    }
951
952    #[test]
953    fn test_authorized_user_ban() {
954        let owner_key = SigningKey::generate(&mut rand::thread_rng());
955        let owner_id: MemberId = owner_key.verifying_key().into();
956        let member_key = SigningKey::generate(&mut rand::thread_rng());
957        let member_id: MemberId = member_key.verifying_key().into();
958
959        let ban = UserBan {
960            owner_member_id: owner_id,
961            banned_at: SystemTime::now(),
962            banned_user: member_id,
963        };
964
965        let authorized_ban = AuthorizedUserBan::new(ban.clone(), owner_id, &owner_key);
966
967        // Test 1: Verify signature
968        assert!(authorized_ban
969            .verify_signature(&owner_key.verifying_key())
970            .is_ok());
971
972        // Test 2: Verify signature with wrong key
973        let wrong_key = SigningKey::generate(&mut rand::thread_rng());
974        assert!(authorized_ban
975            .verify_signature(&wrong_key.verifying_key())
976            .is_err());
977
978        // Test 3: Check ban ID
979        let id1 = authorized_ban.id();
980        let id2 = authorized_ban.id();
981        assert_eq!(id1, id2);
982
983        // Test 4: Different bans should have different IDs
984        let another_ban = AuthorizedUserBan::new(
985            UserBan {
986                owner_member_id: owner_id,
987                banned_at: SystemTime::now() + Duration::from_secs(1),
988                banned_user: member_id,
989            },
990            owner_id,
991            &owner_key,
992        );
993        assert_ne!(authorized_ban.id(), another_ban.id());
994    }
995}