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}