p2panda_rs/entry/
entry.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use std::convert::TryInto;
4use std::hash::Hash as StdHash;
5
6use bamboo_rs_core_ed25519_yasmf::Entry as BambooEntry;
7
8use crate::entry::encode::sign_entry;
9use crate::entry::error::EntryBuilderError;
10use crate::entry::traits::AsEntry;
11use crate::entry::{LogId, SeqNum, Signature};
12use crate::hash::Hash;
13use crate::identity::{KeyPair, PublicKey};
14use crate::operation::EncodedOperation;
15
16/// Create and sign new `Entry` instances.
17#[derive(Clone, Debug, Default)]
18pub struct EntryBuilder {
19    /// Used log for this entry.
20    log_id: LogId,
21
22    /// Sequence number of this entry.
23    seq_num: SeqNum,
24
25    /// Hash of skiplink Bamboo entry.
26    skiplink: Option<Hash>,
27
28    /// Hash of previous Bamboo entry.
29    backlink: Option<Hash>,
30}
31
32impl EntryBuilder {
33    /// Returns a new instance of `EntryBuilder`.
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Set log id of entry.
39    pub fn log_id(mut self, log_id: &LogId) -> Self {
40        self.log_id = log_id.to_owned();
41        self
42    }
43
44    /// Set sequence number of entry.
45    pub fn seq_num(mut self, seq_num: &SeqNum) -> Self {
46        self.seq_num = seq_num.to_owned();
47        self
48    }
49
50    /// Set skiplink hash of entry.
51    pub fn skiplink(mut self, hash: &Hash) -> Self {
52        self.skiplink = Some(hash.to_owned());
53        self
54    }
55
56    /// Set backlink hash of entry.
57    pub fn backlink(mut self, hash: &Hash) -> Self {
58        self.backlink = Some(hash.to_owned());
59        self
60    }
61
62    /// Signs entry and secures payload with the author's key pair, returns a new `Entry` instance.
63    ///
64    /// An `EncodedOperation` is required here for the entry payload. The entry is "pointing" at
65    /// the payload to secure and authenticate it. Later on, the payload can theoretically be
66    /// deleted when it is not needed anymore.
67    ///
68    /// Using this method we can assume that the entry will be correctly signed. This applies only
69    /// basic checks if the backlink and skiplink is correctly set for the given sequence number
70    /// (#E3). Please note though that this method can not check for correct log integrity!
71    pub fn sign(
72        &self,
73        encoded_operation: &EncodedOperation,
74        key_pair: &KeyPair,
75    ) -> Result<Entry, EntryBuilderError> {
76        let entry = sign_entry(
77            &self.log_id,
78            &self.seq_num,
79            self.skiplink.as_ref(),
80            self.backlink.as_ref(),
81            encoded_operation,
82            key_pair,
83        )?;
84
85        Ok(entry)
86    }
87}
88
89/// Entry of an append-only log based on [`Bamboo`] specification.
90///
91/// Bamboo entries are the main data type of p2panda. They describe the actual data in the p2p
92/// network and are shared between nodes. Entries are organised in a distributed, single-writer
93/// append-only log structure, created and signed by holders of private keys and stored inside the
94/// node's database.
95///
96/// Entries are separated from the actual (off-chain) data to be able to delete application data
97/// without loosing the integrity of the log. Payload data is formatted as "operations" in p2panda.
98/// Each entry only holds a hash of the operation payload, this is why an [`Operation`] instance is
99/// required during entry signing.
100///
101/// It is not possible to directly create an `Entry` instance without validation, use the
102/// `EntryBuilder` to programmatically create and sign one or decode it from bytes via the
103/// `EncodedEntry` struct.
104///
105/// [`Bamboo`]: https://github.com/AljoschaMeyer/bamboo
106#[derive(Debug, Clone, Eq, PartialEq, StdHash)]
107pub struct Entry {
108    /// PublicKey of this entry.
109    pub(crate) public_key: PublicKey,
110
111    /// Used log for this entry.
112    pub(crate) log_id: LogId,
113
114    /// Sequence number of this entry.
115    pub(crate) seq_num: SeqNum,
116
117    /// Hash of skiplink Bamboo entry.
118    pub(crate) skiplink: Option<Hash>,
119
120    /// Hash of previous Bamboo entry.
121    pub(crate) backlink: Option<Hash>,
122
123    /// Byte size of payload.
124    pub(crate) payload_size: u64,
125
126    /// Hash of payload.
127    pub(crate) payload_hash: Hash,
128
129    /// Ed25519 signature of entry.
130    pub(crate) signature: Signature,
131}
132
133impl AsEntry for Entry {
134    /// Returns public key of entry.
135    fn public_key(&self) -> &PublicKey {
136        &self.public_key
137    }
138
139    /// Returns log id of entry.
140    fn log_id(&self) -> &LogId {
141        &self.log_id
142    }
143
144    /// Returns sequence number of entry.
145    fn seq_num(&self) -> &SeqNum {
146        &self.seq_num
147    }
148
149    /// Returns hash of skiplink entry when given.
150    fn skiplink(&self) -> Option<&Hash> {
151        self.skiplink.as_ref()
152    }
153
154    /// Returns hash of backlink entry when given.
155    fn backlink(&self) -> Option<&Hash> {
156        self.backlink.as_ref()
157    }
158
159    /// Returns payload size of operation.
160    fn payload_size(&self) -> u64 {
161        self.payload_size
162    }
163
164    /// Returns payload hash of operation.
165    fn payload_hash(&self) -> &Hash {
166        &self.payload_hash
167    }
168
169    /// Returns signature of entry.
170    fn signature(&self) -> &Signature {
171        &self.signature
172    }
173}
174
175impl From<BambooEntry<&[u8], &[u8]>> for Entry {
176    fn from(entry: BambooEntry<&[u8], &[u8]>) -> Self {
177        // Convert all hashes into our types
178        let backlink: Option<Hash> = entry.backlink.map(|link| (&link).into());
179        let skiplink: Option<Hash> = entry.lipmaa_link.map(|link| (&link).into());
180        let payload_hash: Hash = (&entry.payload_hash).into();
181
182        // Unwrap as we assume that there IS a signature coming from bamboo struct at this point
183        let signature = entry.sig.expect("signature expected").into();
184
185        // Unwrap as the sequence number was already checked when decoding the bytes into the
186        // bamboo struct
187        let seq_num = entry.seq_num.try_into().expect("invalid sequence number");
188
189        Entry {
190            public_key: (&entry.author).into(),
191            log_id: entry.log_id.into(),
192            seq_num,
193            skiplink,
194            backlink,
195            payload_hash,
196            payload_size: entry.payload_size,
197            signature,
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use rstest::rstest;
205
206    use crate::entry::traits::AsEntry;
207    use crate::entry::{LogId, SeqNum};
208    use crate::hash::Hash;
209    use crate::identity::KeyPair;
210    use crate::operation::EncodedOperation;
211    use crate::test_utils::fixtures::{encoded_operation, key_pair, random_hash};
212
213    use super::EntryBuilder;
214
215    #[rstest]
216    fn entry_builder(
217        #[from(random_hash)] entry_hash: Hash,
218        encoded_operation: EncodedOperation,
219        key_pair: KeyPair,
220    ) {
221        let log_id = LogId::new(92);
222        let seq_num = SeqNum::new(14002).unwrap();
223
224        let entry = EntryBuilder::new()
225            .log_id(&log_id)
226            .seq_num(&seq_num)
227            .backlink(&entry_hash)
228            .sign(&encoded_operation, &key_pair)
229            .unwrap();
230
231        assert_eq!(entry.public_key(), &key_pair.public_key());
232        assert_eq!(entry.log_id(), &log_id);
233        assert_eq!(entry.seq_num(), &seq_num);
234        assert_eq!(entry.skiplink(), None);
235        assert_eq!(entry.backlink(), Some(&entry_hash));
236        assert_eq!(entry.payload_hash(), &encoded_operation.hash());
237        assert_eq!(entry.payload_size(), encoded_operation.size());
238    }
239
240    #[rstest]
241    fn entry_builder_validation(
242        #[from(random_hash)] entry_hash_1: Hash,
243        #[from(random_hash)] entry_hash_2: Hash,
244        encoded_operation: EncodedOperation,
245        key_pair: KeyPair,
246    ) {
247        // The first entry in a log doesn't need and cannot have references to previous entries
248        assert!(EntryBuilder::new()
249            .sign(&encoded_operation, &key_pair)
250            .is_ok());
251
252        // Can not have back- and skiplinks on first entry
253        assert!(EntryBuilder::new()
254            .skiplink(&entry_hash_1)
255            .backlink(&entry_hash_2)
256            .sign(&encoded_operation, &key_pair)
257            .is_err());
258
259        // Needs backlink on second entry
260        assert!(EntryBuilder::new()
261            .seq_num(&SeqNum::new(2).unwrap())
262            .backlink(&entry_hash_1)
263            .sign(&encoded_operation, &key_pair)
264            .is_ok());
265
266        assert!(EntryBuilder::new()
267            .seq_num(&SeqNum::new(2).unwrap())
268            .sign(&encoded_operation, &key_pair)
269            .is_err());
270
271        // Needs skiplink on forth entry
272        assert!(EntryBuilder::new()
273            .seq_num(&SeqNum::new(4).unwrap())
274            .backlink(&entry_hash_1)
275            .skiplink(&entry_hash_2)
276            .sign(&encoded_operation, &key_pair)
277            .is_ok());
278
279        assert!(EntryBuilder::new()
280            .seq_num(&SeqNum::new(4).unwrap())
281            .backlink(&entry_hash_1)
282            .sign(&encoded_operation, &key_pair)
283            .is_err());
284    }
285
286    #[rstest]
287    fn entry_links_methods(
288        #[from(random_hash)] entry_hash_1: Hash,
289        #[from(random_hash)] entry_hash_2: Hash,
290        encoded_operation: EncodedOperation,
291        key_pair: KeyPair,
292    ) {
293        // First entry does not return any backlink or skiplink sequence number
294        let entry = EntryBuilder::new()
295            .sign(&encoded_operation, &key_pair)
296            .unwrap();
297
298        assert_eq!(entry.seq_num_backlink(), None);
299        // @TODO: This fails ..
300        // https://github.com/p2panda/p2panda/issues/417
301        // assert_eq!(entry.seq_num_skiplink(), None);
302        assert!(!entry.is_skiplink_required());
303
304        // Second entry returns sequence number for backlink
305        let entry = EntryBuilder::new()
306            .seq_num(&SeqNum::new(2).unwrap())
307            .backlink(&entry_hash_1)
308            .sign(&encoded_operation, &key_pair)
309            .unwrap();
310
311        assert_eq!(entry.seq_num_backlink(), Some(SeqNum::new(1).unwrap()));
312        // @TODO: This fails ..
313        // https://github.com/p2panda/p2panda/issues/417
314        // assert_eq!(entry.seq_num_skiplink(), None);
315        assert!(!entry.is_skiplink_required());
316
317        // Fourth entry returns sequence number for backlink and skiplink
318        let entry = EntryBuilder::new()
319            .seq_num(&SeqNum::new(4).unwrap())
320            .backlink(&entry_hash_1)
321            .skiplink(&entry_hash_2)
322            .sign(&encoded_operation, &key_pair)
323            .unwrap();
324
325        assert_eq!(entry.seq_num_backlink(), Some(SeqNum::new(3).unwrap()));
326        assert_eq!(entry.seq_num_skiplink(), Some(SeqNum::new(1).unwrap()));
327        assert!(entry.is_skiplink_required());
328    }
329}