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