simperby_repository/interpret/
read.rs

1use super::*;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum CommitError {
6    #[error("raw repo error: {0}")]
7    Raw(#[from] raw::Error),
8    #[error("failed to parse commit ({1}): {0}")]
9    Commit(eyre::Error, CommitHash),
10    #[error("reserved state error: {0}")]
11    ReservedState(#[from] super::Error),
12}
13
14pub async fn read_local_branches(
15    raw: &RawRepository,
16) -> Result<HashSet<(Branch, CommitHash)>, Error> {
17    let local_branches = raw.list_branches().await?;
18    let mut result = HashSet::new();
19    // TODO: making this concurrent causes a god damn lifetime annoying error
20    for b in local_branches {
21        result.insert((b.clone(), raw.locate_branch(b).await?));
22    }
23    Ok(result)
24}
25
26pub async fn get_last_finalized_block_commit_hash(
27    raw: &RawRepository,
28) -> Result<CommitHash, Error> {
29    raw.locate_branch(FINALIZED_BRANCH_NAME.into())
30        .await
31        .map_err(|e| match e {
32            raw::Error::NotFound(_) => {
33                eyre!(IntegrityError::new(
34                    "cannot locate `finalized` branch".to_string()
35                ))
36            }
37            _ => eyre!(e),
38        })
39}
40
41pub async fn read_last_finalized_reserved_state(
42    raw: &RawRepository,
43) -> Result<ReservedState, Error> {
44    Ok(raw
45        .read_reserved_state_at_commit(get_last_finalized_block_commit_hash(raw).await?)
46        .await?)
47}
48
49pub async fn read_last_finalized_block_header(raw: &RawRepository) -> Result<BlockHeader, Error> {
50    let commit_hash = raw.locate_branch(FINALIZED_BRANCH_NAME.into()).await?;
51    let semantic_commit = raw.read_semantic_commit(commit_hash).await?;
52    let commit = format::from_semantic_commit(semantic_commit).map_err(|e| eyre!(e))?;
53    if let Commit::Block(block_header) = commit {
54        Ok(block_header)
55    } else {
56        Err(eyre!(IntegrityError {
57            msg: "`finalized` branch is not on a block".to_owned(),
58        }))
59    }
60}
61
62pub async fn read_last_finalization_proof(
63    raw: &RawRepository,
64) -> Result<LastFinalizationProof, Error> {
65    if let Ok(last_finalization_proof) = format::fp_from_semantic_commit(
66        raw.read_semantic_commit(raw.locate_branch(FP_BRANCH_NAME.into()).await.map_err(
67            |e| match e {
68                raw::Error::NotFound(_) => {
69                    eyre!(IntegrityError::new("cannot locate `fp` branch".to_string()))
70                }
71                _ => eyre!(e),
72            },
73        )?)
74        .await?,
75    ) {
76        Ok(last_finalization_proof)
77    } else {
78        Err(eyre!(IntegrityError {
79            msg: "`fp` branch is not on a finalization proof".to_owned(),
80        }))
81    }
82}
83
84pub async fn read_finalization_info(
85    raw: &RawRepository,
86    height: BlockHeight,
87) -> Result<FinalizationInfo, Error> {
88    let finalized_block_header = read_last_finalized_block_header(raw).await?;
89    let last_block_height = finalized_block_header.height;
90
91    if height == last_block_height {
92        read_last_finalization_info(raw).await
93    } else {
94        let initial_commit = raw.get_initial_commit().await?;
95        let finalized_commit_hash = get_last_finalized_block_commit_hash(raw).await?;
96
97        let commits = raw
98            .query_commit_path(initial_commit, finalized_commit_hash)
99            .await?;
100        let commits = stream::iter(
101            commits
102                .iter()
103                .cloned()
104                .map(|c| async move { raw.read_semantic_commit(c).await.map(|x| (x, c)) }),
105        )
106        .buffered(256)
107        .collect::<Vec<_>>()
108        .await;
109        let commits = commits.into_iter().collect::<Result<Vec<_>, _>>()?;
110        let commits = commits
111            .into_iter()
112            .filter(|(commit, _hash)| {
113                commit.title == format!(">block: {height}")
114                    || commit.title == format!(">block: {}", height + 1)
115            })
116            .collect::<Vec<_>>();
117
118        let header: BlockHeader = serde_spb::from_str(&commits[0].0.body)?;
119        let next_header: BlockHeader = serde_spb::from_str(&commits[1].0.body)?;
120        let commit_hash = commits[0].1;
121        let reserved_state = raw.read_reserved_state_at_commit(commit_hash).await?;
122        let proof = next_header.prev_block_finalization_proof;
123        Ok(FinalizationInfo {
124            header,
125            commit_hash,
126            reserved_state,
127            proof,
128        })
129    }
130}
131
132pub async fn read_last_finalization_info(raw: &RawRepository) -> Result<FinalizationInfo, Error> {
133    let header = read_last_finalized_block_header(raw).await?;
134    let commit_hash = get_last_finalized_block_commit_hash(raw).await?;
135    let reserved_state = read_last_finalized_reserved_state(raw).await?;
136    let proof = read_last_finalization_proof(raw).await?.proof;
137    Ok(FinalizationInfo {
138        header,
139        commit_hash,
140        reserved_state,
141        proof,
142    })
143}
144
145pub(crate) async fn read_raw_commits(
146    raw: &RawRepository,
147    ancestor: CommitHash,
148    descendant: CommitHash,
149) -> Result<Vec<(RawCommit, CommitHash)>, Error> {
150    let commits = raw.query_commit_path(ancestor, descendant).await?;
151    let commits = stream::iter(
152        commits
153            .iter()
154            .cloned()
155            .map(|c| async move { raw.read_commit(c).await.map(|x| (x, c)) }),
156    )
157    .buffered(256)
158    .collect::<Vec<_>>()
159    .await;
160    let commits = commits.into_iter().collect::<Result<Vec<_>, _>>()?;
161    Ok(commits)
162}
163
164pub(crate) async fn read_commits(
165    raw: &RawRepository,
166    ancestor: CommitHash,
167    descendant: CommitHash,
168) -> Result<Vec<(Commit, CommitHash)>, CommitError> {
169    let commits = raw.query_commit_path(ancestor, descendant).await?;
170    let commits = stream::iter(
171        commits
172            .iter()
173            .cloned()
174            .map(|c| async move { raw.read_semantic_commit(c).await.map(|x| (x, c)) }),
175    )
176    .buffered(256)
177    .collect::<Vec<_>>()
178    .await;
179    let commits = commits.into_iter().collect::<Result<Vec<_>, _>>()?;
180    let commits = commits
181        .into_iter()
182        .map(|(commit, hash)| {
183            from_semantic_commit(commit)
184                .map_err(|e| (e, hash))
185                .map(|x| (x, hash))
186        })
187        .collect::<Result<Vec<_>, _>>()
188        .map_err(|(e, c)| CommitError::Commit(e, c))?;
189    Ok(commits)
190}
191
192/// Reads the sequence of commits from the last finalized block to the given commit,
193/// and verifies them, and returns the CSV that all commits have been applied on.
194/// It does not accept a last finalization proof commit.
195///
196/// - Returns `Ok(Ok(commit))` if the branch (taking the given commit as a tip) is valid.
197/// - Returns `Ok(Err(error))` if the branch is invalid.
198/// - Returns `Err(error)` if there was an error reading the commits.
199pub async fn read_and_verify_commits_from_last_finalized_block(
200    raw: &RawRepository,
201    commit_hash: CommitHash,
202) -> Result<Result<CommitSequenceVerifier, Error>, Error> {
203    let lfi = read_last_finalization_info(raw).await?;
204
205    if raw.find_merge_base(lfi.commit_hash, commit_hash).await? != lfi.commit_hash {
206        return Ok(Err(eyre!(
207            "the given commit is not a descendant of the last finalized block."
208        )));
209    }
210
211    if format::fp_from_semantic_commit(raw.read_semantic_commit(commit_hash).await?).is_ok() {
212        return Ok(Err(eyre!(
213            "the given commit is a finalization proof commit."
214        )));
215    }
216
217    let commits = read_commits(raw, lfi.commit_hash, commit_hash).await?;
218    let mut csv = CommitSequenceVerifier::new(lfi.header, lfi.reserved_state).map_err(|e| {
219        IntegrityError::new(format!("finalized branch is not accepted by CSV: {e}"))
220    })?;
221
222    for commit in commits {
223        if let Err(e) = csv.apply_commit(&commit.0) {
224            return Ok(Err(eyre!(e)));
225        }
226    }
227    Ok(Ok(csv))
228}
229
230pub async fn read_commit(raw: &RawRepository, commit_hash: CommitHash) -> Result<Commit, Error> {
231    let semantic_commit = raw.read_semantic_commit(commit_hash).await?;
232    format::from_semantic_commit(semantic_commit).map_err(|e| eyre!(e))
233}
234
235pub async fn read_agendas(raw: &RawRepository) -> Result<Vec<(CommitHash, Hash256)>, Error> {
236    let mut agendas: Vec<(CommitHash, Hash256)> = vec![];
237    let branches = read_local_branches(raw).await?;
238    let last_header_commit_hash = raw.locate_branch(FINALIZED_BRANCH_NAME.into()).await?;
239    for (branch, branch_commit_hash) in branches {
240        // Check if the branch is an agenda branch
241        if branch.as_str().starts_with("a-") {
242            // Check if the agenda branch is rebased on top of the `finalized` branch
243            let find_merge_base_result = raw
244                .find_merge_base(last_header_commit_hash, branch_commit_hash)
245                .await
246                .map_err(|e| match e {
247                    raw::Error::NotFound(_) => {
248                        eyre!(IntegrityError::new(format!(
249                            "cannot find merge base for branch {branch} and finalized branch"
250                        )))
251                    }
252                    _ => eyre!(e),
253                })?;
254
255            if last_header_commit_hash != find_merge_base_result {
256                log::warn!(
257                    "branch {} should be rebased on top of the {} branch",
258                    branch,
259                    FINALIZED_BRANCH_NAME
260                );
261                continue;
262            }
263
264            // Push currently valid and height-acceptable agendas to the list
265            let commits = read_commits(raw, last_header_commit_hash, branch_commit_hash).await?;
266            let last_header = read_last_finalized_block_header(raw).await?;
267            for (commit, hash) in commits {
268                if let Commit::Agenda(agenda) = commit {
269                    if agenda.height == last_header.height + 1 {
270                        agendas.push((hash, agenda.to_hash256()));
271                    }
272                }
273            }
274        }
275    }
276    Ok(agendas)
277}
278
279pub async fn read_governance_approved_agendas(
280    raw: &RawRepository,
281) -> Result<Vec<(CommitHash, Hash256)>, Error> {
282    let mut agendas: Vec<(CommitHash, Hash256)> = vec![];
283    let branches = read_local_branches(raw).await?;
284    let last_header_commit_hash = raw.locate_branch(FINALIZED_BRANCH_NAME.into()).await?;
285    for (branch, branch_commit_hash) in branches {
286        // Check if the branch is an agenda branch
287        if branch.as_str().starts_with("a-") {
288            // Check if the agenda branch is rebased on top of the `finalized` branch
289            let find_merge_base_result = raw
290                .find_merge_base(last_header_commit_hash, branch_commit_hash)
291                .await
292                .map_err(|e| match e {
293                    raw::Error::NotFound(_) => {
294                        eyre!(IntegrityError::new(format!(
295                            "cannot find merge base for branch {branch} and finalized branch"
296                        )))
297                    }
298                    _ => eyre!(e),
299                })?;
300
301            if last_header_commit_hash != find_merge_base_result {
302                log::warn!(
303                    "branch {} should be rebased on top of the {} branch",
304                    branch,
305                    FINALIZED_BRANCH_NAME
306                );
307                continue;
308            }
309
310            // Push currently valid and height-acceptable agendas to the list
311            let commits = read_commits(raw, last_header_commit_hash, branch_commit_hash).await?;
312            let last_header = read_last_finalized_block_header(raw).await?;
313            for (commit, hash) in commits {
314                if let Commit::AgendaProof(agenda_proof) = commit {
315                    if agenda_proof.height == last_header.height + 1 {
316                        agendas.push((hash, agenda_proof.to_hash256()));
317                    }
318                }
319            }
320        }
321    }
322    Ok(agendas)
323}
324
325pub async fn read_blocks(raw: &RawRepository) -> Result<Vec<(CommitHash, Hash256)>, Error> {
326    let mut blocks: Vec<(CommitHash, Hash256)> = vec![];
327    let branches = read_local_branches(raw).await?;
328    let last_header_commit_hash = raw.locate_branch(FINALIZED_BRANCH_NAME.into()).await?;
329    for (branch, branch_commit_hash) in branches {
330        // Check if the branch is a block branch
331        if branch.as_str().starts_with("b-") {
332            // Check if the block branch is rebased on top of the `finalized` branch
333            let find_merge_base_result = raw
334                .find_merge_base(last_header_commit_hash, branch_commit_hash)
335                .await
336                .map_err(|e| match e {
337                    raw::Error::NotFound(_) => {
338                        eyre!(IntegrityError::new(format!(
339                            "cannot find merge base for branch {branch} and finalized branch"
340                        )))
341                    }
342                    _ => eyre!(e),
343                })?;
344            if last_header_commit_hash != find_merge_base_result {
345                log::warn!(
346                    "branch {} should be rebased on top of the {} branch",
347                    branch,
348                    FINALIZED_BRANCH_NAME
349                );
350                continue;
351            }
352
353            // Push currently valid and height-acceptable blocks to the list
354            let commits = read_commits(raw, last_header_commit_hash, branch_commit_hash).await?;
355            let last_header = read_last_finalized_block_header(raw).await?;
356            for (commit, hash) in commits {
357                if let Commit::Block(block_header) = commit {
358                    if block_header.height == last_header.height + 1 {
359                        blocks.push((hash, block_header.to_hash256()));
360                    }
361                }
362            }
363        }
364    }
365    Ok(blocks)
366}
367
368pub async fn check_gitignore(raw: &RawRepository) -> Result<bool, Error> {
369    let path = raw.get_working_directory_path().await?;
370    let path = std::path::Path::new(&path).join(".gitignore");
371    if !path.exists() {
372        return Ok(false);
373    }
374    let file = tokio::fs::File::open(path).await?;
375    let mut lines = tokio::io::BufReader::new(file).lines();
376    while let Some(line) = lines.next_line().await? {
377        if line == ".simperby/" {
378            return Ok(true);
379        }
380    }
381    Ok(false)
382}