Skip to main content

doublecrypt_core/
transaction.rs

1use rand::RngCore;
2
3use crate::allocator::SlotAllocator;
4use crate::block_store::BlockStore;
5use crate::codec::{write_encrypted_object, ObjectCodec, PostcardCodec};
6use crate::crypto::CryptoEngine;
7use crate::error::FsResult;
8use crate::model::*;
9
10/// Manages copy-on-write commits and root pointer alternation.
11///
12/// Workflow:
13/// 1. Filesystem operations allocate new blocks and write objects.
14/// 2. When ready to commit, call `commit()` with the new superblock.
15/// 3. TransactionManager writes the superblock to a fresh block,
16///    then updates the next root pointer slot (A or B).
17/// 4. Old blocks can then be freed (deferred GC - not yet implemented).
18pub struct TransactionManager {
19    /// Current generation counter.
20    generation: u64,
21    /// Which root pointer slot to write next: false = A (block 1), true = B (block 2).
22    next_is_b: bool,
23}
24
25impl TransactionManager {
26    pub fn new() -> Self {
27        Self {
28            generation: 0,
29            next_is_b: false,
30        }
31    }
32
33    /// Initialize from recovered state.
34    pub fn from_recovered(generation: u64, last_was_b: bool) -> Self {
35        Self {
36            generation,
37            next_is_b: !last_was_b,
38        }
39    }
40
41    /// Current generation.
42    pub fn generation(&self) -> u64 {
43        self.generation
44    }
45
46    /// Commit a new superblock.
47    ///
48    /// 1. Allocate a block for the superblock.
49    /// 2. Write the encrypted superblock to that block.
50    /// 3. Write the root pointer to the next slot (A or B), unencrypted
51    ///    (root pointers are integrity-checked via BLAKE3 but not encrypted
52    ///    so we can read them without the key to find the superblock).
53    ///
54    /// Note: root pointers are written unencrypted but with a checksum.
55    /// The superblock itself is encrypted.
56    pub fn commit(
57        &mut self,
58        store: &dyn BlockStore,
59        crypto: &dyn CryptoEngine,
60        codec: &PostcardCodec,
61        allocator: &dyn SlotAllocator,
62        superblock: &Superblock,
63    ) -> FsResult<()> {
64        self.generation += 1;
65
66        // 1. Allocate a block for the superblock object.
67        let sb_block = allocator.allocate()?;
68
69        // 2. Write encrypted superblock.
70        write_encrypted_object(
71            store,
72            crypto,
73            codec,
74            sb_block,
75            ObjectKind::Superblock,
76            superblock,
77        )?;
78
79        // 3. Build root pointer with checksum.
80        let sb_bytes = codec.serialize_object(superblock)?;
81        let checksum = blake3::hash(&sb_bytes);
82
83        let root_ptr = RootPointer {
84            generation: self.generation,
85            superblock_ref: ObjectRef::new(sb_block),
86            checksum: *checksum.as_bytes(),
87        };
88
89        // 4. Write root pointer (unencrypted, just serialized + padded).
90        let rp_bytes = codec.serialize_object(&root_ptr)?;
91        let block_size = store.block_size();
92        let mut block = vec![0u8; block_size];
93        rand::thread_rng().fill_bytes(&mut block);
94        let len = rp_bytes.len() as u32;
95        block[..4].copy_from_slice(&len.to_le_bytes());
96        block[4..4 + rp_bytes.len()].copy_from_slice(&rp_bytes);
97
98        let slot = if self.next_is_b {
99            BLOCK_ROOT_POINTER_B
100        } else {
101            BLOCK_ROOT_POINTER_A
102        };
103        store.write_block(slot, &block)?;
104
105        self.next_is_b = !self.next_is_b;
106        Ok(())
107    }
108
109    /// Try to read a root pointer from a slot. Returns None if the slot is empty/invalid.
110    pub fn read_root_pointer(
111        store: &dyn BlockStore,
112        codec: &PostcardCodec,
113        slot: u64,
114    ) -> FsResult<Option<RootPointer>> {
115        let block = store.read_block(slot)?;
116        if block.len() < 4 {
117            return Ok(None);
118        }
119        let len = u32::from_le_bytes([block[0], block[1], block[2], block[3]]) as usize;
120        if len == 0 || 4 + len > block.len() {
121            return Ok(None);
122        }
123        let rp_bytes = &block[4..4 + len];
124        match codec.deserialize_object::<RootPointer>(rp_bytes) {
125            Ok(rp) => Ok(Some(rp)),
126            Err(_) => Ok(None),
127        }
128    }
129
130    /// Recover the latest valid root pointer by reading both slots.
131    /// Returns (RootPointer, was_slot_b).
132    pub fn recover_latest(
133        store: &dyn BlockStore,
134        codec: &PostcardCodec,
135    ) -> FsResult<Option<(RootPointer, bool)>> {
136        let rp_a = Self::read_root_pointer(store, codec, BLOCK_ROOT_POINTER_A)?;
137        let rp_b = Self::read_root_pointer(store, codec, BLOCK_ROOT_POINTER_B)?;
138
139        match (rp_a, rp_b) {
140            (Some(a), Some(b)) => {
141                if b.generation >= a.generation {
142                    Ok(Some((b, true)))
143                } else {
144                    Ok(Some((a, false)))
145                }
146            }
147            (Some(a), None) => Ok(Some((a, false))),
148            (None, Some(b)) => Ok(Some((b, true))),
149            (None, None) => Ok(None),
150        }
151    }
152}