simperby_repository/
format.rs

1use crate::{raw::RawCommit, raw::SemanticCommit, UNKNOWN_COMMIT_AUTHOR};
2use eyre::{eyre, Error};
3use regex::Regex;
4use simperby_core::{reserved::ReservedState, *};
5
6/// Converts a commit to a semantic commit.
7pub fn to_semantic_commit(
8    commit: &Commit,
9    mut reserved_state: ReservedState,
10) -> Result<SemanticCommit, Error> {
11    match commit {
12        Commit::Agenda(agenda) => {
13            let title = format!(">agenda: {}", agenda.height);
14            let body = serde_spb::to_string(agenda).unwrap();
15            Ok(SemanticCommit {
16                title,
17                body,
18                diff: Diff::None,
19                author: agenda.author.clone(),
20                timestamp: agenda.timestamp,
21            })
22        }
23        Commit::Block(block_header) => {
24            let title = format!(">block: {}", block_header.height);
25            let body = serde_spb::to_string(block_header).unwrap();
26            Ok(SemanticCommit {
27                title,
28                body,
29                diff: Diff::None,
30                author: if block_header.author == PublicKey::zero() {
31                    "genesis".to_owned()
32                } else {
33                    reserved_state
34                        .query_name(&block_header.author)
35                        .ok_or_else(|| {
36                            eyre!(
37                                "failed to query the name of the author: {}",
38                                block_header.author
39                            )
40                        })?
41                },
42                timestamp: block_header.timestamp,
43            })
44        }
45        Commit::Transaction(transaction) => Ok(SemanticCommit {
46            title: transaction.head.clone(),
47            body: transaction.body.clone(),
48            diff: transaction.diff.clone(),
49            author: transaction.author.clone(),
50            timestamp: transaction.timestamp,
51        }),
52        Commit::AgendaProof(agenda_proof) => {
53            let title = format!(">agenda-proof: {}", agenda_proof.height);
54            let body = serde_spb::to_string(agenda_proof).unwrap();
55            Ok(SemanticCommit {
56                title,
57                body,
58                diff: Diff::None,
59                author: UNKNOWN_COMMIT_AUTHOR.to_owned(),
60                timestamp: agenda_proof.timestamp,
61            })
62        }
63        Commit::ExtraAgendaTransaction(tx) => {
64            let body = serde_spb::to_string(tx).unwrap();
65            match tx {
66                ExtraAgendaTransaction::Delegate(tx) => {
67                    let title = format!(
68                        ">tx-delegate: {} to {}",
69                        tx.data.delegator, tx.data.delegatee
70                    );
71                    let diff = Diff::Reserved(Box::new(reserved_state.apply_delegate(tx).unwrap()));
72                    Ok(SemanticCommit {
73                        title,
74                        body,
75                        diff,
76                        author: tx.data.delegator.clone(),
77                        timestamp: tx.data.timestamp,
78                    })
79                }
80                ExtraAgendaTransaction::Undelegate(tx) => {
81                    let title = format!(">tx-undelegate: {}", tx.data.delegator);
82                    let diff =
83                        Diff::Reserved(Box::new(reserved_state.apply_undelegate(tx).unwrap()));
84                    Ok(SemanticCommit {
85                        title,
86                        body,
87                        diff,
88                        author: tx.data.delegator.clone(),
89                        timestamp: tx.data.timestamp,
90                    })
91                }
92                ExtraAgendaTransaction::Report(_) => {
93                    unimplemented!("report is not implemented yet.")
94                }
95            }
96        }
97        Commit::ChatLog(_) => unimplemented!(),
98    }
99}
100
101/// Converts a semantic commit to a commit.
102///
103/// TODO: retrieve author and timestamp from the commit metadata.
104pub fn from_semantic_commit(semantic_commit: SemanticCommit) -> Result<Commit, Error> {
105    let pattern = Regex::new(
106        r"^>(((agenda)|(block)|(agenda-proof)): (\d+))|((tx-delegate): ((\D+)-(\d+)) to ((\D+)-(\d+)))|((tx-undelegate): ((\D+)-(\d+)))$"
107    )
108    .unwrap();
109    let captures = pattern.captures(&semantic_commit.title);
110    if let Some(captures) = captures {
111        let commit_type = captures
112            .get(2)
113            .or_else(|| captures.get(8))
114            .or_else(|| captures.get(16))
115            .map(|m| m.as_str())
116            .ok_or_else(|| {
117                eyre!(
118                    "failed to parse commit type from the commit title: {}",
119                    semantic_commit.title
120                )
121            })?;
122        match commit_type {
123            "agenda" => {
124                let agenda: Agenda = serde_spb::from_str(&semantic_commit.body)?;
125                let height = captures.get(6).map(|m| m.as_str()).ok_or_else(|| {
126                    eyre!(
127                        "failed to parse height from the commit title: {}",
128                        semantic_commit.title
129                    )
130                })?;
131                let height = height.parse::<u64>()?;
132                if height != agenda.height {
133                    return Err(eyre!(
134                        "agenda height mismatch: expected {}, got {}",
135                        agenda.height,
136                        height
137                    ));
138                }
139                Ok(Commit::Agenda(agenda))
140            }
141            "block" => {
142                let block_header: BlockHeader = serde_spb::from_str(&semantic_commit.body)?;
143                let height = captures.get(6).map(|m| m.as_str()).ok_or_else(|| {
144                    eyre!(
145                        "failed to parse height from the commit title: {}",
146                        semantic_commit.title
147                    )
148                })?;
149                let height = height.parse::<u64>()?;
150                if height != block_header.height {
151                    return Err(eyre!(
152                        "block height mismatch: expected {}, got {}",
153                        block_header.height,
154                        height
155                    ));
156                }
157                Ok(Commit::Block(block_header))
158            }
159            "agenda-proof" => {
160                let agenda_proof: AgendaProof = serde_spb::from_str(&semantic_commit.body)?;
161                let height = captures.get(6).map(|m| m.as_str()).ok_or_else(|| {
162                    eyre!(
163                        "failed to parse height from the commit title: {}",
164                        semantic_commit.title
165                    )
166                })?;
167                let height = height.parse::<u64>()?;
168                if height != agenda_proof.height {
169                    return Err(eyre!(
170                        "agenda-proof height mismatch: expected {}, got {}",
171                        agenda_proof.height,
172                        height
173                    ));
174                }
175                Ok(Commit::AgendaProof(agenda_proof))
176            }
177            "tx-delegate" => {
178                let tx: ExtraAgendaTransaction = serde_spb::from_str(&semantic_commit.body)?;
179                match tx {
180                    ExtraAgendaTransaction::Delegate(ref tx) => {
181                        let delegator = captures.get(9).map(|m| m.as_str()).ok_or_else(|| {
182                            eyre!(
183                                "failed to parse delegator from the commit title: {}",
184                                semantic_commit.title
185                            )
186                        })?;
187                        if delegator != tx.data.delegator {
188                            return Err(eyre!(
189                                "delegator mismatch: expected {}, got {}",
190                                delegator,
191                                tx.data.delegator
192                            ));
193                        }
194                        let delegatee = captures.get(12).map(|m| m.as_str()).ok_or_else(|| {
195                            eyre!(
196                                "failed to parse delegatee from the commit title: {}",
197                                semantic_commit.title
198                            )
199                        })?;
200                        if delegatee != tx.data.delegatee {
201                            return Err(eyre!(
202                                "delegatee mismatch: expected {}, got {}",
203                                delegatee,
204                                tx.data.delegatee
205                            ));
206                        }
207                        Ok(Commit::ExtraAgendaTransaction(
208                            ExtraAgendaTransaction::Delegate(tx.clone()),
209                        ))
210                    }
211                    _ => Err(eyre!("expected delegation transaction, got {:?}", tx)),
212                }
213            }
214            "tx-undelegate" => {
215                let tx: ExtraAgendaTransaction = serde_spb::from_str(&semantic_commit.body)?;
216                match tx {
217                    ExtraAgendaTransaction::Undelegate(ref tx) => {
218                        let delegator = captures.get(17).map(|m| m.as_str()).ok_or_else(|| {
219                            eyre!(
220                                "failed to parse delegator from the commit title: {}",
221                                semantic_commit.title
222                            )
223                        })?;
224                        if delegator != tx.data.delegator {
225                            return Err(eyre!(
226                                "delegator mismatch: expected {}, got {}",
227                                delegator,
228                                tx.data.delegator
229                            ));
230                        }
231                        Ok(Commit::ExtraAgendaTransaction(
232                            ExtraAgendaTransaction::Undelegate(tx.clone()),
233                        ))
234                    }
235                    _ => Err(eyre!("expected undelegation transaction, got {:?}", tx)),
236                }
237            }
238            _ => Err(eyre!("unknown commit type: {}", commit_type)),
239        }
240    } else {
241        Ok(Commit::Transaction(Transaction {
242            author: semantic_commit.author,
243            timestamp: semantic_commit.timestamp,
244            head: semantic_commit.title,
245            body: semantic_commit.body,
246            diff: semantic_commit.diff,
247        }))
248    }
249}
250
251pub fn fp_to_semantic_commit(fp: &LastFinalizationProof) -> SemanticCommit {
252    let title = format!(">fp: {}", fp.height);
253    let body = serde_spb::to_string(&fp).unwrap();
254    SemanticCommit {
255        title,
256        body,
257        diff: Diff::None,
258        author: UNKNOWN_COMMIT_AUTHOR.to_owned(),
259        timestamp: 0,
260    }
261}
262
263pub fn fp_from_semantic_commit(
264    semantic_commit: SemanticCommit,
265) -> Result<LastFinalizationProof, Error> {
266    let pattern = Regex::new(r"^>fp: (\d+)$").unwrap();
267    let captures = pattern.captures(&semantic_commit.title);
268    if let Some(captures) = captures {
269        let height = captures.get(1).map(|m| m.as_str()).ok_or_else(|| {
270            eyre!(
271                "Failed to parse commit height from commit title: {}",
272                semantic_commit.title
273            )
274        })?;
275        let height = height.parse::<u64>()?;
276        let proof: LastFinalizationProof = serde_spb::from_str(&semantic_commit.body)?;
277        if height != proof.height {
278            return Err(eyre!(
279                "proof height mismatch: expected {}, got {}",
280                proof.height,
281                height
282            ));
283        }
284        Ok(proof)
285    } else {
286        Err(eyre!("unknown commit type: {}", semantic_commit.title))
287    }
288}
289
290pub fn raw_commit_to_semantic_commit(raw_commit: RawCommit) -> SemanticCommit {
291    let (title, body) = if let Some((title, body)) = raw_commit.message.split_once("\n\n") {
292        (title.to_string(), body.to_string())
293    } else {
294        (String::new(), String::new())
295    };
296    SemanticCommit {
297        title,
298        body,
299        diff: if raw_commit.diff.is_none() {
300            Diff::None
301        } else {
302            // TODO: should handle cases, `Reserved`, `NonReserved, `General`.
303            unimplemented!()
304        },
305        author: raw_commit.author,
306        timestamp: raw_commit.timestamp / 1000,
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use simperby_core::test_utils::generate_standard_genesis;
314
315    #[test]
316    fn format_transaction_commit() {
317        let (reserved_state, _) = generate_standard_genesis(4);
318        let transaction = Commit::Transaction(Transaction {
319            author: "doesn't matter".to_owned(),
320            timestamp: 0,
321            head: "abc".to_string(),
322            body: "def".to_string(),
323            diff: Diff::None,
324        });
325        assert_eq!(
326            transaction,
327            from_semantic_commit(to_semantic_commit(&transaction, reserved_state).unwrap(),)
328                .unwrap()
329        );
330    }
331
332    #[test]
333    fn format_agenda_commit() {
334        let (reserved_state, _) = generate_standard_genesis(4);
335        let agenda = Commit::Agenda(Agenda {
336            height: 3,
337            author: "doesn't matter".to_owned(),
338            timestamp: 123,
339            transactions_hash: Hash256::hash("hello"),
340            previous_block_hash: Hash256::hash("hello"),
341        });
342        assert_eq!(
343            agenda,
344            from_semantic_commit(to_semantic_commit(&agenda, reserved_state).unwrap(),).unwrap()
345        );
346    }
347
348    #[test]
349    fn format_block_commit() {
350        let (reserved_state, _) = generate_standard_genesis(4);
351        let block = Commit::Block(BlockHeader {
352            height: 3,
353            author: PublicKey::zero(),
354            prev_block_finalization_proof: FinalizationProof {
355                round: 0,
356                signatures: vec![TypedSignature::new(Signature::zero(), PublicKey::zero())],
357            },
358            previous_hash: Hash256::hash("hello1"),
359            timestamp: 0,
360            commit_merkle_root: Hash256::hash("hello2"),
361            repository_merkle_root: Hash256::hash("hello3"),
362            validator_set: vec![(PublicKey::zero(), 1)],
363            version: SIMPERBY_CORE_PROTOCOL_VERSION.to_string(),
364        });
365        assert_eq!(
366            block,
367            from_semantic_commit(to_semantic_commit(&block, reserved_state).unwrap(),).unwrap()
368        );
369    }
370
371    #[test]
372    fn format_agenda_proof_commit() {
373        let (reserved_state, _) = generate_standard_genesis(4);
374        let agenda_proof = Commit::AgendaProof(AgendaProof {
375            height: 3,
376            agenda_hash: Hash256::hash("hello1"),
377            proof: vec![TypedSignature::new(Signature::zero(), PublicKey::zero())],
378            timestamp: 0,
379        });
380        assert_eq!(
381            agenda_proof,
382            from_semantic_commit(to_semantic_commit(&agenda_proof, reserved_state).unwrap(),)
383                .unwrap()
384        );
385    }
386
387    #[test]
388    fn format_extra_agenda_transaction_commit1() {
389        let (reserved_state, keys) = generate_standard_genesis(4);
390        let delegation_transaction_data = DelegationTransactionData {
391            delegator: reserved_state.members[0].name.clone(),
392            delegatee: reserved_state.members[1].name.clone(),
393            governance: true,
394            block_height: 0,
395            timestamp: 0,
396            chain_name: reserved_state.genesis_info.chain_name.clone(),
397        };
398        let delegation_transaction =
399            Commit::ExtraAgendaTransaction(ExtraAgendaTransaction::Delegate(TxDelegate {
400                data: delegation_transaction_data.clone(),
401                proof: TypedSignature::sign(&delegation_transaction_data, &keys[0].1).unwrap(),
402            }));
403        assert_eq!(
404            delegation_transaction,
405            from_semantic_commit(
406                to_semantic_commit(&delegation_transaction, reserved_state).unwrap()
407            )
408            .unwrap()
409        );
410    }
411
412    #[test]
413    fn format_extra_agenda_transaction_commit2() {
414        let (mut reserved_state, keys) = generate_standard_genesis(4);
415        reserved_state.members[0].governance_delegatee = Option::from("member-0000".to_string());
416        reserved_state.members[0].consensus_delegatee = Option::from("member-0000".to_string());
417        let undelegation_transaction_data = UndelegationTransactionData {
418            delegator: reserved_state.members[0].name.clone(),
419            block_height: 0,
420            timestamp: 0,
421            chain_name: reserved_state.genesis_info.chain_name.clone(),
422        };
423        let undelegation_transaction =
424            Commit::ExtraAgendaTransaction(ExtraAgendaTransaction::Undelegate(TxUndelegate {
425                data: undelegation_transaction_data.clone(),
426                proof: TypedSignature::sign(&undelegation_transaction_data, &keys[0].1).unwrap(),
427            }));
428        assert_eq!(
429            undelegation_transaction,
430            from_semantic_commit(
431                to_semantic_commit(&undelegation_transaction, reserved_state.clone()).unwrap()
432            )
433            .unwrap()
434        );
435    }
436
437    #[test]
438    fn format_fp() {
439        let fp = LastFinalizationProof {
440            height: 3,
441            proof: FinalizationProof {
442                round: 0,
443                signatures: vec![
444                    TypedSignature::new(Signature::zero(), PublicKey::zero()),
445                    TypedSignature::new(Signature::zero(), PublicKey::zero()),
446                ],
447            },
448        };
449        assert_eq!(
450            fp,
451            fp_from_semantic_commit(fp_to_semantic_commit(&fp)).unwrap()
452        );
453    }
454}