Skip to main content

amaru_ouroboros_traits/stores/consensus/
mod.rs

1// Copyright 2025 PRAGMA
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub mod in_memory_consensus_store;
16
17use std::{fmt::Display, iter::successors, sync::Arc};
18
19use amaru_kernel::{
20    Block, BlockHeader, HeaderHash, IsHeader, Point, RawBlock, Tip, cardano::network_block::NetworkBlock,
21};
22use thiserror::Error;
23
24use crate::Nonces;
25
26pub trait ReadOnlyChainStore<H>
27where
28    H: IsHeader,
29{
30    /// Try to load a header by its hash.
31    fn load_header(&self, hash: &HeaderHash) -> Option<H>;
32
33    fn get_children(&self, hash: &HeaderHash) -> Vec<HeaderHash>;
34    fn get_anchor_hash(&self) -> HeaderHash;
35    fn get_best_chain_hash(&self) -> HeaderHash;
36
37    /// Load a `HeaderHash` from the best chain.
38    /// Returns `None` if the point is not in the best chain.
39    fn load_from_best_chain(&self, point: &Point) -> Option<HeaderHash>;
40
41    /// Return the next `Point` on the best chain following given
42    /// `Point`, if it exists.
43    fn next_best_chain(&self, point: &Point) -> Option<Point>;
44
45    fn load_block(&self, hash: &HeaderHash) -> Result<Option<RawBlock>, StoreError>;
46    fn get_nonces(&self, header: &HeaderHash) -> Option<Nonces>;
47    fn has_header(&self, hash: &HeaderHash) -> bool;
48
49    /// Retrieve the tip of a block header given its hash.
50    fn load_tip(&self, hash: &HeaderHash) -> Option<Tip> {
51        self.load_header(hash).map(|h| h.tip())
52    }
53
54    /// Return the hashes of the best chain fragment, starting from the anchor.
55    fn retrieve_best_chain(&self) -> Vec<HeaderHash> {
56        let anchor = self.get_anchor_hash();
57        let mut best_chain = vec![];
58        let mut current_hash = self.get_best_chain_hash();
59        while let Some(header) = self.load_header(&current_hash) {
60            best_chain.push(current_hash);
61            if header.hash() != anchor
62                && let Some(parent) = header.parent()
63            {
64                current_hash = parent;
65            } else {
66                break;
67            }
68        }
69        best_chain.reverse();
70        best_chain
71    }
72
73    /// Return the ancestors of the header, including the header itself.
74    /// Stop if the followed chain reaches past the anchor.
75    fn ancestors<'a>(&'a self, start: H) -> Box<dyn Iterator<Item = H> + 'a>
76    where
77        H: 'a,
78    {
79        let anchor = self.get_anchor_hash();
80        let anchor_point = match self.load_header(&anchor) {
81            Some(header) => header.point(),
82            None => Point::Origin,
83        };
84
85        Box::new(successors(Some(start), move |h| {
86            if h.slot() <= anchor_point.slot_or_default() {
87                None
88            } else {
89                h.parent().and_then(|p| self.load_header(&p))
90            }
91        }))
92    }
93
94    /// Return the hashes of the ancestors of the header, including the header hash itself.
95    fn ancestors_hashes<'a>(&'a self, hash: &HeaderHash) -> Box<dyn Iterator<Item = HeaderHash> + 'a>
96    where
97        H: 'a,
98    {
99        if let Some(header) = self.load_header(hash) {
100            Box::new(self.ancestors(header).map(|h| h.hash()))
101        } else {
102            Box::new(vec![*hash].into_iter())
103        }
104    }
105}
106
107/// A chain store interface that exposes diagnostic methods to load raw data.
108pub trait DiagnosticChainStore {
109    /// Load all headers in the store.
110    ///
111    /// NOTE: This can be very expensive for large stores and is only
112    /// used for diagnostics and testing purposes.
113    fn load_headers(&self) -> Box<dyn Iterator<Item = BlockHeader> + '_>;
114
115    /// Load all nonces in the store.
116    fn load_nonces(&self) -> Box<dyn Iterator<Item = (HeaderHash, Nonces)> + '_>;
117    fn load_blocks(&self) -> Box<dyn Iterator<Item = (HeaderHash, RawBlock)> + '_>;
118    fn load_parents_children(&self) -> Box<dyn Iterator<Item = (HeaderHash, Vec<HeaderHash>)> + '_>;
119}
120
121impl<H: IsHeader> ReadOnlyChainStore<H> for Box<dyn ChainStore<H>> {
122    fn load_header(&self, hash: &HeaderHash) -> Option<H> {
123        self.as_ref().load_header(hash)
124    }
125
126    fn get_children(&self, hash: &HeaderHash) -> Vec<HeaderHash> {
127        self.as_ref().get_children(hash)
128    }
129
130    fn get_anchor_hash(&self) -> HeaderHash {
131        self.as_ref().get_anchor_hash()
132    }
133
134    fn get_best_chain_hash(&self) -> HeaderHash {
135        self.as_ref().get_best_chain_hash()
136    }
137
138    fn load_block(&self, hash: &HeaderHash) -> Result<Option<RawBlock>, StoreError> {
139        self.as_ref().load_block(hash)
140    }
141
142    fn get_nonces(&self, header: &HeaderHash) -> Option<Nonces> {
143        self.as_ref().get_nonces(header)
144    }
145
146    fn has_header(&self, hash: &HeaderHash) -> bool {
147        self.as_ref().has_header(hash)
148    }
149
150    fn load_from_best_chain(&self, point: &Point) -> Option<HeaderHash> {
151        self.as_ref().load_from_best_chain(point)
152    }
153
154    fn next_best_chain(&self, point: &Point) -> Option<Point> {
155        self.as_ref().next_best_chain(point)
156    }
157}
158
159/// A simple chain store interface that can store and retrieve headers indexed by their hash.
160pub trait ChainStore<H>: ReadOnlyChainStore<H> + Send + Sync
161where
162    H: IsHeader,
163{
164    fn store_header(&self, header: &H) -> Result<(), StoreError>;
165    fn set_anchor_hash(&self, hash: &HeaderHash) -> Result<(), StoreError>;
166    fn set_best_chain_hash(&self, hash: &HeaderHash) -> Result<(), StoreError>;
167    fn store_block(&self, hash: &HeaderHash, block: &RawBlock) -> Result<(), StoreError>;
168    fn put_nonces(&self, header: &HeaderHash, nonces: &Nonces) -> Result<(), StoreError>;
169
170    /// Roll forward the best chain to the given point.
171    fn roll_forward_chain(&self, point: &Point) -> Result<(), StoreError>;
172
173    /// Rollback the best chain tip at the given point.
174    /// The point must exist on the best chain, and all points on chain after that
175    /// point will be deleted.
176    /// Returns the number of headers that were rolled back.
177    fn rollback_chain(&self, point: &Point) -> Result<usize, StoreError>;
178}
179
180#[derive(Error, PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize)]
181pub enum StoreError {
182    WriteError { error: String },
183    ReadError { error: String },
184    OpenError { error: String },
185    IncompatibleChainStoreVersions { stored: u16, current: u16 },
186}
187
188impl Display for StoreError {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        match self {
191            StoreError::WriteError { error } => write!(f, "WriteError: {}", error),
192            StoreError::ReadError { error } => write!(f, "ReadError: {}", error),
193            StoreError::OpenError { error } => write!(f, "OpenError: {}", error),
194            StoreError::IncompatibleChainStoreVersions { stored, current } => {
195                write!(f, "Incompatible DB Versions: found {}, expected {}", stored, current)
196            }
197        }
198    }
199}
200
201/// Retrieve all blocks from the chain store starting from the anchor to the best chain tip.
202#[cfg(feature = "test-utils")]
203#[expect(clippy::expect_used)]
204pub fn get_blocks(store: Arc<dyn ChainStore<BlockHeader>>) -> Vec<(HeaderHash, Block)> {
205    store
206        .retrieve_best_chain()
207        .iter()
208        .map(|h| {
209            let b = store
210                .load_block(h)
211                .expect("load_block should not raise an error")
212                .expect("missing block for a header on the best chain");
213            (
214                *h,
215                NetworkBlock::try_from(b)
216                    .expect("failed to decode raw block")
217                    .decode_block()
218                    .expect("failed to decode block"),
219            )
220        })
221        .collect()
222}
223
224/// Retrieve all blocks headers from the chain store starting from anchor to the best chain tip.
225#[cfg(feature = "test-utils")]
226#[expect(clippy::expect_used)]
227pub fn get_best_chain_block_headers(store: Arc<dyn ChainStore<BlockHeader>>) -> Vec<BlockHeader> {
228    store
229        .retrieve_best_chain()
230        .iter()
231        .map(|h| store.load_header(h).expect("missing header for the best chain"))
232        .collect()
233}