1use std::io::ErrorKind;
11use std::ops::Not;
12use std::path::{Path, PathBuf};
13
14use miden_node_persistence::generated::BlockStoreFormat;
15use miden_node_persistence::prost::Message;
16use miden_node_tracing::miden_instrument;
17use miden_protocol::block::BlockNumber;
18
19use crate::COMPONENT;
20use crate::genesis::GenesisBlock;
21
22#[derive(Clone, Debug)]
23pub struct BlockStore {
24 store_dir: PathBuf,
25}
26
27impl BlockStore {
28 #[miden_instrument(
38 target = COMPONENT,
39 name = "store.block_store.bootstrap",
40 err,
41 fields(
42 path = store_dir,
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 fs_err::write(
50 block_store.store_dir.join("format.pb"),
51 BlockStoreFormat { version: 1 }.encode_to_vec(),
52 )?;
53 block_store.save_block_blocking(
54 BlockNumber::GENESIS,
55 &miden_node_persistence::encode(genesis_block.inner()),
56 )?;
57
58 block_store.save_proven_tip(BlockNumber::GENESIS)?;
60
61 Ok(block_store)
62 }
63
64 pub fn load(store_dir: PathBuf) -> std::io::Result<Self> {
81 let meta = fs_err::metadata(&store_dir)?;
82 if meta.is_dir().not() {
83 return Err(ErrorKind::NotADirectory.into());
84 }
85
86 let bytes = fs_err::read(store_dir.join("format.pb")).map_err(|error| {
87 std::io::Error::new(
88 error.kind(),
89 format!(
90 "block store format marker is unavailable; recreate the block store: {error}"
91 ),
92 )
93 })?;
94 let marker = BlockStoreFormat::decode(bytes.as_slice())
95 .map_err(|error| std::io::Error::new(ErrorKind::InvalidData, error))?;
96 if marker.version != 1 {
97 let error = miden_node_persistence::PersistenceError::UnsupportedVersion {
98 format: "block store",
99 version: marker.version,
100 };
101 return Err(std::io::Error::new(ErrorKind::InvalidData, error));
102 }
103 Ok(Self { store_dir })
104 }
105
106 pub async fn load_block(&self, block_num: BlockNumber) -> std::io::Result<Option<Vec<u8>>> {
107 match tokio::fs::read(self.block_path(block_num)).await {
108 Ok(data) => Ok(Some(data)),
109 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
110 Err(err) => Err(err),
111 }
112 }
113
114 #[miden_instrument(
115 target = COMPONENT,
116 name = "store.block_store.save_block",
117 err,
118 fields(
119 block.number = block_num,
120 block.size = data.len(),
121 ),
122 )]
123 pub async fn save_block(&self, block_num: BlockNumber, data: &[u8]) -> std::io::Result<()> {
124 let (epoch_path, block_path) = self.epoch_block_path(block_num)?;
125 if !epoch_path.exists() {
126 tokio::fs::create_dir_all(epoch_path).await?;
127 }
128
129 tokio::fs::write(block_path, data).await
130 }
131
132 pub fn save_block_blocking(&self, block_num: BlockNumber, data: &[u8]) -> std::io::Result<()> {
133 let (epoch_path, block_path) = self.epoch_block_path(block_num)?;
134 if !epoch_path.exists() {
135 fs_err::create_dir_all(epoch_path)?;
136 }
137
138 fs_err::write(block_path, data)
139 }
140
141 #[miden_instrument(
145 target = COMPONENT,
146 name = "store.block_store.save_proof",
147 err,
148 fields(
149 block.number = block_num,
150 proof_size = data.len()
151 ),
152 )]
153 async fn save_proof(&self, block_num: BlockNumber, data: &[u8]) -> std::io::Result<()> {
154 let (epoch_path, proof_path) = self.epoch_proof_path(block_num)?;
155 if !epoch_path.exists() {
156 tokio::fs::create_dir_all(epoch_path).await?;
157 }
158
159 tokio::fs::write(proof_path, data).await
160 }
161
162 pub async fn load_proof(&self, block_num: BlockNumber) -> std::io::Result<Option<Vec<u8>>> {
163 match tokio::fs::read(self.proof_path(block_num)).await {
164 Ok(data) => Ok(Some(data)),
165 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
166 Err(err) => Err(err),
167 }
168 }
169
170 #[miden_instrument(
174 target = COMPONENT,
175 name = "store.block_store.save_proving_inputs",
176 err,
177 fields(
178 block.number = block_num,
179 inputs_size = data.len()
180 ),
181 )]
182 pub async fn save_proving_inputs(
183 &self,
184 block_num: BlockNumber,
185 data: &[u8],
186 ) -> std::io::Result<()> {
187 let (epoch_path, inputs_path) = self.epoch_inputs_path(block_num)?;
188 if !epoch_path.exists() {
189 tokio::fs::create_dir_all(epoch_path).await?;
190 }
191 tokio::fs::write(inputs_path, data).await
192 }
193
194 pub async fn load_proving_inputs(
195 &self,
196 block_num: BlockNumber,
197 ) -> std::io::Result<Option<Vec<u8>>> {
198 match tokio::fs::read(self.inputs_path(block_num)).await {
199 Ok(data) => Ok(Some(data)),
200 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
201 Err(err) => Err(err),
202 }
203 }
204
205 pub async fn delete_proving_inputs(&self, block_num: BlockNumber) -> std::io::Result<()> {
206 match tokio::fs::remove_file(self.inputs_path(block_num)).await {
207 Ok(()) => Ok(()),
208 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
209 Err(err) => Err(err),
210 }
211 }
212
213 fn block_path(&self, block_num: BlockNumber) -> PathBuf {
217 let block_num = block_num.as_u32();
218 let epoch = block_num >> 16;
219 let epoch_dir = self.store_dir.join(format!("{epoch:04x}"));
220 epoch_dir.join(format!("block_{block_num:08x}.dat"))
221 }
222
223 fn proof_path(&self, block_num: BlockNumber) -> PathBuf {
224 let block_num = block_num.as_u32();
225 let epoch = block_num >> 16;
226 let epoch_dir = self.store_dir.join(format!("{epoch:04x}"));
227 epoch_dir.join(format!("proof_{block_num:08x}.dat"))
228 }
229
230 fn epoch_block_path(&self, block_num: BlockNumber) -> std::io::Result<(PathBuf, PathBuf)> {
231 let block_path = self.block_path(block_num);
232 let epoch_path = block_path.parent().ok_or(std::io::Error::from(ErrorKind::NotFound))?;
233
234 Ok((epoch_path.to_path_buf(), block_path))
235 }
236
237 fn epoch_proof_path(&self, block_num: BlockNumber) -> std::io::Result<(PathBuf, PathBuf)> {
238 let proof_path = self.proof_path(block_num);
239 let epoch_path = proof_path.parent().ok_or(std::io::Error::from(ErrorKind::NotFound))?;
240
241 Ok((epoch_path.to_path_buf(), proof_path))
242 }
243
244 fn inputs_path(&self, block_num: BlockNumber) -> PathBuf {
245 let block_num = block_num.as_u32();
246 let epoch = block_num >> 16;
247 let epoch_dir = self.store_dir.join(format!("{epoch:04x}"));
248 epoch_dir.join(format!("inputs_{block_num:08x}.dat"))
249 }
250
251 fn epoch_inputs_path(&self, block_num: BlockNumber) -> std::io::Result<(PathBuf, PathBuf)> {
252 let inputs_path = self.inputs_path(block_num);
253 let epoch_path = inputs_path.parent().ok_or(std::io::Error::from(ErrorKind::NotFound))?;
254
255 Ok((epoch_path.to_path_buf(), inputs_path))
256 }
257
258 pub async fn commit_proof(&self, block_num: BlockNumber, proof: &[u8]) -> std::io::Result<()> {
266 self.save_proof(block_num, proof).await?;
267 self.save_proven_tip(block_num)?;
268 self.delete_proving_inputs(block_num).await
269 }
270
271 pub fn load_proven_tip(&self) -> std::io::Result<BlockNumber> {
273 Self::read_proven_tip_from(&self.proven_tip_path())
274 }
275
276 fn save_proven_tip(&self, tip: BlockNumber) -> std::io::Result<()> {
278 let path = self.proven_tip_path();
279 let tmp = path.with_extension("tmp");
280 fs_err::write(&tmp, tip.as_u32().to_le_bytes())?;
281 fs_err::rename(&tmp, &path)
282 }
283
284 fn proven_tip_path(&self) -> PathBuf {
285 self.store_dir.join("proven_tip")
286 }
287
288 fn read_proven_tip_from(path: &Path) -> std::io::Result<BlockNumber> {
289 let bytes = fs_err::read(path)?;
290 let arr: [u8; 4] = bytes.try_into().map_err(|_| {
291 std::io::Error::new(
292 ErrorKind::InvalidData,
293 "proven tip file has unexpected size (expected 4 bytes)",
294 )
295 })?;
296 Ok(BlockNumber::from(u32::from_le_bytes(arr)))
297 }
298
299 pub fn display(&self) -> std::path::Display<'_> {
300 self.store_dir.display()
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn rejects_legacy_and_invalid_markers_without_changing_files() {
310 let root = tempfile::tempdir().unwrap();
311 let legacy = root.path().join("blocks");
312 fs_err::create_dir(&legacy).unwrap();
313 fs_err::write(legacy.join("proven_tip"), [0; 4]).unwrap();
314 assert!(BlockStore::load(legacy.clone()).is_err());
315 assert!(!legacy.join("format.pb").exists());
316 for bytes in [
317 vec![0xff],
318 BlockStoreFormat { version: 0 }.encode_to_vec(),
319 BlockStoreFormat { version: 2 }.encode_to_vec(),
320 ] {
321 fs_err::write(legacy.join("format.pb"), &bytes).unwrap();
322 assert!(BlockStore::load(legacy.clone()).is_err());
323 assert_eq!(fs_err::read(legacy.join("format.pb")).unwrap(), bytes);
324 assert_eq!(fs_err::read(legacy.join("proven_tip")).unwrap(), [0; 4]);
325 }
326 }
327
328 #[test]
329 fn proven_tip_uses_four_little_endian_bytes() {
330 let root = tempfile::tempdir().unwrap();
331 let store = BlockStore { store_dir: root.path().to_path_buf() };
332 fs_err::write(store.proven_tip_path(), [0x78, 0x56, 0x34, 0x12]).unwrap();
333 assert_eq!(store.load_proven_tip().unwrap(), BlockNumber::from(0x1234_5678));
334
335 store.save_proven_tip(BlockNumber::from(0x90ab_cdef)).unwrap();
336 assert_eq!(fs_err::read(store.proven_tip_path()).unwrap(), [0xef, 0xcd, 0xab, 0x90]);
337 }
338
339 #[test]
340 fn rejects_invalid_proven_tip_lengths() {
341 let root = tempfile::tempdir().unwrap();
342 let store = BlockStore { store_dir: root.path().to_path_buf() };
343 for len in [0, 1, 2, 3, 5, 8] {
344 fs_err::write(store.proven_tip_path(), vec![0; len]).unwrap();
345 assert_eq!(store.load_proven_tip().unwrap_err().kind(), ErrorKind::InvalidData);
346 }
347 }
348
349 #[tokio::test]
350 async fn reopens_proven_tip_and_preserves_raw_proof_bytes() {
351 let root = tempfile::tempdir().unwrap();
352 let store_dir = root.path().join("blocks");
353 let genesis = crate::GenesisState::new(
354 Vec::new(),
355 miden_node_utils::fee::test_fee_params(),
356 0,
357 miden_protocol::block::ValidatorConfig::new(
358 vec![miden_protocol::testing::random_secret_key::random_secret_key().public_key()],
359 1,
360 )
361 .unwrap(),
362 miden_node_utils::fee::test_protocol_config(),
363 )
364 .into_block()
365 .unwrap();
366 let store = BlockStore::bootstrap(store_dir.clone(), &genesis).unwrap();
367 let block: miden_protocol::block::SignedBlock = miden_node_persistence::decode(
368 &store.load_block(BlockNumber::GENESIS).await.unwrap().unwrap(),
369 )
370 .unwrap();
371 assert_eq!(&block, genesis.inner());
372 assert!(BlockStore::bootstrap(store_dir.clone(), &genesis).is_err());
373 let tip = BlockNumber::from(1);
374 let proof = miden_protocol::testing::dummy_execution_proof().to_bytes();
375 store.save_proving_inputs(tip, &[1, 2, 3]).await.unwrap();
376 store.commit_proof(tip, &proof).await.unwrap();
377 let reopened = BlockStore::load(store_dir).unwrap();
378 assert_eq!(reopened.load_proven_tip().unwrap(), tip);
379 assert_eq!(reopened.load_proof(tip).await.unwrap().unwrap(), proof);
380 assert_eq!(reopened.load_proving_inputs(tip).await.unwrap(), None);
381 assert!(!reopened.proven_tip_path().with_extension("tmp").exists());
382 }
383}