Skip to main content

miden_node_store/
blocks.rs

1//! File-based storage for raw block data and block proofs.
2//!
3//! Block data is stored under `{store_dir}/{epoch:04x}/block_{block_num:08x}.dat`, and proof data
4//! for proven blocks is stored under `{store_dir}/{epoch:04x}/proof_{block_num:08x}.dat`.
5//!
6//! The epoch is derived from the 16 most significant bits of the block number (i.e.,
7//! `block_num >> 16`), and both the epoch and block number are formatted as zero-padded
8//! hexadecimal strings.
9
10use std::io::ErrorKind;
11use std::ops::Not;
12use std::path::{Path, PathBuf};
13
14use miden_node_utils::tracing::miden_instrument;
15use miden_protocol::block::BlockNumber;
16use miden_protocol::utils::serde::Serializable;
17
18use crate::COMPONENT;
19use crate::genesis::GenesisBlock;
20
21#[derive(Clone, Debug)]
22pub struct BlockStore {
23    store_dir: PathBuf,
24}
25
26impl BlockStore {
27    /// Creates a new [`BlockStore`], creating the directory, inserting the genesis block data
28    /// and initializing the proven tip file.
29    ///
30    /// This _does not_ create any parent directories, so it is expected that the caller has already
31    /// created these.
32    ///
33    /// # Errors
34    ///
35    /// Uses [`std::fs::create_dir`] and therefore has the same error conditions.
36    #[miden_instrument(
37        target = COMPONENT,
38        name = "store.block_store.bootstrap",
39        skip_all,
40        err,
41        fields(
42            path = %store_dir.display(),
43        ),
44    )]
45    pub fn bootstrap(store_dir: PathBuf, genesis_block: &GenesisBlock) -> std::io::Result<Self> {
46        fs_err::create_dir(&store_dir)?;
47
48        let block_store = Self { store_dir };
49        block_store.save_block_blocking(BlockNumber::GENESIS, &genesis_block.inner().to_bytes())?;
50
51        // The genesis block is never proven, but is treated as such.
52        block_store.save_proven_tip(BlockNumber::GENESIS)?;
53
54        Ok(block_store)
55    }
56
57    /// Loads an existing [`BlockStore`].
58    ///
59    /// A new [`BlockStore`] can be created using [`BlockStore::bootstrap`].
60    ///
61    /// A best effort is made to ensure the directory exists and is accessible, but will still run
62    /// afoul of TOCTOU issues as these are impossible to rule out.
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if:
67    ///   - the directory does not exist, or
68    ///   - the directory is not accessible, or
69    ///   - it is not a directory
70    ///
71    /// See also: [`std::fs::metadata`].
72    pub fn load(store_dir: PathBuf) -> std::io::Result<Self> {
73        let meta = fs_err::metadata(&store_dir)?;
74        if meta.is_dir().not() {
75            return Err(ErrorKind::NotADirectory.into());
76        }
77
78        Ok(Self { store_dir })
79    }
80
81    pub async fn load_block(&self, block_num: BlockNumber) -> std::io::Result<Option<Vec<u8>>> {
82        match tokio::fs::read(self.block_path(block_num)).await {
83            Ok(data) => Ok(Some(data)),
84            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
85            Err(err) => Err(err),
86        }
87    }
88
89    #[miden_instrument(
90        target = COMPONENT,
91        name = "store.block_store.save_block",
92        skip(self, data),
93        err,
94        fields(
95            block_size = data.len(),
96        ),
97    )]
98    pub async fn save_block(&self, block_num: BlockNumber, data: &[u8]) -> std::io::Result<()> {
99        let (epoch_path, block_path) = self.epoch_block_path(block_num)?;
100        if !epoch_path.exists() {
101            tokio::fs::create_dir_all(epoch_path).await?;
102        }
103
104        tokio::fs::write(block_path, data).await
105    }
106
107    pub fn save_block_blocking(&self, block_num: BlockNumber, data: &[u8]) -> std::io::Result<()> {
108        let (epoch_path, block_path) = self.epoch_block_path(block_num)?;
109        if !epoch_path.exists() {
110            fs_err::create_dir_all(epoch_path)?;
111        }
112
113        fs_err::write(block_path, data)
114    }
115
116    // PROOF STORAGE
117    // --------------------------------------------------------------------------------------------
118
119    #[miden_instrument(
120        target = COMPONENT,
121        name = "store.block_store.save_proof",
122        skip_all,
123        err,
124        fields(
125            block.number = block_num.as_u32(),
126            proof_size = data.len(),
127        ),
128    )]
129    async fn save_proof(&self, block_num: BlockNumber, data: &[u8]) -> std::io::Result<()> {
130        let (epoch_path, proof_path) = self.epoch_proof_path(block_num)?;
131        if !epoch_path.exists() {
132            tokio::fs::create_dir_all(epoch_path).await?;
133        }
134
135        tokio::fs::write(proof_path, data).await
136    }
137
138    pub async fn load_proof(&self, block_num: BlockNumber) -> std::io::Result<Option<Vec<u8>>> {
139        match tokio::fs::read(self.proof_path(block_num)).await {
140            Ok(data) => Ok(Some(data)),
141            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
142            Err(err) => Err(err),
143        }
144    }
145
146    // PROVING INPUTS STORAGE
147    // --------------------------------------------------------------------------------------------
148
149    #[miden_instrument(
150        target = COMPONENT,
151        name = "store.block_store.save_proving_inputs",
152        skip_all,
153        err,
154        fields(
155            block.number = block_num.as_u32(),
156            inputs_size = data.len(),
157        ),
158    )]
159    pub async fn save_proving_inputs(
160        &self,
161        block_num: BlockNumber,
162        data: &[u8],
163    ) -> std::io::Result<()> {
164        let (epoch_path, inputs_path) = self.epoch_inputs_path(block_num)?;
165        if !epoch_path.exists() {
166            tokio::fs::create_dir_all(epoch_path).await?;
167        }
168        tokio::fs::write(inputs_path, data).await
169    }
170
171    pub async fn load_proving_inputs(
172        &self,
173        block_num: BlockNumber,
174    ) -> std::io::Result<Option<Vec<u8>>> {
175        match tokio::fs::read(self.inputs_path(block_num)).await {
176            Ok(data) => Ok(Some(data)),
177            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
178            Err(err) => Err(err),
179        }
180    }
181
182    pub async fn delete_proving_inputs(&self, block_num: BlockNumber) -> std::io::Result<()> {
183        match tokio::fs::remove_file(self.inputs_path(block_num)).await {
184            Ok(()) => Ok(()),
185            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
186            Err(err) => Err(err),
187        }
188    }
189
190    // HELPER FUNCTIONS
191    // --------------------------------------------------------------------------------------------
192
193    fn block_path(&self, block_num: BlockNumber) -> PathBuf {
194        let block_num = block_num.as_u32();
195        let epoch = block_num >> 16;
196        let epoch_dir = self.store_dir.join(format!("{epoch:04x}"));
197        epoch_dir.join(format!("block_{block_num:08x}.dat"))
198    }
199
200    fn proof_path(&self, block_num: BlockNumber) -> PathBuf {
201        let block_num = block_num.as_u32();
202        let epoch = block_num >> 16;
203        let epoch_dir = self.store_dir.join(format!("{epoch:04x}"));
204        epoch_dir.join(format!("proof_{block_num:08x}.dat"))
205    }
206
207    fn epoch_block_path(&self, block_num: BlockNumber) -> std::io::Result<(PathBuf, PathBuf)> {
208        let block_path = self.block_path(block_num);
209        let epoch_path = block_path.parent().ok_or(std::io::Error::from(ErrorKind::NotFound))?;
210
211        Ok((epoch_path.to_path_buf(), block_path))
212    }
213
214    fn epoch_proof_path(&self, block_num: BlockNumber) -> std::io::Result<(PathBuf, PathBuf)> {
215        let proof_path = self.proof_path(block_num);
216        let epoch_path = proof_path.parent().ok_or(std::io::Error::from(ErrorKind::NotFound))?;
217
218        Ok((epoch_path.to_path_buf(), proof_path))
219    }
220
221    fn inputs_path(&self, block_num: BlockNumber) -> PathBuf {
222        let block_num = block_num.as_u32();
223        let epoch = block_num >> 16;
224        let epoch_dir = self.store_dir.join(format!("{epoch:04x}"));
225        epoch_dir.join(format!("inputs_{block_num:08x}.dat"))
226    }
227
228    fn epoch_inputs_path(&self, block_num: BlockNumber) -> std::io::Result<(PathBuf, PathBuf)> {
229        let inputs_path = self.inputs_path(block_num);
230        let epoch_path = inputs_path.parent().ok_or(std::io::Error::from(ErrorKind::NotFound))?;
231
232        Ok((epoch_path.to_path_buf(), inputs_path))
233    }
234
235    // PROVEN TIP STORAGE
236    // --------------------------------------------------------------------------------------------
237
238    /// Saves the proof, advances the proven tip, and deletes the proving inputs.
239    ///
240    /// Must be called in strictly ascending [`BlockNumber`] order: the proven tip file records
241    /// the highest consecutive proven block, so committing out of order would leave a gap.
242    pub async fn commit_proof(&self, block_num: BlockNumber, proof: &[u8]) -> std::io::Result<()> {
243        self.save_proof(block_num, proof).await?;
244        self.save_proven_tip(block_num)?;
245        self.delete_proving_inputs(block_num).await
246    }
247
248    /// Reads the proven tip from disk and returns it.
249    pub fn load_proven_tip(&self) -> std::io::Result<BlockNumber> {
250        Self::read_proven_tip_from(&self.proven_tip_path())
251    }
252
253    /// Atomically writes `tip` to the proven tip file (write to temp, then rename).
254    fn save_proven_tip(&self, tip: BlockNumber) -> std::io::Result<()> {
255        let path = self.proven_tip_path();
256        let tmp = path.with_extension("tmp");
257        fs_err::write(&tmp, tip.as_u32().to_le_bytes())?;
258        fs_err::rename(&tmp, &path)
259    }
260
261    fn proven_tip_path(&self) -> PathBuf {
262        self.store_dir.join("proven_tip")
263    }
264
265    fn read_proven_tip_from(path: &Path) -> std::io::Result<BlockNumber> {
266        let bytes = fs_err::read(path)?;
267        let arr: [u8; 4] = bytes.try_into().map_err(|_| {
268            std::io::Error::new(
269                std::io::ErrorKind::InvalidData,
270                "proven tip file has unexpected size (expected 4 bytes)",
271            )
272        })?;
273        Ok(BlockNumber::from(u32::from_le_bytes(arr)))
274    }
275
276    pub fn display(&self) -> std::path::Display<'_> {
277        self.store_dir.display()
278    }
279}