Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3e6f29b
feat(private-document-store): add grovedb-private-document-store crate
QuantumExplorer Aug 3, 2026
26a804b
feat: add PrivateDocumentStore element type, gated to GROVE_V4 (#784)
QuantumExplorer Aug 3, 2026
7df52c3
test: raise PrivateDocumentStore patch coverage above the 90% codecov…
QuantumExplorer Aug 3, 2026
fff8363
fix: address CodeRabbit review on PrivateDocumentStore (PR #787)
QuantumExplorer Aug 3, 2026
b94da45
Merge origin/develop into feat/private-document-store
QuantumExplorer Aug 17, 2026
3f1d03e
fix: address the PrivateDocumentStore review findings (PR #787)
QuantumExplorer Aug 19, 2026
677508a
Merge remote-tracking branch 'origin/develop' into feat/private-docum…
QuantumExplorer Aug 19, 2026
4db7b47
fix: thread the declared chunk power into PrivateDocumentStore estimates
QuantumExplorer Aug 19, 2026
637b3ab
fix: address the second review round on PrivateDocumentStore (PR #787)
QuantumExplorer Aug 19, 2026
a6533cd
fix: bill the uncached MMR root read on the lazy path (PR #787)
QuantumExplorer Aug 19, 2026
1d2304c
fix: address the review-body findings on PrivateDocumentStore (PR #787)
QuantumExplorer Aug 19, 2026
2a90139
fix(costs): correct five hash and seek accounting errors on the store…
QuantumExplorer Aug 19, 2026
f05222d
test(pds): cover the corruption and storage-fault paths
QuantumExplorer Aug 19, 2026
540504b
fix(costs): bill compaction work and MMR merge hashes
QuantumExplorer Aug 19, 2026
b7aa74f
fix(costs): drop the duplicated MMR merge charge, and report bagging
QuantumExplorer Aug 19, 2026
f5fb2a7
feat(version): gate the MMR hash-charge corrections behind V0/V1
QuantumExplorer Aug 19, 2026
0e404d6
fix(costs): gate the CommitmentTree compaction under-charge into V4
QuantumExplorer Aug 19, 2026
6fa3277
fix(bulk-append): drop the dead initializer flagged by -D warnings
QuantumExplorer Aug 19, 2026
2c2e95c
refactor: take grove_version directly instead of paired _with_version…
QuantumExplorer Aug 20, 2026
11768ae
refactor(mmr): take grove_version directly on push, get_root and gen_…
QuantumExplorer Aug 20, 2026
f6e76c9
test(pds): cover the empty-at-creation guard on total_count
QuantumExplorer Aug 20, 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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"grovedb-bulk-append-tree",
"grovedb-dense-fixed-sized-merkle-tree",
"grovedb-query",
"grovedb-private-document-store",
]

[workspace.dependencies]
Expand Down
59 changes: 59 additions & 0 deletions grovedb-element/src/element/constructor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,65 @@ impl Element {
Element::DenseAppendOnlyFixedSizeTree(count, height, flags)
}

/// Set element to an empty private document store.
///
/// Returns `InvalidInput` unless `entry_size >= 1` and `chunk_power` is
/// in `1..=16` (the underlying `BulkAppendTree` dense-buffer height
/// range). Unlike `empty_commitment_tree` / `empty_bulk_append_tree`,
/// the constraints are enforced eagerly here: the configuration is
/// committed into the state root, so an unusable config must never be
/// constructible.
pub fn empty_private_document_store(
entry_size: u32,
chunk_power: u8,
) -> Result<Self, ElementError> {
Self::empty_private_document_store_with_flags(entry_size, chunk_power, None)
}

/// Set element to an empty private document store with flags.
///
/// Same validation as [`Element::empty_private_document_store`].
pub fn empty_private_document_store_with_flags(
entry_size: u32,
chunk_power: u8,
flags: Option<ElementFlags>,
) -> Result<Self, ElementError> {
if entry_size == 0 {
return Err(ElementError::InvalidInput(
"private document store entry_size must be non-zero",
));
}
if !(1..=16).contains(&chunk_power) {
return Err(ElementError::InvalidInput(
"private document store chunk_power must be between 1 and 16",
));
}
Ok(Element::PrivateDocumentStore(
0,
entry_size,
chunk_power,
flags,
))
}

/// Set element to a private document store with all fields.
///
/// Restoration constructor: unchecked, mirroring `new_commitment_tree` /
/// `new_bulk_append_tree` — it rebuilds an element from already-validated
/// state (stored bytes, batch metadata). Invalid configurations are
/// rejected at every real ingress: the `empty_*` constructors, the direct
/// and batch insert paths, and both (de)serialization codecs
/// (`Element::serialize` / `Element::deserialize` / serde) via
/// [`Element::validate_private_document_store_config`].
pub fn new_private_document_store(
total_count: u64,
entry_size: u32,
chunk_power: u8,
flags: Option<ElementFlags>,
) -> Self {
Element::PrivateDocumentStore(total_count, entry_size, chunk_power, flags)
}

/// Set element to an empty provable sum-indexed tree without flags.
pub fn empty_provable_sum_indexed_tree() -> Self {
Element::ProvableSumIndexedTree(None, None, 0, None)
Expand Down
20 changes: 20 additions & 0 deletions grovedb-element/src/element/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ impl Element {
| Element::ProvableSumIndexedTree(..)
| Element::ProvableCountIndexedTree(..)
| Element::ProvableCountProvableSumIndexedTree(..)
| Element::PrivateDocumentStore(..)
)
}

Expand Down Expand Up @@ -414,6 +415,12 @@ impl Element {
matches!(self.underlying(), Element::DenseAppendOnlyFixedSizeTree(..))
}

/// Check if the element is a private document store. Looks through
/// `NonCounted`.
pub fn is_private_document_store(&self) -> bool {
matches!(self.underlying(), Element::PrivateDocumentStore(..))
}

/// Check if the element is a tree type that stores data in the data
/// namespace as non-Merk entries. These tree types have an always-empty
/// Merk (root_key = None) and never contain child subtrees. The data
Expand All @@ -430,6 +437,7 @@ impl Element {
| Element::MmrTree(..)
| Element::BulkAppendTree(..)
| Element::DenseAppendOnlyFixedSizeTree(..)
| Element::PrivateDocumentStore(..)
)
}

Expand All @@ -443,6 +451,7 @@ impl Element {
Element::MmrTree(mmr_size, _) => Some(*mmr_size),
Element::BulkAppendTree(count, ..) => Some(*count),
Element::DenseAppendOnlyFixedSizeTree(count, ..) => Some(*count as u64),
Element::PrivateDocumentStore(count, ..) => Some(*count),
_ => None,
}
}
Expand Down Expand Up @@ -470,6 +479,7 @@ impl Element {
| Element::MmrTree(..)
| Element::BulkAppendTree(..)
| Element::DenseAppendOnlyFixedSizeTree(..)
| Element::PrivateDocumentStore(..)
| Element::ProvableSumIndexedTree(Some(_), ..)
| Element::ProvableSumIndexedTree(_, Some(_), ..)
| Element::ProvableCountIndexedTree(Some(_), ..)
Expand Down Expand Up @@ -663,6 +673,7 @@ impl Element {
| Element::MmrTree(.., flags)
| Element::BulkAppendTree(.., flags)
| Element::DenseAppendOnlyFixedSizeTree(.., flags)
| Element::PrivateDocumentStore(.., flags)
| Element::ProvableSumIndexedTree(.., flags)
| Element::ProvableCountIndexedTree(.., flags)
| Element::ReferenceWithSumItem(.., flags) => flags,
Expand Down Expand Up @@ -695,6 +706,7 @@ impl Element {
| Element::MmrTree(.., flags)
| Element::BulkAppendTree(.., flags)
| Element::DenseAppendOnlyFixedSizeTree(.., flags)
| Element::PrivateDocumentStore(.., flags)
| Element::ProvableSumIndexedTree(.., flags)
| Element::ProvableCountIndexedTree(.., flags)
| Element::ReferenceWithSumItem(.., flags) => flags,
Expand Down Expand Up @@ -727,6 +739,7 @@ impl Element {
| Element::MmrTree(.., flags)
| Element::BulkAppendTree(.., flags)
| Element::DenseAppendOnlyFixedSizeTree(.., flags)
| Element::PrivateDocumentStore(.., flags)
| Element::ProvableSumIndexedTree(.., flags)
| Element::ProvableCountIndexedTree(.., flags)
| Element::ReferenceWithSumItem(.., flags) => flags,
Expand Down Expand Up @@ -758,6 +771,7 @@ impl Element {
| Element::MmrTree(.., flags)
| Element::BulkAppendTree(.., flags)
| Element::DenseAppendOnlyFixedSizeTree(.., flags)
| Element::PrivateDocumentStore(.., flags)
| Element::ProvableSumIndexedTree(.., flags)
| Element::ProvableCountIndexedTree(.., flags)
| Element::ReferenceWithSumItem(.., flags) => *flags = new_flags,
Expand Down Expand Up @@ -1320,6 +1334,12 @@ mod flag_accessor_tests {
check_accessors_round_trip(e);
}

#[test]
fn private_document_store_flag_accessors() {
let e = Element::PrivateDocumentStore(0, 64, 4, flags());
check_accessors_round_trip(e);
}

#[test]
fn dense_append_only_tree_flag_accessors() {
let e = Element::DenseAppendOnlyFixedSizeTree(0, 0, flags());
Expand Down
77 changes: 77 additions & 0 deletions grovedb-element/src/element/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,31 @@ pub enum Element {
Vec<(u8, Option<Vec<u8>>)>,
Option<ElementFlags>,
),
/// Private document store: an append-only store of fixed-size opaque
/// entries, a thin wrapper over a `BulkAppendTree` (the same
/// relationship `CommitmentTree` has to it, minus the Sinsemilla
/// frontier). Entries are write-once — there is no per-entry delete or
/// update; immutability is enforced by the type.
///
/// Fields: `(total_count, entry_size, chunk_power, flags)`
/// - `total_count`: Number of entries appended so far.
/// - `entry_size`: Committed byte length of every entry; appends of any
/// other length are rejected.
/// - `chunk_power`: Log2 of the chunk size (actual size = `1 <<
/// chunk_power`).
/// - `flags`: Optional per-element metadata.
///
/// The state root
/// (`blake3("pds_state" || config_hash || bulk_state_root)`, where
/// `config_hash` commits to `{entry_size, chunk_power}`) flows through
/// the Merk child hash mechanism (`insert_subtree`'s
/// `subtree_root_hash` parameter), so the declared configuration is
/// consensus-visible and a proof can never be reinterpreted under a
/// different config.
///
/// Variant order in this enum determines bincode's variant-index
/// encoding on disk. This variant gets index 24.
PrivateDocumentStore(u64, u32, u8, Option<ElementFlags>),
}

pub fn hex_to_ascii(hex_value: &[u8]) -> String {
Expand Down Expand Up @@ -590,6 +615,18 @@ impl fmt::Display for Element {
.map_or(String::new(), |f| format!(", flags: {:?}", f))
)
}
Element::PrivateDocumentStore(total_count, entry_size, chunk_power, flags) => {
write!(
f,
"PrivateDocumentStore(count: {}, entry_size: {}, chunk_power: {}{})",
total_count,
entry_size,
chunk_power,
flags
.as_ref()
.map_or(String::new(), |f| format!(", flags: {:?}", f))
)
}
Element::NotSummed(inner) => {
write!(f, "NotSummed({})", inner)
}
Expand Down Expand Up @@ -667,6 +704,7 @@ impl Element {
Element::ProvableCountProvableSumIndexedTree(..) => {
ElementType::ProvableCountProvableSumIndexedTree
}
Element::PrivateDocumentStore(..) => ElementType::PrivateDocumentStore,
Element::NonCounted(inner) => match inner.element_type() {
ElementType::Item => ElementType::NonCountedItem,
ElementType::Reference => ElementType::NonCountedReference,
Expand Down Expand Up @@ -699,6 +737,7 @@ impl Element {
ElementType::ProvableCountProvableSumIndexedTree => {
ElementType::NonCountedProvableCountProvableSumIndexedTree
}
ElementType::PrivateDocumentStore => ElementType::NonCountedPrivateDocumentStore,
// Inner is always a base type — nested wrappers are
// forbidden at construction and (de)serialization.
already_non_counted => already_non_counted,
Expand Down Expand Up @@ -740,6 +779,34 @@ impl Element {
self.element_type().as_str()
}

/// Validate the committed configuration of a `PrivateDocumentStore`
/// element, looking through `NonCounted`: `entry_size` must be non-zero
/// and `chunk_power` must be in `1..=16` (the underlying `BulkAppendTree`
/// dense-buffer height range). Returns `Ok(())` for every other variant.
///
/// The configuration is committed into the store's state root, so an
/// unusable configuration must not be representable: the checked
/// constructors, the insert paths, and both (de)serialization codecs
/// (bincode and serde) all enforce this. `new_private_document_store`
/// itself stays unchecked — it is the restoration constructor used to
/// rebuild elements from already-validated on-disk state, mirroring
/// `new_commitment_tree` / `new_bulk_append_tree`.
pub fn validate_private_document_store_config(&self) -> Result<(), crate::error::ElementError> {
if let Element::PrivateDocumentStore(_, entry_size, chunk_power, _) = self.underlying() {
if *entry_size == 0 {
return Err(crate::error::ElementError::InvalidInput(
"private document store entry_size must be non-zero",
));
}
if !(1..=16).contains(chunk_power) {
return Err(crate::error::ElementError::InvalidInput(
"private document store chunk_power must be between 1 and 16",
));
}
}
Ok(())
}

/// Verify the wrapper invariants for `self`:
/// - `NonCounted`, `NotSummed`, and `NotCountedOrSummed` may not nest
/// in any combination.
Expand Down Expand Up @@ -892,6 +959,7 @@ mod serde_impl {
Vec<(u8, Option<Vec<u8>>)>,
Option<ElementFlags>,
),
PrivateDocumentStore(u64, u32, u8, Option<ElementFlags>),
}

impl From<ElementShadow> for Element {
Expand Down Expand Up @@ -941,6 +1009,9 @@ mod serde_impl {
ElementShadow::ProvableCountProvableSumIndexedTree(pk, c, s, axes, f) => {
Element::ProvableCountProvableSumIndexedTree(pk, c, s, axes, f)
}
ElementShadow::PrivateDocumentStore(c, e, p, f) => {
Element::PrivateDocumentStore(c, e, p, f)
}
}
}
}
Expand All @@ -956,6 +1027,11 @@ mod serde_impl {
// built by recursive `From<ElementShadow>` calls, so the check
// at each level catches a violation at any depth.
Self::check_recursive_wrapper_invariants(&element).map_err(D::Error::custom)?;
// A PrivateDocumentStore's committed config must be valid at
// every ingress — including this external-tooling codec.
element
.validate_private_document_store_config()
.map_err(D::Error::custom)?;
Ok(element)
}
}
Expand Down Expand Up @@ -990,6 +1066,7 @@ mod serde_impl {
let cases = vec![
Element::Item(b"abc".to_vec(), None),
Element::SumTree(Some(b"r".to_vec()), 42, None),
Element::PrivateDocumentStore(9, 64, 4, Some(vec![1])),
Element::new_non_counted(Element::Item(b"x".to_vec(), None)).unwrap(),
Element::new_not_summed(Element::SumTree(None, 100, None)).unwrap(),
Element::new_not_counted_or_summed(Element::CountSumTree(None, 3, 100, None))
Expand Down
22 changes: 22 additions & 0 deletions grovedb-element/src/element/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ impl Element {
}
}
}
// A PrivateDocumentStore's committed config is bound into its state
// root; an unusable config must never reach disk. The checked
// constructors and insert paths already enforce this — the codec
// check closes the caller-built-element gap.
if let Err(e) = self.validate_private_document_store_config() {
return Err(ElementError::CorruptedData(format!(
"invalid private document store config: {}",
e
)));
}
let config = config::standard().with_big_endian().with_no_limit();
bincode::encode_to_vec(self, config)
.map_err(|e| ElementError::CorruptedData(format!("unable to serialize element {}", e)))
Expand Down Expand Up @@ -188,6 +198,18 @@ impl Element {
}
}
}
// Reject a PrivateDocumentStore with an unusable committed config
// (entry_size 0 or chunk_power outside 1..=16). No such bytes can
// legitimately exist — serialization and every insert path enforce
// the same bound — so this cannot reject previously-valid data;
// it makes the invalid configuration unrepresentable, mirroring
// the wrapper-invariant checks above.
if let Err(e) = elem.validate_private_document_store_config() {
return Err(ElementError::CorruptedData(format!(
"deserialized private document store with invalid config: {}",
e
)));
}
Ok(elem)
}
}
Expand Down
15 changes: 15 additions & 0 deletions grovedb-element/src/element/visualize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,21 @@ impl Visualize for Element {
drawer = f.visualize(drawer)?;
}
}
Element::PrivateDocumentStore(total_count, entry_size, chunk_power, flags) => {
drawer.write(
format!(
"private_document_store: count: {total_count} entry_size: {entry_size} \
chunk_power: {chunk_power}",
)
.as_bytes(),
)?;

if let Some(f) = flags
&& !f.is_empty()
{
drawer = f.visualize(drawer)?;
}
}
Element::NonCounted(inner) => {
drawer.write(b"non_counted(")?;
drawer = inner.visualize(drawer)?;
Expand Down
Loading
Loading