Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1162b50
feat: expose ingest_subtree_sst + replace_commitment_tree_subtree_root
shumkov May 25, 2026
04f2d42
feat: expose raw_storage() on GroveDb
shumkov May 25, 2026
48a000b
feat(commitment-tree): frontier-less bulk seeding for sync/scale testing
QuantumExplorer May 25, 2026
3fd3990
fix(commitment-tree): reject frontier-less seeding on a non-empty fro…
QuantumExplorer May 25, 2026
b7b2090
refactor(commitment-tree): rename feature to test-seeding-ct; drop op…
QuantumExplorer May 25, 2026
d3adb02
test(commitment-tree): cover frontier-less seeding error paths for pa…
QuantumExplorer May 25, 2026
5e4761b
perf(bulk-append-tree): cache MMR root so append is O(1), not O(N) pe…
QuantumExplorer May 25, 2026
60d1219
bench(commitment-tree): add 1M frontier-less seeding throughput bench…
QuantumExplorer May 25, 2026
fa7a230
feat(commitment-tree): batched append_many_raw — replace _without_fro…
QuantumExplorer May 27, 2026
bdc0d6e
docs(commitment-tree): add --features server to seeding bench example…
QuantumExplorer May 27, 2026
5eb7a53
fix(commitment-tree): don't commit_mmr inside append_many_raw
QuantumExplorer May 28, 2026
4cedf18
Merge remote-tracking branch 'origin/develop' into feat/snapshot-appl…
shumkov Jun 1, 2026
bd36a1d
style: cargo fmt --all
shumkov Jun 1, 2026
67acdbc
chore: drop vestigial BulkAppendTree::append_many helper
shumkov Jun 1, 2026
2e9dff5
feat: gate snapshot-bootstrap surface behind `unsafe-dump-load` feature
shumkov Jun 1, 2026
c7f6dc3
style: cargo fmt (cfg-gated import order)
shumkov Jun 2, 2026
62e1296
test(grovedb): unsafe-dump-load subtree roundtrip + non-tree rejection
shumkov Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions grovedb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ rand = { workspace = true }
rand_distr = "0.6"
assert_matches = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "time", "sync"] }
# For the `unsafe-dump-load` dump+ingest roundtrip test: writing the SST and
# ingesting it both need direct rocksdb types. Pinned to the same revision
# used by `grovedb-storage`'s optional dep.
rocksdb = { git = "https://github.com/QuantumExplorer/rust-rocksdb.git", rev = "52772eea7bcd214d1d07d80aa538b1d24e5015b7" }

[[bench]]
name = "insertion_benchmark"
Expand Down Expand Up @@ -119,6 +123,17 @@ grovedbg = [
"zip-extensions",
"tempfile",
]
# Snapshot-bootstrap primitives — escape hatches that bypass grovedb's normal
# validation:
# - `GroveDb::raw_storage` (escape hatch to StorageContext)
# - `RocksDbStorage::ingest_subtree_sst` (bulk-load SST into a CF)
# - `GroveDb::replace_subtree_root` (caller-provided child hash, no
# cross-check against actual subtree state)
# Enable ONLY for build-time tooling that needs to dump+restore a subtree
# out-of-band (e.g. devnet shielded-pool snapshot bake/apply). Misuse will
# produce an inconsistent Merk tree — there is no runtime check that the
# caller-provided hash matches the underlying state.
unsafe-dump-load = ["grovedb-storage/unsafe-dump-load"]

[build-dependencies]
hex-literal = "1.1.0"
Expand Down
48 changes: 48 additions & 0 deletions grovedb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,54 @@ impl GroveDb {
Ok(())
}

/// Reborrow the underlying [`grovedb_storage::rocksdb_storage::RocksDbStorage`]
/// for callers that need to use the public [`grovedb_storage::Storage`]
/// trait directly — notably to open a [`grovedb_storage::StorageContext`]
/// at a path for raw iteration or low-level reads.
///
/// This is intended for snapshot/replication tooling that needs to walk
/// a subtree's raw RocksDB state without going through GroveDb's typed
/// element API. Normal callers should NOT use this — go through GroveDb's
/// typed operations (`insert`, `get`, `commitment_tree_*`) instead.
///
/// Stability: this is an escape hatch. The exact `RocksDbStorage` shape
/// is subject to change as grovedb's internals evolve.
///
/// Gated behind the `unsafe-dump-load` feature — production builds should
/// leave it off so this escape hatch isn't even compiled in.
#[cfg(feature = "unsafe-dump-load")]
pub fn raw_storage(&self) -> &grovedb_storage::rocksdb_storage::RocksDbStorage {
&self.db
}

/// Bulk-ingest a single SST file (produced by `rocksdb::SstFileWriter`)
/// into the named column family of the underlying RocksDB.
///
/// Delegates to
/// [`grovedb_storage::rocksdb_storage::RocksDbStorage::ingest_subtree_sst`].
/// Intended for snapshot-based bootstrap of a single subtree's storage
/// state — see that method's docs for safety contract.
///
/// CF name for ordinary data storage is the default CF
/// (`rocksdb::DEFAULT_COLUMN_FAMILY_NAME`). Aux/roots/meta CFs are also
/// valid targets if a snapshot tool happens to cover them.
///
/// This call bypasses any open transaction. The caller is responsible for
/// transaction semantics at a higher layer (e.g. only call when the
/// destination subtree is known empty, and rely on InitChain
/// abort = wipe-and-restart for failure recovery).
///
/// Gated behind the `unsafe-dump-load` feature.
#[cfg(feature = "unsafe-dump-load")]
pub fn ingest_subtree_sst(
&self,
cf_name: &str,
sst_path: &std::path::Path,
) -> Result<(), Error> {
self.db.ingest_subtree_sst(cf_name, sst_path)?;
Ok(())
}

/// Opens the transactional Merk at the given path. Returns CostResult.
fn open_transactional_merk_at_path<'db, 'b, B>(
&'db self,
Expand Down
6 changes: 6 additions & 0 deletions grovedb/src/operations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,11 @@ pub mod bulk_append_tree;
#[cfg(feature = "minimal")]
pub mod dense_tree;

/// Caller-driven subtree-root replacement. Bypasses grovedb's normal
/// "compute child hash from subtree state" invariant — see the module-level
/// docs in `replace_subtree_root.rs` for the safety contract.
#[cfg(all(feature = "minimal", feature = "unsafe-dump-load"))]
pub mod replace_subtree_root;

#[cfg(feature = "minimal")]
pub use get::{QueryItemOrSumReturnType, MAX_REFERENCE_HOPS};
120 changes: 120 additions & 0 deletions grovedb/src/operations/replace_subtree_root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! Caller-driven subtree-root replacement.
//!
//! Replaces a subtree leaf's child hash + element data inside its parent
//! Merk with caller-provided values, **without** re-reading the subtree to
//! compute the hash. This is the tail of every `*_insert` operation
//! (commitment_tree, mmr_tree, bulk_append_tree, dense_tree) factored out as
//! a single generic helper, intended for **snapshot-based bootstrap** only.
//!
//! # Safety contract
//!
//! The caller is responsible for ensuring `new_combined_root` actually
//! matches the subtree's underlying storage state. Intended pattern:
//!
//! 1. Ingest the subtree's raw storage out-of-band (typically via
//! [`grovedb_storage::rocksdb_storage::RocksDbStorage::ingest_subtree_sst`]).
//! 2. Open a `StorageContext` at the subtree path and reconstruct the
//! appropriate typed tree (CommitmentTree / Mmr / BulkAppend / Dense).
//! 3. Compute the post-ingest combined root via that tree's own root fn.
//! 4. Call [`GroveDb::replace_subtree_root`] with the verified hash and a
//! correctly-constructed [`Element`] (e.g. `Element::new_commitment_tree`).
//!
//! Mismatches between `new_combined_root` and what re-reading the subtree
//! would compute produce an inconsistent Merk tree (the parent's recorded
//! child hash diverges from actual subtree state). This method does NOT
//! detect such mismatches at runtime; misuse is silent corruption.
//!
//! Gated behind the `unsafe-dump-load` Cargo feature.

use std::collections::HashMap;

use grovedb_costs::{
cost_return_on_error, cost_return_on_error_into, CostResult, CostsExt, OperationCost,
};
use grovedb_merk::element::{
insert::ElementInsertToStorageExtensions, tree_type::ElementTreeTypeExtensions,
};
use grovedb_path::SubtreePath;
use grovedb_storage::{Storage, StorageBatch};
use grovedb_version::version::GroveVersion;

use crate::{util::TxRef, Element, Error, GroveDb, TransactionArg};

impl GroveDb {
/// Replace the child hash + element data of a subtree leaf in its parent
/// Merk with caller-provided values. See the
/// [module-level docs](self) for the safety contract.
pub fn replace_subtree_root<'b, B, P>(
&self,
path: P,
key: &[u8],
new_element: Element,
new_combined_root: [u8; 32],
transaction: TransactionArg,
grove_version: &GroveVersion,
) -> CostResult<(), Error>
where
B: AsRef<[u8]> + 'b,
P: Into<SubtreePath<'b, B>>,
{
let mut cost = OperationCost::default();

// Cheap sanity check: must be a tree-shaped Element (Item / Reference
// have no child hash slot). Caller still owns hash-vs-state
// correctness.
if new_element.tree_type().is_none() {
return Err(Error::InvalidInput(
"replace_subtree_root: element is not a tree variant",
))
.wrap_with_cost(cost);
}

let path: SubtreePath<B> = path.into();
let tx = TxRef::new(&self.db, transaction);
let batch = StorageBatch::new();

let mut parent_merk = cost_return_on_error!(
&mut cost,
self.open_transactional_merk_at_path(
path.clone(),
tx.as_ref(),
Some(&batch),
grove_version,
)
);

cost_return_on_error_into!(
&mut cost,
new_element.insert_subtree(
&mut parent_merk,
key,
new_combined_root,
None,
grove_version,
)
);

let mut merk_cache = HashMap::new();
merk_cache.insert(path.clone(), parent_merk);

cost_return_on_error!(
&mut cost,
self.propagate_changes_with_transaction(
merk_cache,
path,
tx.as_ref(),
&batch,
grove_version,
)
);

cost_return_on_error!(
&mut cost,
self.db
.commit_multi_context_batch(batch, Some(tx.as_ref()))
.map_err(Into::into)
);

tx.commit_local().wrap_with_cost(cost)
}
}
Loading
Loading