Skip to main content

qcode/bridge/
rules.rs

1//! The rules a message between two tabs has to pass, and what QCode remembers to apply them.
2//!
3//! In the order they are asked:
4//!
5//! 1. **The network's direction.** A tab whose profile has no network never sends to a tab whose
6//!    profile has it: the second tab could carry the first one's files out, which is exactly what
7//!    taking the network away was meant to prevent. The other way round, and between two tabs of
8//!    the same kind, nothing leaves that could not leave already.
9//! 2. **The chain.** A message sent by a tab that was itself sent one a short while ago continues
10//!    that exchange, one step further; a chain longer than [`MOST_HOPS`] is cut, so two agents
11//!    answering each other stop on their own. What the person types into a tab ends the chain that
12//!    tab was in: its next message is their new task, not an answer.
13//! 3. **The pace.** One tab sends at most [`MOST_PER_WINDOW`] messages in [`RATE_WINDOW`], so an
14//!    agent that sends in a loop is stopped even when every message starts a new chain.
15//! 4. **The person**, only when they turned asking on in the settings. Then the first message
16//!    from one tab to another asks the person, who allows or denies it; the answer holds for that
17//!    pair, that direction, until QCode closes. It is kept in memory only: a pair of tabs does
18//!    not outlive QCode, and a new session starts asking again. With asking off, which is the
19//!    default, a message the first three rules take is sent.
20//!
21//! The first three are never switched off: the network rule is a boundary rather than a
22//! question, and the chain is what stops two agents keeping each other busy, and the person's
23//! balance with them, when nobody is asked.
24//!
25//! Nothing here reads a clock: every question is given the moment it is asked, which is how the
26//! windows are tested without waiting.
27
28use std::collections::{HashMap, VecDeque};
29use std::time::{Duration, Instant};
30
31/// The longest chain of messages. Asking and answering is two steps; a task that goes through
32/// a third tab and comes back with its answer is four. Six leaves room for that and one more
33/// question, and stops two agents that answer each other after three rounds.
34pub const MOST_HOPS: u32 = 6;
35
36/// How long a tab that was sent a message is taken to be answering it. Ten minutes is longer
37/// than an agent takes to work on a message and reply, and short enough that a new task the
38/// person sets later starts a new chain.
39pub const CHAIN_WINDOW: Duration = Duration::from_secs(10 * 60);
40
41/// The most messages one tab sends in [`RATE_WINDOW`]. An agent handing out work writes to a
42/// few tabs at once; five in a minute covers that, and a loop reaches it within seconds.
43pub const MOST_PER_WINDOW: usize = 5;
44
45/// The window [`MOST_PER_WINDOW`] is counted in.
46pub const RATE_WINDOW: Duration = Duration::from_secs(60);
47
48/// One side of a message: the tab, by its key, and whether its container reaches the network.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct End {
51    /// The tab's key.
52    pub tab: u64,
53    /// Whether its profile has the network.
54    pub network: bool,
55}
56
57/// Why a message was not taken.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum Refusal {
60    /// A tab without the network may not send to one with it.
61    Network,
62    /// The chain is longer than [`MOST_HOPS`].
63    Chain,
64    /// The sender sent [`MOST_PER_WINDOW`] messages in the last [`RATE_WINDOW`] already.
65    Pace,
66    /// The person denied messages from this tab to that one.
67    Denied,
68}
69
70/// What the person said about one pair, in one direction.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Decision {
73    /// Messages from the first tab to the second are taken.
74    Allowed,
75    /// They are refused.
76    Denied,
77}
78
79/// What QCode remembers for the rules, for as long as it runs.
80#[derive(Debug, Default)]
81pub struct Rules {
82    /// The person's answers, by sender and receiver.
83    decisions: HashMap<(u64, u64), Decision>,
84    /// When each tab sent its recent messages, oldest first.
85    sent: HashMap<u64, VecDeque<Instant>>,
86    /// The step of the last message each tab was sent, and when.
87    received: HashMap<u64, (u32, Instant)>,
88}
89
90impl Rules {
91    /// Asks the rules that do not need the person about a message from `from` to `to` at `now`,
92    /// and answers the step of the chain it would be.
93    ///
94    /// Nothing is recorded: a message that then waits for the person or is denied by them has
95    /// not been sent.
96    ///
97    /// # Errors
98    ///
99    /// The first rule the message breaks.
100    pub fn check(&self, from: End, to: End, now: Instant) -> Result<u32, Refusal> {
101        if !from.network && to.network {
102            return Err(Refusal::Network);
103        }
104        let hop = self.hop(from.tab, now);
105        if hop > MOST_HOPS {
106            return Err(Refusal::Chain);
107        }
108        let recent = self
109            .sent
110            .get(&from.tab)
111            .map_or(0, |times| times.iter().filter(|&&at| now.saturating_duration_since(at) < RATE_WINDOW).count());
112        if recent >= MOST_PER_WINDOW {
113            return Err(Refusal::Pace);
114        }
115        Ok(hop)
116    }
117
118    /// The step a message from `tab` at `now` would be: one more than the last message it was
119    /// sent, when that came within [`CHAIN_WINDOW`], and the first step otherwise.
120    #[must_use]
121    pub fn hop(&self, tab: u64, now: Instant) -> u32 {
122        match self.received.get(&tab) {
123            Some(&(hop, at)) if now.saturating_duration_since(at) < CHAIN_WINDOW => hop + 1,
124            _ => 1,
125        }
126    }
127
128    /// What the person said about messages from `from` to `to`, when they were asked.
129    #[must_use]
130    pub fn decision(&self, from: u64, to: u64) -> Option<Decision> {
131        self.decisions.get(&(from, to)).copied()
132    }
133
134    /// Records what the person said about messages from `from` to `to`.
135    pub fn decide(&mut self, from: u64, to: u64, decision: Decision) {
136        self.decisions.insert((from, to), decision);
137    }
138
139    /// Counts a message `from` sent at `now` against its pace, whether it is taken at once or
140    /// waits for the person.
141    pub fn sent(&mut self, from: u64, now: Instant) {
142        let times = self.sent.entry(from).or_default();
143        while times.front().is_some_and(|&at| now.saturating_duration_since(at) >= RATE_WINDOW) {
144            times.pop_front();
145        }
146        times.push_back(now);
147    }
148
149    /// Records that `to` was handed a message that was step `hop` of its chain, at `now`; what
150    /// `to` sends next continues the chain.
151    pub fn received(&mut self, to: u64, hop: u32, now: Instant) {
152        self.received.insert(to, (hop, now));
153    }
154
155    /// Ends the chain the tab `tab` was in: what it sends next starts a new one. Called when the
156    /// person gave that tab something of their own since it was last handed a message, so what it
157    /// sends is their new task rather than its answer to another agent.
158    pub fn new_chain(&mut self, tab: u64) {
159        self.received.remove(&tab);
160    }
161
162    /// Forgets everything about the tab `tab`, which was closed: its keys are never used again,
163    /// and nothing of it should be held for as long as QCode runs.
164    pub fn forget(&mut self, tab: u64) {
165        self.decisions.retain(|&(from, to), _| from != tab && to != tab);
166        self.sent.remove(&tab);
167        self.received.remove(&tab);
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    const ONLINE: bool = true;
176    const OFFLINE: bool = false;
177
178    fn end(tab: u64, network: bool) -> End {
179        End { tab, network }
180    }
181
182    #[test]
183    fn a_tab_without_the_network_never_sends_to_one_with_it() {
184        let rules = Rules::default();
185        let now = Instant::now();
186        assert_eq!(rules.check(end(1, OFFLINE), end(2, ONLINE), now), Err(Refusal::Network));
187        assert_eq!(rules.check(end(1, ONLINE), end(2, OFFLINE), now), Ok(1), "the other way leaks nothing");
188        assert_eq!(rules.check(end(1, OFFLINE), end(2, OFFLINE), now), Ok(1));
189        assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now), Ok(1));
190    }
191
192    #[test]
193    fn the_person_saying_yes_does_not_open_the_network_rule() {
194        let mut rules = Rules::default();
195        rules.decide(1, 2, Decision::Allowed);
196        assert_eq!(rules.check(end(1, OFFLINE), end(2, ONLINE), Instant::now()), Err(Refusal::Network));
197    }
198
199    #[test]
200    fn a_decision_holds_for_one_pair_in_one_direction() {
201        let mut rules = Rules::default();
202        assert_eq!(rules.decision(1, 2), None, "nobody was asked yet");
203        rules.decide(1, 2, Decision::Allowed);
204        rules.decide(3, 2, Decision::Denied);
205        assert_eq!(rules.decision(1, 2), Some(Decision::Allowed));
206        assert_eq!(rules.decision(2, 1), None, "answering is asked about on its own");
207        assert_eq!(rules.decision(3, 2), Some(Decision::Denied));
208        assert_eq!(rules.decision(1, 3), None);
209    }
210
211    #[test]
212    fn a_closed_tab_is_forgotten_with_every_decision_about_it() {
213        let mut rules = Rules::default();
214        rules.decide(1, 2, Decision::Allowed);
215        rules.decide(2, 3, Decision::Allowed);
216        rules.decide(3, 4, Decision::Denied);
217        rules.forget(2);
218        assert_eq!(rules.decision(1, 2), None);
219        assert_eq!(rules.decision(2, 3), None);
220        assert_eq!(rules.decision(3, 4), Some(Decision::Denied));
221    }
222
223    #[test]
224    fn two_tabs_answering_each_other_are_stopped_after_the_longest_chain() {
225        let mut rules = Rules::default();
226        let start = Instant::now();
227        let (a, b) = (end(1, ONLINE), end(2, ONLINE));
228        let mut steps = Vec::new();
229        for turn in 0..10u64 {
230            // A minute apart, so the pace never stands in the way: only the chain does.
231            let now = start + Duration::from_secs(60 * turn);
232            let (from, to) = if turn % 2 == 0 { (a, b) } else { (b, a) };
233            match rules.check(from, to, now) {
234                Ok(hop) => {
235                    rules.sent(from.tab, now);
236                    rules.received(to.tab, hop, now);
237                    steps.push(hop);
238                }
239                Err(refusal) => {
240                    assert_eq!(refusal, Refusal::Chain);
241                    break;
242                }
243            }
244        }
245        assert_eq!(steps, [1, 2, 3, 4, 5, 6]);
246    }
247
248    #[test]
249    fn a_chain_ends_when_the_tab_was_not_sent_anything_for_a_while() {
250        let mut rules = Rules::default();
251        let now = Instant::now();
252        rules.received(1, MOST_HOPS, now);
253        assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now), Err(Refusal::Chain));
254        assert_eq!(rules.hop(1, now + CHAIN_WINDOW - Duration::from_secs(1)), MOST_HOPS + 1);
255        assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now + CHAIN_WINDOW), Ok(1), "a new task, a new chain");
256    }
257
258    #[test]
259    fn a_tab_sending_in_a_loop_is_stopped_by_its_pace_and_let_go_a_minute_later() {
260        let mut rules = Rules::default();
261        let start = Instant::now();
262        let (from, to) = (end(1, ONLINE), end(2, ONLINE));
263        for second in 0..MOST_PER_WINDOW as u64 {
264            let now = start + Duration::from_secs(second);
265            assert_eq!(rules.check(from, to, now), Ok(1));
266            rules.sent(from.tab, now);
267        }
268        let later = start + Duration::from_secs(10);
269        assert_eq!(rules.check(from, to, later), Err(Refusal::Pace));
270        assert_eq!(rules.check(end(3, ONLINE), to, later), Ok(1), "another sender has its own pace");
271        assert_eq!(rules.check(from, to, start + RATE_WINDOW), Ok(1), "the oldest message left the window");
272    }
273
274    #[test]
275    fn the_rules_are_asked_in_order_network_chain_then_pace() {
276        let mut rules = Rules::default();
277        let now = Instant::now();
278        rules.received(1, MOST_HOPS, now);
279        for _ in 0..MOST_PER_WINDOW {
280            rules.sent(1, now);
281        }
282        assert_eq!(rules.check(end(1, OFFLINE), end(2, ONLINE), now), Err(Refusal::Network));
283        assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now), Err(Refusal::Chain));
284        rules.received(1, 1, now);
285        assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now), Err(Refusal::Pace));
286    }
287}