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    ///
57    /// Returns the block ID allocated for the superblock object.
58    pub fn commit(
59        &mut self,
60        store: &dyn BlockStore,
61        crypto: &dyn CryptoEngine,
62        codec: &PostcardCodec,
63        allocator: &dyn SlotAllocator,
64        superblock: &Superblock,
65    ) -> FsResult<u64> {
66        self.generation += 1;
67
68        // 1. Allocate a block for the superblock object.
69        let sb_block = allocator.allocate()?;
70
71        // 2. Write encrypted superblock.
72        write_encrypted_object(
73            store,
74            crypto,
75            codec,
76            sb_block,
77            ObjectKind::Superblock,
78            superblock,
79        )?;
80
81        // 3. Build root pointer with checksum.
82        let sb_bytes = codec.serialize_object(superblock)?;
83        let checksum = blake3::hash(&sb_bytes);
84
85        let root_ptr = RootPointer {
86            generation: self.generation,
87            superblock_ref: ObjectRef::new(sb_block),
88            checksum: *checksum.as_bytes(),
89        };
90
91        // 4. Write root pointer (unencrypted, just serialized + padded).
92        let rp_bytes = codec.serialize_object(&root_ptr)?;
93        let block_size = store.block_size();
94        let mut block = vec![0u8; block_size];
95        rand::thread_rng().fill_bytes(&mut block);
96        let len = rp_bytes.len() as u32;
97        block[..4].copy_from_slice(&len.to_le_bytes());
98        block[4..4 + rp_bytes.len()].copy_from_slice(&rp_bytes);
99
100        let slot = if self.next_is_b {
101            BLOCK_ROOT_POINTER_B
102        } else {
103            BLOCK_ROOT_POINTER_A
104        };
105        store.write_block(slot, &block)?;
106
107        self.next_is_b = !self.next_is_b;
108        Ok(sb_block)
109    }
110
111    /// Try to read a root pointer from a slot. Returns None if the slot is empty/invalid.
112    pub fn read_root_pointer(
113        store: &dyn BlockStore,
114        codec: &PostcardCodec,
115        slot: u64,
116    ) -> FsResult<Option<RootPointer>> {
117        let block = store.read_block(slot)?;
118        if block.len() < 4 {
119            return Ok(None);
120        }
121        let len = u32::from_le_bytes([block[0], block[1], block[2], block[3]]) as usize;
122        if len == 0 || 4 + len > block.len() {
123            return Ok(None);
124        }
125        let rp_bytes = &block[4..4 + len];
126        match codec.deserialize_object::<RootPointer>(rp_bytes) {
127            Ok(rp) => Ok(Some(rp)),
128            Err(_) => Ok(None),
129        }
130    }
131
132    /// Recover the latest valid root pointer by reading both slots.
133    /// Returns (RootPointer, was_slot_b).
134    pub fn recover_latest(
135        store: &dyn BlockStore,
136        codec: &PostcardCodec,
137    ) -> FsResult<Option<(RootPointer, bool)>> {
138        let rp_a = Self::read_root_pointer(store, codec, BLOCK_ROOT_POINTER_A)?;
139        let rp_b = Self::read_root_pointer(store, codec, BLOCK_ROOT_POINTER_B)?;
140
141        match (rp_a, rp_b) {
142            (Some(a), Some(b)) => {
143                if b.generation >= a.generation {
144                    Ok(Some((b, true)))
145                } else {
146                    Ok(Some((a, false)))
147                }
148            }
149            (Some(a), None) => Ok(Some((a, false))),
150            (None, Some(b)) => Ok(Some((b, true))),
151            (None, None) => Ok(None),
152        }
153    }
154}