p2panda_rs/entry/
encode.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! Methods to sign and encode an entry.
4//!
5//! Create a new `Entry` instance using the `EntryBuilder` or the alternative low-level
6//! `sign_entry` method which takes in the entry arguments and `KeyPair` for signing. Use
7//! `encode_entry` to create an `EncodedEntry` instance which can then be serialised and sent to a
8//! p2panda node.
9//!
10//! ```text
11//! ┌─────┐                     ┌────────────┐
12//! │Entry│ ──encode_entry()──► │EncodedEntry│ ─────► bytes
13//! └─────┘                     └────────────┘
14//! ```
15use bamboo_rs_core_ed25519_yasmf::entry::{is_lipmaa_required, MAX_ENTRY_SIZE};
16use bamboo_rs_core_ed25519_yasmf::{Entry as BambooEntry, Signature as BambooSignature};
17
18use crate::entry::error::EncodeEntryError;
19use crate::entry::traits::AsEntry;
20use crate::entry::validate::validate_links;
21use crate::entry::{EncodedEntry, Entry, LogId, SeqNum};
22use crate::hash::Hash;
23use crate::identity::KeyPair;
24use crate::operation::EncodedOperation;
25
26/// Takes entry arguments (log id, sequence number, etc.), operation payload and a [`KeyPair`],
27/// returns signed `Entry` instance.
28///
29/// The result can be converted to an `EncodedEntry` using the `encode_entry` method and is then
30/// ready to be sent to a p2panda node.
31///
32/// Using this method we can assume that the entry will be correctly signed. This applies only
33/// basic checks if the backlink and skiplink is correctly set for the given sequence number (#E3).
34/// Please note though that this method not check for correct log integrity!
35pub fn sign_entry(
36    log_id: &LogId,
37    seq_num: &SeqNum,
38    skiplink_hash: Option<&Hash>,
39    backlink_hash: Option<&Hash>,
40    payload: &EncodedOperation,
41    key_pair: &KeyPair,
42) -> Result<Entry, EncodeEntryError> {
43    // Generate payload hash and size from operation bytes
44    let payload_hash = payload.hash();
45    let payload_size = payload.size();
46
47    // Convert entry links to bamboo-rs `YasmfHash` type
48    let backlink = backlink_hash.map(|link| link.into());
49    let lipmaa_link = if is_lipmaa_required(seq_num.as_u64()) {
50        skiplink_hash.map(|link| link.into())
51    } else {
52        // Omit skiplink when it is the same as backlink, this saves us some bytes
53        None
54    };
55
56    // Create Bamboo entry instance.
57    //
58    // See: https://github.com/AljoschaMeyer/bamboo#encoding for encoding details and definition of
59    // entry fields.
60    let entry: BambooEntry<_, &[u8]> = BambooEntry {
61        is_end_of_feed: false,
62        author: key_pair.public_key().into(),
63        log_id: log_id.as_u64(),
64        seq_num: seq_num.as_u64(),
65        lipmaa_link,
66        backlink,
67        payload_size,
68        payload_hash: (&payload_hash).into(),
69        sig: None,
70    };
71
72    let mut entry_bytes = [0u8; MAX_ENTRY_SIZE];
73
74    // Get unsigned entry bytes
75    let entry_size = entry.encode(&mut entry_bytes)?;
76
77    // Sign entry
78    let signature = key_pair.sign(&entry_bytes[..entry_size]);
79
80    let signed_entry = Entry {
81        public_key: key_pair.public_key(),
82        log_id: log_id.to_owned(),
83        seq_num: seq_num.to_owned(),
84        skiplink: skiplink_hash.cloned(),
85        backlink: backlink_hash.cloned(),
86        payload_size,
87        payload_hash,
88        signature: signature.into(),
89    };
90
91    // Make sure the links are correct (#E3)
92    validate_links(&signed_entry)?;
93
94    Ok(signed_entry)
95}
96
97/// Encodes an entry into bytes and returns them as `EncodedEntry` instance. After encoding this is
98/// ready to be sent to a p2panda node.
99///
100/// This method only fails if something went wrong with the encoder or if a backlink was provided
101/// on an entry with sequence number 1 (#E3).
102pub fn encode_entry(entry: &Entry) -> Result<EncodedEntry, EncodeEntryError> {
103    let signature_bytes = entry.signature().into_bytes();
104
105    let entry: BambooEntry<_, &[u8]> = BambooEntry {
106        is_end_of_feed: false,
107        author: entry.public_key().into(),
108        log_id: entry.log_id().as_u64(),
109        seq_num: entry.seq_num().as_u64(),
110        lipmaa_link: entry.skiplink().map(|link| link.into()),
111        backlink: entry.backlink().map(|link| link.into()),
112        payload_size: entry.payload_size(),
113        payload_hash: entry.payload_hash().into(),
114        sig: Some(BambooSignature(&signature_bytes[..])),
115    };
116
117    let mut entry_bytes = [0u8; MAX_ENTRY_SIZE];
118
119    // Together with signing the entry before, one could think that encoding the entry a second
120    // time is a waste, but actually it is the only way to do signatures. This step is not
121    // redundant.
122    //
123    // Calling this also checks if the backlink is not set for the first entry (#E3).
124    let signed_entry_size = entry.encode(&mut entry_bytes)?;
125
126    Ok(EncodedEntry::from_bytes(&entry_bytes[..signed_entry_size]))
127}
128
129/// High-level method which applies both signing and encoding an entry into one step, returns an
130/// `EncodedEntry` instance which is ready to be sent to a p2panda node.
131///
132/// See low-level methods for details.
133pub fn sign_and_encode_entry(
134    log_id: &LogId,
135    seq_num: &SeqNum,
136    skiplink_hash: Option<&Hash>,
137    backlink_hash: Option<&Hash>,
138    payload: &EncodedOperation,
139    key_pair: &KeyPair,
140) -> Result<EncodedEntry, EncodeEntryError> {
141    let entry = sign_entry(
142        log_id,
143        seq_num,
144        skiplink_hash,
145        backlink_hash,
146        payload,
147        key_pair,
148    )?;
149
150    let encoded_entry = encode_entry(&entry)?;
151
152    Ok(encoded_entry)
153}
154
155#[cfg(test)]
156mod tests {
157    use std::collections::HashMap;
158    use std::convert::TryInto;
159
160    use rstest::rstest;
161    use rstest_reuse::apply;
162
163    use crate::entry::traits::AsEncodedEntry;
164    use crate::entry::{EncodedEntry, Entry, LogId, SeqNum};
165    use crate::hash::Hash;
166    use crate::identity::KeyPair;
167    use crate::operation::EncodedOperation;
168    use crate::test_utils::fixtures::{
169        encoded_entry, encoded_operation, entry, key_pair, random_hash, Fixture,
170    };
171    use crate::test_utils::templates::{many_valid_entries, version_fixtures};
172
173    use super::{encode_entry, sign_and_encode_entry, sign_entry};
174
175    #[rstest]
176    #[case(1, false, false)]
177    #[case(2, true, false)]
178    #[case(3, true, false)]
179    #[case(4, true, true)]
180    #[case(5, true, false)]
181    #[case(6, true, false)]
182    #[case(7, true, false)]
183    #[case(8, true, true)]
184    #[case(9, true, false)]
185    #[should_panic]
186    #[case::backlink_missing(2, false, false)]
187    #[should_panic]
188    #[case::skiplink_missing(4, true, false)]
189    fn signing_entry_validation(
190        #[case] seq_num: u64,
191        #[case] backlink: bool,
192        #[case] skiplink: bool,
193        #[from(random_hash)] entry_hash_1: Hash,
194        #[from(random_hash)] entry_hash_2: Hash,
195        #[from(encoded_operation)] operation: EncodedOperation,
196        #[from(key_pair)] key_pair: KeyPair,
197    ) {
198        sign_entry(
199            &LogId::default(),
200            &seq_num.try_into().unwrap(),
201            skiplink.then_some(&entry_hash_1),
202            backlink.then_some(&entry_hash_2),
203            &operation,
204            &key_pair,
205        )
206        .unwrap();
207
208        sign_and_encode_entry(
209            &LogId::default(),
210            &seq_num.try_into().unwrap(),
211            skiplink.then_some(&entry_hash_1),
212            backlink.then_some(&entry_hash_2),
213            &operation,
214            &key_pair,
215        )
216        .unwrap();
217    }
218
219    #[rstest]
220    fn encode_entry_to_hex(#[from(entry)] entry: Entry) {
221        assert_eq!(
222            encode_entry(&entry).unwrap().to_string(),
223            concat!(
224                "002f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc",
225                "960001f901b200205610cb28a37deed208bd52980f54132a062a5f8e3eac7fb9",
226                "e6d404f3b1b76b32e6897d47a56691d0d2ea2ba14c676a4154d7226d678c6fbe",
227                "b0a2ffb70ad245c942b0194e7ac73f38902c08d19a4a44cfa73083e296c256f3",
228                "c7be49843e52a402"
229            )
230        )
231    }
232
233    #[rstest]
234    fn invalid_sign_entry_links(
235        #[from(random_hash)] entry_hash: Hash,
236        #[from(encoded_operation)] operation: EncodedOperation,
237        #[from(key_pair)] key_pair: KeyPair,
238    ) {
239        assert_eq!(
240            sign_entry(
241                &LogId::new(9),
242                &SeqNum::new(4).unwrap(),
243                Some(&entry_hash),
244                None,
245                &operation,
246                &key_pair
247            )
248            .unwrap_err()
249            .to_string(),
250            "backlink and skiplink not valid for this sequence number"
251        );
252
253        assert_eq!(
254            sign_and_encode_entry(
255                &LogId::new(9),
256                &SeqNum::new(4).unwrap(),
257                Some(&entry_hash),
258                None,
259                &operation,
260                &key_pair
261            )
262            .unwrap_err()
263            .to_string(),
264            "backlink and skiplink not valid for this sequence number"
265        );
266    }
267
268    #[rstest]
269    fn it_hashes(encoded_entry: EncodedEntry) {
270        // Use encoded entry as key in hash map
271        let mut hash_map = HashMap::new();
272        let key_value = "Value identified by a hash".to_string();
273        hash_map.insert(&encoded_entry, key_value.clone());
274
275        // Check if we can retrieve it again with that key
276        let key_value_retrieved = hash_map.get(&encoded_entry).unwrap().to_owned();
277        assert_eq!(key_value, key_value_retrieved)
278    }
279
280    #[apply(version_fixtures)]
281    fn fixture_encode(#[case] fixture: Fixture) {
282        // Encode fixture
283        let entry_encoded = encode_entry(&fixture.entry).unwrap();
284
285        // Fixture hash should equal newly encoded entry hash
286        assert_eq!(fixture.entry_encoded.hash(), entry_encoded.hash(),);
287    }
288
289    #[apply(many_valid_entries)]
290    fn fixture_encode_valid_entries(#[case] entry: Entry) {
291        assert!(encode_entry(&entry).is_ok());
292    }
293}