Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/transaction/
mod.rs

1//! Transaction system for atomic database modifications
2//!
3//! This module provides the transaction API for making atomic changes to an Eidetica database.
4//! Transactions ensure that all changes within a transaction are applied atomically and maintain
5//! proper parent-child relationships in the Merkle-CRDT DAG structure.
6//!
7//! # Subtree Parent Management
8//!
9//! One of the critical responsibilities of the transaction system is establishing proper
10//! subtree parent relationships. When a store (subtree) is accessed for the first time
11//! in a transaction, the system must determine the correct parent entries for that subtree.
12//! This involves:
13//!
14//! 1. Checking for existing subtree tips (leaf nodes)
15//! 2. If no tips exist, traversing the DAG to find reachable subtree entries
16//! 3. Setting appropriate parent relationships (empty for first entry, or proper parents)
17
18pub mod errors;
19
20#[cfg(test)]
21mod tests;
22
23use std::{
24    collections::HashMap,
25    sync::{Arc, Mutex},
26};
27
28use base64ct::{Base64, Encoding};
29pub use errors::TransactionError;
30use serde::{Deserialize, Serialize};
31
32use crate::{
33    Database, Result, Store,
34    auth::{
35        AuthSettings,
36        crypto::{PrivateKey, sign_entry},
37        types::{SigInfo, SigKey},
38        validation::AuthValidator,
39    },
40    backend::VerificationStatus,
41    constants::{INDEX, ROOT, SETTINGS},
42    crdt::{CRDT, Data, Doc, doc::Value},
43    entry::{Entry, EntryBuilder, ID},
44    height::HeightStrategy,
45    instance::WriteSource,
46    store::{Registry, SettingsStore, StoreError},
47};
48
49/// Creates a synthetic entry ID for multi-tip merged CRDT state caching.
50///
51/// Tips are sorted to ensure deterministic keys regardless of input order.
52/// The resulting ID has format `merge:{tip1}:{tip2}:...` which is distinct
53/// from real content-addressed entry IDs.
54fn create_merge_cache_id(tip_ids: &[ID]) -> ID {
55    let mut sorted_tips = tip_ids.to_vec();
56    sorted_tips.sort();
57
58    // Format: merge:{tip1}:{tip2}:...
59    let capacity = 5 + sorted_tips
60        .iter()
61        .map(|id| id.as_str().len() + 1)
62        .sum::<usize>();
63    let mut key = String::with_capacity(capacity);
64    key.push_str("merge");
65    for tip in &sorted_tips {
66        key.push(':');
67        key.push_str(tip.as_str());
68    }
69    ID::new(key)
70}
71
72/// Trait for encrypting/decrypting subtree data transparently
73///
74/// Encryptors are registered with a Transaction for specific subtrees, allowing
75/// transparent encryption/decryption at the transaction boundary. When an encryptor
76/// is registered:
77///
78/// - `get_full_state()` decrypts each historical entry before CRDT merging
79/// - `get_local_data()` returns plaintext (cached in EntryBuilder)
80/// - `update_subtree()` stores plaintext in cache, encrypted on commit
81///
82/// This ensures proper CRDT merge semantics while keeping data encrypted at rest.
83///
84/// # Wire Format
85///
86/// The trait operates on raw bytes, allowing implementations to define their own
87/// wire format. For example, AES-GCM implementations typically use `nonce || ciphertext`.
88/// The Transaction handles base64 encoding/decoding for storage.
89///
90/// # Example
91///
92/// ```rust,ignore
93/// struct PasswordEncryptor { /* ... */ }
94///
95/// impl Encryptor for PasswordEncryptor {
96///     fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
97///         let (nonce, ct) = ciphertext.split_at(12);
98///         // decrypt with nonce and ciphertext...
99///     }
100///
101///     fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
102///         let nonce = generate_nonce();
103///         let ct = encrypt(plaintext, &nonce);
104///         // return nonce || ciphertext
105///     }
106/// }
107/// ```
108pub(crate) trait Encryptor: Send + Sync {
109    /// Decrypt ciphertext bytes to plaintext bytes
110    ///
111    /// # Arguments
112    /// * `ciphertext` - Encrypted data in implementation-defined format
113    ///
114    /// # Returns
115    /// Plaintext bytes (typically UTF-8 encoded JSON for CRDT data)
116    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>>;
117
118    /// Encrypt plaintext bytes to ciphertext bytes
119    ///
120    /// # Arguments
121    /// * `plaintext` - Data to encrypt (typically UTF-8 encoded JSON for CRDT data)
122    ///
123    /// # Returns
124    /// Encrypted data in implementation-defined format
125    fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>>;
126}
127
128/// Metadata structure for entries
129#[derive(Debug, Clone, Serialize, Deserialize)]
130struct EntryMetadata {
131    /// Tips of the _settings subtree at the time this entry was created
132    /// This is used for improving sync performance and for validation in sparse checkouts.
133    settings_tips: Vec<ID>,
134    /// Random entropy for ensuring unique IDs for root entries
135    entropy: Option<u64>,
136}
137
138/// Represents a single, atomic transaction for modifying a `Database`.
139///
140/// An `Transaction` encapsulates a mutable `EntryBuilder` being constructed. Users interact with
141/// specific `Store` instances obtained via `Transaction::get_store` to stage changes.
142/// All staged changes across different subtrees within the transaction are recorded
143/// in the internal `EntryBuilder`.
144///
145/// When `commit()` is called, the transaction:
146/// 1. Finalizes the `EntryBuilder` by building an immutable `Entry`
147/// 2. Calculates the entry's content-addressable ID
148/// 3. Ensures the correct parent links are set based on the tree's state
149/// 4. Removes any empty subtrees that didn't have data staged
150/// 5. Signs the entry if authentication is configured
151/// 6. Persists the resulting immutable `Entry` to the backend
152///
153/// `Transaction` instances are typically created via `Database::new_transaction()`.
154#[derive(Clone)]
155pub struct Transaction {
156    /// The entry builder being modified, wrapped in Option to support consuming on commit
157    entry_builder: Arc<Mutex<Option<EntryBuilder>>>,
158    /// The database this transaction belongs to
159    db: Database,
160    /// Provided signing key paired with its auth identity
161    provided_signing_key: Option<(PrivateKey, SigKey)>,
162    /// Registered encryptors for transparent encryption/decryption of specific subtrees
163    /// Maps subtree name -> encryptor implementation
164    /// When an encryptor is registered, the transaction automatically encrypts writes
165    /// and decrypts reads for that subtree
166    encryptors: Arc<Mutex<HashMap<String, Box<dyn Encryptor>>>>,
167}
168
169impl Transaction {
170    /// Creates a new atomic transaction for a specific `Database` with custom parent tips.
171    ///
172    /// Initializes an internal `EntryBuilder` with its main parent pointers set to the
173    /// specified tips instead of the current database tips. This allows creating
174    /// transactions that branch from specific points in the database history.
175    ///
176    /// This enables creating diamond patterns and other complex DAG structures
177    /// for testing and advanced use cases.
178    ///
179    /// # Arguments
180    /// * `database` - The `Database` this transaction will modify.
181    /// * `tips` - The specific parent tips to use for this transaction. Must contain at least one tip.
182    ///
183    /// # Returns
184    /// A `Result<Self>` containing the new transaction or an error if tips are empty or invalid.
185    pub(crate) async fn new_with_tips(database: &Database, tips: &[ID]) -> Result<Self> {
186        // Validate that tips are not empty, unless we're creating the root entry
187        if tips.is_empty() {
188            // Check if this is a root entry creation by seeing if the database root exists in backend
189            let root_exists = database.backend()?.get(database.root_id()).await.is_ok();
190
191            if root_exists {
192                return Err(TransactionError::EmptyTipsNotAllowed.into());
193            }
194            // If root doesn't exist, this is valid (creating the root entry)
195        }
196
197        // Validate that all tips belong to the same tree
198        let backend = database.backend()?;
199        for tip_id in tips {
200            let entry = backend.get(tip_id).await?;
201            if !entry.in_tree(database.root_id()) {
202                return Err(TransactionError::InvalidTip {
203                    tip_id: tip_id.to_string(),
204                }
205                .into());
206            }
207        }
208
209        // Start with a basic entry linked to the database's root.
210        // Data and parents will be filled based on the transaction type.
211        let mut builder = Entry::builder(database.root_id().clone());
212
213        // Use the provided tips as parents (only if not empty)
214        if !tips.is_empty() {
215            builder.set_parents_mut(tips.to_vec());
216        }
217
218        Ok(Self {
219            entry_builder: Arc::new(Mutex::new(Some(builder))),
220            db: database.clone(),
221            provided_signing_key: None,
222            encryptors: Arc::new(Mutex::new(HashMap::new())),
223        })
224    }
225
226    /// Set signing key directly for user context (internal API).
227    ///
228    /// This method is used when a Database is created with a user-provided key
229    /// (via `Database::open()`). The provided SigningKey is already
230    /// decrypted and ready to use, eliminating the need for backend key lookup.
231    ///
232    /// # Arguments
233    /// * `signing_key` - The decrypted signing key from UserKeyManager
234    /// * `identity` - The SigKey identity used in database auth settings
235    pub(crate) fn set_provided_key(&mut self, signing_key: PrivateKey, identity: SigKey) {
236        self.provided_signing_key = Some((signing_key, identity));
237    }
238
239    /// Get current time as RFC3339 string.
240    ///
241    /// Delegates to the underlying instance's clock.
242    pub(crate) fn now_rfc3339(&self) -> Result<String> {
243        Ok(self.db.instance()?.clock().now_rfc3339())
244    }
245
246    /// Register an encryptor for transparent encryption/decryption of a specific subtree.
247    ///
248    /// Once registered, the transaction will automatically:
249    /// - Decrypt each historical entry before CRDT merging in `get_full_state()`
250    /// - Return plaintext data from `get_local_data()` (cached in EntryBuilder)
251    /// - Encrypt plaintext data before persisting in `commit()`
252    ///
253    /// This ensures proper CRDT merge semantics while keeping data encrypted at rest.
254    ///
255    /// # Arguments
256    /// * `subtree` - The name of the subtree to encrypt/decrypt
257    /// * `encryptor` - The encryptor implementation to use
258    ///
259    /// # Example
260    ///
261    /// For password-based encryption, use [`PasswordStore`] which handles
262    /// encryptor registration automatically:
263    ///
264    /// ```rust,ignore
265    /// let mut encrypted = tx.get_store::<PasswordStore<DocStore>>("secrets")?;
266    /// encrypted.initialize("my_password", Doc::new())?;
267    ///
268    /// // PasswordStore registers the encryptor internally
269    /// let docstore = encrypted.inner()?;
270    /// ```
271    ///
272    /// For custom encryption, implement the [`Encryptor`] trait:
273    ///
274    /// ```rust,ignore
275    /// struct MyEncryptor { /* ... */ }
276    /// impl Encryptor for MyEncryptor {
277    ///     fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> { /* ... */ }
278    ///     fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> { /* ... */ }
279    /// }
280    ///
281    /// transaction.register_encryptor("secrets", Box::new(MyEncryptor::new()))?;
282    /// ```
283    ///
284    /// [`PasswordStore`]: crate::store::PasswordStore
285    /// [`Encryptor`]: crate::Encryptor
286    pub(crate) fn register_encryptor(
287        &self,
288        subtree: impl Into<String>,
289        encryptor: Box<dyn Encryptor>,
290    ) -> Result<()> {
291        self.encryptors
292            .lock()
293            .unwrap()
294            .insert(subtree.into(), encryptor);
295        Ok(())
296    }
297
298    /// Decrypt data if an encryptor is registered, otherwise return as-is.
299    ///
300    /// This is used throughout Transaction to transparently decrypt encrypted data
301    /// before deserializing into CRDT types. Encrypted data is stored as base64-encoded
302    /// bytes in the entry.
303    fn decrypt_if_needed(&self, subtree: &str, data: &str) -> Result<String> {
304        if let Some(encryptor) = self.encryptors.lock().unwrap().get(subtree) {
305            // Decode base64 to get ciphertext bytes
306            let ciphertext =
307                Base64::decode_vec(data).map_err(|e| StoreError::DeserializationFailed {
308                    store: subtree.to_string(),
309                    reason: format!("Failed to decode base64: {e}"),
310                })?;
311            // Decrypt the data
312            let plaintext_bytes = encryptor.decrypt(&ciphertext)?;
313            // Convert to UTF-8 string
314            String::from_utf8(plaintext_bytes).map_err(|e| {
315                StoreError::DeserializationFailed {
316                    store: subtree.to_string(),
317                    reason: format!("Invalid UTF-8 in decrypted data: {e}"),
318                }
319                .into()
320            })
321        } else {
322            // No encryptor, return as-is
323            Ok(data.to_string())
324        }
325    }
326
327    /// Encrypts data if an encryptor is registered for the subtree.
328    /// Returns the original data unchanged if no encryptor is registered.
329    fn encrypt_if_needed(&self, subtree: &str, plaintext: &str) -> Result<String> {
330        if let Some(encryptor) = self.encryptors.lock().unwrap().get(subtree) {
331            // Encrypt the data
332            let ciphertext = encryptor.encrypt(plaintext.as_bytes())?;
333            // Encode as base64 for storage
334            Ok(Base64::encode_string(&ciphertext))
335        } else {
336            // No encryptor, return as-is
337            Ok(plaintext.to_string())
338        }
339    }
340
341    /// Get a SettingsStore handle for the settings subtree within this transaction.
342    ///
343    /// This method returns a `SettingsStore` that provides specialized access to the `_settings` subtree,
344    /// allowing you to read and modify settings data within this atomic transaction.
345    /// The DocStore automatically merges historical settings from the database with any
346    /// staged changes in this transaction.
347    ///
348    /// # Returns
349    ///
350    /// Returns a `Result<SettingsStore>` that can be used to:
351    /// - Read current settings values (including both historical and staged data)
352    /// - Stage new settings changes within this transaction
353    /// - Access nested settings structures
354    ///
355    /// # Example
356    ///
357    /// ```rust,no_run
358    /// # use eidetica::Database;
359    /// # async fn example(database: Database) -> eidetica::Result<()> {
360    /// let txn = database.new_transaction().await?;
361    /// let settings = txn.get_settings()?;
362    ///
363    /// // Read a setting
364    /// if let Ok(name) = settings.get_name().await {
365    ///     println!("Database name: {}", name);
366    /// }
367    ///
368    /// // Modify a setting
369    /// settings.set_name("Updated Database Name").await?;
370    /// # Ok(())
371    /// # }
372    /// ```
373    ///
374    /// # Errors
375    ///
376    /// Returns an error if:
377    /// - Unable to create the SettingsStore for the settings subtree
378    /// - Operation has already been committed
379    pub fn get_settings(&self) -> Result<SettingsStore> {
380        // Create a SettingsStore for the settings subtree
381        SettingsStore::new(self)
382    }
383
384    /// Gets a handle to the Index for managing subtree registry and metadata.
385    ///
386    /// The Index provides access to the `_index` subtree, which stores metadata
387    /// about all subtrees in the database including their type identifiers and configurations.
388    ///
389    /// # Returns
390    ///
391    /// A `Result<Registry>` containing the handle for managing the index.
392    ///
393    /// # Errors
394    ///
395    /// Returns an error if:
396    /// - Unable to create the Registry for the _index subtree
397    /// - Operation has already been committed
398    pub async fn get_index(&self) -> Result<Registry> {
399        Registry::new(self, INDEX).await
400    }
401
402    /// Set the tree root field for the entry being built.
403    ///
404    /// This is primarily used during tree creation to ensure the root entry
405    /// has an empty tree.root field, making it a proper top-level root.
406    ///
407    /// # Arguments
408    /// * `root` - The tree root ID to set (use empty string for top-level roots)
409    pub(crate) fn set_entry_root(&self, root: impl Into<String>) -> Result<()> {
410        let mut builder_ref = self.entry_builder.lock().unwrap();
411        let builder = builder_ref
412            .as_mut()
413            .ok_or(TransactionError::TransactionAlreadyCommitted)?;
414        builder.set_root_mut(root.into());
415        Ok(())
416    }
417
418    /// Set entropy in the entry metadata.
419    ///
420    /// This is used during database creation to ensure unique IDs for databases
421    /// even when they have identical settings.
422    ///
423    /// # Arguments
424    /// * `entropy` - Random entropy value
425    pub(crate) fn set_metadata_entropy(&self, entropy: u64) -> Result<()> {
426        let mut builder_ref = self.entry_builder.lock().unwrap();
427        let builder = builder_ref
428            .as_mut()
429            .ok_or(TransactionError::TransactionAlreadyCommitted)?;
430
431        // Parse existing metadata if present, or create new
432        let mut metadata = builder
433            .metadata()
434            .and_then(|m| serde_json::from_str::<EntryMetadata>(m).ok())
435            .unwrap_or_else(|| EntryMetadata {
436                settings_tips: Vec::new(),
437                entropy: None,
438            });
439
440        // Set entropy
441        metadata.entropy = Some(entropy);
442
443        // Serialize and set metadata
444        let metadata_json = serde_json::to_string(&metadata)?;
445        builder.set_metadata_mut(metadata_json);
446
447        Ok(())
448    }
449
450    /// Stages an update for a specific subtree within this atomic transaction.
451    ///
452    /// This method is primarily intended for internal use by `Store` implementations
453    /// (like `DocStore::set`). It records the serialized `data` for the given `subtree`
454    /// name within the transaction's internal `EntryBuilder`.
455    ///
456    /// If this is the first modification to the named subtree within this transaction,
457    /// it also fetches and records the current tips of that subtree from the backend
458    /// to set the correct `subtree_parents` for the new entry.
459    ///
460    /// # Arguments
461    /// * `subtree` - The name of the subtree to update.
462    /// * `data` - The serialized CRDT data to stage for the subtree.
463    ///
464    /// # Returns
465    /// A `Result<()>` indicating success or an error.
466    pub(crate) async fn update_subtree(
467        &self,
468        subtree: impl AsRef<str>,
469        data: impl AsRef<str>,
470    ) -> Result<()> {
471        let subtree = subtree.as_ref();
472        let data = data.as_ref();
473
474        // Check if we need to fetch tips (check without holding borrow across await)
475        let needs_tips = {
476            let builder_ref = self.entry_builder.lock().unwrap();
477            let builder = builder_ref
478                .as_ref()
479                .ok_or(TransactionError::TransactionAlreadyCommitted)?;
480            !builder.subtrees().contains(&subtree.to_string())
481        };
482
483        // Fetch tips if needed (no borrow held across this await)
484        let tips = if needs_tips {
485            let backend = self.db.backend()?;
486            // FIXME: we should get the subtree tips while still using the parent pointers
487            Some(backend.get_store_tips(self.db.root_id(), subtree).await?)
488        } else {
489            None
490        };
491
492        // Now update the builder
493        let mut builder_ref = self.entry_builder.lock().unwrap();
494        let builder = builder_ref
495            .as_mut()
496            .ok_or(TransactionError::TransactionAlreadyCommitted)?;
497
498        builder.set_subtree_data_mut(subtree.to_string(), data.to_string());
499        if let Some(tips) = tips {
500            builder.set_subtree_parents_mut(subtree, tips);
501        }
502
503        Ok(())
504    }
505
506    /// Gets a handle to a specific `Store` for modification within this transaction.
507    ///
508    /// This method creates and returns an instance of the specified `Store` type `T`,
509    /// associated with this `Transaction`. The returned `Store` handle can be used to
510    /// stage changes (e.g., using `DocStore::set`).
511    /// These changes are recorded within this `Transaction`.
512    ///
513    /// If this is the first time this subtree is accessed within the transaction,
514    /// its parent tips will be fetched and stored.
515    ///
516    /// # Type Parameters
517    /// * `T` - The concrete `Store` implementation type to create.
518    ///
519    /// # Arguments
520    /// * `subtree_name` - The name of the subtree to get a modification handle for.
521    ///
522    /// # Returns
523    /// A `Result<T>` containing the `Store` handle.
524    pub async fn get_store<T>(&self, subtree_name: impl Into<String> + Send) -> Result<T>
525    where
526        T: Store + Send,
527    {
528        let subtree_name = subtree_name.into();
529
530        // Initialize subtree parents before checking _index
531        self.init_subtree_parents(&subtree_name).await?;
532
533        // Skip special system subtrees to avoid circular dependencies
534        let is_system_subtree =
535            subtree_name == INDEX || subtree_name == SETTINGS || subtree_name == ROOT;
536
537        if is_system_subtree {
538            // System subtrees don't use _index registration
539            return T::new(self, subtree_name).await;
540        }
541
542        // Check _index to determine if this is a new or existing subtree
543        let index_store = self.get_index().await?;
544        if index_store.contains(&subtree_name).await {
545            // Type validation for existing subtree
546            let subtree_info = index_store.get_entry(&subtree_name).await?;
547
548            if !T::supports_type_id(&subtree_info.type_id) {
549                return Err(StoreError::TypeMismatch {
550                    store: subtree_name,
551                    expected: T::type_id().to_string(),
552                    actual: subtree_info.type_id,
553                }
554                .into());
555            }
556
557            // Type supported - create the Store
558            T::new(self, subtree_name).await
559        } else {
560            // New subtree - init registers it in _index
561            T::init(self, subtree_name).await
562        }
563    }
564
565    /// Get the subtree tips reachable from the given main tree entries.
566    async fn get_subtree_tips(&self, subtree_name: &str, main_parents: &[ID]) -> Result<Vec<ID>> {
567        self.db
568            .backend()?
569            .get_store_tips_up_to_entries(self.db.root_id(), subtree_name, main_parents)
570            .await
571    }
572
573    /// Initialize subtree parents if this is the first time accessing this subtree
574    /// in this transaction.
575    pub(crate) async fn init_subtree_parents(&self, subtree_name: &str) -> Result<()> {
576        let main_parents = {
577            let builder_ref = self.entry_builder.lock().unwrap();
578            let builder = builder_ref
579                .as_ref()
580                .ok_or(TransactionError::TransactionAlreadyCommitted)?;
581
582            let subtrees = builder.subtrees();
583            if subtrees.contains(&subtree_name.to_string()) {
584                return Ok(()); // Already initialized
585            }
586            builder.parents().unwrap_or_default()
587        };
588
589        let tips = self.get_subtree_tips(subtree_name, &main_parents).await?;
590
591        let mut builder_ref = self.entry_builder.lock().unwrap();
592        let builder = builder_ref
593            .as_mut()
594            .ok_or(TransactionError::TransactionAlreadyCommitted)?;
595
596        // Initialize the subtree with proper parent relationships
597        // set_subtree_parents_mut creates the subtree with data=None if it doesn't exist
598        builder.set_subtree_parents_mut(subtree_name, tips);
599
600        Ok(())
601    }
602
603    /// Gets the currently staged data for a specific subtree within this transaction.
604    ///
605    /// This is intended for use by `Store` implementations to retrieve the data
606    /// they have staged locally within the `Transaction` before potentially merging
607    /// it with historical data.
608    ///
609    /// # Type Parameters
610    /// * `T` - The data type (expected to be a CRDT) to deserialize the staged data into.
611    ///
612    /// # Arguments
613    /// * `subtree_name` - The name of the subtree whose staged data is needed.
614    ///
615    /// # Returns
616    /// A `Result<Option<T>>`:
617    ///
618    /// # Behavior
619    /// - If the subtree doesn't exist or has no data, returns `Ok(None)`
620    /// - If the subtree exists but has empty data (empty string or whitespace), returns `Ok(None)`
621    /// - Otherwise deserializes the JSON data to type `T` and returns `Ok(Some(T))`
622    ///
623    /// # Errors
624    /// Returns an error if the transaction has already been committed or if the
625    /// subtree data exists but cannot be deserialized to type `T`.
626    pub fn get_local_data<T>(&self, subtree_name: impl AsRef<str>) -> Result<Option<T>>
627    where
628        T: Data,
629    {
630        let subtree_name = subtree_name.as_ref();
631        let builder_ref = self.entry_builder.lock().unwrap();
632        let builder = builder_ref
633            .as_ref()
634            .ok_or(TransactionError::TransactionAlreadyCommitted)?;
635
636        if let Ok(data) = builder.data(subtree_name) {
637            if data.trim().is_empty() {
638                Ok(None)
639            } else {
640                serde_json::from_str(data).map(Some).map_err(|e| {
641                    TransactionError::StoreDeserializationFailed {
642                        store: subtree_name.to_string(),
643                        reason: e.to_string(),
644                    }
645                    .into()
646                })
647            }
648        } else {
649            Ok(None)
650        }
651    }
652
653    /// Gets the fully merged historical state of a subtree up to the point this transaction began.
654    ///
655    /// This retrieves all relevant historical entries for the `subtree_name` from the backend,
656    /// considering the parent tips recorded when this `Transaction` was created (or when the
657    /// subtree was first accessed within the transaction). It deserializes the data from each
658    /// relevant entry into the CRDT type `T` and merges them according to `T`'s `CRDT::merge`
659    /// implementation.
660    ///
661    /// This is intended for use by `Store` implementations (e.g., in their `get` or `get_all` methods)
662    /// to provide the historical context against which staged changes might be applied or compared.
663    ///
664    /// # Type Parameters
665    /// * `T` - The CRDT type to deserialize and merge the historical subtree data into.
666    ///
667    /// # Arguments
668    /// * `subtree_name` - The name of the subtree.
669    ///
670    /// # Returns
671    /// A `Result<T>` containing the merged historical data of type `T`. Returns `Ok(T::default())`
672    /// if the subtree has no history prior to this transaction.
673    pub(crate) async fn get_full_state<T>(&self, subtree_name: impl AsRef<str> + Send) -> Result<T>
674    where
675        T: CRDT + Send,
676    {
677        let subtree_name = subtree_name.as_ref();
678
679        // Check if we need to initialize subtree tips (get data from RefCell before await)
680        let (needs_init, main_parents) = {
681            let builder_ref = self.entry_builder.lock().unwrap();
682            let builder = builder_ref
683                .as_ref()
684                .ok_or(TransactionError::TransactionAlreadyCommitted)?;
685
686            let subtrees = builder.subtrees();
687            if subtrees.contains(&subtree_name.to_string()) {
688                (false, Vec::new())
689            } else {
690                (true, builder.parents().unwrap_or_default())
691            }
692        };
693
694        // Initialize subtree tips if needed (async operations)
695        if needs_init {
696            let current_database_tips = self.db.backend()?.get_tips(self.db.root_id()).await?;
697
698            let tips = if main_parents == current_database_tips {
699                let backend = self.db.backend()?;
700                backend
701                    .get_store_tips(self.db.root_id(), subtree_name)
702                    .await?
703            } else {
704                // This transaction uses custom tips - use special handler
705                self.db
706                    .backend()?
707                    .get_store_tips_up_to_entries(self.db.root_id(), subtree_name, &main_parents)
708                    .await?
709            };
710
711            // Update RefCell after async operations
712            let mut builder_ref = self.entry_builder.lock().unwrap();
713            let builder = builder_ref
714                .as_mut()
715                .ok_or(TransactionError::TransactionAlreadyCommitted)?;
716            builder.set_subtree_parents_mut(subtree_name, tips);
717        }
718
719        // Get the parent pointers for this subtree
720        let parents = {
721            let builder_ref = self.entry_builder.lock().unwrap();
722            let builder = builder_ref
723                .as_ref()
724                .ok_or(TransactionError::TransactionAlreadyCommitted)?;
725            builder.subtree_parents(subtree_name).unwrap_or_default()
726        };
727
728        // If there are no parents, return a default
729        if parents.is_empty() {
730            return Ok(T::default());
731        }
732
733        // Compute the CRDT state using merge-base ROOT-to-target computation
734        self.compute_subtree_state_merge_based(subtree_name, &parents)
735            .await
736    }
737
738    /// Computes the CRDT state for a subtree using correct recursive merge-base algorithm.
739    ///
740    /// Algorithm:
741    /// 1. If no entries, return default state
742    /// 2. If single entry, compute its state recursively
743    /// 3. If multiple entries, find their merge base and compute state from there
744    ///
745    /// # Type Parameters
746    /// * `T` - The CRDT type to compute the state for
747    ///
748    /// # Arguments
749    /// * `subtree_name` - The name of the subtree
750    /// * `entry_ids` - The entry IDs to compute the merged state for (tips)
751    ///
752    /// # Returns
753    /// A `Result<T>` containing the computed CRDT state
754    async fn compute_subtree_state_merge_based<T>(
755        &self,
756        subtree_name: impl AsRef<str> + Send,
757        entry_ids: &[ID],
758    ) -> Result<T>
759    where
760        T: CRDT + Send,
761    {
762        // Base case: no entries
763        if entry_ids.is_empty() {
764            return Ok(T::default());
765        }
766
767        let subtree_name = subtree_name.as_ref();
768
769        // If we have a single entry, compute its state recursively
770        if entry_ids.len() == 1 {
771            return self
772                .compute_single_entry_state_recursive(subtree_name, &entry_ids[0])
773                .await;
774        }
775
776        // Multiple entries: check multi-tip cache first
777        let cache_id = create_merge_cache_id(entry_ids);
778
779        if let Some(cached_state) = self
780            .db
781            .backend()?
782            .get_cached_crdt_state(&cache_id, subtree_name)
783            .await?
784        {
785            let decrypted = self.decrypt_if_needed(subtree_name, &cached_state)?;
786            let result: T = serde_json::from_str(&decrypted)?;
787            return Ok(result);
788        }
789
790        // Cache miss: find merge base and compute state from there
791        let merge_base_id = self
792            .db
793            .backend()?
794            .find_merge_base(self.db.root_id(), subtree_name, entry_ids)
795            .await?;
796
797        // Get the merge base state recursively
798        let mut result = self
799            .compute_single_entry_state_recursive(subtree_name, &merge_base_id)
800            .await?;
801
802        // Get all entries from merge base to all tip entries (deduplicated and sorted)
803        let path_entries = {
804            self.db
805                .backend()?
806                .get_path_from_to(self.db.root_id(), subtree_name, &merge_base_id, entry_ids)
807                .await?
808        };
809
810        // Merge all path entries in order
811        result = self
812            .merge_path_entries(subtree_name, result, &path_entries)
813            .await?;
814
815        // Cache the computed merge result
816        let serialized = serde_json::to_string(&result)?;
817        let to_cache = self.encrypt_if_needed(subtree_name, &serialized)?;
818        // FIXME: Multiple tips in the cache is a hack
819        // cache_crdt_state is supposed to take in (ID, subtree, data)
820        // This caching only technically works by constructing a custom ID
821        self.db
822            .backend()?
823            .cache_crdt_state(&cache_id, subtree_name, to_cache)
824            .await?;
825
826        Ok(result)
827    }
828
829    /// Computes the CRDT state for a single entry using batch fetching.
830    ///
831    /// Algorithm:
832    /// 1. Check if entry state is cached → return it
833    /// 2. Fetch all ancestors in one batch query (sorted by height)
834    /// 3. Merge all entries in order from root to target
835    /// 4. Cache only the final result
836    ///
837    /// # Type Parameters
838    /// * `T` - The CRDT type to compute the state for
839    ///
840    /// # Arguments
841    /// * `subtree_name` - The name of the subtree
842    /// * `entry_id` - The entry ID to compute the state for
843    ///
844    /// # Returns
845    /// A `Result<T>` containing the computed CRDT state for the entry
846    fn compute_single_entry_state_recursive<'a, T>(
847        &'a self,
848        subtree_name: &'a str,
849        entry_id: &'a ID,
850    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T>> + Send + 'a>>
851    where
852        T: CRDT + Send + 'a,
853    {
854        Box::pin(async move {
855            // Step 1: Check if already cached
856            if let Some(cached_state) = self
857                .db
858                .backend()?
859                .get_cached_crdt_state(entry_id, subtree_name)
860                .await?
861            {
862                // Decrypt cached state if encryptor is registered
863                let decrypted = self.decrypt_if_needed(subtree_name, &cached_state)?;
864                let result: T = serde_json::from_str(&decrypted)?;
865                return Ok(result);
866            }
867
868            // Step 2: Batch fetch all ancestors sorted by height (root first)
869            // This single query replaces N recursive queries
870            let entries = self
871                .db
872                .backend()?
873                .get_store_from_tips(
874                    self.db.root_id(),
875                    subtree_name,
876                    std::slice::from_ref(entry_id),
877                )
878                .await?;
879
880            // Step 3: Merge all entries in order (already sorted by height, root first)
881            let mut result = T::default();
882            for entry in &entries {
883                let local_data = if let Ok(data) = entry.data(subtree_name) {
884                    // Decrypt before deserializing
885                    let plaintext = self.decrypt_if_needed(subtree_name, data)?;
886                    serde_json::from_str::<T>(&plaintext)?
887                } else {
888                    T::default()
889                };
890                result = result.merge(&local_data)?;
891            }
892
893            // Step 4: Cache only the final result (encrypted if encryptor is registered)
894            let serialized_state = serde_json::to_string(&result)?;
895            let to_cache = self.encrypt_if_needed(subtree_name, &serialized_state)?;
896            self.db
897                .backend()?
898                .cache_crdt_state(entry_id, subtree_name, to_cache)
899                .await?;
900
901            Ok(result)
902        })
903    }
904
905    /// Merges a sequence of entries into a CRDT state.
906    ///
907    /// # Arguments
908    /// * `subtree_name` - The name of the subtree
909    /// * `initial_state` - The initial CRDT state to merge into
910    /// * `entry_ids` - The entry IDs to merge in order
911    ///
912    /// # Returns
913    /// A `Result<T>` containing the merged CRDT state
914    async fn merge_path_entries<T>(
915        &self,
916        subtree_name: &str,
917        mut state: T,
918        entry_ids: &[ID],
919    ) -> Result<T>
920    where
921        T: CRDT,
922    {
923        for entry_id in entry_ids {
924            let entry = self.db.backend()?.get(entry_id).await?;
925
926            // Get local data for this entry in the subtree
927            let local_data = if let Ok(data) = entry.data(subtree_name) {
928                // Decrypt before deserializing
929                let plaintext = self.decrypt_if_needed(subtree_name, data)?;
930                serde_json::from_str::<T>(&plaintext)?
931            } else {
932                T::default()
933            };
934
935            state = state.merge(&local_data)?;
936        }
937
938        Ok(state)
939    }
940
941    /// Commits the transaction, finalizing and persisting the entry to the backend.
942    ///
943    /// This method:
944    /// 1. Takes ownership of the `EntryBuilder` from the internal `Option`
945    /// 2. Removes any empty subtrees
946    /// 3. Adds metadata if appropriate
947    /// 4. Sets authentication if configured
948    /// 5. Builds the immutable `Entry` using `EntryBuilder::build()`
949    /// 6. Signs the entry if authentication is configured
950    /// 7. Validates authentication if present
951    /// 8. Calculates the entry's content-addressable ID
952    /// 9. Persists the entry to the backend
953    /// 10. Returns the ID of the newly created entry
954    ///
955    /// After commit, the transaction cannot be used again, as the internal
956    /// `EntryBuilder` has been consumed.
957    ///
958    /// # Returns
959    /// A `Result<ID>` containing the ID of the committed entry.
960    pub async fn commit(self) -> Result<ID> {
961        // Check if this is a settings subtree update and get the effective settings before any borrowing
962        let has_settings_update = {
963            let builder_cell = self.entry_builder.lock().unwrap();
964            let builder = builder_cell
965                .as_ref()
966                .ok_or(TransactionError::TransactionAlreadyCommitted)?;
967            builder.subtrees().contains(&SETTINGS.to_string())
968        };
969
970        // Get settings using full CRDT state computation
971        let historical_settings = self.get_full_state::<Doc>(SETTINGS).await?;
972
973        // However, if this is a settings update and there's no historical auth but staged auth exists,
974        // use the staged settings for validation (this handles initial database creation with auth)
975        let effective_settings_for_validation = if has_settings_update {
976            let historical_has_auth = matches!(historical_settings.get("auth"), Some(Value::Doc(auth_map)) if !auth_map.is_empty());
977            if !historical_has_auth {
978                let staged_settings = self.get_local_data::<Doc>(SETTINGS)?.unwrap_or_default();
979                let staged_has_auth = matches!(staged_settings.get("auth"), Some(Value::Doc(auth_map)) if !auth_map.is_empty());
980                if staged_has_auth {
981                    staged_settings
982                } else {
983                    historical_settings
984                }
985            } else {
986                historical_settings
987            }
988        } else {
989            historical_settings
990        };
991
992        // VALIDATION: Ensure that the new settings state (after this transaction) doesn't corrupt auth
993        // This prevents committing entries that would corrupt the database's auth configuration
994        if has_settings_update {
995            // Compute what the new settings state will be after merging local changes
996            let local_settings = self.get_local_data::<Doc>(SETTINGS)?.unwrap_or_default();
997            let new_settings = effective_settings_for_validation.merge(&local_settings)?;
998
999            // Check if the new settings would have corrupted auth
1000            if new_settings.is_tombstone("auth") {
1001                // Auth was explicitly deleted - this would corrupt the database
1002                return Err(TransactionError::CorruptedAuthConfiguration.into());
1003            } else if let Some(auth_value) = new_settings.get("auth") {
1004                // Auth exists in new settings - check if it's the right type
1005                if !matches!(auth_value, Value::Doc(_)) {
1006                    // Auth exists but has wrong type (not a Doc) - this would corrupt the database
1007                    return Err(TransactionError::CorruptedAuthConfiguration.into());
1008                }
1009            }
1010            // If auth is None (not configured), that's fine - we allow empty auth
1011        }
1012
1013        // Ensure _index constraint: subtrees referenced in _index must appear in Entry.
1014        // This adds subtrees with None data if they're referenced in _index but not yet in builder.
1015        // First, get the data we need before any async operations
1016        let (_index_data_opt, main_parents, missing_subtrees) = {
1017            let builder_ref = self.entry_builder.lock().unwrap();
1018            let builder = builder_ref
1019                .as_ref()
1020                .ok_or(TransactionError::TransactionAlreadyCommitted)?;
1021
1022            let index_data_opt = builder.data(INDEX).ok().map(String::from);
1023            let main_parents = builder.parents().unwrap_or_default();
1024            let existing_subtrees = builder.subtrees();
1025
1026            // Find missing subtrees
1027            let missing = if let Some(ref index_data) = index_data_opt
1028                && let Ok(index_doc) = serde_json::from_str::<Doc>(index_data)
1029            {
1030                index_doc
1031                    .keys()
1032                    .filter(|name| !existing_subtrees.contains(&name.to_string()))
1033                    .cloned()
1034                    .collect::<Vec<_>>()
1035            } else {
1036                Vec::new()
1037            };
1038
1039            (index_data_opt, main_parents, missing)
1040        };
1041
1042        // Get tips for missing subtrees (async)
1043        let mut subtree_tips: Vec<(String, Vec<ID>)> = Vec::new();
1044        for subtree_name in missing_subtrees {
1045            let tips = self.get_subtree_tips(&subtree_name, &main_parents).await?;
1046            subtree_tips.push((subtree_name, tips));
1047        }
1048
1049        // Now update the builder with the tips
1050        {
1051            let mut builder_ref = self.entry_builder.lock().unwrap();
1052            let builder = builder_ref
1053                .as_mut()
1054                .ok_or(TransactionError::TransactionAlreadyCommitted)?;
1055
1056            for (subtree_name, tips) in subtree_tips {
1057                builder.set_subtree_parents_mut(&subtree_name, tips);
1058            }
1059
1060            builder.remove_empty_subtrees_mut()?;
1061        }
1062
1063        // Add metadata with settings tips for all entries
1064        // Get the backend to access settings tips (do async ops before RefCell borrow)
1065        let db_tips = self.db.get_tips().await?;
1066        let settings_tips = self
1067            .db
1068            .backend()?
1069            .get_store_tips_up_to_entries(self.db.root_id(), SETTINGS, &db_tips)
1070            .await?;
1071
1072        // Clone the builder from RefCell (limit borrow scope to avoid holding across await)
1073        let mut builder = {
1074            let builder_cell = self.entry_builder.lock().unwrap();
1075            let builder_from_cell = builder_cell
1076                .as_ref()
1077                .ok_or(TransactionError::TransactionAlreadyCommitted)?;
1078            builder_from_cell.clone()
1079        };
1080
1081        // Parse existing metadata if present, or create new
1082        let mut metadata = builder
1083            .metadata()
1084            .and_then(|m| serde_json::from_str::<EntryMetadata>(m).ok())
1085            .unwrap_or_else(|| EntryMetadata {
1086                settings_tips: Vec::new(),
1087                entropy: None,
1088            });
1089
1090        // Update settings tips
1091        metadata.settings_tips = settings_tips;
1092
1093        // Serialize the metadata
1094        let metadata_json = serde_json::to_string(&metadata)?;
1095
1096        // Add metadata to the entry builder
1097        builder.set_metadata_mut(metadata_json);
1098
1099        // Handle authentication configuration before building
1100        // All entries must now be authenticated - fail if no auth key is configured
1101
1102        // Use provided signing key
1103        let signing_key = if let Some((ref provided_key, ref identity)) = self.provided_signing_key
1104        {
1105            // Use provided signing key directly (already decrypted from UserKeyManager or device key)
1106            let key_clone = provided_key.clone();
1107
1108            // Build SigInfo from the already-typed SigKey identity
1109            let sig_builder = SigInfo::builder().key(identity.clone());
1110
1111            // Set auth ID on the entry builder (without signature initially)
1112            builder.set_sig_mut(sig_builder.build());
1113
1114            Some(key_clone)
1115        } else {
1116            // No authentication key configured
1117            return Err(TransactionError::AuthenticationRequired.into());
1118        };
1119        // Encrypt subtree data if encryptors are registered
1120        // This must happen before building the entry to ensure encrypted data is persisted
1121        {
1122            let encryptors = self.encryptors.lock().unwrap();
1123            for subtree_name in builder.subtrees() {
1124                if let Some(encryptor) = encryptors.get(&subtree_name) {
1125                    // Get the plaintext data from the builder
1126                    if let Ok(plaintext_data) = builder.data(&subtree_name)
1127                        && !plaintext_data.trim().is_empty()
1128                    {
1129                        // Encrypt the plaintext data (as bytes)
1130                        let ciphertext = encryptor.encrypt(plaintext_data.as_bytes())?;
1131                        // Encode as base64 for storage
1132                        let encoded = Base64::encode_string(&ciphertext);
1133                        // Update the builder with encrypted data
1134                        builder.set_subtree_data_mut(subtree_name.clone(), encoded);
1135                    }
1136                }
1137            }
1138        }
1139
1140        // Extract height strategy from settings (defaults to Incremental)
1141        // If this transaction includes settings updates, merge them to get the effective strategy
1142        let settings_for_height = if has_settings_update {
1143            let local_settings = self.get_local_data::<Doc>(SETTINGS)?.unwrap_or_default();
1144            effective_settings_for_validation.merge(&local_settings)?
1145        } else {
1146            effective_settings_for_validation.clone()
1147        };
1148        let height_strategy: HeightStrategy = settings_for_height
1149            .get_json("height_strategy")
1150            .unwrap_or_default();
1151
1152        // Compute heights from parent entries using the configured strategy
1153        {
1154            let backend = self.db.backend()?;
1155            let instance = self.db.instance()?;
1156            let calculator = height_strategy.into_calculator(instance.clock_arc());
1157
1158            // Compute main tree height using the height strategy
1159            let main_parents = builder.parents().unwrap_or_default();
1160            let max_parent_height = if main_parents.is_empty() {
1161                None
1162            } else {
1163                let mut max_height = 0u64;
1164                for parent_id in &main_parents {
1165                    if let Ok(parent) = backend.get(parent_id).await {
1166                        max_height = max_height.max(parent.height());
1167                    }
1168                }
1169                Some(max_height)
1170            };
1171            let tree_height = calculator.calculate_height(max_parent_height);
1172            builder.set_height_mut(tree_height);
1173
1174            // Compute subtree heights based on per-subtree settings from _index
1175            // System subtrees (prefixed with _) always inherit from tree.
1176            // Regular subtrees check _index for a height_strategy override.
1177            //
1178            // If a subtree has no override, its height is left as None, which means
1179            // Entry.subtree_height() will return the tree height (inheritance).
1180            let index = self.get_index().await.ok();
1181
1182            for subtree_name in builder.subtrees() {
1183                // Determine the effective strategy for this subtree:
1184                // - System subtrees (_settings, _index, etc.): inherit (None)
1185                // - User subtrees: look up in _index, default to inherit (None)
1186                let subtree_strategy: Option<HeightStrategy> = if subtree_name.starts_with('_') {
1187                    // System subtrees always inherit from tree
1188                    None
1189                } else if let Some(ref idx) = index {
1190                    idx.get_subtree_settings(&subtree_name)
1191                        .await
1192                        .ok()
1193                        .and_then(|s| s.height_strategy)
1194                } else {
1195                    None
1196                };
1197
1198                match subtree_strategy {
1199                    None => {
1200                        // Inherit from tree - height stays None (default)
1201                        // Entry.subtree_height() will return tree height
1202                    }
1203                    Some(strategy) => {
1204                        // Calculate independent height from subtree parents
1205                        let subtree_calculator = strategy.into_calculator(instance.clock_arc());
1206                        let subtree_parents =
1207                            builder.subtree_parents(&subtree_name).unwrap_or_default();
1208                        let max_subtree_parent_height = if subtree_parents.is_empty() {
1209                            None
1210                        } else {
1211                            let mut max_height = 0u64;
1212                            for parent_id in &subtree_parents {
1213                                if let Ok(parent) = backend.get(parent_id).await
1214                                    && let Ok(height) = parent.subtree_height(&subtree_name)
1215                                {
1216                                    max_height = max_height.max(height);
1217                                }
1218                            }
1219                            Some(max_height)
1220                        };
1221                        let subtree_height =
1222                            subtree_calculator.calculate_height(max_subtree_parent_height);
1223                        builder.set_subtree_height_mut(&subtree_name, Some(subtree_height));
1224                    }
1225                }
1226            }
1227        }
1228
1229        // Build the final immutable Entry
1230        let mut entry = builder.build()?;
1231
1232        // CRITICAL VALIDATION: Ensure entry structural integrity before commit
1233        //
1234        // This validation is crucial because the transaction layer has already:
1235        // 1. Discovered proper parent relationships through DAG traversal
1236        // 2. Set up correct subtree parents via find_subtree_parents_from_main_parents()
1237        // 3. Ensured all references point to valid entries in the backend
1238        //
1239        // The validate() call here ensures that:
1240        // - Non-root entries have main tree parents (preventing orphaned nodes)
1241        // - Parent IDs are not empty strings (preventing reference errors)
1242        // - The entry structure is valid before signing and storage
1243        //
1244        // This catches any issues early in the transaction, providing clear error
1245        // messages before the entry is signed or reaches the backend storage layer.
1246        entry.validate()?;
1247
1248        // Sign the entry if we have a signing key
1249        if let Some(signing_key) = signing_key {
1250            let signature = sign_entry(&entry, &signing_key)?;
1251            entry.sig.sig = Some(signature);
1252        }
1253
1254        // Validate authentication (all entries must be authenticated)
1255        let mut validator = AuthValidator::new();
1256
1257        // Get the final settings state for validation
1258        // IMPORTANT: For permission checking, we must use the historical auth configuration
1259        // (before this transaction), not the auth configuration from the current entry.
1260        // This prevents operations from modifying their own permission requirements.
1261
1262        // Extract AuthSettings from effective settings for validation
1263        // IMPORTANT: Distinguish between empty auth vs corrupted/deleted auth:
1264        // - None: No auth ever configured → Allow unsigned operations (empty AuthSettings)
1265        // - Some(Doc): Normal auth configuration → Use it for validation
1266        // - Tombstone (deleted): Auth was configured then deleted → CORRUPTED (fail-safe)
1267        // - Some(other types): Wrong type in auth field → CORRUPTED (fail-safe)
1268        //
1269        // NOTE: Doc::get() hides tombstones (returns None for deleted values), so we need
1270        // to check for tombstones explicitly using is_tombstone() before using get().
1271        let auth_settings_for_validation = if effective_settings_for_validation.is_tombstone("auth")
1272        {
1273            // Auth was configured then explicitly deleted - this is corrupted
1274            return Err(TransactionError::CorruptedAuthConfiguration.into());
1275        } else {
1276            match effective_settings_for_validation.get("auth") {
1277                Some(Value::Doc(auth_doc)) => auth_doc.clone().into(),
1278                None => AuthSettings::new(), // Empty auth - never configured
1279                Some(_) => {
1280                    // Auth exists but has wrong type (not a Doc) - this is corrupted
1281                    return Err(TransactionError::CorruptedAuthConfiguration.into());
1282                }
1283            }
1284        };
1285
1286        let instance = self.db.instance()?;
1287
1288        // Validate entry (signature + permissions)
1289        let is_valid = validator
1290            .validate_entry(&entry, &auth_settings_for_validation, Some(&instance))
1291            .await?;
1292
1293        if !is_valid {
1294            return Err(TransactionError::EntryValidationFailed.into());
1295        }
1296
1297        let verification_status = VerificationStatus::Verified;
1298
1299        // Get the entry's ID
1300        let id = entry.id();
1301
1302        // Write entry through Instance which handles backend storage and callback dispatch
1303        let instance = self.db.instance()?;
1304        instance
1305            .put_entry(
1306                self.db.root_id(),
1307                verification_status,
1308                entry.clone(),
1309                WriteSource::Local,
1310            )
1311            .await?;
1312
1313        Ok(id)
1314    }
1315}