Skip to main content

p2panda_core/
logs.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Identify append-only logs and compute their differences.
4use std::collections::BTreeMap;
5use std::hash::Hash as StdHash;
6
7use serde::{Deserialize, Serialize};
8
9use crate::traits::Author;
10
11/// Uniquely identify a single-author log.
12///
13/// The `LogId` exists purely to group a set of operations and is intended to be implemented for
14/// any type which meets the design requirements of a particular application.
15///
16/// A blanket implementation is provided for any type meeting the required trait bounds.
17///
18/// Here we briefly outline several implementation scenarios:
19///
20/// An application relying on a one-log-per-author design might choose to implement `LogId` for a
21/// thin wrapper around an Ed25519 public key; this effectively ties the log to the public key of
22/// the author. Secure Scuttlebutt (SSB) is an example of a protocol which relies on this model.
23///
24/// In an application where one author may produce operations grouped into multiple logs, `LogId`
25/// might be represented a unique number for each log instance.
26///
27/// Some applications might require semantic grouping of operations. For example, a chat
28/// application may choose to create a separate log for each author-channel pairing. In such a
29/// scenario, `LogId` might be implemented for a `struct` containing a `String` representation of
30/// the channel name.
31///
32/// Finally, please note that implementers of `LogId` must take steps to ensure their log design
33/// is fit for purpose and that all operations have been thoroughly validated before being
34/// persisted. No such validation checks are provided by `p2panda-store`.
35pub trait LogId: Clone + Eq + Ord + StdHash + Serialize + for<'de> Deserialize<'de> {}
36
37impl<T> LogId for T where T: Clone + Eq + Ord + StdHash + Serialize + for<'de> Deserialize<'de> {}
38
39/// Sequence number of an entry in an append-only log.
40///
41/// ```text
42/// [0] <- [1] <- [2]
43///                ^
44///            log height
45/// ```
46///
47/// To express the state vector of a log we can use the _log height_ (also HEAD or "frontier") which
48/// is a `SeqNum`.
49pub type SeqNum = u32;
50
51/// Author logs mapping.
52///
53/// Each log is identified with `L` and grouped by author `A`.
54///
55/// ```text
56/// Author "panda":
57/// - "trees" log
58/// - "animals" log
59///
60/// Author "icebear":
61/// - "trees" log
62/// ```
63pub type Logs<A, L> = BTreeMap<A, Vec<L>>;
64
65/// Map of [`SeqNum`] log heights grouped by author `A`. Each log is identified with `L`.
66///
67/// ```text
68/// Author "panda":
69/// - "trees" log: 6
70/// - "animals" log: 12
71///
72/// Author "icebear":
73/// - "trees" log: 4
74/// ```
75pub type LogHeights<A, L> = BTreeMap<A, BTreeMap<L, SeqNum>>;
76
77/// Map of log ranges grouped by author `A`. Each log is identified with `L`.
78///
79/// Log ranges are used to express the _difference_ between two [`LogHeights`] state vectors. Use
80/// the [`compare_logs`] method to compute it.
81///
82/// ```text
83/// Author "panda":
84/// - "trees" log: [0..6]
85/// - "animals" log: [4..12]
86///
87/// Author "icebear":
88/// - "trees" log: [3..4]
89/// ```
90pub type LogRanges<A, L> = BTreeMap<A, BTreeMap<L, (Option<SeqNum>, Option<SeqNum>)>>;
91
92/// Compare two sets of logs (local and remote) and calculate the "diff" representing ranges of
93/// entries that should be sent to the remote.
94///
95/// Local and remote states are represented by a map of authors to logs, where the logs are
96/// represented by their unique identifier and current log height. If the remote is not aware of a
97/// log, then the range containing all entries in the local log will be contained in the diff, if
98/// the remote knows of some entries in a log, then the range representing only the entries they
99/// need will be included.
100///
101/// Log ranges are represented by `(Option<u32>, Option<u32>)` tuples where the first value is an
102/// exclusive "from" sequence number and the later is an inclusive "until" sequence number. If
103/// either values are `None` that signifies that all entries from the start, or to the end, are
104/// required.
105///
106/// The returned ranges can be used in a sync protocol to then fetch entries from a store and send
107/// them to the remote. If both local and remote replicas do this then they will arrive at the
108/// same state. If pruned logs are being replicated and a range has been returned from this
109/// method, then it is expected only the remaining "frontier" will be replicated for each log.
110pub fn compare_logs<A, L>(local: &LogHeights<A, L>, remote: &LogHeights<A, L>) -> LogRanges<A, L>
111where
112    A: Author,
113    L: LogId,
114{
115    let mut remote_needs: LogRanges<A, L> = BTreeMap::default();
116
117    // Iterate over all authors.
118    for (verifying_key, local_logs) in local {
119        let Some(remote_logs) = remote.get(verifying_key) else {
120            // If the remote did not know of this author, then they need all entries in all of
121            // their logs that the local knows of.
122            let needs = local_logs
123                .iter()
124                .map(|(log_id, log_height)| (log_id.clone(), (None, Some(*log_height))))
125                .collect();
126            remote_needs.insert(verifying_key.to_owned(), needs);
127            continue;
128        };
129
130        // If the local and remote logs are equal then nothing needs to be sent.
131        if local_logs == remote_logs {
132            continue;
133        }
134
135        // Iterate over all local logs for this author.
136        for (log_id, local_log_height) in local_logs {
137            let Some(remote_log_height) = remote_logs.get(log_id) else {
138                // If the remote did not know of this log, then they need all entries from the
139                // local.
140                remote_needs
141                    .entry(verifying_key.to_owned())
142                    .or_default()
143                    .insert(log_id.clone(), (None, Some(*local_log_height)));
144                continue;
145            };
146
147            // If the remote log height is less than the local, then include the exact range they
148            // need in the diff.
149            if remote_log_height < local_log_height {
150                remote_needs
151                    .entry(verifying_key.to_owned())
152                    .or_default()
153                    .insert(
154                        log_id.clone(),
155                        (Some(*remote_log_height), Some(*local_log_height)),
156                    );
157            }
158        }
159    }
160
161    remote_needs
162}
163
164#[cfg(test)]
165mod tests {
166    use std::collections::BTreeMap;
167
168    use crate::logs::compare_logs;
169
170    type Author = u8;
171
172    impl crate::traits::Author for Author {}
173
174    const ALICE: Author = 0;
175    const BOB: Author = 1;
176
177    #[test]
178    fn both_empty() {
179        let local: BTreeMap<Author, BTreeMap<u32, u32>> = BTreeMap::new();
180        let remote: BTreeMap<Author, BTreeMap<u32, u32>> = BTreeMap::new();
181        let result = compare_logs(&local, &remote);
182        assert!(result.is_empty());
183    }
184
185    #[test]
186    fn remote_empty() {
187        let mut local: BTreeMap<Author, BTreeMap<u32, u32>> = BTreeMap::new();
188        let logs = BTreeMap::from([(1, 5), (2, 10)]);
189        local.insert(ALICE, logs);
190
191        let remote: BTreeMap<Author, BTreeMap<u32, u32>> = BTreeMap::new();
192
193        let result = compare_logs(&local, &remote);
194        let needs = result.get(&ALICE).unwrap();
195
196        assert_eq!(needs.get(&1), Some(&(None, Some(5))));
197        assert_eq!(needs.get(&2), Some(&(None, Some(10))));
198    }
199
200    #[test]
201    fn remote_missing_single_log() {
202        let mut local = BTreeMap::new();
203        local.insert(ALICE, BTreeMap::from([(1, 5), (2, 10)]));
204
205        let mut remote = BTreeMap::new();
206        remote.insert(ALICE, BTreeMap::from([(1, 5)]));
207
208        let result = compare_logs(&local, &remote);
209        let needs = result.get(&ALICE).unwrap();
210
211        assert_eq!(needs.get(&2), Some(&(None, Some(10))));
212        assert!(!needs.contains_key(&1));
213    }
214
215    #[test]
216    fn remote_behind() {
217        let mut local = BTreeMap::new();
218        local.insert(ALICE, BTreeMap::from([(1, 20)]));
219
220        let mut remote = BTreeMap::new();
221        remote.insert(ALICE, BTreeMap::from([(1, 10)]));
222
223        let result = compare_logs(&local, &remote);
224        let needs = result.get(&ALICE).unwrap();
225
226        assert_eq!(needs.get(&1), Some(&(Some(10), Some(20))));
227    }
228
229    #[test]
230    fn remote_ahead() {
231        let mut local = BTreeMap::new();
232        local.insert(ALICE, BTreeMap::from([(1, 20)]));
233
234        let mut remote = BTreeMap::new();
235        remote.insert(ALICE, BTreeMap::from([(1, 30)]));
236
237        let result = compare_logs(&local, &remote);
238        assert!(result.is_empty());
239    }
240
241    #[test]
242    fn equal() {
243        let mut local = BTreeMap::new();
244        local.insert(ALICE, BTreeMap::from([(1, 20)]));
245
246        let mut remote = BTreeMap::new();
247        remote.insert(ALICE, BTreeMap::from([(1, 20)]));
248
249        let result = compare_logs(&local, &remote);
250        assert!(result.is_empty());
251    }
252
253    #[test]
254    fn remote_missing_multiple_logs() {
255        let mut local = BTreeMap::new();
256        local.insert(ALICE, BTreeMap::from([(1, 5), (2, 10), (3, 15)]));
257
258        let mut remote = BTreeMap::new();
259        remote.insert(ALICE, BTreeMap::from([(1, 5)]));
260
261        let result = compare_logs(&local, &remote);
262        let needs = result.get(&ALICE).unwrap();
263
264        assert_eq!(needs.get(&2), Some(&(None, Some(10))));
265        assert_eq!(needs.get(&3), Some(&(None, Some(15))));
266        assert!(!needs.contains_key(&1));
267    }
268
269    #[test]
270    fn remote_missing_author() {
271        let mut local = BTreeMap::new();
272        local.insert(ALICE, BTreeMap::from([(1, 5)]));
273        local.insert(BOB, BTreeMap::from([(1, 5)]));
274
275        let mut remote = BTreeMap::new();
276        remote.insert(ALICE, BTreeMap::from([(1, 5)]));
277
278        let result = compare_logs(&local, &remote);
279        let needs = result.get(&BOB).unwrap();
280
281        assert_eq!(needs.get(&1), Some(&(None, Some(5))));
282    }
283}