Count Index Examples
This chapter walks through a representative contract and shows what a count-query proof actually proves — both the path query the prover signs and the verified element the verifier extracts. Every example uses the same widget contract (the same one the count-query bench at packages/rs-drive/benches/document_count_worst_case.rs populates) so the proof bytes, verified elements, and diagrams can all be cross-referenced against the same data.
The chapter assumes you've read Document Count Trees — that chapter explains the three tree variants (NormalTree / CountTree / ProvableCountTree), what Element::NonCounted does, and how the schema's documentsCountable / rangeCountable flags select between them. Here we take that machinery as given and trace what each query sees.
The Widget Contract
The widget document type carries three properties (brand, color, serial), opts into total counts at the doctype level via documentsCountable: true, and declares three indexes covering the count-query surface:
{
"type": "object",
"documentsCountable": true,
"properties": {
"brand": { "type": "string", "position": 0, "maxLength": 32 },
"color": { "type": "string", "position": 1, "maxLength": 32 },
"serial": { "type": "integer", "position": 2 }
},
"required": ["brand", "color", "serial"],
"indices": [
{
"name": "byBrand",
"properties": [{ "brand": "asc" }],
"countable": "countable"
},
{
"name": "byColor",
"properties": [{ "color": "asc" }],
"countable": "countable",
"rangeCountable": true
},
{
"name": "byBrandColor",
"properties": [{ "brand": "asc" }, { "color": "asc" }],
"countable": "countable",
"rangeCountable": true
}
],
"additionalProperties": false
}
Three things to notice:
documentsCountable: trueat the document-type level upgrades the doctype's primary-key subtree (atwidget/[0]) fromNormalTreetoCountTree. The unfiltered total count is one read against this element'scount_value.byBrandiscountable: "countable"only. It doesn't opt intorangeCountable, sobrand > Xrange counts aren't supported. But every countable terminator's value tree is stored as aCountTreeregardless ofrangeCountable(seeadd_indices_for_index_level_for_contract_operations/v0/mod.rs), so point-lookup count proofs (e.g.brand == "X"orbrand IN [...]) get the same compact value-tree-direct shape on byBrand that they do on rangeCountable indexes.rangeCountableis strictly an opt-in forAggregateCountOnRangesupport — orthogonal to proof-size shape.byColorandbyBrandColorarerangeCountable: true. Their property-name subtrees (e.g.widget/color) are stored asProvableCountTreerather thanNormalTree, which is whatAggregateCountOnRangewalks forcolor > floorstyle queries.
The bench populates 100 000 documents under a deterministic schedule — row → (brand_(row % 100), color_(row / 100), serial=row). That gives exactly 1 000 docs per brand, exactly 100 docs per color, and exactly 1 doc per (brand, color) pair. Those numbers show up in every verified count below.
GroveDB Layout
The contract above produces this storage shape. Tree elements (the wrapping Element GroveDB stores under each key) are drawn as subgraphs; children inside each tree are merk-tree nodes. The doctype root and the per-property name subtrees are separate Element trees nested under the contract-documents prefix, just like every other index in Drive.
Diagram conventions: green nodes carry a count_value committed to the merk root; gray are regular subtrees; dashed boxes highlight Element::NonCounted wrappers (children that store data but contribute 0 to their parent CountTree's count).
flowchart TB
WD["@/contract_id/0x01/widget"]:::tree
WD --> PK["[0]: CountTree count=100000<br/>(documentsCountable primary key)"]:::countnode
WD --> BR["brand: NormalTree<br/>(byBrand property-name)"]:::node
WD --> CO["color: ProvableCountTree<br/>(byColor property-name)"]:::pctnode
BR --> B000["brand_000: CountTree count=1000"]:::countnode
BR --> B050["brand_050: CountTree count=1000"]:::countnode
BR --> BMore["... brand_001 ... brand_099<br/>(all CountTree count=1000)"]:::countnode
B050 --> B050_0["[0]: CountTree count=1000<br/>(byBrand refs)"]:::countnode
B050 --> B050_C["color: NonCounted(ProvableCountTree)<br/>(byBrandColor continuation, contributes 0)"]:::noncounted
B050_C --> B050_C_500["color_00000500: CountTree count=1<br/>(byBrandColor terminator)"]:::countnode
B050_C_500 --> B050_C_500_0["[0]: CountTree count=1<br/>(byBrandColor ref)"]:::countnode
CO --> C500["color_00000500: CountTree count=100<br/>(byColor terminator)"]:::countnode
CO --> CMore["... color_00000000 ... color_00000999"]:::countnode
C500 --> C500_0["[0]: CountTree count=100<br/>(byColor refs)"]:::countnode
classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px;
classDef node fill:#6e7681,color:#fff,stroke:#6e7681;
classDef countnode fill:#3fb950,color:#0d1117,stroke:#3fb950,stroke-width:2px;
classDef pctnode fill:#d29922,color:#0d1117,stroke:#d29922,stroke-width:2px;
classDef noncounted fill:#21262d,color:#c9d1d9,stroke:#fb8500,stroke-width:2px,stroke-dasharray: 6 4;
Three layout facts to internalize before reading the queries:
brand_050is aCountTreewithcount_value = 1000. That's true becausebyBrandis countable; the rule applies uniformly to every countability tier (seeadd_indices_for_index_level_for_contract_operations/v0/mod.rs). Thecolorcontinuation that branches off this value tree isNonCounted-wrapped so the parent's count equals exactly the 1 000 refs in[0].widget/coloris aProvableCountTree, not a regularNormalTree. The yellow class above marks that — each internal merk node carries its subtree's count, which is what makesAggregateCountOnRangea single-pass primitive.color_00000500is aCountTreewithcount_value = 100under either parent. The same element layout would result from a query againstbyColoror againstbyBrandColor's second level; the path that gets there differs, but the destination is structurally the same.
How To Read The Proofs
Every example below has four sections:
- Path query — the spec the prover hands GroveDB.
pathis the list of subtree segments to descend through (the proof carries merk-path bytes for each of these);query itemsis what to select once at the bottom;subquery items(when present) descends one more layer. - Verified element — what
GroveDB::verify_query(orverify_aggregate_count_queryfor the range primitive) returns after walking the proof bytes. Thecount_value_or_defaultfield on aCountTreeelement is what the count surface ultimately surfaces to the caller. - Proof display — the proof bytes, decoded via
bincodeinto the structuredGroveDBProofAST and rendered through itsDisplayimpl. This is the same view dash-evo-tool's Proof Log screen shows when its display mode is set to "JSON" — each layer is a separateLayerProofcarrying its merk-tree operations (Push/Parent/ChildoverHash/KVValueHash/KVHash) plus alower_layersmap naming the children to descend into. Wrapped in a collapsible block per example because the merk path through 4-5 grovedb layers makes for long output. - Diagram — the path the proof walks through the layout. Blue arrows trace the descent; the cyan node is the verified element; faded gray nodes show context.
All proof-size numbers come from running the bench against a 100 000-row fixture; see document_count_worst_case.rs's report_proof_sizes / display_proofs / report_group_by_matrix helpers. The proof bytes are reproducible — run the bench, grep [proof] from stderr, and you'll get the same hashes shown here.
Queries in this Chapter
| # | Query | Filter | Complexity | Avg time | Proof size |
|---|---|---|---|---|---|
| 1 | Unfiltered Total Count | (none — total at doctype level) | O(1) | 22.5 µs | 585 B |
| 2 | Equal on a Single Property (byBrand) | brand == "brand_050" | O(log B) | 35.7 µs | 1 041 B |
| 3 | Equal on a RangeCountable Property (byColor) | color == "color_00000500" | O(log C) | 54.0 µs | 1 327 B |
| 4 | Compound Equal-only (byBrandColor) | brand == "brand_050" AND color == "color_00000500" | O(log B + log C') | 71.4 µs | 1 911 B |
| 5 | In on byBrand | brand IN ["brand_000", "brand_001"] | O(k · log B) | 40.0 µs | 1 102 B |
| 6 | In on byColor (RangeCountable) | color IN ["color_00000000", "color_00000001"] | O(k · log C) | 61.9 µs | 1 381 B |
| 7 | Range Query (AggregateCountOnRange) | color > "color_00000500" | O(log C) | 69.2 µs | 2 072 B |
| 8 | Compound == + Range (byBrandColor) | brand == "brand_050" AND color > "color_00000500" | O(log B + log C') | 84.9 µs | 2 656 B |
Complexity variables. B = distinct brands in the byBrand merk-tree (≈ 100 in the fixture); C = distinct colors in the byColor merk-tree (≈ 1 000); C' = distinct colors per brand in byBrandColor's continuation (≈ 1 000 — every brand carries the full color namespace in this fixture); k = number of values in the IN clause (2 here). Notably absent: the total document count N (100 000 here). Count proofs read pre-committed count_values from CountTree merk roots — they never enumerate the underlying documents, so proof generation cost is polylog(distinct index values), independent of N. The grove-descent overhead (5–8 layers) is treated as a constant. The O() column captures shape only, not constants — for instance Q3's O(log C) is ~50% slower than Q2's O(log B) because in this fixture C ≈ 10 × B, and byColor's ProvableCountTree carries extra running-count metadata per merk node on top of that (37 merk ops in L6 vs 25 for byBrand — see Query 2 and Query 3's proof displays).
Avg time is the criterion-reported median of cargo bench --bench document_count_worst_case -- 'document_count_worst_case/query_' on a 100 000-row warmed fixture (no group_by — single-query latency on the prover side, including merk-proof construction and serialization). Each row reflects 10 samples × 67k–220k iterations per sample with 2 s warm-up and 5 s measurement; the median sits within ±2 % of the mean across reruns. For GROUP BY variants of these queries, see Count Index Group By Examples.
Each query has the same four sections (Path query, Verified element, Proof display, Diagram) plus a per-layer merk-tree diagram starting at Layer 5 (Layers 1–4 are byte-for-byte identical across every query — they're the root → @ → contract_id → 0x01 descent shown in full only on Query 1). The bottom of the chapter has an at-a-glance comparison summarizing the structural differences.
Query 1 — Unfiltered Total Count
select = COUNT
where = (empty)
prove = true
Path query (primary-key CountTree fast path; no index walk needed):
path: ["@", contract_id, 0x01, "widget"]
query items: [Key(0x00)]
Verified element:
path: ["@", contract_id, 0x01, "widget"]
key: 0x00
element: CountTree { count_value_or_default: 100000 }
Proof size: 585 B.
Proof display (GroveDBProof::Display):
Expand to see the structured proof (4 layers) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[4a5a28cb1b40226aa35b2f0d502767df13268bdf4678627dbfde26a557acdf73]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b90e1e952b7eef903cc9db2d9098e334a37f7e08cade52c6b2ea3bf4b56b645])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[49e7191075272395ed72cf03e973987ede6e4945e08574fe77d725f4ce7ecdf8]))
1: Push(KVValueHash(0x01, Tree(776964676574), HASH[5d9a0fad8a3f32560f8e8950c1e84a7feabaab21b79bc72fec4482442844e2ef]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(widget, Tree(6272616e64), HASH[6c505f53f2ebf3de030cc2aca463d4b429aeb320a9fadb8ae68bb7903a22bb68])))
lower_layers: {
widget => {
LayerProof {
proof: Merk(
0: Push(KVValueHashFeatureTypeWithChildHash(0x00, CountTree(0000000000010000fffffffffffeffff00000000000000000000000000000000, 100000), HASH[85843d8e6353dd6caf52f659c454b4a1352f510daa965df594b27319abf1d8a1], BasicMerkNode, HASH[0e6a5047f0600cafc385ed52b516c1fbbaf4994aa50dfcbd1e824b4ad9f55fa1]))
1: Push(KVHash(HASH[a29ee8f206a253362b6da4fcacf8643ee8e5925cd979fcd449e5906f0f9f8be3]))
2: Parent
3: Push(Hash(HASH[6c36729e93b1a316cbf60fe282eb630c0ed6e45db088e365110302b6c9caba86]))
4: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Each LayerProof is one GroveDB tree's merk proof. The descent goes: top-level GroveDB root → @ (DataContractDocuments root tree) → contract id → 0x01 (documents storage prefix) → widget doctype → finally the Key(0x00) payload at the bottom, where CountTree(…, 100000) is the verified element with its count_value of 100 000 visible inside.
The descent stops at the doctype's primary-key tree — the green node at the top of the layout. Because documentsCountable: true upgraded that tree to a CountTree, the count is one O(1) read.
Diagram: per-layer merk-tree structure
Each LayerProof above is its own GroveDB sub-tree whose contents form a merk binary tree. The merk-proof operations (Push / Parent / Child over KVValueHash / KVHash / Hash nodes) describe exactly which nodes of each layer's binary tree the proof reveals — the queried key gets its full kv-hash exposed; opaque siblings only commit their subtree-hash so the verifier can re-hash up to the merk root.
Cyan = the verified target. Blue = a kv-hash that's also a queried-key on the descent path (its value = Tree(...) is the merk-root pointer for the next layer). Gray = opaque sibling subtrees committed by hash only.
flowchart TB
subgraph L1["Layer 1 — root GroveDB merk-tree"]
direction TB
L1_root["<b>@</b><br/>kv_hash=HASH[4a5a...]<br/>value: Tree(0x4ed2…)"]:::queried
L1_left["HASH[bd29...]<br/>(left subtree, opaque)"]:::sibling
L1_right["HASH[19c9...]<br/>(right subtree, opaque)"]:::sibling
L1_root --> L1_left
L1_root --> L1_right
end
subgraph L2["Layer 2 — @ subtree merk-tree (single key)"]
direction TB
L2_q["<b>contract_id 0x4ed2…</b><br/>kv_hash=HASH[5b90...]<br/>value: Tree(0x01)"]:::queried
end
subgraph L3["Layer 3 — contract_id subtree merk-tree"]
direction TB
L3_q["<b>0x01</b><br/>kv_hash=HASH[5d9a...]<br/>value: Tree(widget)"]:::queried
L3_left["HASH[49e7...]<br/>(left subtree, opaque)"]:::sibling
L3_q --> L3_left
end
subgraph L4["Layer 4 — 0x01 documents-prefix subtree (single key)"]
direction TB
L4_q["<b>widget</b><br/>kv_hash=HASH[6c50...]<br/>value: Tree(0x00/brand/color)"]:::queried
end
subgraph L5["Layer 5 — widget doctype merk-tree (TARGET layer)"]
direction TB
L5_root["KVHash[a29e...]<br/>(opaque internal kv: brand or color)"]:::sibling
L5_target["<b>0x00</b><br/>kv_hash=HASH[8584...]<br/>value: <b>CountTree count=100000</b>"]:::target
L5_right["HASH[6c36...]<br/>(right subtree, opaque)"]:::sibling
L5_root --> L5_target
L5_root --> L5_right
end
L1_root -. "value=Tree(merk_root[5b90…])" .-> L2_q
L2_q -. "value=Tree(merk_root[5d9a…])" .-> L3_q
L3_q -. "value=Tree(merk_root[6c50…])" .-> L4_q
L4_q -. "value=Tree(merk_root[a29e…])" .-> L5_root
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
A few things this diagram makes explicit that the prose can't:
- Each layer is its own merk binary tree, not a single graph. The 5
LayerProofblocks in the structured proof above each describe one of these binary trees. The hashes named in each block'sPush(Hash(HASH[…]))ops are this diagram's opaque siblings; thePush(KVValueHash(K, …))ops are this diagram's blue / cyan nodes. - The "descent" between layers is via a
value: Tree(…). When a queried key's value isTree(merk_root_hash), that hash IS the merk root of the next layer's binary tree. So Layer 1's@doesn't descend to Layer 2'scontract_iddirectly — it descends to Layer 2's merk root, which in this case happens to be the only node in Layer 2. - Single-key layers have a 1-node merk tree. Layers 2 and 4 contain exactly one entry (
@contains exactly one contract id;0x01contains exactly one doctype here), so their merk trees have no siblings to commit. - The merk root of a layer can be an opaque sibling, not the queried key. Layer 5's merk root is
KVHash[a29e...]— a key (brandorcolor, we can't tell from the proof) whose kv_hash is committed but whose value isn't revealed. The queried0x00is reached as a child of that opaque root. This is why the merk-tree structure matters: the prover sometimes has to commit one merk-tree-depth's worth of hashes to prove the queried key's position, even if the verifier only cares about the target's value.
Query 2 — Equal on a Single Property (byBrand)
select = COUNT
where = brand == "brand_050"
prove = true
Path query:
path: ["@", contract_id, 0x01, "widget", "brand"]
query items: [Key("brand_050")]
Verified element:
path: ["@", contract_id, 0x01, "widget", "brand"]
key: "brand_050"
element: CountTree { count_value_or_default: 1000 }
Proof size: 1 041 B.
Proof display:
Expand to see the structured proof (verbatim, 5 layers) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[4a5a28cb1b40226aa35b2f0d502767df13268bdf4678627dbfde26a557acdf73]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b90e1e952b7eef903cc9db2d9098e334a37f7e08cade52c6b2ea3bf4b56b645])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[49e7191075272395ed72cf03e973987ede6e4945e08574fe77d725f4ce7ecdf8]))
1: Push(KVValueHash(0x01, Tree(776964676574), HASH[5d9a0fad8a3f32560f8e8950c1e84a7feabaab21b79bc72fec4482442844e2ef]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(widget, Tree(6272616e64), HASH[6c505f53f2ebf3de030cc2aca463d4b429aeb320a9fadb8ae68bb7903a22bb68])))
lower_layers: {
widget => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[9862894b16a0792688fdcf64edcb2ceade5c8b234649bfc6cfc6426869b0e9d9]))
1: Push(KVValueHash(brand, Tree(6272616e645f303633), HASH[68b697da99d6ea70a83eb41794dca7ba3938d0ba98fbfaeb3cd0c19b3b5d0ff2]))
2: Parent
3: Push(Hash(HASH[6c36729e93b1a316cbf60fe282eb630c0ed6e45db088e365110302b6c9caba86]))
4: Child)
lower_layers: {
brand => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[fb5eb23b3135d9c226e61f004ffb43abae104238d8a1ea7bc60e8ec6ba271596]))
1: Push(KVHash(HASH[3ed48a5e35cb7546d329487b0e1ab8a81d7c5bec358c37449e6cbd956e3bb069]))
2: Parent
3: Push(Hash(HASH[19ec5730af134e9ac980bbea92c2978212c8efe750a467ab54f073626e0ca2f5]))
4: Push(KVHash(HASH[87bc6e7e1e465b8dcdaf95db9957a455d6bd7c75976db122f33e592fe75f1e4a]))
5: Parent
6: Push(Hash(HASH[a0a354f2bb59b8169253aebabb52afcc3c59c4c60da203c8887abb679d747168]))
7: Push(KVHash(HASH[fc6b1d0237f8ff89b555e9a14480ae1c5b80d529a0f9fb5e681ea7ecd157d3da]))
8: Parent
9: Push(KVValueHashFeatureTypeWithChildHash(brand_050, CountTree(636f6c6f72, 1000, flags: [0, 0, 0]), HASH[53dbd6216cccdddf16f3eb0f849aed0c0cea987a718f5b43493abf0a14e83eb9], BasicMerkNode, HASH[4947457e230f87ce0f75a7f1502f64f24ee4d3e27eb5d2210680822a3b17afa4]))
10: Child
11: Push(KVHash(HASH[027ac8b1bc9788118b27c13d0b3c3bd3661ef6a89a775a6b6bf78aa7e6f8ed3d]))
12: Parent
13: Push(Hash(HASH[7a5dc3002e6cb6c92e54d554e5af85e9c2ba64ee9c5f80e6489075cc5f3f0d55]))
14: Child
15: Push(KVHash(HASH[3363630479f1abe6e003b1e1d50b5118e55ad2efb7a3f4b3b6df902bea72ac9a]))
16: Parent
17: Push(Hash(HASH[3857faef5ddb06e201f1e65cf42f15d6c9b0dc67e7f73eb182b520854e9bb648]))
18: Child
19: Child
20: Child
21: Push(KVHash(HASH[f776417ede76e6194706e483ac14ab7b3db6aa0461ec14ed5f8e5d20071363af]))
22: Parent
23: Push(Hash(HASH[b3fccba79c14fcc5e97ff6a3cd051228dc755e6de147bef690ba9681264b2b9f]))
24: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
The bottom layer is the byBrand property-name tree; it has 100 distinct brand_NNN keys, so the merk path proves brand_050's position with 25 ops total (0–24). The verified payload is the inline KVValueHashFeatureTypeWithChildHash(brand_050, CountTree(636f6c6f72, 1000, flags: [0, 0, 0]), …) on op 9 — the 636f6c6f72 value slot is the ASCII bytes for "color" (the byBrandColor continuation pointer; NonCounted-wrapped at the storage layer so it contributes 0 to the parent count), and the 1000 is the doc count. The remaining 24 ops are the merk-binary boundary walk: each Push(Hash(…)) is an opaque subtree the proof commits but doesn't descend into, each Push(KVHash(…)) is an opaque internal sibling kv whose hash is committed, and each Parent / Child re-attaches them so the verifier can recompute the byBrand merk root.
brand_050 is itself a CountTree — every countable terminator's value tree carries the doc count directly, with sibling continuations wrapped NonCounted so they don't pollute the parent. The proof shape is the same as the rangeCountable case below, even though byBrand doesn't opt into rangeCountable: true. rangeCountable is the orthogonal opt-in for AggregateCountOnRange (Query 7), not for proof-size shape.
flowchart TB
WD["@/contract_id/0x01/widget"]:::tree
WD ==> BR["brand: NormalTree"]:::path
BR ==> B050["brand_050: CountTree count=1000"]:::target
BR -.-> B000["brand_000"]:::faded
BR -.-> BMore["..."]:::faded
WD -.-> PK["[0]"]:::faded
WD -.-> CO["color"]:::faded
B050 -.-> B050_0["[0]: 1000 refs"]:::faded
B050 -.-> B050_C["color (NonCounted)"]:::faded
classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px;
classDef path fill:#6e7681,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef faded fill:#21262d,color:#6e7681,stroke:#484f58;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
linkStyle 0 stroke:#1f6feb,stroke-width:3px;
linkStyle 1 stroke:#1f6feb,stroke-width:3px;
Diagram: per-layer merk-tree structure (Layer 5+)
Layers 1–4 are byte-for-byte identical to Query 1's diagram (root → @ → contract_id → 0x01). The descent diverges at Layer 5, where this query takes the brand branch (rather than 0x00) and descends one extra grove layer to land on the verified target.
flowchart TB
subgraph L5["Layer 5 — widget doctype merk-tree (proof view for `brand`)"]
direction TB
L5_q["<b>brand</b><br/>kv_hash=HASH[68b6...]<br/>value: Tree (descent into byBrand)"]:::queried
L5_left["HASH[9862...]<br/>(left subtree, opaque)"]:::sibling
L5_right["HASH[6c36...]<br/>(right subtree, opaque)"]:::sibling
L5_q --> L5_left
L5_q --> L5_right
end
subgraph L6["Layer 6 — byBrand merk-tree (TARGET layer)"]
direction TB
L6_target["<b>brand_050</b><br/>kv_hash=HASH[53db...]<br/>value: <b>CountTree count=1000</b><br/>child_hash=HASH[4947...]"]:::target
L6_boundary["Boundary commitments (24 merk ops):<br/>6 KVHash opaque sibling brands<br/>+ 6 Hash subtree commitments<br/>(prove brand_050's position in byBrand's<br/>binary merk tree of ~100 brand entries)"]:::sibling
L6_target --> L6_boundary
end
L5_q -. "value=Tree(merk_root[byBrand])" .-> L6_target
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
The boundary commitments at L6 are what scale linearly with the byBrand tree's depth — they bind brand_050 to its claimed position so the verifier can recompute byBrand's merk root. The verified target itself is just one KVValueHashFeatureTypeWithChildHash op whose count_value_or_default = 1000 is the answer.
Query 3 — Equal on a RangeCountable Property (byColor)
select = COUNT
where = color == "color_00000500"
prove = true
Path query:
path: ["@", contract_id, 0x01, "widget", "color"]
query items: [Key("color_00000500")]
Verified element:
path: ["@", contract_id, 0x01, "widget", "color"]
key: "color_00000500"
element: CountTree { count_value_or_default: 100 }
Proof size: 1 327 B.
Proof display:
Expand to see the structured proof (verbatim, 5 layers; note `KVHashCount` ops in the byColor `ProvableCountTree` layer) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[4a5a28cb1b40226aa35b2f0d502767df13268bdf4678627dbfde26a557acdf73]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b90e1e952b7eef903cc9db2d9098e334a37f7e08cade52c6b2ea3bf4b56b645])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[49e7191075272395ed72cf03e973987ede6e4945e08574fe77d725f4ce7ecdf8]))
1: Push(KVValueHash(0x01, Tree(776964676574), HASH[5d9a0fad8a3f32560f8e8950c1e84a7feabaab21b79bc72fec4482442844e2ef]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(widget, Tree(6272616e64), HASH[6c505f53f2ebf3de030cc2aca463d4b429aeb320a9fadb8ae68bb7903a22bb68])))
lower_layers: {
widget => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[9862894b16a0792688fdcf64edcb2ceade5c8b234649bfc6cfc6426869b0e9d9]))
1: Push(KVHash(HASH[a29ee8f206a253362b6da4fcacf8643ee8e5925cd979fcd449e5906f0f9f8be3]))
2: Parent
3: Push(KVValueHash(color, ProvableCountTree(636f6c6f725f3030303030353131, 100000), HASH[79569d595db75bbf2e9dca93a15c90b7eecf7b299632668ec410e2076d27f71c]))
4: Child)
lower_layers: {
color => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[864c8a53cdfc17560ea304fe40ae87570699a6920eae3dcb6075f71ca2d79b02]))
1: Push(KVHashCount(HASH[3684347a67ceedad2ff4a7fce6ae303086543c1f146f5865dfdc23612308c05b], 51100))
2: Parent
3: Push(Hash(HASH[56422e033fcffda5514eaef88096da995646207f3f5e349a6840003b4297098e]))
4: Push(KVHashCount(HASH[aa27604017cfc457ccd56aabeb4686a988b0b073d1c1c03a4fdf78164c31c8ea], 25500))
5: Parent
6: Push(Hash(HASH[09bcdaa37a5ae46f9059a7c026bf9cdf1c2d1ddecfcfe72fafe73f30abf2bccc]))
7: Push(KVHashCount(HASH[525df42449bd5e881d55f94c11be2b1c95cd112123864fc249e6c170ea026f5a], 12700))
8: Parent
9: Push(Hash(HASH[ffe58ba46b2d1f91b04e9c78185b474828f8ad165757847d9178020e55ad6c26]))
10: Push(KVHashCount(HASH[abbcbcef405f19e0a096a902993b3c76c77c59abdb8a3dcc95369e8c17b401c7], 6300))
11: Parent
12: Push(Hash(HASH[472879d66cf8e01e77bf4828d6a6f530a016cf7a99d712deb00c8fa5920b8495]))
13: Push(KVHashCount(HASH[3ac3896404268efc1bbfc9a2a8925adcc9eff7248fc7ca3aaec6f62587cdaffd], 3100))
14: Parent
15: Push(Hash(HASH[1c40306956f164e416e74a69ce0fff8c7ca152904ad47f44c6142c7822d3d2fb]))
16: Push(KVHashCount(HASH[494935a3d102495beb504953539d204ecd5b5ca8f5a03aa4a3cdbf16a3926335], 700))
17: Parent
18: Push(KVValueHashFeatureTypeWithChildHash(color_00000500, CountTree(00, 100, flags: [0, 0, 0]), HASH[47b0ade593a2e4e99e7d7363f5d1f692882007397f025226f19d097ca2f407fa], ProvableCountedMerkNode(100), HASH[4f7f13f56e087e7b19751c067671b75cda83156231cd3186f7c4172dccc8e97b]))
19: Push(KVHashCount(HASH[4866192fb6beda0888f828d7bbf008fa725a1141cf19ae3b1e9d245c6cb12c7c], 300))
20: Parent
21: Push(Hash(HASH[f56dd41a87f9b487ee9893c310a8bdd2fe70eb573e2e22e048cef7e3dec5fc1d]))
22: Child
23: Child
24: Push(KVHashCount(HASH[a646e152e4bfb609f5372833f5b8c001b4e523c3154f6fea43b154fe04c6e120], 1500))
25: Parent
26: Push(Hash(HASH[f434d46bb16f841310d2e120a259ad1aca2d679fd330ac0fd13d145c11a6b335]))
27: Child
28: Child
29: Child
30: Child
31: Child
32: Child
33: Push(KVHashCount(HASH[c32ae0189f148c2390791534ff4bc205fabb53a7c7d15f109a4354170045308c], 100000))
34: Parent
35: Push(Hash(HASH[1a1c99166d7b1e1eb9087404f3bfae82d749a3a7a763da654f48c5d314e21e76]))
36: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
This is the most interesting layer in the chapter. The byColor property-name tree (widget/color) is a ProvableCountTree, so every internal merk node carries its subtree's running count — visible here as KVHashCount(HASH[…], N) ops where N is the count contribution of that subtree (the values 51100, 25500, 12700, 6300, 3100, 700, 300, 1500, 100000 show up in the literal output above). The verified element (color_00000500) lands on op 18 in the middle of these KVHashCount ops, and the surrounding ops walk the binary boundary path so the prover can recompute the parent merk hash. For a point-lookup query like this, the ProvableCountTree machinery is overkill — it carries running counts the verifier doesn't need. Query 7 is where this pays off.
Structurally identical to Query 2 — only the property name and the count-tree depth differ. The intermediate widget/color tree is a ProvableCountTree here (vs NormalTree for byBrand), but the proof doesn't care about that: it descends through the property-name tree and surfaces the value-tree CountTree at the bottom. The ProvableCountTree upgrade matters for Query 7 (range aggregate), not for point lookup.
flowchart TB
WD["@/contract_id/0x01/widget"]:::tree
WD ==> CO["color: ProvableCountTree"]:::path
CO ==> C500["color_00000500: CountTree count=100"]:::target
CO -.-> C000["color_00000000"]:::faded
CO -.-> CMore["..."]:::faded
WD -.-> PK["[0]"]:::faded
WD -.-> BR["brand"]:::faded
C500 -.-> C500_0["[0]: 100 refs"]:::faded
classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px;
classDef path fill:#d29922,color:#0d1117,stroke:#1f6feb,stroke-width:2px;
classDef faded fill:#21262d,color:#6e7681,stroke:#484f58;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
linkStyle 0 stroke:#1f6feb,stroke-width:3px;
linkStyle 1 stroke:#1f6feb,stroke-width:3px;
Diagram: per-layer merk-tree structure (Layer 5+)
Layers 1–4 are byte-for-byte identical to Query 1. The L5 widget doctype merk-tree proof differs from Query 2: here color is the queried key (under an opaque KVHash[a29e...] root in the proof view), and the descent into Layer 6 enters a ProvableCountTree rather than a NormalTree.
flowchart TB
subgraph L5["Layer 5 — widget doctype merk-tree (proof view for `color`)"]
direction TB
L5_root["KVHash[a29e...]<br/>(opaque kv root)"]:::sibling
L5_left["HASH[9862...]<br/>(left subtree, opaque)"]:::sibling
L5_q["<b>color</b><br/>kv_hash=HASH[7956...]<br/>value: ProvableCountTree<br/>(descent into byColor)"]:::queried
L5_root --> L5_left
L5_root --> L5_q
end
subgraph L6["Layer 6 — byColor ProvableCountTree merk-tree (TARGET layer)"]
direction TB
L6_target["<b>color_00000500</b><br/>KVValueHashFeatureTypeWithChildHash:<br/>kv_hash=HASH[47b0...]<br/>value: <b>CountTree count=100</b><br/>feature: ProvableCountedMerkNode(100)<br/>child_hash=HASH[4f7f...]"]:::target
L6_boundary["Boundary commitments (36 merk ops):<br/>9 KVHashCount running totals carrying<br/>per-subtree counts<br/>(700, 1500, 3100, 6300, 12700,<br/>25500, 51100, 100000, 300)<br/>+ 9 Hash subtree commitments"]:::sibling
L6_target --> L6_boundary
end
L5_q -. "ProvableCountTree(merk_root[byColor])" .-> L6_target
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
Same structural shape as Query 2 (point lookup → CountTree), but the boundary commitments carry KVHashCount(HASH, N) ops instead of plain KVHash(HASH). The running totals are dead weight for this point lookup — the verifier never reads them — but they're the same commitments Query 7 will integrate over.
Query 4 — Compound Equal-only (byBrandColor)
select = COUNT
where = brand == "brand_050" AND color == "color_00000500"
prove = true
Path query:
path: ["@", contract_id, 0x01, "widget", "brand", "brand_050", "color"]
query items: [Key("color_00000500")]
Verified element:
path: ["@", contract_id, 0x01, "widget", "brand", "brand_050", "color"]
key: "color_00000500"
element: CountTree { count_value_or_default: 1 }
Proof size: 1 911 B.
Proof display:
Expand to see the structured proof (6 layers — the deepest descent in the chapter) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[4a5a28cb1b40226aa35b2f0d502767df13268bdf4678627dbfde26a557acdf73]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b90e1e952b7eef903cc9db2d9098e334a37f7e08cade52c6b2ea3bf4b56b645])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[49e7191075272395ed72cf03e973987ede6e4945e08574fe77d725f4ce7ecdf8]))
1: Push(KVValueHash(0x01, Tree(776964676574), HASH[5d9a0fad8a3f32560f8e8950c1e84a7feabaab21b79bc72fec4482442844e2ef]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(widget, Tree(6272616e64), HASH[6c505f53f2ebf3de030cc2aca463d4b429aeb320a9fadb8ae68bb7903a22bb68])))
lower_layers: {
widget => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[9862894b16a0792688fdcf64edcb2ceade5c8b234649bfc6cfc6426869b0e9d9]))
1: Push(KVValueHash(brand, Tree(6272616e645f303633), HASH[68b697da99d6ea70a83eb41794dca7ba3938d0ba98fbfaeb3cd0c19b3b5d0ff2]))
2: Parent
3: Push(Hash(HASH[6c36729e93b1a316cbf60fe282eb630c0ed6e45db088e365110302b6c9caba86]))
4: Child)
lower_layers: {
brand => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[fb5eb23b3135d9c226e61f004ffb43abae104238d8a1ea7bc60e8ec6ba271596]))
1: Push(KVHash(HASH[3ed48a5e35cb7546d329487b0e1ab8a81d7c5bec358c37449e6cbd956e3bb069]))
2: Parent
3: Push(Hash(HASH[19ec5730af134e9ac980bbea92c2978212c8efe750a467ab54f073626e0ca2f5]))
4: Push(KVHash(HASH[87bc6e7e1e465b8dcdaf95db9957a455d6bd7c75976db122f33e592fe75f1e4a]))
5: Parent
6: Push(Hash(HASH[a0a354f2bb59b8169253aebabb52afcc3c59c4c60da203c8887abb679d747168]))
7: Push(KVHash(HASH[fc6b1d0237f8ff89b555e9a14480ae1c5b80d529a0f9fb5e681ea7ecd157d3da]))
8: Parent
9: Push(KVValueHash(brand_050, CountTree(636f6c6f72, 1000, flags: [0, 0, 0]), HASH[53dbd6216cccdddf16f3eb0f849aed0c0cea987a718f5b43493abf0a14e83eb9]))
10: Child
11: Push(KVHash(HASH[027ac8b1bc9788118b27c13d0b3c3bd3661ef6a89a775a6b6bf78aa7e6f8ed3d]))
12: Parent
13: Push(Hash(HASH[7a5dc3002e6cb6c92e54d554e5af85e9c2ba64ee9c5f80e6489075cc5f3f0d55]))
14: Child
15: Push(KVHash(HASH[3363630479f1abe6e003b1e1d50b5118e55ad2efb7a3f4b3b6df902bea72ac9a]))
16: Parent
17: Push(Hash(HASH[3857faef5ddb06e201f1e65cf42f15d6c9b0dc67e7f73eb182b520854e9bb648]))
18: Child
19: Child
20: Child
21: Push(KVHash(HASH[f776417ede76e6194706e483ac14ab7b3db6aa0461ec14ed5f8e5d20071363af]))
22: Parent
23: Push(Hash(HASH[b3fccba79c14fcc5e97ff6a3cd051228dc755e6de147bef690ba9681264b2b9f]))
24: Child)
lower_layers: {
brand_050 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[2190c6fcd140792fd12be66cd631f97475b9ab3f19417a26d94798115ee46160]))
1: Push(KVValueHash(color, NonCounted(ProvableCountTree(636f6c6f725f3030303030353131, 1000, flags: [0, 0, 0])), HASH[b1cedc48940faedea8b64bff8c8113344acdb1fd8eff37c567099b167b3c5861]))
2: Parent)
lower_layers: {
color => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[7e2704a94ce3e08ee1e8249a7272e1860251f66b9816581f6191010bc0f15dfe]))
1: Push(KVHashCount(HASH[ccfe3e95a84b22305b9815064d7d4e54b6ab0ca8efab26ca408391f2fad2b83e], 511))
2: Parent
3: Push(Hash(HASH[3dac20af894289bb36212087f48cfdd2c05713c5e134804638428a8d0ac8c905]))
4: Push(KVHashCount(HASH[1a3db8540380b26ead4be363cd35a4a0036ec9a92cf3c9527db540f0878ab168], 255))
5: Parent
6: Push(Hash(HASH[61f333ba1ad78624c009fa514ac69407a1438cb7d7807e9d5de1d75223137235]))
7: Push(KVHashCount(HASH[94e2ea0c17ffbf050dcba5e04928ae2ecf9fe21567e7fac5b93ad30040df8dfe], 127))
8: Parent
9: Push(Hash(HASH[a8571229cee7010a54ae9890a410edd246c079930d672fb4cdcd4a13c2bcc437]))
10: Push(KVHashCount(HASH[6b04a6eb8e698272ec0ff801c76dc9d65a1d47ef5ae1beb9747058cdda05e2d6], 63))
11: Parent
12: Push(Hash(HASH[8c12a68cebf211bbbd3937519662a7c3ed5bce92cf1e99869548c06a5639de15]))
13: Push(KVHashCount(HASH[9533ef417b8eed113b81bb2d7e56c81012d11835819a59769deebfc1a7e0eafd], 31))
14: Parent
15: Push(Hash(HASH[2f385d9fd5157a78a1cb1456050d3fb87f809e30b93a1963582edcedd31bfd0e]))
16: Push(KVHashCount(HASH[74ad467d4132703ae845149ce86de7c71d9c6fc7472e76e9bb1b81bc182abe53], 7))
17: Parent
18: Push(KVValueHashFeatureTypeWithChildHash(color_00000500, CountTree(00, 1, flags: [0, 0, 0]), HASH[7f1d988845d9c82b9d1146f2188b09bf704d31647ee2a26054e69ed897de3750], ProvableCountedMerkNode(1), HASH[078e3476060013c48bfc77330dc75d4fedc585469f581a66dc6b7b32f6d4d60a]))
19: Push(KVHashCount(HASH[48fc5c3cca2265eaeb5b86505f628660ffae9deee96cda8c26d7139f22ce0410], 3))
20: Parent
21: Push(Hash(HASH[c6e38bf64efcfd1d46d4ccd7937858870c7406f3abf31ca36148860d12c6b950]))
22: Child
23: Child
24: Push(KVHashCount(HASH[67ab38f74160b7c15e62a37fee3d0c193156061f3b013190ce2a154e4164c7b0], 15))
25: Parent
26: Push(Hash(HASH[49e4ecf80eead3552c93208b39c4fa9a5a3b64b7c63b385e53e47cb6e7bd8759]))
27: Child
28: Child
29: Child
30: Child
31: Child
32: Child
33: Push(KVHashCount(HASH[e735a44484a03e4f67ef4c79f370e2b2c4b0b98d942c5b1dca53039a031354b3], 1000))
34: Parent
35: Push(Hash(HASH[bf41c24632983b5858dcd20a04e0e0da6e7cacef58679e69e7859619dded444e]))
36: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
This is the deepest descent in the chapter. The path threads:
- The two intermediate GroveDB-wrapper layers (
@and0x01). - The widget doctype.
- The byBrand property-name tree.
- The byBrand value tree for
brand_050(visible in Query 2 already, here it's an intermediate stop withCountTree(636f6c6f72, 1000, …)— same element, same count). - The byBrandColor continuation (
color—NonCounted(ProvableCountTree)). - The byBrandColor terminator value tree, finally arriving at
color_00000500withCountTree(00, 1, …)— the bench's deterministic schedule gives exactly 1 doc per(brand, color)pair.
The proof descends through byBrandColor's prefix value tree (brand_050) into its continuation (color, the NonCounted-wrapped subtree shown earlier) and resolves at the terminator value tree color_00000500. The count is 1 because the bench's fixture has exactly one document per (brand, color) pair.
flowchart TB
WD["@/contract_id/0x01/widget"]:::tree
WD ==> BR["brand: NormalTree"]:::path
BR ==> B050["brand_050: CountTree count=1000"]:::path
B050 ==> B050_C["color: NonCounted(ProvableCountTree)"]:::path
B050_C ==> B050_C_500["color_00000500: CountTree count=1"]:::target
B050_C -.-> Other["other colors"]:::faded
B050 -.-> B050_0["[0]: 1000 byBrand refs"]:::faded
BR -.-> Brands["other brands"]:::faded
WD -.-> CO["color"]:::faded
WD -.-> PK["[0]"]:::faded
classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px;
classDef path fill:#6e7681,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef faded fill:#21262d,color:#6e7681,stroke:#484f58;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
linkStyle 0 stroke:#1f6feb,stroke-width:3px;
linkStyle 1 stroke:#1f6feb,stroke-width:3px;
linkStyle 2 stroke:#1f6feb,stroke-width:3px;
linkStyle 3 stroke:#1f6feb,stroke-width:3px;
Diagram: per-layer merk-tree structure (Layer 5+)
This is the deepest descent in the chapter — four extra grove layers below the common Layers 1–4. Layer 5 enters brand (same as Query 2); Layer 6 lands on brand_050 but doesn't terminate (its CountTree has a continuation child); Layer 7 is brand_050's single-key sub-merk-tree carrying the byBrandColor continuation; Layer 8 is the byBrandColor terminator value tree where color_00000500 is the actual target.
flowchart TB
subgraph L5["Layer 5 — widget doctype merk-tree"]
direction TB
L5_q["<b>brand</b><br/>kv_hash=HASH[68b6...]<br/>value: Tree (descent into byBrand)"]:::queried
L5_left["HASH[9862...]"]:::sibling
L5_right["HASH[6c36...]"]:::sibling
L5_q --> L5_left
L5_q --> L5_right
end
subgraph L6["Layer 6 — byBrand merk-tree (intermediate stop)"]
direction TB
L6_q["<b>brand_050</b><br/>kv_hash=HASH[53db...]<br/>value: CountTree count=1000<br/>(continuation child via lower_layer)"]:::queried
L6_boundary["Boundary commitments (24 merk ops):<br/>6 KVHash sibling brands + 6 Hash subtrees"]:::sibling
L6_q --> L6_boundary
end
subgraph L7["Layer 7 — brand_050's continuation merk-tree (single key)"]
direction TB
L7_q["<b>color</b><br/>kv_hash=HASH[b1ce...]<br/>value: NonCounted(ProvableCountTree)<br/>(descent into byBrandColor)"]:::queried
L7_left["HASH[2190...]"]:::sibling
L7_q --> L7_left
end
subgraph L8["Layer 8 — byBrandColor color sub-tree (TARGET layer)"]
direction TB
L8_target["<b>color_00000500</b><br/>KVValueHashFeatureTypeWithChildHash:<br/>kv_hash=HASH[7f1d...]<br/>value: <b>CountTree count=1</b><br/>feature: ProvableCountedMerkNode(1)<br/>child_hash=HASH[078e...]"]:::target
L8_boundary["Boundary commitments (36 merk ops):<br/>KVHashCount running totals<br/>(3, 15, 1000, ...) + Hash subtrees<br/>covering ~1000 colors under brand_050"]:::sibling
L8_target --> L8_boundary
end
L5_q -. "Tree(merk_root[byBrand])" .-> L6_q
L6_q -. "CountTree continuation (child_hash)" .-> L7_q
L7_q -. "NonCounted(ProvableCountTree)" .-> L8_target
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
The two extra grove layers (L7 + L8) over Query 2's two-layer descent are what makes this the chapter's heaviest proof (1 911 B). The L7 layer is structurally trivial (one key) — its cost is the descent overhead, not the merk-tree boundary. L8 carries the same ProvableCountTree shape as Query 3's L6, but the contained key namespace is restricted to colors that co-occur with brand_050.
Query 5 — In on byBrand
select = COUNT
where = brand IN ["brand_000", "brand_001"]
prove = true
Path query:
path: ["@", contract_id, 0x01, "widget", "brand"]
query items: [Key("brand_000"), Key("brand_001")]
Verified elements (one per In value, returned in lex-asc order):
path: ["@", contract_id, 0x01, "widget", "brand"]
key: "brand_000"
element: CountTree { count_value_or_default: 1000 }
path: ["@", contract_id, 0x01, "widget", "brand"]
key: "brand_001"
element: CountTree { count_value_or_default: 1000 }
Proof size: 1 102 B.
Proof display:
Expand to see the structured proof (5 layers, two `KVValueHash` items at the byBrand level) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[4a5a28cb1b40226aa35b2f0d502767df13268bdf4678627dbfde26a557acdf73]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b90e1e952b7eef903cc9db2d9098e334a37f7e08cade52c6b2ea3bf4b56b645])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[49e7191075272395ed72cf03e973987ede6e4945e08574fe77d725f4ce7ecdf8]))
1: Push(KVValueHash(0x01, Tree(776964676574), HASH[5d9a0fad8a3f32560f8e8950c1e84a7feabaab21b79bc72fec4482442844e2ef]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(widget, Tree(6272616e64), HASH[6c505f53f2ebf3de030cc2aca463d4b429aeb320a9fadb8ae68bb7903a22bb68])))
lower_layers: {
widget => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[9862894b16a0792688fdcf64edcb2ceade5c8b234649bfc6cfc6426869b0e9d9]))
1: Push(KVValueHash(brand, Tree(6272616e645f303633), HASH[68b697da99d6ea70a83eb41794dca7ba3938d0ba98fbfaeb3cd0c19b3b5d0ff2]))
2: Parent
3: Push(Hash(HASH[6c36729e93b1a316cbf60fe282eb630c0ed6e45db088e365110302b6c9caba86]))
4: Child)
lower_layers: {
brand => {
LayerProof {
proof: Merk(
0: Push(KVValueHashFeatureTypeWithChildHash(brand_000, CountTree(636f6c6f72, 1000, flags: [0, 0, 0]), HASH[90ff6f6d9a3d901195982128130677243bfd27b75736206f3c8400966ef0d37b], BasicMerkNode, HASH[19b58883c492e746861db1e6ad07529a5a91cc8330af522682486db9346d6875]))
1: Push(KVValueHashFeatureTypeWithChildHash(brand_001, CountTree(636f6c6f72, 1000, flags: [0, 0, 0]), HASH[484ca11fb4ec8f479be1f78af903ce0c9d4fe630517579fb0172c2576d6b9652], BasicMerkNode, HASH[0bf12023f8e067c12db4cec1583909a0283878d6d909c76196736299750b5879]))
2: Parent
3: Push(Hash(HASH[8ca09dadc802a7efe03534ce4ad991b2f191f368878754a37b5e5c03d9498dab]))
4: Child
5: Push(KVHash(HASH[e5297b3ebe81c6435c29f712074da5f7c90265e12ed3d4f5af1f6d900e50c9f1]))
6: Parent
7: Push(Hash(HASH[50f373fd01dea89c992779764dff82cc7200b492be8f5cf3721627d5323bcbff]))
8: Child
9: Push(KVHash(HASH[cf78c9f1b1a1204bb2e437806f52c21e331392de3436388572bd1fa4bce1cdc7]))
10: Parent
11: Push(Hash(HASH[4a8dc186a95c8c4a1252fb51dbc407727f588eb5bdc8313c96f5c29889e13926]))
12: Child
13: Push(KVHash(HASH[d00ee7653e34e47d46004929b13ded33dff069ed9cc88342cecdf66a65fd8401]))
14: Parent
15: Push(Hash(HASH[7f1d17b9632f0bd440dacf5e841025482bc1d8145df3650301a95a5ee71ce8c8]))
16: Child
17: Push(KVHash(HASH[3ed48a5e35cb7546d329487b0e1ab8a81d7c5bec358c37449e6cbd956e3bb069]))
18: Parent
19: Push(Hash(HASH[eaef9fc530408393bc321409414814b290309a861f474a925a922250327affc6]))
20: Child
21: Push(KVHash(HASH[f776417ede76e6194706e483ac14ab7b3db6aa0461ec14ed5f8e5d20071363af]))
22: Parent
23: Push(Hash(HASH[b3fccba79c14fcc5e97ff6a3cd051228dc755e6de147bef690ba9681264b2b9f]))
24: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
The two Push(KVValueHashFeatureTypeWithChildHash(brand_NNN, CountTree(…, 1000, …), …)) ops are the actual verified elements — both inlined in the byBrand layer's merk proof. They share the same parent path (@/.../widget/brand); the verifier-side verify_query returns both as siblings rather than descending one more layer per value (which is what the legacy Key([0]) shape would have forced for a normal-countable index, but no longer does — every countable terminator's value tree is a CountTree). The remaining 22 ops are the boundary-path hashes that prove brand_000 and brand_001 actually occupy the merk-tree positions claimed.
The outer query enumerates Key(in_value) items at the property-name subtree; each resolved element is itself a value-tree CountTree. No subquery is set — the In values' value trees are the count-bearing elements. The verifier reads the per-In value from grove_key (rather than from path[base_path_len], which is how it would for a trailing-Equal compound). The caller sums the two count_value_or_default reads (or surfaces them as per-group entries if group_by = ["brand"]).
flowchart TB
WD["@/contract_id/0x01/widget"]:::tree
WD ==> BR["brand: NormalTree"]:::path
BR ==> B000["brand_000: CountTree count=1000"]:::target
BR ==> B001["brand_001: CountTree count=1000"]:::target
BR -.-> BMore["brand_002 ... brand_099"]:::faded
B000 -.-> B000_0["[0]: 1000 refs"]:::faded
B001 -.-> B001_0["[0]: 1000 refs"]:::faded
WD -.-> PK["[0]"]:::faded
WD -.-> CO["color"]:::faded
classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px;
classDef path fill:#6e7681,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef faded fill:#21262d,color:#6e7681,stroke:#484f58;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
linkStyle 0 stroke:#1f6feb,stroke-width:3px;
linkStyle 1 stroke:#1f6feb,stroke-width:3px;
linkStyle 2 stroke:#1f6feb,stroke-width:3px;
Diagram: per-layer merk-tree structure (Layer 5+)
Same L5 shape as Query 2 (brand queried, two opaque sibling subtrees). At Layer 6 the proof inlines two KVValueHashFeatureTypeWithChildHash(brand_NNN, ...) ops at the byBrand layer — both verified elements share the same parent path, so no extra grove descent is needed.
flowchart TB
subgraph L5["Layer 5 — widget doctype merk-tree"]
direction TB
L5_q["<b>brand</b><br/>kv_hash=HASH[68b6...]<br/>value: Tree (descent into byBrand)"]:::queried
L5_left["HASH[9862...]"]:::sibling
L5_right["HASH[6c36...]"]:::sibling
L5_q --> L5_left
L5_q --> L5_right
end
subgraph L6["Layer 6 — byBrand merk-tree (TWO TARGETS)"]
direction TB
L6_t1["<b>brand_001</b><br/>kv_hash=HASH[484c...]<br/>value: <b>CountTree count=1000</b><br/>child_hash=HASH[0bf1...]"]:::target
L6_t0["<b>brand_000</b><br/>kv_hash=HASH[90ff...]<br/>value: <b>CountTree count=1000</b><br/>child_hash=HASH[19b5...]"]:::target
L6_boundary["Boundary commitments (22 merk ops):<br/>7 KVHash opaque sibling brands<br/>+ 7 Hash subtree commitments<br/>(prove the two targets' adjacent positions)"]:::sibling
L6_t1 --> L6_t0
L6_t1 --> L6_boundary
end
L5_q -. "Tree(merk_root[byBrand])" .-> L6_t1
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
Marginally larger than Query 2 (1 102 B vs 1 041 B) — the extra cost is one extra KVValueHashFeatureTypeWithChildHash op plus one merge op. The boundary commitments shrink slightly because the two adjacent targets share part of the merk-tree path.
Query 6 — In on byColor (RangeCountable)
select = COUNT
where = color IN ["color_00000000", "color_00000001"]
prove = true
Path query:
path: ["@", contract_id, 0x01, "widget", "color"]
query items: [Key("color_00000000"), Key("color_00000001")]
Verified elements:
path: ["@", contract_id, 0x01, "widget", "color"]
key: "color_00000000"
element: CountTree { count_value_or_default: 100 }
path: ["@", contract_id, 0x01, "widget", "color"]
key: "color_00000001"
element: CountTree { count_value_or_default: 100 }
Proof size: 1 381 B.
Proof display:
Expand to see the structured proof (5 layers; bottom layer carries `KVHashCount` running totals from the `ProvableCountTree`) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[4a5a28cb1b40226aa35b2f0d502767df13268bdf4678627dbfde26a557acdf73]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b90e1e952b7eef903cc9db2d9098e334a37f7e08cade52c6b2ea3bf4b56b645])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[49e7191075272395ed72cf03e973987ede6e4945e08574fe77d725f4ce7ecdf8]))
1: Push(KVValueHash(0x01, Tree(776964676574), HASH[5d9a0fad8a3f32560f8e8950c1e84a7feabaab21b79bc72fec4482442844e2ef]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(widget, Tree(6272616e64), HASH[6c505f53f2ebf3de030cc2aca463d4b429aeb320a9fadb8ae68bb7903a22bb68])))
lower_layers: {
widget => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[9862894b16a0792688fdcf64edcb2ceade5c8b234649bfc6cfc6426869b0e9d9]))
1: Push(KVHash(HASH[a29ee8f206a253362b6da4fcacf8643ee8e5925cd979fcd449e5906f0f9f8be3]))
2: Parent
3: Push(KVValueHash(color, ProvableCountTree(636f6c6f725f3030303030353131, 100000), HASH[79569d595db75bbf2e9dca93a15c90b7eecf7b299632668ec410e2076d27f71c]))
4: Child)
lower_layers: {
color => {
LayerProof {
proof: Merk(
0: Push(KVValueHashFeatureTypeWithChildHash(color_00000000, CountTree(00, 100, flags: [0, 0, 0]), HASH[ce582ad80dab7f822798cbdcd4a7e2d454339ef5da50af688e31acb463f13bc6], ProvableCountedMerkNode(100), HASH[ad2891a5a377d25ef300546faaa2acef14cb3431490a86ed1d16d5fd69ec9e3f]))
1: Push(KVValueHashFeatureTypeWithChildHash(color_00000001, CountTree(00, 100, flags: [0, 0, 0]), HASH[c4024227f61350e128189bbfdb9cb3de893aef09626680a3d2336f991c1dbb14], ProvableCountedMerkNode(300), HASH[45e2452816d75b27baa9d1b8a82a251ce218d949d003bceb2e22ce1988312c4d]))
2: Parent
3: Push(Hash(HASH[cb34b6fa0bd36bf67c93768f3bdbadc7c5f4f143215222ff8bc8bbff5df0dc93]))
4: Child
5: Push(KVHashCount(HASH[2e045e449ad64fe27461182e3f335ee8fb65183c18a3fd3e4ff175c9e767b04b], 700))
6: Parent
7: Push(Hash(HASH[8d73c136c1428e6cca5c6579faeb12b9cc4e7094bdbdba383097d2d05032a414]))
8: Child
9: Push(KVHashCount(HASH[a9f7d6ebc19c3405af2ef32cbdf4f4ec0d4a96592bb5d389f9ab0462389c6fb5], 1500))
10: Parent
11: Push(Hash(HASH[e131726e58ca916c5d2c3fdff06be027b7bca567b45a1854b38774b7eb429b47]))
12: Child
13: Push(KVHashCount(HASH[c982b92207e31779affbc3c4495d175948ca647b9c15740c0cb0f6b7fede6d0d], 3100))
14: Parent
15: Push(Hash(HASH[c8f1d0d58823e8fb60dbd838fdd5b984c6940e1d4d4976473e8718a638dcd64c]))
16: Child
17: Push(KVHashCount(HASH[8dbbcf0d3b51cfa3f8c40c815b8904b650fd51e3bb55ae40f741f7341248ac38], 6300))
18: Parent
19: Push(Hash(HASH[28f1a2ab09b0920e50bdfd4d062412ba9c1d39d33579d485360e7a0941675a43]))
20: Child
21: Push(KVHashCount(HASH[6bf705340b0ff3872a4f692fc10bae0dd9e63fa2726bb3fd284fbfc273ef24af], 12700))
22: Parent
23: Push(Hash(HASH[8ebe73647e431636fe22547384c36bfd83d77a0e109dd3e3f5a69e691c860f9e]))
24: Child
25: Push(KVHashCount(HASH[b2fa1534ef346372a7d2df562fe4fc4938bd07bc72af5a147529478af878972d], 25500))
26: Parent
27: Push(Hash(HASH[db461b2f973111b65f34f31313ccff5530b24fa17bc7e5313d4794783336df24]))
28: Child
29: Push(KVHashCount(HASH[3684347a67ceedad2ff4a7fce6ae303086543c1f146f5865dfdc23612308c05b], 51100))
30: Parent
31: Push(Hash(HASH[e8c957f1d52f9ae3932f1f8d3e3d7f761569b52b29ffd7dc3f4c0c976405b3b4]))
32: Child
33: Push(KVHashCount(HASH[c32ae0189f148c2390791534ff4bc205fabb53a7c7d15f109a4354170045308c], 100000))
34: Parent
35: Push(Hash(HASH[1a1c99166d7b1e1eb9087404f3bfae82d749a3a7a763da654f48c5d314e21e76]))
36: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Same layer count as Query 5 (5 layers) and same inline-two-elements pattern at the bottom. The difference is in the merk-tree node type surrounding the verified elements: byColor's bottom layer is a ProvableCountTree, so each sibling's merk-path operation is a KVHashCount(HASH[...], N) (carrying the sibling's running count) rather than the plain KVHash(HASH[...]) you see in Query 5's byBrand layer. The boundary-proof ops here read like a histogram of the byColor tree's per-subtree counts (700, 1500, 3100, 6300, 12700, 25500, 51100, 100000) — that's the same information Query 7 will sum over directly without descending to any specific value tree.
Same query shape as Query 5 — outer Key-per-In-value, no subquery, per-In CountTrees resolved at the bottom. The difference vs Query 5 is the property-name tree above is a ProvableCountTree instead of NormalTree. That doesn't change the proof's structural shape, but it does mean a future color > X range query against this property has a fast path Query 5's brand doesn't.
flowchart TB
WD["@/contract_id/0x01/widget"]:::tree
WD ==> CO["color: ProvableCountTree"]:::path
CO ==> C000["color_00000000: CountTree count=100"]:::target
CO ==> C001["color_00000001: CountTree count=100"]:::target
CO -.-> CMore["color_00000002 ... color_00000999"]:::faded
C000 -.-> C000_0["[0]: 100 refs"]:::faded
C001 -.-> C001_0["[0]: 100 refs"]:::faded
WD -.-> PK["[0]"]:::faded
WD -.-> BR["brand"]:::faded
classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px;
classDef path fill:#d29922,color:#0d1117,stroke:#1f6feb,stroke-width:2px;
classDef faded fill:#21262d,color:#6e7681,stroke:#484f58;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
linkStyle 0 stroke:#1f6feb,stroke-width:3px;
linkStyle 1 stroke:#1f6feb,stroke-width:3px;
linkStyle 2 stroke:#1f6feb,stroke-width:3px;
Diagram: per-layer merk-tree structure (Layer 5+)
Same L5 shape as Query 3 (color queried under an opaque kv root). L6 inlines two KVValueHashFeatureTypeWithChildHash targets in the byColor ProvableCountTree — the difference from Query 5's L6 is the surrounding boundary ops carry KVHashCount running totals instead of plain KVHash.
flowchart TB
subgraph L5["Layer 5 — widget doctype merk-tree (proof view for `color`)"]
direction TB
L5_root["KVHash[a29e...]<br/>(opaque kv root)"]:::sibling
L5_left["HASH[9862...]"]:::sibling
L5_q["<b>color</b><br/>kv_hash=HASH[7956...]<br/>value: ProvableCountTree (descent)"]:::queried
L5_root --> L5_left
L5_root --> L5_q
end
subgraph L6["Layer 6 — byColor ProvableCountTree merk-tree (TWO TARGETS)"]
direction TB
L6_t1["<b>color_00000001</b><br/>kv_hash=HASH[c402...]<br/>value: <b>CountTree count=100</b><br/>feature: ProvableCountedMerkNode(300)"]:::target
L6_t0["<b>color_00000000</b><br/>kv_hash=HASH[ce58...]<br/>value: <b>CountTree count=100</b><br/>feature: ProvableCountedMerkNode(100)"]:::target
L6_boundary["Boundary commitments (34 merk ops):<br/>8 KVHashCount running totals<br/>(700, 1500, 3100, 6300, 12700,<br/>25500, 51100, 100000)<br/>+ Hash subtree commitments"]:::sibling
L6_t1 --> L6_t0
L6_t1 --> L6_boundary
end
L5_q -. "ProvableCountTree(merk_root[byColor])" .-> L6_t1
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
The two-In-values pattern carries over from Query 5; the per-subtree counts on the boundary KVHashCount ops are the same data Query 7 uses as the integrand for its range aggregate.
Query 7 — Range Query (AggregateCountOnRange)
select = COUNT
where = color > "color_00000500"
prove = true
Path query (different primitive — note the AggregateCountOnRange query item):
path: ["@", contract_id, 0x01, "widget", "color"]
query items: [AggregateCountOnRange([RangeAfter("color_00000500"..)])]
Verified payload (different verifier — GroveDb::verify_aggregate_count_query returns a single u64, not an element list):
root_hash: 0x62ee7348f4d28dd9d7cf86a6c725fa8276cfd446f6007a6000fb0e1dfefa6468
count: 49900
Proof size: 2 072 B.
Proof display:
Expand to see the structured proof (5 layers; bottom layer uses `HashWithCount` + `KVDigestCount` ops instead of `KVValueHash` — the AggregateCountOnRange-specific merk primitive) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[4a5a28cb1b40226aa35b2f0d502767df13268bdf4678627dbfde26a557acdf73]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b90e1e952b7eef903cc9db2d9098e334a37f7e08cade52c6b2ea3bf4b56b645])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[49e7191075272395ed72cf03e973987ede6e4945e08574fe77d725f4ce7ecdf8]))
1: Push(KVValueHash(0x01, Tree(776964676574), HASH[5d9a0fad8a3f32560f8e8950c1e84a7feabaab21b79bc72fec4482442844e2ef]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(widget, Tree(6272616e64), HASH[6c505f53f2ebf3de030cc2aca463d4b429aeb320a9fadb8ae68bb7903a22bb68])))
lower_layers: {
widget => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[9862894b16a0792688fdcf64edcb2ceade5c8b234649bfc6cfc6426869b0e9d9]))
1: Push(KVHash(HASH[a29ee8f206a253362b6da4fcacf8643ee8e5925cd979fcd449e5906f0f9f8be3]))
2: Parent
3: Push(KVValueHash(color, ProvableCountTree(636f6c6f725f3030303030353131, 100000), HASH[79569d595db75bbf2e9dca93a15c90b7eecf7b299632668ec410e2076d27f71c]))
4: Child)
lower_layers: {
color => {
LayerProof {
proof: Merk(
0: Push(HashWithCount(kv_hash=HASH[b2fa1534ef346372a7d2df562fe4fc4938bd07bc72af5a147529478af878972d], left=HASH[e8368be0ff72f87a2132f09d8d68d6dca140bc3c5b048d5f4f6fc8ab9b7bc554], right=HASH[db461b2f973111b65f34f31313ccff5530b24fa17bc7e5313d4794783336df24], count=25500))
1: Push(KVDigestCount(color_00000255, HASH[adfb158116847927badc07be9745a21be7e2660a8b75f8a310aba9025f91feec], 51100))
2: Parent
3: Push(HashWithCount(kv_hash=HASH[e4f3a5c9fdf17ccb2c7508839b2fdcfd4cd878ed1d59270929ac69ef63179402], left=HASH[848d5873de457b1be03c8c7d74733b92874f2071028fdd6d30e8ca16c18a9770], right=HASH[676f04d3603911ecd1e0d2d01c2691b173df672b40daf8a7730f73c50d39e07e], count=12700))
4: Push(KVDigestCount(color_00000383, HASH[14f48ee200148a9c4c673809297bdfb71e79fe9902b130e7842fbdb18c2e1a31], 25500))
5: Parent
6: Push(HashWithCount(kv_hash=HASH[42a257d9bc608c6b1a419f8e081b08df9056832c72e36b5dc07c4b724fb37578], left=HASH[65b3058c7b4d9bcfcf6022645f66bbaed9dbbdb74b7dbf367bbe2240263db767], right=HASH[315927383b45959aa67b32fb26b0b7c21baf6afbb1fcdc05e9c8c43a3c02b6c6], count=6300))
7: Push(KVDigestCount(color_00000447, HASH[dcbfdf897e1b1d83a55172b6fa463446cd5e016331ba075440f7f1091d02467b], 12700))
8: Parent
9: Push(HashWithCount(kv_hash=HASH[ada831d9c38535694323d9092ab9c42e39949c9d2e4567fafd084b0f5754b0d9], left=HASH[09229789d4fdf4baba7646d3bd12e6b77b83ce19f7f1c0918b60b3c1de5bd8ea], right=HASH[ce92f20c6b464d3ff4c95f8f1ee49149aacc50298eaed2c6a2849d588bd4a667], count=3100))
10: Push(KVDigestCount(color_00000479, HASH[1e6eb9e928e8bb229309db3a4a2c0f3041c63e90eb646061e4f5d82b1d65a1ac], 6300))
11: Parent
12: Push(HashWithCount(kv_hash=HASH[ae65499e6a1c105c878c418b09732df2dee29cf7db74c4b2e93b989710b449d0], left=HASH[94eac0807596d751092d12f27195dc72324f45999f4fc483688a9c15c554ecf3], right=HASH[fb4298cd62e8a90af17f9133fd4c106ec1da4b16be2954fc542af6ad0f6e316e], count=1500))
13: Push(KVDigestCount(color_00000495, HASH[cca12136fed93b88094fc80ceb5722b752860000478404c62f7862eb652e268f], 3100))
14: Parent
15: Push(HashWithCount(kv_hash=HASH[db1493f4f683045aa7604c6a06c0280fecb34b352503b148eab16e245938492f], left=HASH[50f064fdcdd8e0f3e1eb86b98dc8eb6f7a8df0b26037df202b21726a05edeb79], right=HASH[d6e96c2078316fcd74e62265173c2bb52a94ad4ef0bccac569557f675307b382], count=300))
16: Push(KVDigestCount(color_00000499, HASH[66e2d072be547070b1d433cb0f05f09ef508ec4d4f0702db4f49e71896ad91bc], 700))
17: Parent
18: Push(KVDigestCount(color_00000500, HASH[47b0ade593a2e4e99e7d7363f5d1f692882007397f025226f19d097ca2f407fa], 100))
19: Push(KVDigestCount(color_00000501, HASH[9146433eb6d43db2f109f5f7714146624bd646b27c7310f3c2cad7155eb7c741], 300))
20: Parent
21: Push(HashWithCount(kv_hash=HASH[bbac5fc7646d820e2912c1771333ebc83b1012619347aa04cce3c4ad13c11eea], left=HASH[0000000000000000000000000000000000000000000000000000000000000000], right=HASH[0000000000000000000000000000000000000000000000000000000000000000], count=100))
22: Child
23: Child
24: Push(KVDigestCount(color_00000503, HASH[66ea1280c29a6ea350e0c6695ab80430f5d3b5dc2df0f5a4d544a918d9fba29a], 1500))
25: Parent
26: Push(HashWithCount(kv_hash=HASH[4d7b5c895a6fb1e451ce85a522ecf18484fd1e406945cde8df9c75ec2152757e], left=HASH[6be0f9637caa5b6c09adb59618a8a90494e2f43a5e9948dc32d68af74528578a], right=HASH[ce1146de6de82a9767edf38a5cc11b5498e57023684acbe9e20bc3104ade94cf], count=700))
27: Child
28: Child
29: Child
30: Child
31: Child
32: Child
33: Push(KVDigestCount(color_00000511, HASH[c7fdd609ef67f184976b1bdfeb97245fdfcb33e53ff6841277def88f55bc9c41], 100000))
34: Parent
35: Push(HashWithCount(kv_hash=HASH[6abc81973aeff51137a002d32ac447e6b91ebf507e34a4a13ec9d1bed4516d23], left=HASH[99323fb716110f45836334025ec154fcc56193c11ee0811bdd86320c0f8164ed], right=HASH[33b9e5cbdf27883150262112aaefda71c0b725a58c3f929ad1ce1cdd3f90aacd], count=48800))
36: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
The bottom layer uses different merk-proof operations than every other query so far (Query 8 shares this shape one level deeper). AggregateCountOnRange doesn't return individual elements; it walks the boundary of the requested range (color > "color_00000500") over the ProvableCountTree's internal nodes and uses two specialized operations:
HashWithCount(kv_hash, left, right, count)— a boundary node that hides its full subtree behind a single hash + count. Thecountfield is the load-bearing piece: the verifier just sums these without descending. In this proof you can seecount=48800at the bottom-right boundary node (everything to the right of the range cut, plus anothercount=100000showing somewhere in the in-range path), and the prover walks the cut so eachHashWithCountcovers a different chunk of the range.KVDigestCount(key, kv_hash, count)— a boundary key inside the in-range region; the prover names the key so the verifier knows exactly where the cut is, but only commits the hash + count, not the value. Note the keys here climb monotonically (color_00000255 → 383 → 447 → 479 → 495 → 499 → 500 → 501 → 511); each one names a binary-tree boundary node on the path from the range start (color_00000500) to the right edge of the tree.
The final summed count: 49900 is what the verifier returns. There's no CountTree(…) element in this proof — the running totals inside HashWithCount / KVDigestCount are the proof's count surface, committed into the ProvableCountTree's merk root at insertion time.
Together with Query 8 (the compound brand == X AND color > Y variant), this is one of two queries in the chapter that use a different GroveDB primitive. Instead of resolving N specific keys, AggregateCountOnRange walks the boundary of the requested range over widget/color's ProvableCountTree and sums the per-node counts already committed inside that tree. The proof carries the boundary merk path and the running total; the verifier returns just the count.
The reason this works only with rangeCountable: true (Query 5's byBrand couldn't do the equivalent) is that widget/color is a ProvableCountTree — its internal merk nodes carry running counts. widget/brand is a plain NormalTree; it would have to enumerate every brand and sum their counts (which is what brand IN [...] does, but for an unbounded range that's not a feasible proof shape).
flowchart TB
WD["@/contract_id/0x01/widget"]:::tree
WD ==> CO["color: ProvableCountTree<br/>(internal merk nodes carry running counts)"]:::target
CO -.-> C500["color_00000500 (boundary)"]:::faded
CO -.-> CMore["color_00000501 ... color_00000999<br/>(in range, summed via merk-node counts)"]:::faded
CO -.-> CBelow["color_00000000 ... color_00000499<br/>(below range, skipped)"]:::faded
WD -.-> PK["[0]"]:::faded
WD -.-> BR["brand"]:::faded
classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px;
classDef faded fill:#21262d,color:#6e7681,stroke:#484f58;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
linkStyle 0 stroke:#1f6feb,stroke-width:3px;
Diagram: per-layer merk-tree structure (Layer 5+)
Same L5 shape as Query 3 / Query 6 (color queried under an opaque kv root). L6 is fundamentally different from every other query: no individual elements are returned — the proof walks the boundary of the range color > "color_00000500" over the ProvableCountTree and sums per-node counts directly. The merk ops at L6 are HashWithCount (boundary subtree hashes carrying their full subtree count) and KVDigestCount (named boundary keys with hash + count, no value).
flowchart TB
subgraph L5["Layer 5 — widget doctype merk-tree (proof view for `color`)"]
direction TB
L5_root["KVHash[a29e...]<br/>(opaque kv root)"]:::sibling
L5_left["HASH[9862...]"]:::sibling
L5_q["<b>color</b><br/>kv_hash=HASH[7956...]<br/>value: ProvableCountTree count=100000<br/>(descent into byColor)"]:::queried
L5_root --> L5_left
L5_root --> L5_q
end
subgraph L6["Layer 6 — byColor ProvableCountTree merk-tree (range-aggregate cut)"]
direction TB
L6_result["<b>Aggregate count = 49900</b><br/>(returned by verify_aggregate_count_query —<br/>a single u64, not an element list)"]:::target
L6_inrange["KVDigestCount ops along the in-range path:<br/>color_00000500 (count=100), color_00000501 (300),<br/>color_00000503 (1500), color_00000511 (100000)"]:::sibling
L6_boundary["HashWithCount boundary nodes covering<br/>chunks of the cut: counts<br/>(25500, 12700, 6300, 3100, 1500, 300, 100, 700, 48800)<br/>+ KVDigestCount path keys above the cut<br/>(color_00000255, 383, 447, 479, 495, 499)"]:::sibling
L6_result --> L6_inrange
L6_result --> L6_boundary
end
L5_q -. "ProvableCountTree(merk_root[byColor])" .-> L6_result
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
The ProvableCountTree's value isn't to expose individual elements — it's to make the summation itself O(log n) instead of O(distinct values in range). The proof bytes are larger than Query 6's two-element point lookup (~2 KB vs ~1.4 KB) because the AggregateCountOnRange primitive has more structural overhead per result, but it scales to any size range in fixed proof bytes, where the point-lookup shape grows linearly with the number of resolved keys.
Query 8 — Compound Equal-plus-Range (byBrandColor)
select = COUNT
where = brand == "brand_050" AND color > "color_00000500"
prove = true
Path query (the prefix brand == X fixes one byBrandColor leg; the range walks the terminator's ProvableCountTree):
path: ["@", contract_id, 0x01, "widget", "brand", "brand_050", "color"]
query items: [AggregateCountOnRange([RangeAfter("color_00000500"..)])]
Verified payload (same primitive as Query 7 — GroveDb::verify_aggregate_count_query returns a single u64):
root_hash: 0x62ee7348f4d28dd9d7cf86a6c725fa8276cfd446f6007a6000fb0e1dfefa6468
count: 499
The bench's deterministic schedule gives every brand all 1 000 colors; the strict > cut at color_00000500 leaves color_00000501..color_00000999 = 499 colors paired with brand_050, each contributing exactly 1 document.
Proof size: 2 656 B.
Proof display:
Expand to see the structured proof (8 layers — same descent as Query 4 down to `brand_050`'s color subtree, then `HashWithCount` / `KVDigestCount` ops over the byBrandColor terminator) — or open interactively in the visualizer ↗
GroveDBProofV1 {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[bd291f29893fb6f6d6201087746ca1f23a178dd08e1346cb6c127e91ae3623b3]))
1: Push(KVValueHash(@, Tree(4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289), HASH[4a5a28cb1b40226aa35b2f0d502767df13268bdf4678627dbfde26a557acdf73]))
2: Parent
3: Push(Hash(HASH[19c924989e473a90d0848277d0b1498ccc8db3dc870cbc130e773f3d79ea5b71]))
4: Child)
lower_layers: {
@ => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289, Tree(01), HASH[5b90e1e952b7eef903cc9db2d9098e334a37f7e08cade52c6b2ea3bf4b56b645])))
lower_layers: {
0x4ed22624752972af97fb71abf4067b23e6d296a61a02f35b2098819fde39d289 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[49e7191075272395ed72cf03e973987ede6e4945e08574fe77d725f4ce7ecdf8]))
1: Push(KVValueHash(0x01, Tree(776964676574), HASH[5d9a0fad8a3f32560f8e8950c1e84a7feabaab21b79bc72fec4482442844e2ef]))
2: Parent)
lower_layers: {
0x01 => {
LayerProof {
proof: Merk(
0: Push(KVValueHash(widget, Tree(6272616e64), HASH[6c505f53f2ebf3de030cc2aca463d4b429aeb320a9fadb8ae68bb7903a22bb68])))
lower_layers: {
widget => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[9862894b16a0792688fdcf64edcb2ceade5c8b234649bfc6cfc6426869b0e9d9]))
1: Push(KVValueHash(brand, Tree(6272616e645f303633), HASH[68b697da99d6ea70a83eb41794dca7ba3938d0ba98fbfaeb3cd0c19b3b5d0ff2]))
2: Parent
3: Push(Hash(HASH[6c36729e93b1a316cbf60fe282eb630c0ed6e45db088e365110302b6c9caba86]))
4: Child)
lower_layers: {
brand => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[fb5eb23b3135d9c226e61f004ffb43abae104238d8a1ea7bc60e8ec6ba271596]))
1: Push(KVHash(HASH[3ed48a5e35cb7546d329487b0e1ab8a81d7c5bec358c37449e6cbd956e3bb069]))
2: Parent
3: Push(Hash(HASH[19ec5730af134e9ac980bbea92c2978212c8efe750a467ab54f073626e0ca2f5]))
4: Push(KVHash(HASH[87bc6e7e1e465b8dcdaf95db9957a455d6bd7c75976db122f33e592fe75f1e4a]))
5: Parent
6: Push(Hash(HASH[a0a354f2bb59b8169253aebabb52afcc3c59c4c60da203c8887abb679d747168]))
7: Push(KVHash(HASH[fc6b1d0237f8ff89b555e9a14480ae1c5b80d529a0f9fb5e681ea7ecd157d3da]))
8: Parent
9: Push(KVValueHash(brand_050, CountTree(636f6c6f72, 1000, flags: [0, 0, 0]), HASH[53dbd6216cccdddf16f3eb0f849aed0c0cea987a718f5b43493abf0a14e83eb9]))
10: Child
11: Push(KVHash(HASH[027ac8b1bc9788118b27c13d0b3c3bd3661ef6a89a775a6b6bf78aa7e6f8ed3d]))
12: Parent
13: Push(Hash(HASH[7a5dc3002e6cb6c92e54d554e5af85e9c2ba64ee9c5f80e6489075cc5f3f0d55]))
14: Child
15: Push(KVHash(HASH[3363630479f1abe6e003b1e1d50b5118e55ad2efb7a3f4b3b6df902bea72ac9a]))
16: Parent
17: Push(Hash(HASH[3857faef5ddb06e201f1e65cf42f15d6c9b0dc67e7f73eb182b520854e9bb648]))
18: Child
19: Child
20: Child
21: Push(KVHash(HASH[f776417ede76e6194706e483ac14ab7b3db6aa0461ec14ed5f8e5d20071363af]))
22: Parent
23: Push(Hash(HASH[b3fccba79c14fcc5e97ff6a3cd051228dc755e6de147bef690ba9681264b2b9f]))
24: Child)
lower_layers: {
brand_050 => {
LayerProof {
proof: Merk(
0: Push(Hash(HASH[2190c6fcd140792fd12be66cd631f97475b9ab3f19417a26d94798115ee46160]))
1: Push(KVValueHash(color, NonCounted(ProvableCountTree(636f6c6f725f3030303030353131, 1000, flags: [0, 0, 0])), HASH[b1cedc48940faedea8b64bff8c8113344acdb1fd8eff37c567099b167b3c5861]))
2: Parent)
lower_layers: {
color => {
LayerProof {
proof: Merk(
0: Push(HashWithCount(kv_hash=HASH[4f8d29f51f626326fa5a3d4aa210a07eddf53121888aa5788625ae774be9bc37], left=HASH[ec92140543f4bd56112e8eaf4cb9796b1986d56b0bf721d81fc7d6a699d16a50], right=HASH[1eb29f80ffaac4878420ecfc9337e6181c9e6fc30608fc5475cf0b808f51a31d], count=255))
1: Push(KVDigestCount(color_00000255, HASH[2ed4d50b30e917eceacb3356eb88057e490f9d98ebf6123d25535ff502d2da2b], 511))
2: Parent
3: Push(HashWithCount(kv_hash=HASH[80de09ce45f1c62d0532139ca67a93d88a293ce8139354e0e4751381346f64e7], left=HASH[32a8b4be78632242774668fd49f8db72d5f261d964f33ed9c0780ca99708ba20], right=HASH[af60a5ba4e39fa6326fd77dff0de304cc4cbac75c0702200a97d099f33617496], count=127))
4: Push(KVDigestCount(color_00000383, HASH[fe27bc251ea815fbd838146098daf0662fe214425a5befaec84c960dadbff89b], 255))
5: Parent
6: Push(HashWithCount(kv_hash=HASH[827791c9001bdf85512aed74a917156299ad6b1a50abe27e03939cb745000dfa], left=HASH[4b5363b3bd01883530360ef09c2b645c6f744988e24c17e432bc2c3321d41541], right=HASH[c444ec932284bb1bae3b45f3f54ed9f3922fd85aeecea01dd10341b889c41137], count=63))
7: Push(KVDigestCount(color_00000447, HASH[e7d9bb66af76a1b8600a9fb5d904f54825b61fb5e8004cdbfb4f42134455953a], 127))
8: Parent
9: Push(HashWithCount(kv_hash=HASH[11a374adf740d562dde32325c07b28949981033d310beafc1d90a3d44fb0bd6a], left=HASH[826dab51d3fd831414ae5344e837343104f0212e1c5ca57014951beba53f89d0], right=HASH[02ebfd24b8c7fd10b74bda8856344c4fd7287ce31ebdd6e9676c9a1a6e5943cc], count=31))
10: Push(KVDigestCount(color_00000479, HASH[6151f4f40176302ed6a27f77fd687bbae015a09ca80ad4af6f80e7c29e8a3595], 63))
11: Parent
12: Push(HashWithCount(kv_hash=HASH[b93259768b6500a9b757c4a90e981f0e3a8a848b275b862f16f5b242310cb65a], left=HASH[b1f724e4b2546d1d92059a72076868112b5b6187b6d227fa834d3ff3579f7b8c], right=HASH[fd1765117ce2a3f6deca713039d81726b05eecab4119e223753dee5fd989d610], count=15))
13: Push(KVDigestCount(color_00000495, HASH[034b88a8dfaf46db8b679fd72d342643d64b6937c44b06f983e5dbebd6f3b69b], 31))
14: Parent
15: Push(HashWithCount(kv_hash=HASH[bd58344e0fbbca9dee08997443550d1630adc59696701fb1f99c5a7e1fdb855a], left=HASH[22ef7d33de4e1d93a27009c5a3ae849ac8713c84dbac046dc615170a6b0e89a8], right=HASH[fbc94ef6e1255b8f0fffdb496258eb03a0b54c29d9f074830159d78a86e05621], count=3))
16: Push(KVDigestCount(color_00000499, HASH[12672ddf0e18d172679f7ebf0ba5f6976b337066e0373ffdac8c176a6a160dcc], 7))
17: Parent
18: Push(KVDigestCount(color_00000500, HASH[7f1d988845d9c82b9d1146f2188b09bf704d31647ee2a26054e69ed897de3750], 1))
19: Push(KVDigestCount(color_00000501, HASH[f0a8f993f517cee96055d69e48dfe51e70fe303424885194d3b7e71924af5df7], 3))
20: Parent
21: Push(HashWithCount(kv_hash=HASH[3b75b6239307e1a00f8596386421e623e365d4adc8451dae07cc3bcf589efc44], left=HASH[0000000000000000000000000000000000000000000000000000000000000000], right=HASH[0000000000000000000000000000000000000000000000000000000000000000], count=1))
22: Child
23: Child
24: Push(KVDigestCount(color_00000503, HASH[aba3bbc16aa5a2413fd60261c5efe4d42c97f0f4b82fcc8e74af8562cc2fdfed], 15))
25: Parent
26: Push(HashWithCount(kv_hash=HASH[64d94410c9ae982091bff1d2fe0cf3edae7af54b43f24f613ea08f465e9fa29f], left=HASH[8d2afe8b42330b1bdd677daffde7238cf93a52146e60eeec8e08b4ce095a9ad1], right=HASH[663bf105cdfa9ffd5431d8190c55a87891da0c13c74eb6a16437526c74de889c], count=7))
27: Child
28: Child
29: Child
30: Child
31: Child
32: Child
33: Push(KVDigestCount(color_00000511, HASH[fb4d7e1e5013a3c804045c72bd920ff81985ee986e87c9373c7041b78953d12e], 1000))
34: Parent
35: Push(HashWithCount(kv_hash=HASH[4ba23a437c91a135eb087602db30021bbbeeba7416d4af9317c2b1a7762ab0a4], left=HASH[cd9697f159ba87524f129190317680dd33f96cf5e8a444c9caaf264fe998746c], right=HASH[f4c9e984a836b6b3739392239b4e35c28c153dd513038a6da5294a4e327c07c0], count=488))
36: Child)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
The descent is identical to Query 4's first six layers (root → @ → contract_id → 0x01 → widget → byBrand → brand_050's value tree → color continuation), then forks at the deepest layer: where Query 4 read one KVValueHashFeatureTypeWithChildHash(color_00000500, CountTree count=1), Query 8 walks the ProvableCountTree boundary using the same HashWithCount / KVDigestCount ops Query 7 used at the doctype level. The boundary keys (color_00000255 → 383 → 447 → 479 → 495 → 499 → 500 → 501 → 503 → 511) name the same binary-merk-tree positions as Query 7's color subtree — predictably, since the bench's deterministic schedule means every brand's color subtree has the same shape.
The final count=488 at the bottom-right HashWithCount covers the upper portion of the in-range subtree (everything to the right of the visible cut); the in-range KVDigestCount ops (color_00000501 count=3, color_00000503 count=15, color_00000511 count=1000) cover named boundary positions inside that subtree. The count field on each merk node is the subtree count (including descendants), not just the named key's contribution — which is why color_00000511 count=1000 and not 1. Summing the boundary contributions yields the final count: 499.
Query 8 is the "compound == then range" shape — and the most expensive query in the chapter. It threads through the same 4 grove layers above the byBrand tree as every other query, descends through byBrand → brand_050's value tree → byBrandColor's color continuation (matching Query 4's path), then runs AggregateCountOnRange over brand_050's ProvableCountTree (matching Query 7's primitive). The result: 8 grove layers of merk-proof bytes — 2 656 B total, ~28 % larger than Query 7's single-leg range and ~39 % larger than Query 4's point-lookup compound.
The reason this even works is that byBrandColor's terminator (brand_X's color continuation) is itself a ProvableCountTree (see Document Count Trees). The compound index has rangeCountable: true, and the parent_value_tree_is_count_tree flag propagates through add_indices_for_index_level_for_contract_operations so the continuation becomes NonCounted(ProvableCountTree(...)) rather than NonCounted(NormalTree(...)). Without that, the boundary nodes wouldn't carry running counts and the verifier would have to enumerate every (brand_050, color) pair.
flowchart TB
WD["@/contract_id/0x01/widget"]:::tree
WD ==> BR["brand: NormalTree"]:::path
BR ==> B050["brand_050: CountTree count=1000"]:::path
B050 ==> B050_C["color: NonCounted(ProvableCountTree count=1000)<br/>(internal merk nodes carry running counts)"]:::target
B050_C -.-> CGT["color_00000501 ... color_00000999<br/>(in range, summed via merk-node counts)"]:::faded
B050_C -.-> CBelow["color_00000000 ... color_00000500<br/>(below range, skipped)"]:::faded
B050 -.-> B050_0["[0]: 1000 byBrand refs"]:::faded
BR -.-> Brands["other brands"]:::faded
WD -.-> CO["color"]:::faded
WD -.-> PK["[0]"]:::faded
classDef tree fill:#21262d,color:#c9d1d9,stroke:#1f6feb,stroke-width:2px;
classDef path fill:#6e7681,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef faded fill:#21262d,color:#6e7681,stroke:#484f58;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
linkStyle 0 stroke:#1f6feb,stroke-width:3px;
linkStyle 1 stroke:#1f6feb,stroke-width:3px;
linkStyle 2 stroke:#1f6feb,stroke-width:3px;
linkStyle 3 stroke:#1f6feb,stroke-width:3px;
Diagram: per-layer merk-tree structure (Layer 5+)
Layers 5–7 are identical to Query 4 (widget → byBrand → brand_050's value tree → color continuation). The difference from Query 4 is entirely at Layer 8: Query 4 resolved a single color_X element with a KVValueHashFeatureTypeWithChildHash op, whereas Query 8 walks the boundary with HashWithCount and KVDigestCount ops (the same shape as Query 7's L6, just one grove layer deeper).
flowchart TB
subgraph L5["Layer 5 — widget doctype merk-tree"]
direction TB
L5_q["<b>brand</b><br/>kv_hash=HASH[68b6...]<br/>value: Tree (descent into byBrand)"]:::queried
L5_left["HASH[9862...]"]:::sibling
L5_right["HASH[6c36...]"]:::sibling
L5_q --> L5_left
L5_q --> L5_right
end
subgraph L6["Layer 6 — byBrand merk-tree (intermediate stop)"]
direction TB
L6_q["<b>brand_050</b><br/>kv_hash=HASH[53db...]<br/>value: CountTree count=1000<br/>(continuation child via lower_layer)"]:::queried
L6_boundary["Boundary commitments (24 merk ops):<br/>6 KVHash sibling brands + 6 Hash subtrees"]:::sibling
L6_q --> L6_boundary
end
subgraph L7["Layer 7 — brand_050's continuation merk-tree (single key)"]
direction TB
L7_q["<b>color</b><br/>kv_hash=HASH[b1ce...]<br/>value: NonCounted(ProvableCountTree count=1000)<br/>(descent into byBrandColor terminator)"]:::queried
L7_left["HASH[2190...]"]:::sibling
L7_q --> L7_left
end
subgraph L8["Layer 8 — byBrandColor color sub-tree (range-aggregate cut)"]
direction TB
L8_result["<b>Aggregate count = 499</b><br/>(returned by verify_aggregate_count_query)"]:::target
L8_inrange["KVDigestCount ops in the in-range path:<br/>color_00000500 (count=1), color_00000501 (3),<br/>color_00000503 (15), color_00000511 (1000)"]:::sibling
L8_boundary["HashWithCount boundary nodes covering<br/>chunks of the cut (counts):<br/>255, 127, 63, 31, 15, 3, 1, 7, 488<br/>+ KVDigestCount path keys above the cut:<br/>color_00000255, 383, 447, 479, 495, 499"]:::sibling
L8_result --> L8_inrange
L8_result --> L8_boundary
end
L5_q -. "Tree(merk_root[byBrand])" .-> L6_q
L6_q -. "CountTree continuation (child_hash)" .-> L7_q
L7_q -. "NonCounted(ProvableCountTree(merk_root))" .-> L8_result
classDef queried fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px;
classDef sibling fill:#6e7681,color:#fff,stroke:#6e7681;
classDef target fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:3px;
Query 8 sits at the intersection of Query 4 (compound descent) and Query 7 (range-aggregate primitive). Its proof carries the descent overhead of both — the boundary commitments at L6 to position brand_050, plus the boundary commitments at L8 to position the color cut — explaining why it's the heaviest proof in the chapter despite verifying a smaller count (499) than Query 7 (49 900).
Diagram: Layer 8 binary merk-tree (the range-aggregate cut)
The 37 merk ops at Layer 8 reconstruct the entire boundary path through brand_050's color ProvableCountTree. Unlike every other query's bottom layer (which abstracts the binary tree into one "target + opaque siblings" box), the AggregateCountOnRange primitive forces the prover to reveal the structural shape of the in-range descent — so we can draw it literally.
Cyan = in-range contributions the verifier adds to the aggregate. Yellow-dashed = the boundary node color_00000500, named so the verifier can position the cut but excluded by the strict > operator. Gray = nodes/subtrees outside the range (boundary commitments needed to prove the rest of the tree's structure, but not summed).
The count field on each node is its subtree count (the node itself + all descendants in the binary merk tree), not just the named key's contribution. So color_00000511 (the root) carries count=1000 because every key in brand_050's color subtree is a descendant — not because there are 1 000 of that one key.
flowchart TB
R["<b>color_00000511</b> (merk root)<br/>KVDigestCount, count=1000<br/>contributes <b>1</b> (itself, in-range)"]:::inrange
R --> L1L["color_00000255<br/>KVDigestCount, count=511<br/>(out-of-range; descend right)"]:::outrange
R --> L1R["HASH[4ba2...] (opaque subtree)<br/>HashWithCount, count=488<br/>(color_00000512 … color_00000999)<br/>contributes <b>488</b> (full subtree in-range)"]:::inrange
L1L --> L2L["HASH[4f8d...] (opaque subtree)<br/>HashWithCount, count=255<br/>(color_00000000 … color_00000254,<br/>all out-of-range)"]:::outrange
L1L --> L2R["color_00000383<br/>KVDigestCount, count=255<br/>(out-of-range)"]:::outrange
L2R --> L3L["HASH[80de...]<br/>HashWithCount, count=127"]:::outrange
L2R --> L3R["color_00000447<br/>KVDigestCount, count=127"]:::outrange
L3R --> L4L["HASH[8277...]<br/>HashWithCount, count=63"]:::outrange
L3R --> L4R["color_00000479<br/>KVDigestCount, count=63"]:::outrange
L4R --> L5L["HASH[11a3...]<br/>HashWithCount, count=31"]:::outrange
L4R --> L5R["color_00000495<br/>KVDigestCount, count=31"]:::outrange
L5R --> L6L["HASH[b932...]<br/>HashWithCount, count=15"]:::outrange
L5R --> L6R["color_00000503<br/>KVDigestCount, count=15<br/>contributes <b>1</b> (itself, in-range)"]:::inrange
L6R --> L7L["color_00000499<br/>KVDigestCount, count=7<br/>(out-of-range; descend right)"]:::outrange
L6R --> L7R["HASH[64d9...] (opaque subtree)<br/>HashWithCount, count=7<br/>(color_00000504 … color_00000510)<br/>contributes <b>7</b> (full subtree in-range)"]:::inrange
L7L --> L8L["HASH[bd58...]<br/>HashWithCount, count=3<br/>(color_00000496 … color_00000498,<br/>all out-of-range)"]:::outrange
L7L --> L8R["color_00000501<br/>KVDigestCount, count=3<br/>contributes <b>1</b> (itself, in-range)"]:::inrange
L8R --> L9L["<b>color_00000500</b><br/>KVDigestCount, count=1<br/>boundary key — strict `>` excludes it<br/>(named so the verifier can place the cut)"]:::boundary
L8R --> L9R["HASH[3b75...] (opaque subtree)<br/>HashWithCount, count=1<br/>(color_00000502)<br/>contributes <b>1</b> (full subtree in-range)"]:::inrange
classDef inrange fill:#39c5cf,color:#0d1117,stroke:#39c5cf,stroke-width:2px;
classDef outrange fill:#6e7681,color:#fff,stroke:#6e7681;
classDef boundary fill:#d29922,color:#0d1117,stroke:#d29922,stroke-width:2px,stroke-dasharray: 6 3;
The verifier's aggregation walks this tree and sums the cyan nodes' contributions: 1 (color_00000511) + 488 (H_4ba2) + 1 (color_00000503) + 7 (H_64d9) + 1 (color_00000501) + 1 (H_3b75) = 499. Notice the asymmetry — the proof reveals a long boundary-descent path down the left of the tree (through KD_255 → KD_383 → KD_447 → KD_479 → KD_495 → KD_499) just to position the cut, even though none of those nodes contribute to the count. That's the structural floor for AggregateCountOnRange: the prover must commit one merk-binary-tree-depth's worth of boundary nodes per side of the range, regardless of how many keys actually fall inside it.
For a worst-case range (color > color_00000000, i.e. essentially the full tree), the boundary descent collapses to one node and H_4ba2-like fully-in-range subtree commitments dominate. For a narrow range like this one (cutting deep into the tree), the descent path costs more than the in-range commitments. Either way the total is O(log C') boundary nodes — Query 8 just happens to land at the unfavourable end of the constant factor.
The same in-order traversal also explains the keys' positions: a balanced merk binary tree over the 1 000 sorted color keys puts color_00000511 at the root (the ~midpoint by tree-depth, not by sort order — color_00000511 happens to land here because of grovedb-merk's AVL rotation rules on the insertion order), color_00000255 and color_00000383 (along with their descendants color_00000447 / 479 / 495 / 499) at progressive left-of-cut descents, and color_00000503 at the right child of color_00000495 (which is itself the right child of the descent path). The keys are sorted by in-order traversal, not by tree position, so don't expect them to look orderly in the diagram above.
Worked Example: How node_hash_with_count Rebuilds the Merk Root
This section uses Query 8's Layer 8 to make one thing concrete: what the verifier actually computes when it folds the proof's Push / Parent / Child ops up to the merk root. It's the same machinery underpinning every other query in the chapter — Q8 just exposes the most interesting node-hash variant (node_hash_with_count, used by ProvableCountTree).
All hashes are Blake3-256. The hash primitives live at merk/src/tree/hash.rs in grovedb. Six functions compose every node-hash in the chapter:
value_hash(v) = Blake3( varint_len(v) || v )
kv_hash(k, v) = Blake3( varint_len(k) || k || value_hash(v) )
kv_digest_to_kv_hash(k, vh) = Blake3( varint_len(k) || k || vh )
combine_hash(h1, h2) = Blake3( h1 || h2 )
node_hash(kv_h, l, r) = Blake3( kv_h || l || r )
node_hash_with_count(kv_h, l, r, c)
= Blake3( kv_h || l || r || c.to_be_bytes() )
(varint_len is integer_encoding::VarInt::encode_var — the same unsigned-varint encoding used throughout grovedb. c.to_be_bytes() is the 8-byte big-endian encoding of the u64 count.)
Each proof op carries enough information to compute its subtree's node_hash. The reconstruction rule per op variant (from merk/src/proofs/tree.rs's compute_hash):
| Proof op | What's revealed | Subtree node_hash formula |
|---|---|---|
Hash(h) | the subtree hash directly | h (no recompute) |
KVHash(kv_h) | the node's kv-hash only | node_hash(kv_h, left_node_hash, right_node_hash) |
KVHashCount(kv_h, c) | kv-hash + node count | node_hash_with_count(kv_h, left, right, c) |
HashWithCount(kv_h, left, right, c) | kv-hash + both children's node-hashes + count | node_hash_with_count(kv_h, left, right, c) (no recursion — children are already pre-hashed) |
KVValueHash(k, v, kv_h) | full key+value + kv-hash | node_hash(kv_h, left, right) |
KVDigest(k, vh) | key + value-hash | node_hash(kv_digest_to_kv_hash(k, vh), left, right) |
KVDigestCount(k, vh, c) | key + value-hash + count | node_hash_with_count(kv_digest_to_kv_hash(k, vh), left, right, c) |
KVValueHashFeatureTypeWithChildHash(k, v, kv_h, feature, child_hash) | key + value + kv-hash + feature type + opaque child-layer hash | combines kv_h with child_hash (via combine_hash), then node_hash[_with_count] depending on feature |
The left / right arguments are the children's reconstructed node-hashes (computed recursively from the stack as Parent / Child ops glue the subtrees together). For nodes at the leaf level of the proof's revealed structure, both children are NULL_HASH (32 zero bytes).
The example: rebuilding color_00000511's node_hash (Q8, Layer 8)
At the top of Layer 8's binary merk-tree, the proof has three ops that together produce the merk root hash for brand_050's color ProvableCountTree:
op 33: Push(KVDigestCount(
key = "color_00000511",
vh = HASH[fb4d7e1e5013a3c804045c72bd920ff81985ee986e87c9373c7041b78953d12e],
count = 1000))
op 34: Parent (links the running left-side subtree onto color_00000511 as its left child)
op 35: Push(HashWithCount(
kv_h = HASH[4ba23a437c91a135eb087602db30021bbbeeba7416d4af9317c2b1a7762ab0a4],
left = HASH[cd9697f159ba87524f129190317680dd33f96cf5e8a444c9caaf264fe998746c],
right = HASH[f4c9e984a836b6b3739392239b4e35c28c153dd513038a6da5294a4e327c07c0],
count = 488))
op 36: Child (links the just-pushed HashWithCount as color_00000511's right child)
Call them KD_511, HWC_4ba2. By the time we reach op 33 the left-side stack already holds node_hash_left — the recursively-computed node-hash of the whole left subtree rooted at color_00000255 (255 + 1 + 255 = 511 keys, including the boundary-descent path). We'll trust that value here; it's the output of folding ops 0–32 with the same machinery applied recursively.
Step 1: Compute the right child's node_hash directly from HWC_4ba2.
Because HashWithCount already carries the children's node-hashes (cd96... and f4c9...) and the node's own kv-hash (4ba2...) and count (488), no recursion is needed — the verifier just plugs the four values into node_hash_with_count:
node_hash_right
= node_hash_with_count(kv_h=4ba2..., left=cd96..., right=f4c9..., count=488)
= Blake3( 4ba23a437c91a135eb087602db30021bbbeeba7416d4af9317c2b1a7762ab0a4
|| cd9697f159ba87524f129190317680dd33f96cf5e8a444c9caaf264fe998746c
|| f4c9e984a836b6b3739392239b4e35c28c153dd513038a6da5294a4e327c07c0
|| 0x00000000000001E0 ) // 488 as big-endian u64
That's a single Blake3 call over 32 + 32 + 32 + 8 = 104 bytes.
Step 2: Compute color_00000511's kv-hash from its KVDigestCount op.
The proof reveals the key ("color_00000511", 14 bytes ASCII) and the value-hash (fb4d..., 32 bytes). kv_digest_to_kv_hash folds them into the node's kv-hash:
kv_h_511
= kv_digest_to_kv_hash(key="color_00000511", value_hash=fb4d...)
= Blake3( varint_len(14) // = 0x0E (one byte)
|| "color_00000511" // 14 ASCII bytes
|| fb4d7e1e5013a3c804045c72bd920ff81985ee986e87c9373c7041b78953d12e )
That's one Blake3 call over 1 + 14 + 32 = 47 bytes.
Step 3: Compute color_00000511's node_hash by folding the kv-hash with both children's node-hashes plus the running count.
node_hash_511
= node_hash_with_count(
kv_h = kv_h_511, // from Step 2
left = node_hash_left, // from ops 0..32 (the descent path)
right = node_hash_right, // from Step 1
count = 1000)
= Blake3( kv_h_511 || node_hash_left || node_hash_right || 0x00000000000003E8 )
// 1000 as big-endian u64
Another single Blake3 call over 32 + 32 + 32 + 8 = 104 bytes.
Step 4: That's it. node_hash_511 is the merk root of Layer 8 — the byBrandColor color subtree for brand_050. The verifier then checks this against what Layer 7 claimed Layer 8's merk root would be (the NonCounted(ProvableCountTree(...)) value stored against the key "color" inside brand_050's value tree), and so on up the GroveDB layer stack until Layer 1 lands the entire chapter's root_hash = 0x62ee7348f4d28dd9d7cf86a6c725fa8276cfd446f6007a6000fb0e1dfefa6468.
Why the count is part of the hash
The crucial structural feature is the || count.to_be_bytes() tail in node_hash_with_count. Without it, the proof's running counts would be unsigned hints the verifier couldn't trust — a malicious prover could ship a KVDigestCount(color_00000511, fb4d..., 9_999_999_999) and there'd be no way to detect the lie without re-counting the documents (which is exactly what count proofs are supposed to avoid).
Binding the count into the merk root via node_hash_with_count is what lets AggregateCountOnRange skip enumeration: the verifier reads the count off the boundary commitments and trusts it because changing the count would change the merk root, which is consensus-committed.
Concretely, that's why every ProvableCountTree-derived op (KVHashCount, KVDigestCount, HashWithCount, KVValueHashFeatureTypeWithChildHash with feature ProvableCountedMerkNode(_)) routes through node_hash_with_count rather than the cheaper node_hash — see merk/src/proofs/tree.rs's compute_hash and the TreeFeatureType::ProvableCountedMerkNode branch in particular. NormalTree nodes (e.g. byBrand) use plain node_hash — their kv-hash + child hashes don't commit a count, which is why byBrand can't answer AggregateCountOnRange queries even though it's countable: "countable".
One last simplification
For nodes whose proof op already carries the kv-hash (the kvh-prefixed ops — KVHashCount, HashWithCount, KVHash), the verifier skips Step 2 entirely. For nodes whose proof op carries only the key and value-hash (KVDigest, KVDigestCount), the verifier folds them via kv_digest_to_kv_hash first (one extra Blake3 call). For nodes with KVValueHash (full key+value), the verifier recomputes the kv-hash all the way from scratch via kv_hash, which means it also re-hashes the value through value_hash. The choice is driven by how much of the node the proof needs to reveal:
- A node on the descent path that the verifier doesn't need to materialize → emit
Hash(node_hash)(1 hash, opaque). - A node whose existence the verifier must prove but whose value can stay opaque → emit
KVHash(kv_h)orKVHashCount(kv_h, c)(kv-hash committed, value hidden). - A boundary node whose key the verifier needs to compare against the range → emit
KVDigest(k, vh)orKVDigestCount(k, vh, c)(key revealed, value still digested). - A target whose full value the verifier must read → emit
KVValueHash(k, v, kv_h)or the feature-typed variant.
The user-facing trade-off is proof bytes vs information revealed. The verifier's reconstruction logic is uniform: every op feeds the same node-hash formula one variant or another.
At-a-Glance Comparison
| # | Query | Primitive | Verified shape | Proof size |
|---|---|---|---|---|
| 1 | (empty) | primary-key CountTree | 1 CountTree, count=100000 | 585 B |
| 2 | brand == X | PointLookupProof / byBrand | 1 CountTree, count=1000 | 1 041 B |
| 3 | color == X | PointLookupProof / byColor | 1 CountTree, count=100 | 1 327 B |
| 4 | brand == X AND color == Y | PointLookupProof / byBrandColor | 1 CountTree, count=1 | 1 911 B |
| 5 | brand IN [b0, b1] | PointLookupProof / byBrand | 2 CountTrees, sum=2000 | 1 102 B |
| 6 | color IN [c0, c1] | PointLookupProof / byColor | 2 CountTrees, sum=200 | 1 381 B |
| 7 | color > floor | AggregateCountOnRange / byColor | u64=49900 | 2 072 B |
| 8 | brand == X AND color > floor | AggregateCountOnRange / byBrandColor | u64=499 | 2 656 B |
Four takeaways:
- Query 1 is the cheapest. A doctype-level total count is one merk read; everything else descends through an index tree.
- Query 2 and Query 6 are structurally identical despite covering different indexes (
byBrandcountable-only,byColorrangeCountable). The value-tree-direct shape is uniform across countability tiers —rangeCountable: trueonly matters for Queries 7 and 8. - Queries 7 and 8 use a fundamentally different verifier (
verify_aggregate_count_queryvsverify_query). Queries 1–6 return an element list and readcount_value_or_defaultper branch; Queries 7 and 8 return a pre-summedu64. Query 8 is just Query 7 one grove layer deeper — same primitive, applied to byBrandColor's terminator rather than byColor's. - Query 8 is the most expensive. It pays for both the compound descent (Query 4's 4-extra-layer cost) and the range-aggregate boundary (Query 7's primitive). The verified count is far smaller than Query 7 (499 vs 49 900), but the proof bytes are 28 % larger because the merk-tree boundary at L8 has roughly the same shape regardless of how many keys the cut spans.
The path-query builder these examples decode lives at packages/rs-drive/src/query/drive_document_count_query/path_query.rs; the verifier mirror sits in packages/rs-drive/src/verify/document_count/. Both the prover and the verifier reconstruct the exact same PathQuery via the shared builder — touching one without the other is a Merkle-root mismatch waiting to happen, and the byte-identical contract is what makes the proof bytes here reproducible against the bench fixture.